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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +59 -0
  5. data/exe/opinionated_rails +7 -0
  6. data/lib/opinionated_rails/answer_shell.rb +131 -0
  7. data/lib/opinionated_rails/cli.rb +291 -0
  8. data/lib/opinionated_rails/context.rb +39 -0
  9. data/lib/opinionated_rails/error.rb +6 -0
  10. data/lib/opinionated_rails/generators/file_writer.rb +14 -0
  11. data/lib/opinionated_rails/generators/rails_new_invoker.rb +26 -0
  12. data/lib/opinionated_rails/kamal_config.rb +94 -0
  13. data/lib/opinionated_rails/manifest.rb +42 -0
  14. data/lib/opinionated_rails/pins.rb +83 -0
  15. data/lib/opinionated_rails/prerequisites.rb +39 -0
  16. data/lib/opinionated_rails/rails_version_guard.rb +49 -0
  17. data/lib/opinionated_rails/step.rb +41 -0
  18. data/lib/opinionated_rails/step_graph.rb +53 -0
  19. data/lib/opinionated_rails/steps/addons/auth_step.rb +170 -0
  20. data/lib/opinionated_rails/steps/addons/ci_step.rb +226 -0
  21. data/lib/opinionated_rails/steps/addons/docker_kamal_step.rb +133 -0
  22. data/lib/opinionated_rails/steps/addons/docs_step.rb +35 -0
  23. data/lib/opinionated_rails/steps/addons/factory_or_fixtures_step.rb +128 -0
  24. data/lib/opinionated_rails/steps/addons/faker_step.rb +55 -0
  25. data/lib/opinionated_rails/steps/addons/js_lint_step.rb +132 -0
  26. data/lib/opinionated_rails/steps/addons/mailer_step.rb +183 -0
  27. data/lib/opinionated_rails/steps/addons/pagination_step.rb +116 -0
  28. data/lib/opinionated_rails/steps/addons/pr_issue_templates_step.rb +43 -0
  29. data/lib/opinionated_rails/steps/addons/rubocop_step.rb +111 -0
  30. data/lib/opinionated_rails/steps/addons/secrets_handling_step.rb +138 -0
  31. data/lib/opinionated_rails/steps/addons/services_step.rb +64 -0
  32. data/lib/opinionated_rails/steps/addons/solid_stack_step.rb +252 -0
  33. data/lib/opinionated_rails/steps/addons/spa_controller_step.rb +87 -0
  34. data/lib/opinionated_rails/steps/addons/staging_env_step.rb +108 -0
  35. data/lib/opinionated_rails/steps/addons/tailwind_step.rb +148 -0
  36. data/lib/opinionated_rails/steps/addons/vcr_step.rb +170 -0
  37. data/lib/opinionated_rails/steps/core/app_name_step.rb +85 -0
  38. data/lib/opinionated_rails/steps/core/app_type_step.rb +51 -0
  39. data/lib/opinionated_rails/steps/core/database_step.rb +53 -0
  40. data/lib/opinionated_rails/steps/core/frontend_step.rb +282 -0
  41. data/lib/opinionated_rails/steps/core/testing_step.rb +82 -0
  42. data/lib/opinionated_rails/templates/docs/STYLE.md +39 -0
  43. data/lib/opinionated_rails/templates/github/ISSUE_TEMPLATE/bug_report.md +25 -0
  44. data/lib/opinionated_rails/templates/github/ISSUE_TEMPLATE/feature_request.md +17 -0
  45. data/lib/opinionated_rails/templates/github/PULL_REQUEST_TEMPLATE.md +9 -0
  46. data/lib/opinionated_rails/templates/rubocop/custom_overrides.yml +5 -0
  47. data/lib/opinionated_rails/templates/services/application_service.rb +50 -0
  48. data/lib/opinionated_rails/templates/services/result.rb +44 -0
  49. data/lib/opinionated_rails/version.rb +5 -0
  50. data/lib/opinionated_rails.rb +41 -0
  51. 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