Class: Railstart::Generator

Inherits:
Object
  • Object
show all
Defined in:
lib/railstart/generator.rb

Overview

Orchestrates the interactive Rails app generation flow.

Handles configuration loading, prompting, summary display, command execution, and optional post-generation actions while remaining easy to test.

Examples:

Run generator with provided config

config = Railstart::Config.load
Railstart::Generator.new("blog", config: config).run

Run generator non-interactively

Railstart::Generator.new("blog", use_defaults: true).run

Constant Summary collapse

APP_NAME_PATTERN =
/\A[a-z0-9_-]+\z/
APP_NAME_ERROR =
"Must be lowercase letters, numbers, underscores, or hyphens"

Instance Method Summary collapse

Constructor Details

#initialize(app_name = nil, config: nil, use_defaults: false, prompt: nil) ⇒ Generator

Returns a new instance of Generator.

Parameters:

  • app_name (String, nil) (defaults to: nil)

    preset app name, prompted if nil

  • config (Hash, nil) (defaults to: nil)

    injected config for testing, defaults to Config.load

  • use_defaults (Boolean) (defaults to: false)

    skip interactive questions, use config defaults

  • prompt (TTY::Prompt) (defaults to: nil)

    injectable prompt for testing



27
28
29
30
31
32
33
# File 'lib/railstart/generator.rb', line 27

def initialize(app_name = nil, config: nil, use_defaults: false, prompt: nil)
  @app_name = app_name
  @config = config || Config.load
  @use_defaults = use_defaults
  @prompt = prompt || TTY::Prompt.new
  @answers = {}
end

Instance Method Details

#ask_app_nameObject (private)



74
75
76
77
78
# File 'lib/railstart/generator.rb', line 74

def ask_app_name
  @app_name = @prompt.ask("App name?", default: "my_app") do |q|
    q.validate(APP_NAME_PATTERN, APP_NAME_ERROR)
  end
end

#ask_input(question) ⇒ Object (private)



162
163
164
# File 'lib/railstart/generator.rb', line 162

def ask_input(question)
  @prompt.ask(question["prompt"], default: question["default"])
end

#ask_interactive_questionsObject (private)



96
97
98
99
100
# File 'lib/railstart/generator.rb', line 96

def ask_interactive_questions
  Array(@config["questions"]).each do |question|
    handle_question(question)
  end
end

#ask_multi_select(question) ⇒ Object (private)



143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/railstart/generator.rb', line 143

def ask_multi_select(question)
  # Convert to hash format: { 'Display Name' => 'value' }
  choices = question["choices"].to_h { |choice| [choice["name"], choice["value"]] }

  # Transform value-based defaults to name-based defaults for TTY::Prompt
  # Config uses stable values (e.g., "action_mailer"), TTY::Prompt needs display names
  value_defaults = question["default"] || []
  name_defaults = value_defaults.map do |value|
    choice = question["choices"].find { |c| c["value"] == value }
    choice ? choice["name"] : nil
  end.compact

  @prompt.multi_select(question["prompt"], choices, default: name_defaults)
end

#ask_question(question) ⇒ Object (private)



119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/railstart/generator.rb', line 119

def ask_question(question)
  case question["type"]
  when "select"
    ask_select(question)
  when "multi_select"
    ask_multi_select(question)
  when "yes_no"
    ask_yes_no?(question)
  when "input"
    ask_input(question)
  end
end

#ask_select(question) ⇒ Object (private)



132
133
134
135
136
137
138
139
140
141
# File 'lib/railstart/generator.rb', line 132

def ask_select(question)
  # Convert to hash format: { 'Display Name' => 'value' }
  choices = question["choices"].to_h { |choice| [choice["name"], choice["value"]] }
  default_val = find_default(question)

  # TTY::Prompt expects 1-based index for default
  default_index = (question["choices"].index { |c| c["value"] == default_val }&.+(1) if default_val)

  @prompt.select(question["prompt"], choices, default: default_index)
