opinionated_rails 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +59 -0
- data/exe/opinionated_rails +7 -0
- data/lib/opinionated_rails/answer_shell.rb +131 -0
- data/lib/opinionated_rails/cli.rb +291 -0
- data/lib/opinionated_rails/context.rb +39 -0
- data/lib/opinionated_rails/error.rb +6 -0
- data/lib/opinionated_rails/generators/file_writer.rb +14 -0
- data/lib/opinionated_rails/generators/rails_new_invoker.rb +26 -0
- data/lib/opinionated_rails/kamal_config.rb +94 -0
- data/lib/opinionated_rails/manifest.rb +42 -0
- data/lib/opinionated_rails/pins.rb +83 -0
- data/lib/opinionated_rails/prerequisites.rb +39 -0
- data/lib/opinionated_rails/rails_version_guard.rb +49 -0
- data/lib/opinionated_rails/step.rb +41 -0
- data/lib/opinionated_rails/step_graph.rb +53 -0
- data/lib/opinionated_rails/steps/addons/auth_step.rb +170 -0
- data/lib/opinionated_rails/steps/addons/ci_step.rb +226 -0
- data/lib/opinionated_rails/steps/addons/docker_kamal_step.rb +133 -0
- data/lib/opinionated_rails/steps/addons/docs_step.rb +35 -0
- data/lib/opinionated_rails/steps/addons/factory_or_fixtures_step.rb +128 -0
- data/lib/opinionated_rails/steps/addons/faker_step.rb +55 -0
- data/lib/opinionated_rails/steps/addons/js_lint_step.rb +132 -0
- data/lib/opinionated_rails/steps/addons/mailer_step.rb +183 -0
- data/lib/opinionated_rails/steps/addons/pagination_step.rb +116 -0
- data/lib/opinionated_rails/steps/addons/pr_issue_templates_step.rb +43 -0
- data/lib/opinionated_rails/steps/addons/rubocop_step.rb +111 -0
- data/lib/opinionated_rails/steps/addons/secrets_handling_step.rb +138 -0
- data/lib/opinionated_rails/steps/addons/services_step.rb +64 -0
- data/lib/opinionated_rails/steps/addons/solid_stack_step.rb +252 -0
- data/lib/opinionated_rails/steps/addons/spa_controller_step.rb +87 -0
- data/lib/opinionated_rails/steps/addons/staging_env_step.rb +108 -0
- data/lib/opinionated_rails/steps/addons/tailwind_step.rb +148 -0
- data/lib/opinionated_rails/steps/addons/vcr_step.rb +170 -0
- data/lib/opinionated_rails/steps/core/app_name_step.rb +85 -0
- data/lib/opinionated_rails/steps/core/app_type_step.rb +51 -0
- data/lib/opinionated_rails/steps/core/database_step.rb +53 -0
- data/lib/opinionated_rails/steps/core/frontend_step.rb +282 -0
- data/lib/opinionated_rails/steps/core/testing_step.rb +82 -0
- data/lib/opinionated_rails/templates/docs/STYLE.md +39 -0
- data/lib/opinionated_rails/templates/github/ISSUE_TEMPLATE/bug_report.md +25 -0
- data/lib/opinionated_rails/templates/github/ISSUE_TEMPLATE/feature_request.md +17 -0
- data/lib/opinionated_rails/templates/github/PULL_REQUEST_TEMPLATE.md +9 -0
- data/lib/opinionated_rails/templates/rubocop/custom_overrides.yml +5 -0
- data/lib/opinionated_rails/templates/services/application_service.rb +50 -0
- data/lib/opinionated_rails/templates/services/result.rb +44 -0
- data/lib/opinionated_rails/version.rb +5 -0
- data/lib/opinionated_rails.rb +41 -0
- metadata +141 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class CiStep < Step
|
|
7
|
+
def self.id = :ci
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = [ :rubocop, :testing ]
|
|
10
|
+
def self.summary = "Configure CI"
|
|
11
|
+
def self.description = "Generates a GitHub Actions workflow for tests, RuboCop, security scans and JS lint."
|
|
12
|
+
def self.prompts = "Enable CI workflow? (default: yes)"
|
|
13
|
+
|
|
14
|
+
POSTGRES_SERVICE = <<~YAML.gsub(/^/, " ")
|
|
15
|
+
postgres:
|
|
16
|
+
image: postgres:16
|
|
17
|
+
env:
|
|
18
|
+
POSTGRES_USER: postgres
|
|
19
|
+
POSTGRES_PASSWORD: postgres
|
|
20
|
+
ports: [ "5432:5432" ]
|
|
21
|
+
options: >-
|
|
22
|
+
--health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=5
|
|
23
|
+
YAML
|
|
24
|
+
|
|
25
|
+
MYSQL_SERVICE = <<~YAML.gsub(/^/, " ")
|
|
26
|
+
mysql:
|
|
27
|
+
image: mysql:8.4
|
|
28
|
+
env:
|
|
29
|
+
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
|
|
30
|
+
ports: [ "3306:3306" ]
|
|
31
|
+
options: >-
|
|
32
|
+
--health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
|
|
33
|
+
YAML
|
|
34
|
+
|
|
35
|
+
REDIS_SERVICE = <<~YAML.gsub(/^/, " ")
|
|
36
|
+
redis:
|
|
37
|
+
image: redis:7
|
|
38
|
+
ports: [ "6379:6379" ]
|
|
39
|
+
options: >-
|
|
40
|
+
--health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=5
|
|
41
|
+
YAML
|
|
42
|
+
|
|
43
|
+
def if_condition(_context) = true
|
|
44
|
+
|
|
45
|
+
def prompt(context)
|
|
46
|
+
shell = context.shell
|
|
47
|
+
choice = shell.ask("Enable CI workflow? (default: yes):").to_s.strip
|
|
48
|
+
choice = "yes" if choice.empty?
|
|
49
|
+
|
|
50
|
+
loop do
|
|
51
|
+
case choice.downcase
|
|
52
|
+
when "yes", "y"
|
|
53
|
+
context.answer(:ci, true)
|
|
54
|
+
break
|
|
55
|
+
when "no", "n"
|
|
56
|
+
context.answer(:ci, false)
|
|
57
|
+
break
|
|
58
|
+
else
|
|
59
|
+
shell.say("Invalid. Enter 'yes' or 'no'.", :red)
|
|
60
|
+
choice = shell.ask("Enable CI workflow? (default: yes):").to_s.strip
|
|
61
|
+
choice = "yes" if choice.empty?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def generate(context)
|
|
67
|
+
enabled = context.get(:ci) != false
|
|
68
|
+
|
|
69
|
+
write_workflow(context) if enabled
|
|
70
|
+
|
|
71
|
+
context.set_output(:ci, { ci: enabled })
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def outputs(context)
|
|
75
|
+
context.output(:ci)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def write_workflow(context)
|
|
81
|
+
path = File.join(context.app_path, ".github/workflows/ci.yml")
|
|
82
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
83
|
+
File.write(path, workflow_content(context))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def workflow_content(context)
|
|
87
|
+
jobs = [ security_job(context), test_job(context), rubocop_job ]
|
|
88
|
+
jobs << js_lint_job(context) if context.output(:js_lint, :js_lint) || false
|
|
89
|
+
|
|
90
|
+
<<~YAML + jobs.compact.join("\n")
|
|
91
|
+
name: CI
|
|
92
|
+
|
|
93
|
+
on:
|
|
94
|
+
pull_request:
|
|
95
|
+
push:
|
|
96
|
+
branches: [ main ]
|
|
97
|
+
|
|
98
|
+
permissions:
|
|
99
|
+
contents: read
|
|
100
|
+
|
|
101
|
+
concurrency:
|
|
102
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
103
|
+
cancel-in-progress: true
|
|
104
|
+
|
|
105
|
+
jobs:
|
|
106
|
+
YAML
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
CHECKOUT_AND_RUBY = <<~YAML.gsub(/^/, " ")
|
|
110
|
+
- uses: actions/checkout@v4
|
|
111
|
+
- uses: ruby/setup-ruby@v1
|
|
112
|
+
with:
|
|
113
|
+
bundler-cache: true
|
|
114
|
+
YAML
|
|
115
|
+
|
|
116
|
+
# `rails new --skip-ci` drops Rails' own scan job, so keep the same scans here.
|
|
117
|
+
def security_job(context)
|
|
118
|
+
scans = %w[ brakeman bundler-audit ].select { |tool| File.exist?(File.join(context.app_path, "bin", tool)) }
|
|
119
|
+
return if scans.empty?
|
|
120
|
+
|
|
121
|
+
steps = scans.map { |tool| " - name: Run #{tool}\n run: bin/#{tool}#{' --no-pager' if tool == 'brakeman'}\n" }
|
|
122
|
+
|
|
123
|
+
<<~YAML.chomp + "\n" + CHECKOUT_AND_RUBY + steps.join
|
|
124
|
+
\x20\x20security:
|
|
125
|
+
runs-on: ubuntu-latest
|
|
126
|
+
timeout-minutes: 15
|
|
127
|
+
steps:
|
|
128
|
+
YAML
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_job(context)
|
|
132
|
+
header = <<~YAML.chomp
|
|
133
|
+
\x20\x20test:
|
|
134
|
+
runs-on: ubuntu-latest
|
|
135
|
+
timeout-minutes: 15
|
|
136
|
+
YAML
|
|
137
|
+
|
|
138
|
+
setup = []
|
|
139
|
+
setup << " - uses: actions/setup-node@v4\n with:\n node-version: lts/*\n" if react?(context)
|
|
140
|
+
setup << " - uses: oven-sh/setup-bun@v2\n" if react?(context) && js_runner(context) == "bun"
|
|
141
|
+
setup << " - name: Install system libraries\n run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libmysqlclient-dev\n" if database(context) == :mysql2
|
|
142
|
+
|
|
143
|
+
steps = CHECKOUT_AND_RUBY + setup.join
|
|
144
|
+
steps += react_build_steps(context) if react?(context)
|
|
145
|
+
# db:migrate first because a fresh app has migrations but no schema.rb yet; db:test:prepare then
|
|
146
|
+
# loads it and triggers the Tailwind/JS build hooks. DATABASE_URL carries a database name because
|
|
147
|
+
# dockerfile-rails' database.yml appends "_cache" to its path.
|
|
148
|
+
steps += " - name: Set up database\n run: bin/rails db:create db:migrate db:test:prepare\n"
|
|
149
|
+
steps += " - name: Run tests\n run: #{test_command(context)}\n"
|
|
150
|
+
|
|
151
|
+
"#{header}\n#{services_and_env(context)} steps:\n#{steps}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def services_and_env(context)
|
|
155
|
+
services = []
|
|
156
|
+
env = [ "RAILS_ENV: test" ]
|
|
157
|
+
|
|
158
|
+
case database(context)
|
|
159
|
+
when :postgresql
|
|
160
|
+
services << POSTGRES_SERVICE
|
|
161
|
+
env << "DATABASE_URL: postgres://postgres:postgres@localhost:5432/ci_test"
|
|
162
|
+
when :mysql2
|
|
163
|
+
services << MYSQL_SERVICE
|
|
164
|
+
env << "DATABASE_URL: mysql2://root@127.0.0.1:3306/ci_test"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
if context.output(:solid_stack, :solid_stack) == :redis
|
|
168
|
+
services << REDIS_SERVICE
|
|
169
|
+
env << "REDIS_URL: redis://localhost:6379/1"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
block = +""
|
|
173
|
+
block << " services:\n#{services.join}" unless services.empty?
|
|
174
|
+
block << " env:\n#{env.map { |line| " #{line}\n" }.join}"
|
|
175
|
+
block
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def database(context) = context.output(:database, :database)
|
|
179
|
+
|
|
180
|
+
# The app is installed with yarn (or bun); npm would ignore its lockfile.
|
|
181
|
+
def js_runner(context) = context.get(:bundler) == :bun ? "bun" : "yarn"
|
|
182
|
+
|
|
183
|
+
def react_build_steps(context)
|
|
184
|
+
runner = js_runner(context)
|
|
185
|
+
" - run: #{runner} install\n" \
|
|
186
|
+
" - name: Build JavaScript and CSS\n run: #{runner} run build && #{runner} run build:css\n"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def react?(context) = context.output(:frontend, :frontend) == :react
|
|
190
|
+
|
|
191
|
+
def rubocop_job
|
|
192
|
+
<<~YAML.chomp + "\n" + CHECKOUT_AND_RUBY + " - name: Run RuboCop\n run: bin/rubocop -f github\n"
|
|
193
|
+
\x20\x20rubocop:
|
|
194
|
+
runs-on: ubuntu-latest
|
|
195
|
+
timeout-minutes: 15
|
|
196
|
+
steps:
|
|
197
|
+
YAML
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def test_command(context)
|
|
201
|
+
if context.output(:testing, :test_framework) == :rspec
|
|
202
|
+
"bundle exec rspec"
|
|
203
|
+
else
|
|
204
|
+
"bin/rails test"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def js_lint_job(context)
|
|
209
|
+
runner = js_runner(context)
|
|
210
|
+
bun_setup = runner == "bun" ? " - uses: oven-sh/setup-bun@v2\n" : ""
|
|
211
|
+
|
|
212
|
+
<<~YAML.chomp + "\n" + bun_setup + " - run: #{runner} install\n - name: Run JS lint\n run: #{runner} run lint:js\n"
|
|
213
|
+
\x20\x20js_lint:
|
|
214
|
+
runs-on: ubuntu-latest
|
|
215
|
+
timeout-minutes: 15
|
|
216
|
+
steps:
|
|
217
|
+
- uses: actions/checkout@v4
|
|
218
|
+
- uses: actions/setup-node@v4
|
|
219
|
+
with:
|
|
220
|
+
node-version: lts/*
|
|
221
|
+
YAML
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class DockerKamalStep < Step
|
|
7
|
+
def self.id = :docker_kamal
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = [ :database, :frontend, :solid_stack ]
|
|
10
|
+
def self.summary = "Configure Docker and Kamal"
|
|
11
|
+
def self.description = "Generates a Dockerfile and optional Kamal deployment files."
|
|
12
|
+
def self.prompts = "Enable Kamal deployment? (default: no) — Docker itself is always generated"
|
|
13
|
+
|
|
14
|
+
DOCKERFILE_RAILS_VERSION = "1.7.10".freeze
|
|
15
|
+
KAMAL_VERSION = "2.12.0".freeze
|
|
16
|
+
|
|
17
|
+
def if_condition(_context) = true
|
|
18
|
+
|
|
19
|
+
def prompt(context)
|
|
20
|
+
shell = context.shell
|
|
21
|
+
choice = shell.ask("Enable Kamal deployment? (default: no):").to_s.strip
|
|
22
|
+
choice = "no" if choice.empty?
|
|
23
|
+
|
|
24
|
+
loop do
|
|
25
|
+
case choice.downcase
|
|
26
|
+
when "yes", "y"
|
|
27
|
+
context.answer(:docker_kamal, true)
|
|
28
|
+
break
|
|
29
|
+
when "no", "n"
|
|
30
|
+
context.answer(:docker_kamal, false)
|
|
31
|
+
break
|
|
32
|
+
else
|
|
33
|
+
shell.say("Invalid. Enter 'yes' or 'no'.", :red)
|
|
34
|
+
choice = shell.ask("Enable Kamal deployment? (default: no):").to_s.strip
|
|
35
|
+
choice = "no" if choice.empty?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def generate(context)
|
|
41
|
+
kamal_enabled = context.get(:docker_kamal) == true
|
|
42
|
+
|
|
43
|
+
install_dockerfile_rails(context)
|
|
44
|
+
install_kamal(context) if kamal_enabled
|
|
45
|
+
|
|
46
|
+
context.set_output(:docker_kamal, { docker: true, kamal: kamal_enabled })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def outputs(context)
|
|
50
|
+
context.output(:docker_kamal)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def install_dockerfile_rails(context)
|
|
56
|
+
add_development_group_gem(context, "gem \"dockerfile-rails\", \"#{DOCKERFILE_RAILS_VERSION}\"")
|
|
57
|
+
|
|
58
|
+
return if context.skip_bundle_install
|
|
59
|
+
|
|
60
|
+
run_in_app(context, "bundle", "install", "--quiet") ||
|
|
61
|
+
raise(OpinionatedRails::Error, "bundle install failed")
|
|
62
|
+
run_in_app(context, "bundle", "exec", "rails", "generate", "dockerfile", "--force") ||
|
|
63
|
+
raise(OpinionatedRails::Error, "rails generate dockerfile failed")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def install_kamal(context)
|
|
67
|
+
add_top_level_gem(context, "gem \"kamal\", \"#{KAMAL_VERSION}\", require: false")
|
|
68
|
+
|
|
69
|
+
return if context.skip_bundle_install
|
|
70
|
+
|
|
71
|
+
run_in_app(context, "bundle", "install", "--quiet") ||
|
|
72
|
+
raise(OpinionatedRails::Error, "bundle install failed")
|
|
73
|
+
run_in_app(context, "bundle", "exec", "kamal", "init") ||
|
|
74
|
+
raise(OpinionatedRails::Error, "kamal init failed")
|
|
75
|
+
|
|
76
|
+
write_kamal_files(context)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Replace `kamal init`'s commented-out templates with files complete for the chosen stack.
|
|
80
|
+
def write_kamal_files(context)
|
|
81
|
+
database = context.output(:database, :database)
|
|
82
|
+
stack = context.output(:solid_stack, :solid_stack)
|
|
83
|
+
app_name = context.output(:app_name, :app_name) || File.basename(context.app_path)
|
|
84
|
+
|
|
85
|
+
File.write(File.join(context.app_path, "config/deploy.yml"),
|
|
86
|
+
KamalConfig.deploy_yml(app_name: app_name, database: database, stack: stack))
|
|
87
|
+
File.write(File.join(context.app_path, ".kamal/secrets"), KamalConfig.secrets(database: database))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def run_in_app(context, *command)
|
|
91
|
+
Dir.chdir(context.app_path) do
|
|
92
|
+
if defined?(Bundler)
|
|
93
|
+
Bundler.with_unbundled_env { system(*command) }
|
|
94
|
+
else
|
|
95
|
+
system(*command)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def add_top_level_gem(context, gem_line)
|
|
101
|
+
gemfile_path = File.join(context.app_path, "Gemfile")
|
|
102
|
+
return unless File.exist?(gemfile_path)
|
|
103
|
+
|
|
104
|
+
content = File.read(gemfile_path)
|
|
105
|
+
return if content.include?(gem_line)
|
|
106
|
+
|
|
107
|
+
File.open(gemfile_path, "a") { |f| f.puts(gem_line) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def add_development_group_gem(context, gem_line)
|
|
111
|
+
gemfile_path = File.join(context.app_path, "Gemfile")
|
|
112
|
+
return unless File.exist?(gemfile_path)
|
|
113
|
+
|
|
114
|
+
content = File.read(gemfile_path)
|
|
115
|
+
return if content.include?(gem_line)
|
|
116
|
+
|
|
117
|
+
lines = content.lines
|
|
118
|
+
group_start = lines.index { |line| line.strip == "group :development do" }
|
|
119
|
+
|
|
120
|
+
if group_start
|
|
121
|
+
end_index = (group_start + 1...lines.size).find { |i| lines[i].strip == "end" }
|
|
122
|
+
lines.insert(end_index, " #{gem_line}\n")
|
|
123
|
+
else
|
|
124
|
+
lines << "\n" unless lines.last.nil? || lines.last.end_with?("\n")
|
|
125
|
+
lines << "\ngroup :development do\n #{gem_line}\nend\n"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
File.write(gemfile_path, lines.join)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class DocsStep < Step
|
|
7
|
+
def self.id = :docs
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = []
|
|
10
|
+
def self.summary = "Generate project documentation"
|
|
11
|
+
def self.description = "Creates AGENT.md and STYLE.md files."
|
|
12
|
+
def self.prompts = "None"
|
|
13
|
+
|
|
14
|
+
AGENT_TEXT = "Read STYLE.md before making any changes to this codebase."
|
|
15
|
+
STYLE_TEMPLATE = File.expand_path("../../templates/docs/STYLE.md", __dir__)
|
|
16
|
+
|
|
17
|
+
def if_condition(_context) = true
|
|
18
|
+
|
|
19
|
+
def prompt(_context); end
|
|
20
|
+
|
|
21
|
+
def generate(context)
|
|
22
|
+
writer = OpinionatedRails::Generators::FileWriter.new
|
|
23
|
+
writer.write(File.join(context.app_path, "AGENT.md"), AGENT_TEXT)
|
|
24
|
+
writer.write(File.join(context.app_path, "STYLE.md"), File.read(STYLE_TEMPLATE))
|
|
25
|
+
|
|
26
|
+
context.set_output(:docs, { docs: true })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def outputs(context)
|
|
30
|
+
context.output(:docs)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class FactoryOrFixturesStep < Step
|
|
7
|
+
def self.id = :factory_or_fixtures
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = [ :testing ]
|
|
10
|
+
def self.summary = "Configure factories or fixtures"
|
|
11
|
+
def self.description = "Adds FactoryBot for RSpec or keeps Minitest fixtures."
|
|
12
|
+
def self.prompts = "FactoryBot or Fixtures for RSpec (default: FactoryBot)"
|
|
13
|
+
|
|
14
|
+
FACTORY_BOT_VERSION = "6.4.3".freeze
|
|
15
|
+
|
|
16
|
+
MINIMAL_RAILS_HELPER = <<~RUBY
|
|
17
|
+
require 'spec_helper'
|
|
18
|
+
ENV['RAILS_ENV'] ||= 'test'
|
|
19
|
+
require_relative '../config/environment'
|
|
20
|
+
require 'rspec/rails'
|
|
21
|
+
|
|
22
|
+
RSpec.configure do |config|
|
|
23
|
+
config.fixture_paths = [
|
|
24
|
+
Rails.root.join('spec/fixtures')
|
|
25
|
+
]
|
|
26
|
+
config.use_transactional_fixtures = true
|
|
27
|
+
end
|
|
28
|
+
RUBY
|
|
29
|
+
|
|
30
|
+
def if_condition(context)
|
|
31
|
+
context.output(:testing, :test_framework) == :rspec
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def prompt(context)
|
|
35
|
+
shell = context.shell
|
|
36
|
+
shell.say("Choose a factory/fixture strategy for RSpec:")
|
|
37
|
+
shell.say(" 1. FactoryBot (default)")
|
|
38
|
+
shell.say(" 2. Fixtures")
|
|
39
|
+
|
|
40
|
+
choice = shell.ask("Factory strategy (default: FactoryBot):").to_s.strip
|
|
41
|
+
choice = "1" if choice.empty?
|
|
42
|
+
|
|
43
|
+
loop do
|
|
44
|
+
case choice.downcase
|
|
45
|
+
when "factorybot", "factory_bot", "1"
|
|
46
|
+
context.answer(:factory_or_fixtures, :factory_bot)
|
|
47
|
+
break
|
|
48
|
+
when "fixtures", "2"
|
|
49
|
+
context.answer(:factory_or_fixtures, :fixtures)
|
|
50
|
+
break
|
|
51
|
+
else
|
|
52
|
+
shell.say("Invalid. Enter 'FactoryBot', 'Fixtures', 1, or 2.", :red)
|
|
53
|
+
choice = shell.ask("Factory strategy (default: FactoryBot):").to_s.strip
|
|
54
|
+
choice = "1" if choice.empty?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def generate(context)
|
|
60
|
+
unless if_condition(context)
|
|
61
|
+
context.set_output(:factory_or_fixtures, nil)
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
strategy = context.get(:factory_or_fixtures) || :factory_bot
|
|
66
|
+
install_factory_bot(context) if strategy == :factory_bot
|
|
67
|
+
|
|
68
|
+
context.set_output(:factory_or_fixtures, { factory_or_fixtures: strategy })
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def outputs(context)
|
|
72
|
+
context.output(:factory_or_fixtures)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def install_factory_bot(context)
|
|
78
|
+
add_test_group_gem(context, "gem \"factory_bot_rails\", \"#{FACTORY_BOT_VERSION}\"")
|
|
79
|
+
FileUtils.mkdir_p(File.join(context.app_path, "spec/factories"))
|
|
80
|
+
ensure_rails_helper(context)
|
|
81
|
+
include_factory_bot_syntax(context)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def add_test_group_gem(context, gem_line)
|
|
85
|
+
gemfile_path = File.join(context.app_path, "Gemfile")
|
|
86
|
+
return unless File.exist?(gemfile_path)
|
|
87
|
+
|
|
88
|
+
content = File.read(gemfile_path)
|
|
89
|
+
return if content.include?(gem_line)
|
|
90
|
+
|
|
91
|
+
lines = content.lines
|
|
92
|
+
group_start = lines.index { |line| line.strip == "group :test do" }
|
|
93
|
+
|
|
94
|
+
if group_start
|
|
95
|
+
end_index = (group_start + 1...lines.size).find { |i| lines[i].strip == "end" }
|
|
96
|
+
lines.insert(end_index, " #{gem_line}\n")
|
|
97
|
+
else
|
|
98
|
+
lines << "\n" unless lines.last.nil? || lines.last.end_with?("\n")
|
|
99
|
+
lines << "\ngroup :test do\n #{gem_line}\nend\n"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
File.write(gemfile_path, lines.join)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ensure_rails_helper(context)
|
|
106
|
+
rails_helper_path = File.join(context.app_path, "spec/rails_helper.rb")
|
|
107
|
+
return if File.exist?(rails_helper_path)
|
|
108
|
+
|
|
109
|
+
FileUtils.mkdir_p(File.dirname(rails_helper_path))
|
|
110
|
+
File.write(rails_helper_path, MINIMAL_RAILS_HELPER)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def include_factory_bot_syntax(context)
|
|
114
|
+
rails_helper_path = File.join(context.app_path, "spec/rails_helper.rb")
|
|
115
|
+
content = File.read(rails_helper_path)
|
|
116
|
+
return if content.include?("FactoryBot::Syntax::Methods")
|
|
117
|
+
|
|
118
|
+
lines = content.lines
|
|
119
|
+
end_index = lines.rindex { |line| line.strip == "end" }
|
|
120
|
+
return unless end_index
|
|
121
|
+
|
|
122
|
+
lines.insert(end_index, " config.include FactoryBot::Syntax::Methods\n")
|
|
123
|
+
File.write(rails_helper_path, lines.join)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class FakerStep < Step
|
|
7
|
+
def self.id = :faker
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = []
|
|
10
|
+
def self.summary = "Add Faker"
|
|
11
|
+
def self.description = "Adds the Faker gem for generating fake data."
|
|
12
|
+
def self.prompts = "None"
|
|
13
|
+
|
|
14
|
+
FAKER_VERSION = "3.8.0".freeze
|
|
15
|
+
|
|
16
|
+
def if_condition(_context) = true
|
|
17
|
+
|
|
18
|
+
def prompt(_context); end
|
|
19
|
+
|
|
20
|
+
def generate(context)
|
|
21
|
+
add_dev_test_group_gem(context, "gem \"faker\", \"#{FAKER_VERSION}\"")
|
|
22
|
+
|
|
23
|
+
context.set_output(:faker, { faker: true })
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def outputs(context)
|
|
27
|
+
context.output(:faker)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def add_dev_test_group_gem(context, gem_line)
|
|
33
|
+
gemfile_path = File.join(context.app_path, "Gemfile")
|
|
34
|
+
return unless File.exist?(gemfile_path)
|
|
35
|
+
|
|
36
|
+
content = File.read(gemfile_path)
|
|
37
|
+
return if content.include?(gem_line)
|
|
38
|
+
|
|
39
|
+
lines = content.lines
|
|
40
|
+
group_start = lines.index { |line| line.strip == "group :development, :test do" }
|
|
41
|
+
|
|
42
|
+
if group_start
|
|
43
|
+
end_index = (group_start + 1...lines.size).find { |i| lines[i].strip == "end" }
|
|
44
|
+
lines.insert(end_index, " #{gem_line}\n")
|
|
45
|
+
else
|
|
46
|
+
lines << "\n" unless lines.last.nil? || lines.last.end_with?("\n")
|
|
47
|
+
lines << "\ngroup :development, :test do\n #{gem_line}\nend\n"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
File.write(gemfile_path, lines.join)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|