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

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



24
25
26
27
28
29
30
# File 'lib/railstart/generator.rb', line 24

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)



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

def ask_app_name
  @app_name = @prompt.ask("App name?", default: "my_app") do |q|
    q.validate(/\A[a-z0-9_-]+\z/, "Must be lowercase letters, numbers, underscores, or hyphens")
  end
end

#ask_input(question) ⇒ Object (private)



156
157
158
# File 'lib/railstart/generator.rb', line 156

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

#ask_interactive_questionsObject (private)



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

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

#ask_multi_select(question) ⇒ Object (private)



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/railstart/generator.rb', line 135

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

  # 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)



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

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)



122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/railstart/generator.rb', line 122

def ask_select(question)
  # Convert to hash format: { 'Display Name' => 'value' }
  choices = question["choices"].each_with_object({}) do |choice, hash|
    hash[choice["name"]] = choice["value"]
  end
  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)


152
153
154
# File 'lib/railstart/generator.rb', line 152

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

#collect_defaultsObject (private)



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

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)


258
259
260
261
262
# File 'lib/railstart/generator.rb', line 258

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)


208
209
210
# File 'lib/railstart/generator.rb', line 208

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

#find_default(question) ⇒ Object (private)



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

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:



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/railstart/generator.rb', line 212

def generate_app
  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(command) }
            else
              system(command)
            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)



92
93
94
95
96
# File 'lib/railstart/generator.rb', line 92

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)



247
248
249
250
251
252
253
254
255
256
# File 'lib/railstart/generator.rb', line 247

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:



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/railstart/generator.rb', line 45

def run
  show_welcome_screen unless @use_defaults

  ask_app_name unless @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)



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

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)



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/railstart/generator.rb', line 231

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)



270
271
272
273
274
275
276
277
278
279
# File 'lib/railstart/generator.rb', line 270

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)


291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/railstart/generator.rb', line 291

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)


98
99
100
101
102
103
104
105
106
107
# File 'lib/railstart/generator.rb', line 98

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)



167
168
169
170
171
172
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
# File 'lib/railstart/generator.rb', line 167

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)



65
66
67
68
# File 'lib/railstart/generator.rb', line 65

def show_welcome_screen
  UI.
  UI.show_welcome
end

#template_action?(action) ⇒ Boolean (private)

Returns:

  • (Boolean)


287
288
289
# File 'lib/railstart/generator.rb', line 287

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

#template_variables(action) ⇒ Object (private)



281
282
283
284
285
# File 'lib/railstart/generator.rb', line 281

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