Class: Belt::CLI::GenerateCommand
- Inherits:
-
Object
- Object
- Belt::CLI::GenerateCommand
show all
- Includes:
- AppDetection
- Defined in:
- lib/belt/cli/generate_command.rb
Constant Summary
collapse
- TEMPLATE_DIR =
File.expand_path('../../templates/generate', __dir__)
- GENERATORS =
%w[scaffold resource model controller environment frontend views].freeze
- FIELD_TYPES =
%w[string text integer float boolean date datetime references].freeze
- GENERATOR_HELP =
{
'scaffold' => {
description: 'Generate a model, controller, routes, schema, and views for a REST resource.',
usage: 'belt generate scaffold <name> [field:type ...] [options]',
options: [
['--skip-views', 'Skip generating frontend view pages'],
['--force, -f', 'Overwrite existing resource files (skip collision check)']
],
examples: [
['belt g scaffold post title body:text'],
['belt g scaffold comment post:references body:text'],
['belt g scaffold task --skip-views'],
['belt g scaffold post title body:text --force']
],
notes: <<~NOTES
Creates:
lambda/models/<name>.rb Model with validations and fields
lambda/controllers/<app>/<names>_controller.rb RESTful controller (index, show, create, update, destroy)
config/routes.rb Route entry added
config/contracts.rb API response contract added
lambda/lib/routes/<app>_routes.rb Route manifest updated
infrastructure/modules/app/dynamodb.tf DynamoDB table generated
frontend/src/pages/<names>/ React pages (if frontend exists)
Nested Resources:
Use `<parent>:references` to create a nested resource. This will:
- Add `belongs_to :<parent>` in the child model
- Add `has_many :<children>` in the parent model (if it exists)
- Generate a GSI on `<parent>_id` for efficient queries
- Nest routes under the parent (e.g., /posts/{post_id}/comments)
- Scope the controller index action to the parent
Example: `belt g scaffold comment post:references body:text`
Alias: `belt g resource` works the same way.
NOTES
},
'model' => {
description: 'Generate an ActiveItem model with validations and DynamoDB field definitions.',
usage: 'belt generate model <name> [field:type ...] [options]',
options: [
['--force, -f', 'Overwrite existing model files (skip collision check)']
],
examples: [
['belt g model user email name'],
['belt g model event title starts_at:datetime']
],
notes: <<~NOTES
Creates:
lambda/models/<name>.rb Model class inheriting from ApplicationRecord
config/contracts.rb API response contract added
infrastructure/modules/app/dynamodb.tf DynamoDB table generated
NOTES
},
'controller' => {
description: 'Generate a controller with standard REST actions.',
usage: 'belt generate controller <name>',
options: [],
examples: [
['belt g controller posts'],
['belt g controller admin/users']
],
notes: <<~NOTES
Creates:
lambda/controllers/<app>/<name>_controller.rb Controller class
NOTES
}
}.freeze
- RESERVED_NAMES =
%w[
help new edit create update destroy index show delete remove
application base controller model resource generate
belt setup environment frontend views routes
].freeze
Class Method Summary
collapse
Instance Method Summary
collapse
#detect_app_name, #detect_environments, #detect_namespace, #find_contracts_file_path, #find_routes_file_path, #find_schema_file_path, #s3_safe_name
Constructor Details
#initialize(generator, name, fields, skip_views: false, force: false) ⇒ GenerateCommand
Returns a new instance of GenerateCommand.
267
268
269
270
271
272
273
274
275
276
277
278
279
|
# File 'lib/belt/cli/generate_command.rb', line 267
def initialize(generator, name, fields, skip_views: false, force: false)
@generator = generator
@name = name.downcase.gsub(/[^a-z0-9_]/, '_')
@fields = fields
@skip_views = skip_views
@force = force
@app_name = detect_namespace
@module_name = @app_name.split(/[-_]/).map(&:capitalize).join
@singular_name = Belt::Inflector.singularize(@name)
@resource_name = Belt::Inflector.pluralize(@singular_name)
@class_name = Belt::Inflector.classify(@singular_name)
@references, @regular_fields = @fields.partition { |f| f[:type] == 'references' }
end
|
Class Method Details
.all_generator_names ⇒ Object
.parse_field(arg) ⇒ Object
143
144
145
146
147
148
149
150
151
152
|
# File 'lib/belt/cli/generate_command.rb', line 143
def self.parse_field(arg)
name, type = arg.split(':', 2)
type ||= 'string'
if %w[references belongs_to].include?(type)
{ name: name, type: 'references', referenced_model: name }
else
{ name: name, type: type }
end
end
|
.print_generate_help ⇒ Object
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
|
# File 'lib/belt/cli/generate_command.rb', line 184
def self.print_generate_help
gem_generators = GeneratorRegistry.discovered_generators
puts <<~HELP
Usage: belt generate <generator> <name> [field:type ...] [options]
belt g <generator> <name> [field:type ...] [options]
Generators:
scaffold Generate model, controller, routes, schema, and views (full REST resource)
model Generate an ActiveItem model
controller Generate a controller
environment Create a new deployment environment
frontend Scaffold a frontend app (react, vue, svelte)
views Generate React pages for a resource
Aliases:
resource Same as scaffold
HELP
if gem_generators.any?
puts ' Gem Generators:'
gem_generators.each do |name, klass|
desc = klass.respond_to?(:description) ? klass.description : "Generate #{name} (from gem)"
puts format(' %<name>-14s %<desc>s', name: name, desc: desc)
end
puts
end
puts <<~HELP
Field Types:
#{FIELD_TYPES.join(', ')}
(defaults to string if omitted)
Examples:
belt g scaffold post title body:text status
belt g model user email name
belt g controller comments
belt g environment staging
belt g frontend react
belt g views post title body:text
Run 'belt generate <generator> --help' for detailed help on a specific generator.
HELP
end
|
.print_generator_help(generator) ⇒ Object
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
|
# File 'lib/belt/cli/generate_command.rb', line 230
def self.print_generator_help(generator)
info = GENERATOR_HELP[generator]
unless info
puts "Usage: belt generate #{generator} <name>"
puts "\nRun 'belt generate #{generator} --help' is not yet documented."
puts "Try 'belt generate --help' for the full list of generators."
return
end
puts info[:description]
puts
puts "Usage: #{info[:usage]}"
if info[:options].any?
puts "\nOptions:"
info[:options].each do |flag, desc|
puts format(' %<flag>-20s %<desc>s', flag: flag, desc: desc)
end
end
puts "\nField Types:"
puts " #{FIELD_TYPES.join(', ')}"
puts ' (defaults to string if omitted)'
puts "\nExamples:"
info[:examples].each do |cmd, desc|
if desc
puts " #{cmd} ā #{desc}"
else
puts " #{cmd}"
end
end
puts "\n#{info[:notes]}" if info[:notes]
end
|
.run(args) ⇒ Object
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
# File 'lib/belt/cli/generate_command.rb', line 92
def self.run(args)
generator = args.shift
if generator.nil? || generator =~ /\A-/
print_generate_help
exit 0
end
unless GENERATORS.include?(generator) || GeneratorRegistry.generator_names.include?(generator)
puts "Unknown generator: '#{generator}'"
puts "Available generators: #{all_generator_names.join(', ')}"
puts "\nRun 'belt generate --help' for usage information."
exit 1
end
unless GENERATORS.include?(generator)
klass = GeneratorRegistry.find(generator)
return klass.run(args)
end
generator = 'scaffold' if generator == 'resource'
if args.include?('--help') || args.include?('-h')
print_generator_help(generator)
exit 0
end
return Belt::CLI::EnvironmentCommand.run(args) if generator == 'environment'
return Belt::CLI::FrontendCommand.run(args) if generator == 'frontend'
return Belt::CLI::ViewsCommand.run(args) if generator == 'views'
name = args.shift
if name.nil? || name.empty?
print_generator_help(generator)
exit 0
end
validate_resource_name!(name, generator)
force = args.delete('--force') || args.delete('-f')
skip_views = args.delete('--skip-views')
fields = args.map { |arg| parse_field(arg) }
new(generator, name, fields, skip_views: skip_views, force: force).generate
end
|
.validate_resource_name!(name, generator) ⇒ Object
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
# File 'lib/belt/cli/generate_command.rb', line 164
def self.validate_resource_name!(name, generator)
errors = []
errors << 'must start with a letter' unless name.match?(/\A[a-zA-Z]/)
errors << 'must be at least 2 characters' if name.length < 2
errors << 'must only contain letters, numbers, and underscores' unless name.match?(/\A[a-zA-Z][a-zA-Z0-9_]*\z/)
errors << 'must not start or end with an underscore' if name.match?(/\A_|_\z/)
errors << 'must not contain consecutive underscores' if name.include?('__')
errors << 'is a reserved name' if RESERVED_NAMES.include?(Belt::Inflector.singularize(name.downcase))
errors << 'is a reserved name' if RESERVED_NAMES.include?(name.downcase)
return if errors.empty?
puts "\nā Invalid #{generator} name '#{name}':"
errors.uniq.each { |e| puts " - #{e}" }
puts "\nName must start with a letter, be at least 2 characters, " \
'and contain only letters, numbers, and single underscores.'
exit 1
end
|
Instance Method Details
#generate ⇒ Object
281
282
283
284
285
286
287
288
289
290
291
|
# File 'lib/belt/cli/generate_command.rb', line 281
def generate
case @generator
when 'scaffold'
check_resource_collision! unless @force
generate_resource
when 'model'
check_model_collision! unless @force
generate_model_standalone
when 'controller' then generate_controller
end
end
|