end

#ask_yes_no?(question) ⇒ Boolean (private)

Returns:

  • (Boolean)


158
159
160
# File 'lib/railstart/generator.rb', line 158

def ask_yes_no?(question)
  @prompt.yes?(question["prompt"], default: question.fetch("default", false))
end

#collect_defaultsObject (private)



86
87
88
89
90
91
92
93
94
# File 'lib/railstart/generator.rb', line 86

def collect_defaults
  Array(@config["questions"]).each do |question|
    next if should_skip_question?(question)

    question_id = question["id"]
    default_value = find_default(question)
    @answers[question_id] = default_value unless default_value.nil?
  end
end

#confirm_action?(action) ⇒ Boolean (private)

Returns:

  • (Boolean)


265
266
267
268
269
# File 'lib/railstart/generator.rb', line 265

def confirm_action?(action)
  return true unless action["prompt"]

  @prompt.yes?(action["prompt"], default: action.fetch("default", true))
end

#confirm_proceed?Boolean (private)

Returns:

  • (Boolean)


214
215
216
# File 'lib/railstart/generator.rb', line 214

def confirm_proceed?
  @prompt.yes?("Proceed with app generation?")
end

#find_default(question) ⇒ Object (private)



166
167
168
169
170
171
# File 'lib/railstart/generator.rb', line 166

def find_default(question)
  # Support both default at question level and default: true on choice
  return question["default"] if question.key?("default")

  Array(question["choices"]).find { |c| c["default"] }&.[]("value")
end

#generate_appObject (private)

Raises:



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/railstart/generator.rb', line 218

def generate_app
  arguments = CommandBuilder.arguments(@app_name, @config, @answers)
  command = CommandBuilder.build(@app_name, @config, @answers)

  UI.info("Running: #{command}")
  puts

  # Run rails command outside of bundler context to use system Rails
  success = if defined?(Bundler)
              Bundler.with_unbundled_env { system(*arguments) }
            else
              system(*arguments)
            end

  return if success

  UI.error("Failed to generate Rails app. Check the output above for details.")
  raise Error, "Failed to generate Rails app. Check the output above for details."
end

#handle_question(question) ⇒ Object (private)



102
103
104
105
106
# File 'lib/railstart/generator.rb', line 102

def handle_question(question)
  return if should_skip_question?(question)

  @answers[question["id"]] = ask_question(question)
end

#process_post_action(action, template_runner) ⇒ Object (private)



254
255
256
257
258
259
260
261
262
263
# File 'lib/railstart/generator.rb', line 254

def process_post_action(action, template_runner)
  return unless should_run_action?(action)
  return unless confirm_action?(action)

  if template_action?(action)
    run_template_action(action, template_runner)
  else
    run_command_action(action)
  end
end

#runvoid

This method returns an undefined value.

Run the complete generation flow, prompting the user and invoking Rails.

Mode selection:

- use_defaults: false (default) → interactive wizard
- use_defaults: true → collect config defaults, show summary, confirm, run

Examples:

Run interactively

Railstart::Generator.new("blog").run

Run with defaults (noninteractive questions)

Railstart::Generator.new("blog", use_defaults: true).run

Raises:



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/railstart/generator.rb', line 48

def run
  show_welcome_screen unless @use_defaults

  ask_app_name unless @app_name
  validate_app_name!

  if @use_defaults
    collect_defaults
  else
    ask_interactive_questions
  end

  show_summary
  return unless confirm_proceed?

  generate_app
  run_post_actions
end

#run_command_action(action) ⇒ Object (private)



271
272
273
274
275
# File 'lib/railstart/generator.rb', line 271

def run_command_action(action)
  UI.info(action["name"].to_s)
  success = system(action["command"])
  UI.warning("Post-action '#{action["name"]}' failed. Continuing anyway.") unless success
end

#run_post_actionsObject (private)



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/railstart/generator.rb', line 238

