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,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class TailwindStep < Step
|
|
7
|
+
def self.id = :tailwind
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = [ :frontend ]
|
|
10
|
+
def self.summary = "Configure Tailwind CSS"
|
|
11
|
+
def self.description = "Installs Tailwind v3 with a path appropriate for the chosen frontend."
|
|
12
|
+
def self.prompts = "None"
|
|
13
|
+
|
|
14
|
+
TAILWIND_VERSION = "3.4.1".freeze
|
|
15
|
+
|
|
16
|
+
# The standalone CLI only auto-detects ./tailwind.config.js, but the config lives in config/.
|
|
17
|
+
REACT_CLI = "npx tailwindcss -c ./config/tailwind.config.js " \
|
|
18
|
+
"-i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/tailwind.css".freeze
|
|
19
|
+
TAILWIND_RAILS_VERSION = "3.3.0".freeze
|
|
20
|
+
|
|
21
|
+
DIRECTIVES = <<~CSS
|
|
22
|
+
@tailwind base;
|
|
23
|
+
@tailwind components;
|
|
24
|
+
@tailwind utilities;
|
|
25
|
+
CSS
|
|
26
|
+
|
|
27
|
+
def if_condition(context)
|
|
28
|
+
context.output(:frontend, :frontend) != :none
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def prompt(_context); end
|
|
32
|
+
|
|
33
|
+
def generate(context)
|
|
34
|
+
unless if_condition(context)
|
|
35
|
+
context.set_output(:tailwind, nil)
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if context.output(:frontend, :frontend) == :react
|
|
40
|
+
configure_for_react(context)
|
|
41
|
+
else
|
|
42
|
+
configure_for_stimulus(context)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
write_stylesheet(context)
|
|
46
|
+
update_layout(context)
|
|
47
|
+
|
|
48
|
+
context.set_output(:tailwind, { tailwind: true })
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def outputs(context)
|
|
52
|
+
context.output(:tailwind)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def configure_for_stimulus(context)
|
|
58
|
+
gemfile = File.join(context.app_path, "Gemfile")
|
|
59
|
+
if File.exist?(gemfile) && !File.read(gemfile).include?("tailwindcss-rails")
|
|
60
|
+
File.open(gemfile, "a") { |f| f.puts("gem \"tailwindcss-rails\", \"#{TAILWIND_RAILS_VERSION}\"") }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
write_tailwind_config(context, stimulus_content)
|
|
64
|
+
append_procfile(context, "css: bin/rails tailwindcss:watch")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def configure_for_react(context)
|
|
68
|
+
package_json_path = File.join(context.app_path, "package.json")
|
|
69
|
+
package = File.exist?(package_json_path) ? JSON.parse(File.read(package_json_path)) : {}
|
|
70
|
+
|
|
71
|
+
package["devDependencies"] ||= {}
|
|
72
|
+
package["devDependencies"]["tailwindcss"] = TAILWIND_VERSION
|
|
73
|
+
|
|
74
|
+
package["scripts"] ||= {}
|
|
75
|
+
package["scripts"]["build:css"] = "#{REACT_CLI} --minify"
|
|
76
|
+
|
|
77
|
+
File.write(package_json_path, JSON.pretty_generate(package))
|
|
78
|
+
|
|
79
|
+
write_tailwind_config(context, react_content)
|
|
80
|
+
append_procfile(context, "css: #{REACT_CLI} --watch")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def write_stylesheet(context)
|
|
84
|
+
css_path = File.join(context.app_path, "app/assets/stylesheets/application.tailwind.css")
|
|
85
|
+
FileUtils.mkdir_p(File.dirname(css_path))
|
|
86
|
+
File.write(css_path, DIRECTIVES)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def write_tailwind_config(context, content)
|
|
90
|
+
config_path = File.join(context.app_path, "config/tailwind.config.js")
|
|
91
|
+
FileUtils.mkdir_p(File.dirname(config_path))
|
|
92
|
+
File.write(config_path, content)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def stimulus_content
|
|
96
|
+
<<~JS
|
|
97
|
+
module.exports = {
|
|
98
|
+
content: [
|
|
99
|
+
"./app/views/**/*.html.erb",
|
|
100
|
+
"./app/helpers/**/*.rb",
|
|
101
|
+
"./app/javascript/**/*.js",
|
|
102
|
+
"./app/assets/stylesheets/**/*.css"
|
|
103
|
+
],
|
|
104
|
+
theme: {},
|
|
105
|
+
plugins: []
|
|
106
|
+
}
|
|
107
|
+
JS
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def react_content
|
|
111
|
+
<<~JS
|
|
112
|
+
module.exports = {
|
|
113
|
+
content: ["./app/views/**/*.html.erb", "./app/javascript/**/*.{js,jsx}"],
|
|
114
|
+
theme: {},
|
|
115
|
+
plugins: []
|
|
116
|
+
}
|
|
117
|
+
JS
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def append_procfile(context, line)
|
|
121
|
+
procfile = File.join(context.app_path, "Procfile.dev")
|
|
122
|
+
|
|
123
|
+
if File.exist?(procfile)
|
|
124
|
+
content = File.read(procfile)
|
|
125
|
+
return if content.include?(line)
|
|
126
|
+
|
|
127
|
+
File.open(procfile, "a") { |f| f.puts(line) }
|
|
128
|
+
else
|
|
129
|
+
File.write(procfile, "web: bin/rails server\n#{line}\n")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def update_layout(context)
|
|
134
|
+
layout = File.join(context.app_path, "app/views/layouts/application.html.erb")
|
|
135
|
+
return unless File.exist?(layout)
|
|
136
|
+
|
|
137
|
+
content = File.read(layout)
|
|
138
|
+
return if content.include?('stylesheet_link_tag "tailwind"')
|
|
139
|
+
|
|
140
|
+
tag = ' <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>'
|
|
141
|
+
content = content.sub(/\s*<\/head>/, "\n#{tag}\n </head>")
|
|
142
|
+
|
|
143
|
+
File.write(layout, content)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class VcrStep < Step
|
|
7
|
+
def self.id = :vcr
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = [ :testing ]
|
|
10
|
+
def self.summary = "Configure VCR"
|
|
11
|
+
def self.description = "Installs VCR for recording and replaying HTTP interactions in tests."
|
|
12
|
+
def self.prompts = "Enable VCR for HTTP interaction recording? (default: no)"
|
|
13
|
+
|
|
14
|
+
VCR_VERSION = "6.2.0".freeze
|
|
15
|
+
WEBMOCK_VERSION = "3.23.1".freeze
|
|
16
|
+
|
|
17
|
+
MINIMAL_RAILS_HELPER = <<~RUBY
|
|
18
|
+
require 'spec_helper'
|
|
19
|
+
ENV['RAILS_ENV'] ||= 'test'
|
|
20
|
+
require_relative '../config/environment'
|
|
21
|
+
require 'rspec/rails'
|
|
22
|
+
|
|
23
|
+
RSpec.configure do |config|
|
|
24
|
+
config.fixture_paths = [
|
|
25
|
+
Rails.root.join('spec/fixtures')
|
|
26
|
+
]
|
|
27
|
+
config.use_transactional_fixtures = true
|
|
28
|
+
end
|
|
29
|
+
RUBY
|
|
30
|
+
|
|
31
|
+
def if_condition(_context) = true
|
|
32
|
+
|
|
33
|
+
def prompt(context)
|
|
34
|
+
shell = context.shell
|
|
35
|
+
choice = shell.ask("Enable VCR for HTTP interaction recording? (default: no):").to_s.strip
|
|
36
|
+
choice = "no" if choice.empty?
|
|
37
|
+
|
|
38
|
+
loop do
|
|
39
|
+
case choice.downcase
|
|
40
|
+
when "yes", "y"
|
|
41
|
+
context.answer(:vcr, true)
|
|
42
|
+
break
|
|
43
|
+
when "no", "n"
|
|
44
|
+
context.answer(:vcr, false)
|
|
45
|
+
break
|
|
46
|
+
else
|
|
47
|
+
shell.say("Invalid. Enter 'yes' or 'no'.", :red)
|
|
48
|
+
choice = shell.ask("Enable VCR for HTTP interaction recording? (default: no):").to_s.strip
|
|
49
|
+
choice = "no" if choice.empty?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def generate(context)
|
|
55
|
+
enabled = context.get(:vcr) == true
|
|
56
|
+
|
|
57
|
+
unless enabled
|
|
58
|
+
context.set_output(:vcr, { vcr: false })
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
add_test_group_gems(context)
|
|
63
|
+
|
|
64
|
+
if context.output(:testing, :test_framework) == :minitest
|
|
65
|
+
install_for_minitest(context)
|
|
66
|
+
else
|
|
67
|
+
install_for_rspec(context)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
context.set_output(:vcr, { vcr: true })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def outputs(context)
|
|
74
|
+
context.output(:vcr)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def add_test_group_gems(context)
|
|
80
|
+
add_test_group_gem(context, "gem \"vcr\", \"#{VCR_VERSION}\"")
|
|
81
|
+
add_test_group_gem(context, "gem \"webmock\", \"#{WEBMOCK_VERSION}\"")
|
|
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 vcr_config(cassette_dir)
|
|
106
|
+
<<~RUBY
|
|
107
|
+
require "vcr"
|
|
108
|
+
require "webmock"
|
|
109
|
+
|
|
110
|
+
VCR.configure do |config|
|
|
111
|
+
config.cassette_library_dir = "#{cassette_dir}"
|
|
112
|
+
config.hook_into :webmock
|
|
113
|
+
|
|
114
|
+
# Redact secrets from recorded cassettes before they're written to disk.
|
|
115
|
+
# Example: strip a bearer token from the Authorization header.
|
|
116
|
+
config.filter_sensitive_data("<API_KEY>") { ENV["API_KEY"] }
|
|
117
|
+
end
|
|
118
|
+
RUBY
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def install_for_rspec(context)
|
|
122
|
+
support_path = File.join(context.app_path, "spec/support/vcr.rb")
|
|
123
|
+
FileUtils.mkdir_p(File.dirname(support_path))
|
|
124
|
+
File.write(support_path, vcr_config("spec/cassettes"))
|
|
125
|
+
|
|
126
|
+
ensure_rails_helper(context)
|
|
127
|
+
require_support_files(context)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def install_for_minitest(context)
|
|
131
|
+
support_path = File.join(context.app_path, "test/support/vcr.rb")
|
|
132
|
+
FileUtils.mkdir_p(File.dirname(support_path))
|
|
133
|
+
File.write(support_path, vcr_config("test/cassettes"))
|
|
134
|
+
|
|
135
|
+
require_line = "require_relative \"support/vcr\"\n"
|
|
136
|
+
test_helper_path = File.join(context.app_path, "test/test_helper.rb")
|
|
137
|
+
return unless File.exist?(test_helper_path)
|
|
138
|
+
|
|
139
|
+
content = File.read(test_helper_path)
|
|
140
|
+
return if content.include?(require_line)
|
|
141
|
+
|
|
142
|
+
File.write(test_helper_path, "#{require_line}#{content}")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ensure_rails_helper(context)
|
|
146
|
+
rails_helper_path = File.join(context.app_path, "spec/rails_helper.rb")
|
|
147
|
+
return if File.exist?(rails_helper_path)
|
|
148
|
+
|
|
149
|
+
FileUtils.mkdir_p(File.dirname(rails_helper_path))
|
|
150
|
+
File.write(rails_helper_path, MINIMAL_RAILS_HELPER)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def require_support_files(context)
|
|
154
|
+
rails_helper_path = File.join(context.app_path, "spec/rails_helper.rb")
|
|
155
|
+
require_line = "require \"support/vcr\"\n"
|
|
156
|
+
|
|
157
|
+
content = File.read(rails_helper_path)
|
|
158
|
+
return if content.include?(require_line) || content.include?("support/**/*.rb")
|
|
159
|
+
|
|
160
|
+
lines = content.lines
|
|
161
|
+
require_index = lines.rindex { |line| line.start_with?("require") }
|
|
162
|
+
insert_at = require_index ? require_index + 1 : lines.size
|
|
163
|
+
|
|
164
|
+
lines.insert(insert_at, require_line)
|
|
165
|
+
File.write(rails_helper_path, lines.join)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Core
|
|
6
|
+
class AppNameStep < Step
|
|
7
|
+
def self.id = :app_name
|
|
8
|
+
def self.summary = "Set the application name"
|
|
9
|
+
def self.description = "Records the Rails application name used for the generated app."
|
|
10
|
+
def self.tier = :core
|
|
11
|
+
def self.prompts = "App name (free text, valid Ruby constant once camelized, no leading digit)"
|
|
12
|
+
|
|
13
|
+
RESERVED_NAMES = %w[ test application ].freeze
|
|
14
|
+
|
|
15
|
+
def prompt(context)
|
|
16
|
+
return if preset_name?(context)
|
|
17
|
+
|
|
18
|
+
loop do
|
|
19
|
+
raw = context.shell.ask("App name:")
|
|
20
|
+
raise OpinionatedRails::Error, "No app name given (input ended)" if raw.nil?
|
|
21
|
+
|
|
22
|
+
normalized = normalize(raw)
|
|
23
|
+
errors = validate(normalized)
|
|
24
|
+
|
|
25
|
+
if errors.empty?
|
|
26
|
+
context.answer(:app_name, normalized)
|
|
27
|
+
break
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context.shell.say("Invalid: #{errors.join(', ')}", :red)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# `opinionated_rails new my_app` supplies the name up front; fall back to asking if it's invalid.
|
|
35
|
+
def preset_name?(context)
|
|
36
|
+
preset = context.get(:app_name_input)
|
|
37
|
+
return false if preset.to_s.strip.empty?
|
|
38
|
+
|
|
39
|
+
normalized = normalize(preset)
|
|
40
|
+
errors = validate(normalized)
|
|
41
|
+
if errors.empty?
|
|
42
|
+
context.answer(:app_name, normalized)
|
|
43
|
+
return true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
context.shell.say("Invalid app name #{preset.inspect}: #{errors.join(', ')}", :red)
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def generate(context)
|
|
51
|
+
name = context.get(:app_name)
|
|
52
|
+
errors = validate(name)
|
|
53
|
+
|
|
54
|
+
raise ArgumentError, "Invalid app name: #{errors.join(', ')}" unless errors.empty?
|
|
55
|
+
|
|
56
|
+
context.set_output(:app_name, { app_name: name })
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def outputs(context)
|
|
60
|
+
context.output(:app_name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def normalize(raw)
|
|
66
|
+
raw.to_s.strip.downcase.gsub(/[-\s]+/, "_").gsub(/[^a-z0-9_]/, "")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate(name)
|
|
70
|
+
errors = []
|
|
71
|
+
errors << "cannot be blank" if name.empty?
|
|
72
|
+
errors << "must start with a letter" if name.match?(/\A\d/)
|
|
73
|
+
errors << "must contain only letters, digits, underscores and hyphens" unless name.match?(/\A[a-z0-9_-]*\z/)
|
|
74
|
+
errors << "is a reserved name" if RESERVED_NAMES.include?(name)
|
|
75
|
+
errors << "collides with an existing constant" if name.match?(/\A[a-z]/) && Object.const_defined?(camelize(name))
|
|
76
|
+
errors
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def camelize(name)
|
|
80
|
+
name.split("_").map(&:capitalize).join
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Core
|
|
6
|
+
class AppTypeStep < Step
|
|
7
|
+
def self.id = :app_type
|
|
8
|
+
def self.summary = "Choose API-only or full-stack"
|
|
9
|
+
def self.description = "Decides whether the app is API-only or a full-stack Rails app."
|
|
10
|
+
def self.tier = :core
|
|
11
|
+
def self.prompts = "App type: API-only or Full-stack (default: Full-stack)"
|
|
12
|
+
|
|
13
|
+
def prompt(context)
|
|
14
|
+
shell = context.shell
|
|
15
|
+
shell.say("Choose an app type:")
|
|
16
|
+
shell.say(" 1. Full-stack")
|
|
17
|
+
shell.say(" 2. API-only")
|
|
18
|
+
|
|
19
|
+
choice = shell.ask("App type (default: Full-stack):").to_s.strip
|
|
20
|
+
choice = "full-stack" if choice.empty?
|
|
21
|
+
|
|
22
|
+
loop do
|
|
23
|
+
case choice.downcase
|
|
24
|
+
when "full-stack", "1"
|
|
25
|
+
context.answer(:app_type, :full_stack)
|
|
26
|
+
break
|
|
27
|
+
when "api-only", "2"
|
|
28
|
+
context.answer(:app_type, :api_only)
|
|
29
|
+
break
|
|
30
|
+
else
|
|
31
|
+
shell.say("Invalid. Enter 'Full-stack', 'API-only', 1, or 2.", :red)
|
|
32
|
+
choice = shell.ask("App type (default: Full-stack):").to_s.strip
|
|
33
|
+
choice = "full-stack" if choice.empty?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def generate(context)
|
|
39
|
+
type = context.get(:app_type)
|
|
40
|
+
raise ArgumentError, "No app type answer" unless type
|
|
41
|
+
|
|
42
|
+
context.set_output(:app_type, { app_type: type })
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def outputs(context)
|
|
46
|
+
context.output(:app_type)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Core
|
|
6
|
+
class DatabaseStep < Step
|
|
7
|
+
def self.id = :database
|
|
8
|
+
def self.summary = "Choose the database"
|
|
9
|
+
def self.description = "Prompts for the database adapter (SQLite, PostgreSQL, MySQL)."
|
|
10
|
+
def self.tier = :core
|
|
11
|
+
def self.prompts = "Database adapter: SQLite, PostgreSQL, or MySQL (default: SQLite)"
|
|
12
|
+
|
|
13
|
+
ADAPTERS = {
|
|
14
|
+
"sqlite" => :sqlite3,
|
|
15
|
+
"postgresql" => :postgresql,
|
|
16
|
+
"mysql" => :mysql2
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
DISPLAY_NAMES = %w[ SQLite PostgreSQL MySQL ].freeze
|
|
20
|
+
|
|
21
|
+
def prompt(context)
|
|
22
|
+
shell = context.shell
|
|
23
|
+
shell.say("Choose a database:")
|
|
24
|
+
DISPLAY_NAMES.each_with_index { |name, index| shell.say(" #{index + 1}. #{name}") }
|
|
25
|
+
|
|
26
|
+
loop do
|
|
27
|
+
choice = shell.ask("Database (default: SQLite):").to_s.strip.downcase
|
|
28
|
+
choice = "sqlite" if choice.empty?
|
|
29
|
+
choice = ADAPTERS.keys[choice.to_i - 1] if %w[1 2 3].include?(choice)
|
|
30
|
+
|
|
31
|
+
if ADAPTERS.key?(choice)
|
|
32
|
+
context.answer(:database, ADAPTERS[choice])
|
|
33
|
+
break
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
shell.say("Invalid choice. Enter #{DISPLAY_NAMES.join(', ')}.", :red)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def generate(context)
|
|
41
|
+
adapter = context.get(:database)
|
|
42
|
+
raise ArgumentError, "No database answer" unless adapter
|
|
43
|
+
|
|
44
|
+
context.set_output(:database, { database: adapter })
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def outputs(context)
|
|
48
|
+
context.output(:database)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|