def run_post_actions
  Dir.chdir(@app_name) do
    template_runner = nil

    Array(@config["post_actions"]).each do |action|
      template_runner ||= TemplateRunner.new(app_path: Dir.pwd) if template_action?(action)
      process_post_action(action, template_runner)
    end

    puts
    UI.success("Rails app created successfully at ./#{@app_name}")
  end
rescue Errno::ENOENT
  UI.warning("Could not change to app directory. Post-actions skipped.")
end

#run_template_action(action, template_runner) ⇒ Object (private)



277
278
279
280
281
282
283
284
285
286
# File 'lib/railstart/generator.rb', line 277

def run_template_action(action, template_runner)
  return unless template_runner

  UI.info(action["name"].to_s)
  source = action["source"]
  variables = template_variables(action)
  template_runner.apply(source, variables: variables)
rescue TemplateError => e
  UI.warning("Post-action '#{action["name"]}' failed. #{e.message}")
end

#should_run_action?(action) ⇒ Boolean (private)

Returns:

  • (Boolean)


298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/railstart/generator.rb', line 298

def should_run_action?(action)
  return false unless action.fetch("enabled", true)

  if_condition = action["if"]
  return true unless if_condition

  question_id = if_condition["question"]
  answer = @answers[question_id]

  if if_condition.key?("equals")
    answer == if_condition["equals"]
  elsif if_condition.key?("includes")
    expected = Array(if_condition["includes"])
    actual = Array(answer)
    expected.intersect?(actual)
  else
    true
  end
end

#should_skip_question?(question) ⇒ Boolean (private)

Returns:

  • (Boolean)


108
109
110
111
112
113
114
115
116
117
# File 'lib/railstart/generator.rb', line 108

def should_skip_question?(question)
  depends = question["depends_on"]
  return false unless depends

  dep_question_id = depends["question"]
  dep_value = depends["value"]

  actual_value = @answers[dep_question_id]
  actual_value != dep_value
end

#show_summaryObject (private)



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/railstart/generator.rb', line 173

def show_summary
  puts
  UI.section("Configuration Summary")
  puts

  summary_lines = ["App name: #{UI.pastel.cyan(@app_name)}"]

  Array(@config["questions"]).each do |question|
    question_id = question["id"]
    next unless @answers.key?(question_id)

    answer = @answers[question_id]
    label = question["prompt"].delete_suffix("?").delete_suffix(":").strip

    value_str = case answer
                when Array
                  answer.empty? ? "none" : answer.join(", ")
                when false
                  "No"
                when true
                  "Yes"
                else
                  answer.to_s
                end

    summary_lines << "#{label}: #{UI.pastel.green(value_str)}"
  end

  box = TTY::Box.frame(
    width: 60,
    padding: [0, 2],
    border: :light,
    style: {
      border: { fg: :bright_black }
    }
  ) { summary_lines.join("\n") }

  puts box
  puts
end

#show_welcome_screenObject (private)



69
70
71
72
# File 'lib/railstart/generator.rb', line 69

def show_welcome_screen
  UI.
  UI.show_welcome
end

#template_action?(action) ⇒ Boolean (private)

Returns:

  • (Boolean)


294
295
296
# File 'lib/railstart/generator.rb', line 294

def template_action?(action)
  action["type"].to_s == "template"
end

#template_variables(action) ⇒ Object (private)



288
289
290
291
292
# File 'lib/railstart/generator.rb', line 288

def template_variables(action)
  base = { app_name: @app_name, answers: @answers }
  extras = action["variables"].is_a?(Hash) ? action["variables"].transform_keys(&:to_sym) : {}
  base.merge(extras)
end

#validate_app_name!Object (private)

Raises:



80
81
82
83
84
# File 'lib/railstart/generator.rb', line 80

def validate_app_name!
  return if @app_name.to_s.match?(APP_NAME_PATTERN)

  raise Error, "Invalid app name '#{@app_name}': #{APP_NAME_ERROR}"
end