belt 0.2.11 → 0.2.12
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 +4 -4
- data/CHANGELOG.md +31 -0
- data/lib/belt/cli/app_detection.rb +19 -5
- data/lib/belt/cli/contracts_command.rb +142 -0
- data/lib/belt/cli/destroy_command.rb +74 -0
- data/lib/belt/cli/generate_command.rb +5 -5
- data/lib/belt/cli/new_command.rb +2 -2
- data/lib/belt/cli/routes_command/schema_loader.rb +17 -7
- data/lib/belt/cli/routes_command.rb +7 -3
- data/lib/belt/cli/setup_command.rb +1 -1
- data/lib/belt/cli/views_command.rb +7 -2
- data/lib/belt/cli.rb +3 -0
- data/lib/belt/root.rb +12 -3
- data/lib/belt/route_dsl.rb +2 -2
- data/lib/belt/version.rb +1 -1
- data/lib/templates/frontend_infra/frontend.tf.erb +2 -1
- data/lib/templates/module/frontend.tf.erb +2 -1
- data/lib/templates/module/main.tf.erb +1 -1
- data/lib/templates/new_app/AGENTS.md.erb +4 -4
- data/lib/templates/new_app/README.md.erb +2 -2
- data/lib/templates/plugin/AGENTS.md.erb +1 -1
- data/lib/templates/plugin/README.md.erb +1 -1
- metadata +4 -3
- /data/lib/templates/new_app/config/{schema.tf.rb.erb → contracts.rb.erb} +0 -0
- /data/lib/templates/new_app/config/{routes.tf.rb.erb → routes.rb.erb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 10e92ad2ab3fe79f48f07cca6495ce8b9ccdad09755c7a6517e651deadcc118d
|
|
4
|
+
data.tar.gz: 6cd8a8ee7ad1023fb949183122c3aca465c216cf91d175e8f9395e8f413ad880
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb7d5dacc2b90e0ae7c4dd9853d76ea42ef8d6e324d30edd91c5fbe6d934affafd2c3bee6fb752960d9c8462a59663244ae448797ab7e57de7ad05c50afa627f
|
|
7
|
+
data.tar.gz: fd7f4ff9af68c36b34c02c2f4af2c42649f0e9494ac192eaaefeae3111e0da81ced4091b88510b82ac183fefaadfd0677e4d34f270e16a7dcf389b54259e8c52
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.12
|
|
4
|
+
|
|
5
|
+
### Rename: `routes.tf.rb` → `routes.rb`, `schema.tf.rb` → `contracts.rb`
|
|
6
|
+
|
|
7
|
+
The `.tf.rb` extension was a vestige of when these files produced HCL output — that hasn't been the case for a while. New apps now get clean, Rails-familiar names:
|
|
8
|
+
|
|
9
|
+
- `config/routes.rb` — API route definitions
|
|
10
|
+
- `config/contracts.rb` — API request/response contracts
|
|
11
|
+
|
|
12
|
+
**Backward compatible:** All detection helpers (`Belt.root`, `Belt.routes_file`, `Belt.contracts_file`, `find_routes_file_path`, `find_contracts_file_path`) check new names first, then fall back to `*.tf.rb` and `infrastructure/` paths. Existing apps continue working without changes.
|
|
13
|
+
|
|
14
|
+
### New command: `belt contracts`
|
|
15
|
+
|
|
16
|
+
Dedicated CLI command for inspecting API contracts, separate from `belt routes`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
belt contracts # Human-readable table of request/response models
|
|
20
|
+
belt contracts -f json # JSON output (for tooling/CI)
|
|
21
|
+
belt contracts -g post # Filter by pattern
|
|
22
|
+
belt contracts --file path.rb # Explicit file override
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Previously, contracts were only accessible as a side-effect of `belt routes -f json --schema <file>`. Now they have their own first-class command. `belt routes -f json` continues to include models for backward compatibility.
|
|
26
|
+
|
|
27
|
+
### Other changes
|
|
28
|
+
|
|
29
|
+
- `Belt.schema_file` is now an alias for `Belt.contracts_file`
|
|
30
|
+
- `find_schema_file_path` is now an alias for `find_contracts_file_path`
|
|
31
|
+
- Module template updated: `source` points to `config/routes.rb`
|
|
32
|
+
- All scaffold/generator help text and templates reference new filenames
|
|
33
|
+
|
|
3
34
|
## Unreleased
|
|
4
35
|
|
|
5
36
|
## 0.2.11
|
|
@@ -40,17 +40,31 @@ module Belt
|
|
|
40
40
|
name.to_s.downcase.tr('_', '-')
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# Finds routes
|
|
43
|
+
# Finds routes file checking config/routes.rb first, then legacy paths.
|
|
44
44
|
def find_routes_file_path
|
|
45
|
-
candidates = [
|
|
45
|
+
candidates = [
|
|
46
|
+
'config/routes.rb',
|
|
47
|
+
'config/routes.tf.rb',
|
|
48
|
+
'infrastructure/routes.tf.rb'
|
|
49
|
+
]
|
|
46
50
|
candidates.find { |f| File.exist?(f) }
|
|
47
51
|
end
|
|
48
52
|
|
|
49
|
-
# Finds
|
|
50
|
-
def
|
|
51
|
-
candidates = [
|
|
53
|
+
# Finds contracts file checking config/contracts.rb first, then legacy paths.
|
|
54
|
+
def find_contracts_file_path
|
|
55
|
+
candidates = [
|
|
56
|
+
'config/contracts.rb',
|
|
57
|
+
'config/contracts.tf.rb',
|
|
58
|
+
'config/schema.tf.rb',
|
|
59
|
+
'infrastructure/schema.tf.rb'
|
|
60
|
+
]
|
|
52
61
|
candidates.find { |f| File.exist?(f) }
|
|
53
62
|
end
|
|
63
|
+
|
|
64
|
+
# Legacy alias for backward compatibility
|
|
65
|
+
def find_schema_file_path
|
|
66
|
+
find_contracts_file_path
|
|
67
|
+
end
|
|
54
68
|
end
|
|
55
69
|
end
|
|
56
70
|
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require_relative '../route_dsl'
|
|
6
|
+
require_relative 'routes_command/schema_loader'
|
|
7
|
+
|
|
8
|
+
module Belt
|
|
9
|
+
module CLI
|
|
10
|
+
class ContractsCommand
|
|
11
|
+
include RoutesCommand::SchemaLoader
|
|
12
|
+
|
|
13
|
+
def self.run(args)
|
|
14
|
+
new(args).run
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(args)
|
|
18
|
+
@options = {}
|
|
19
|
+
parse_options(args)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run
|
|
23
|
+
contracts_file = find_contracts_file
|
|
24
|
+
unless contracts_file
|
|
25
|
+
abort 'Error: No contracts file found. ' \
|
|
26
|
+
'Expected config/contracts.rb (or config/contracts.tf.rb, config/schema.tf.rb)'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
models = load_contracts(contracts_file)
|
|
30
|
+
|
|
31
|
+
if @options[:format] == 'json'
|
|
32
|
+
output_json(models)
|
|
33
|
+
else
|
|
34
|
+
output_concise(models)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def parse_options(args)
|
|
41
|
+
OptionParser.new do |opts|
|
|
42
|
+
opts.banner = 'Usage: belt contracts [options]'
|
|
43
|
+
|
|
44
|
+
opts.on('-f', '--format FORMAT', 'Output format: concise (default), json') do |format|
|
|
45
|
+
@options[:format] = format
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
opts.on('-g', '--grep PATTERN', 'Filter contracts matching pattern') do |pattern|
|
|
49
|
+
@options[:grep] = pattern
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
opts.on('--file FILE', 'Path to contracts file (overrides auto-detection)') do |file|
|
|
53
|
+
@options[:contracts_file] = file
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
57
|
+
puts opts
|
|
58
|
+
exit
|
|
59
|
+
end
|
|
60
|
+
end.parse!(args)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def find_contracts_file
|
|
64
|
+
if @options[:contracts_file]
|
|
65
|
+
file = @options[:contracts_file]
|
|
66
|
+
return file if File.exist?(file)
|
|
67
|
+
|
|
68
|
+
abort "Error: Specified contracts file not found: #{file}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
candidates = [
|
|
72
|
+
'config/contracts.rb',
|
|
73
|
+
'config/contracts.tf.rb',
|
|
74
|
+
'config/schema.tf.rb',
|
|
75
|
+
'infrastructure/schema.tf.rb'
|
|
76
|
+
]
|
|
77
|
+
candidates.find { |f| File.exist?(f) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def load_contracts(file)
|
|
81
|
+
Belt.instance_variable_set(:@application, nil)
|
|
82
|
+
begin
|
|
83
|
+
eval(File.read(file), binding, file) # rubocop:disable Security/Eval
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
abort "Error: Failed to load contracts file #{file}: #{e.message}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
schema = Belt.application.schema.to_h
|
|
89
|
+
models = build_models_from_schema(schema)
|
|
90
|
+
models = apply_grep(models) if @options[:grep]
|
|
91
|
+
models
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def apply_grep(models)
|
|
95
|
+
pattern = Regexp.new(@options[:grep], Regexp::IGNORECASE)
|
|
96
|
+
models.select do |m|
|
|
97
|
+
m[:name].match?(pattern) ||
|
|
98
|
+
m[:kind].match?(pattern) ||
|
|
99
|
+
m[:description].match?(pattern)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def output_json(models)
|
|
104
|
+
puts JSON.pretty_generate(models: models)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def output_concise(models)
|
|
108
|
+
return puts('No contracts defined.') if models.empty?
|
|
109
|
+
|
|
110
|
+
requests = models.select { |m| m[:kind] == 'request' }
|
|
111
|
+
responses = models.select { |m| m[:kind] == 'response' }
|
|
112
|
+
|
|
113
|
+
if requests.any?
|
|
114
|
+
puts 'REQUEST MODELS'
|
|
115
|
+
puts '-' * 60
|
|
116
|
+
requests.each { |m| print_model(m) }
|
|
117
|
+
puts
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if responses.any?
|
|
121
|
+
puts 'RESPONSE MODELS'
|
|
122
|
+
puts '-' * 60
|
|
123
|
+
responses.each { |m| print_model(m) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
puts "\n#{models.length} contract(s) total (#{requests.length} request, #{responses.length} response)"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def print_model(model)
|
|
130
|
+
required = model[:required] || []
|
|
131
|
+
props = (model[:properties] || {}).map do |name, meta|
|
|
132
|
+
type = meta['type'] || meta[:type] || 'string'
|
|
133
|
+
req_marker = required.include?(name.to_s) ? ' *' : ''
|
|
134
|
+
"#{name}:#{type}#{req_marker}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
puts " #{model[:name]} (#{model[:kind]})"
|
|
138
|
+
puts " #{props.join(', ')}" if props.any?
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -258,6 +258,9 @@ module Belt
|
|
|
258
258
|
end
|
|
259
259
|
end
|
|
260
260
|
|
|
261
|
+
# Empty S3 buckets before destroy — terraform can't delete non-empty buckets
|
|
262
|
+
empty_s3_buckets_in_state
|
|
263
|
+
|
|
261
264
|
# Run destroy with auto-approve (user already confirmed)
|
|
262
265
|
unless system('terraform', 'destroy', '-auto-approve')
|
|
263
266
|
puts "\n✗ terraform destroy failed."
|
|
@@ -269,6 +272,77 @@ module Belt
|
|
|
269
272
|
puts ' ✓ Infrastructure destroyed.'
|
|
270
273
|
end
|
|
271
274
|
|
|
275
|
+
def empty_s3_buckets_in_state
|
|
276
|
+
output = `terraform state list 2>/dev/null`
|
|
277
|
+
return unless Process.last_status.success?
|
|
278
|
+
|
|
279
|
+
bucket_resources = output.lines.map(&:strip).grep(/\Aaws_s3_bucket\./)
|
|
280
|
+
return if bucket_resources.empty?
|
|
281
|
+
|
|
282
|
+
bucket_resources.each do |resource|
|
|
283
|
+
bucket_name = resolve_bucket_name(resource)
|
|
284
|
+
next unless bucket_name
|
|
285
|
+
|
|
286
|
+
empty_s3_bucket(bucket_name)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def resolve_bucket_name(resource)
|
|
291
|
+
output = `terraform state show '#{resource}' 2>/dev/null`
|
|
292
|
+
return nil unless Process.last_status.success?
|
|
293
|
+
|
|
294
|
+
match = output.match(/^\s*bucket\s*=\s*"([^"]+)"/)
|
|
295
|
+
match&.[](1)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def empty_s3_bucket(bucket_name)
|
|
299
|
+
# Check if bucket exists
|
|
300
|
+
`aws s3api head-bucket --bucket '#{bucket_name}' 2>&1`
|
|
301
|
+
return unless Process.last_status.success?
|
|
302
|
+
|
|
303
|
+
puts " Emptying S3 bucket: #{bucket_name}"
|
|
304
|
+
|
|
305
|
+
# Delete all object versions (handles versioned buckets)
|
|
306
|
+
`aws s3 rm 's3://#{bucket_name}' --recursive 2>/dev/null`
|
|
307
|
+
|
|
308
|
+
# Also delete versioned objects and delete markers
|
|
309
|
+
delete_all_versions(bucket_name)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def delete_all_versions(bucket_name)
|
|
313
|
+
loop do
|
|
314
|
+
output = `aws s3api list-object-versions --bucket '#{bucket_name}' --max-items 1000 2>/dev/null`
|
|
315
|
+
break unless Process.last_status.success?
|
|
316
|
+
|
|
317
|
+
begin
|
|
318
|
+
data = JSON.parse(output)
|
|
319
|
+
rescue JSON::ParserError
|
|
320
|
+
break
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
versions = (data['Versions'] || []) + (data['DeleteMarkers'] || [])
|
|
324
|
+
break if versions.empty?
|
|
325
|
+
|
|
326
|
+
# Build delete objects payload
|
|
327
|
+
objects = versions.map do |v|
|
|
328
|
+
{ 'Key' => v['Key'], 'VersionId' => v['VersionId'] }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
delete_payload = JSON.generate({ 'Objects' => objects, 'Quiet' => true })
|
|
332
|
+
|
|
333
|
+
# Use a temp file for the payload since it can be large
|
|
334
|
+
require 'tempfile'
|
|
335
|
+
Tempfile.create(['delete-objects', '.json']) do |f|
|
|
336
|
+
f.write(delete_payload)
|
|
337
|
+
f.flush
|
|
338
|
+
`aws s3api delete-objects --bucket '#{bucket_name}' --delete 'file://#{f.path}' 2>/dev/null`
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# If there's no next token, we're done
|
|
342
|
+
break unless data['NextToken'] || data['IsTruncated']
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
272
346
|
def destroy_frontend
|
|
273
347
|
dir = 'frontend'
|
|
274
348
|
if Dir.exist?(dir)
|
|
@@ -38,8 +38,8 @@ module Belt
|
|
|
38
38
|
Creates:
|
|
39
39
|
lambda/models/<name>.rb Model with validations and fields
|
|
40
40
|
lambda/controllers/<app>/<names>_controller.rb RESTful controller (index, show, create, update, destroy)
|
|
41
|
-
config/routes.
|
|
42
|
-
config/
|
|
41
|
+
config/routes.rb Route entry added
|
|
42
|
+
config/contracts.rb API response contract added
|
|
43
43
|
lambda/lib/routes/<app>_routes.rb Route manifest updated
|
|
44
44
|
infrastructure/modules/app/dynamodb.tf DynamoDB table generated
|
|
45
45
|
frontend/src/pages/<names>/ React pages (if frontend exists)
|
|
@@ -60,7 +60,7 @@ module Belt
|
|
|
60
60
|
notes: <<~NOTES
|
|
61
61
|
Creates:
|
|
62
62
|
lambda/models/<name>.rb Model class inheriting from ApplicationRecord
|
|
63
|
-
config/
|
|
63
|
+
config/contracts.rb API response contract added
|
|
64
64
|
infrastructure/modules/app/dynamodb.tf DynamoDB table generated
|
|
65
65
|
NOTES
|
|
66
66
|
},
|
|
@@ -333,8 +333,8 @@ module Belt
|
|
|
333
333
|
puts "\nFiles created/updated:"
|
|
334
334
|
puts " lambda/models/#{@singular_name}.rb"
|
|
335
335
|
puts " lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
|
|
336
|
-
puts " #{find_routes_file_path || 'config/routes.
|
|
337
|
-
puts " #{
|
|
336
|
+
puts " #{find_routes_file_path || 'config/routes.rb'} (updated)"
|
|
337
|
+
puts " #{find_contracts_file_path || 'config/contracts.rb'} (updated)"
|
|
338
338
|
puts " lambda/lib/routes/#{@app_name}_routes.rb (updated)"
|
|
339
339
|
puts " frontend/src/pages/#{@resource_name}/ (views)" if Dir.exist?('frontend/src')
|
|
340
340
|
end
|
data/lib/belt/cli/new_command.rb
CHANGED
|
@@ -167,8 +167,8 @@ module Belt
|
|
|
167
167
|
'lambda/controllers/application_controller.rb.erb' =>
|
|
168
168
|
"#{@app_name}/lambda/controllers/api/application_controller.rb",
|
|
169
169
|
'lambda/lib/routes/routes.rb.erb' => "#{@app_name}/lambda/lib/routes/api_routes.rb",
|
|
170
|
-
'config/routes.
|
|
171
|
-
'config/
|
|
170
|
+
'config/routes.rb.erb' => "#{@app_name}/config/routes.rb",
|
|
171
|
+
'config/contracts.rb.erb' => "#{@app_name}/config/contracts.rb",
|
|
172
172
|
'config/lambda/api.yml.erb' => "#{@app_name}/config/lambda/api.yml",
|
|
173
173
|
'README.md.erb' => "#{@app_name}/README.md",
|
|
174
174
|
'AGENTS.md.erb' => "#{@app_name}/AGENTS.md",
|
|
@@ -8,14 +8,14 @@ module Belt
|
|
|
8
8
|
private
|
|
9
9
|
|
|
10
10
|
def load_schema_models(routes_file)
|
|
11
|
-
schema_file =
|
|
11
|
+
schema_file = resolve_contracts_file(routes_file)
|
|
12
12
|
return [] unless schema_file && File.exist?(schema_file)
|
|
13
13
|
|
|
14
14
|
Belt.instance_variable_set(:@application, nil)
|
|
15
15
|
begin
|
|
16
16
|
eval(File.read(schema_file), binding, schema_file) # rubocop:disable Security/Eval
|
|
17
17
|
rescue StandardError => e
|
|
18
|
-
warn "Warning: Failed to load
|
|
18
|
+
warn "Warning: Failed to load contracts file #{schema_file}: #{e.message}"
|
|
19
19
|
return []
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -23,15 +23,25 @@ module Belt
|
|
|
23
23
|
build_models_from_schema(schema)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def
|
|
26
|
+
def resolve_contracts_file(routes_file)
|
|
27
27
|
schema_file = @options[:schema_file]
|
|
28
28
|
unless schema_file
|
|
29
29
|
routes_dir = File.dirname(File.expand_path(routes_file))
|
|
30
|
-
|
|
30
|
+
# Check new convention first, then legacy names
|
|
31
|
+
candidates = [
|
|
32
|
+
File.join(routes_dir, 'contracts.rb'),
|
|
33
|
+
File.join(routes_dir, 'contracts.tf.rb'),
|
|
34
|
+
File.join(routes_dir, 'schema.tf.rb')
|
|
35
|
+
]
|
|
36
|
+
schema_file = candidates.find { |f| File.exist?(f) }
|
|
37
|
+
|
|
31
38
|
# Fall back to infrastructure/ if not found in same directory as routes
|
|
32
|
-
unless
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
unless schema_file
|
|
40
|
+
legacy_candidates = [
|
|
41
|
+
'infrastructure/contracts.rb',
|
|
42
|
+
'infrastructure/schema.tf.rb'
|
|
43
|
+
]
|
|
44
|
+
schema_file = legacy_candidates.find { |f| File.exist?(f) }
|
|
35
45
|
end
|
|
36
46
|
end
|
|
37
47
|
schema_file
|
|
@@ -27,7 +27,7 @@ module Belt
|
|
|
27
27
|
routes_file = find_routes_file
|
|
28
28
|
unless routes_file
|
|
29
29
|
abort 'Error: No routes file found. ' \
|
|
30
|
-
'Expected config/routes.
|
|
30
|
+
'Expected config/routes.rb (or config/routes.tf.rb, infrastructure/routes.tf.rb)'
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
dsl = load_routes(routes_file)
|
|
@@ -71,7 +71,7 @@ module Belt
|
|
|
71
71
|
@options[:output_dir] = dir
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
opts.on('--schema FILE', 'Path to
|
|
74
|
+
opts.on('--schema FILE', 'Path to contracts.rb for model definitions') do |file|
|
|
75
75
|
@options[:schema_file] = file
|
|
76
76
|
end
|
|
77
77
|
|
|
@@ -87,7 +87,11 @@ module Belt
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def find_routes_file
|
|
90
|
-
candidates = [
|
|
90
|
+
candidates = [
|
|
91
|
+
'config/routes.rb',
|
|
92
|
+
'config/routes.tf.rb',
|
|
93
|
+
'infrastructure/routes.tf.rb'
|
|
94
|
+
]
|
|
91
95
|
candidates.find { |f| File.exist?(f) }
|
|
92
96
|
end
|
|
93
97
|
|
|
@@ -32,7 +32,7 @@ module Belt
|
|
|
32
32
|
puts 'Usage: belt setup <state|tables|frontend> [options]'
|
|
33
33
|
puts "\nSubcommands:"
|
|
34
34
|
puts ' state Set up S3 bucket for Terraform state'
|
|
35
|
-
puts ' tables Generate DynamoDB table definitions from
|
|
35
|
+
puts ' tables Generate DynamoDB table definitions from contracts.rb'
|
|
36
36
|
puts ' frontend Generate S3 + CloudFront infrastructure for frontend hosting'
|
|
37
37
|
exit 1
|
|
38
38
|
end
|
|
@@ -25,14 +25,19 @@ module Belt
|
|
|
25
25
|
{ name: n, type: t || 'string' }
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# If no fields provided, try to read from
|
|
28
|
+
# If no fields provided, try to read from contracts.rb
|
|
29
29
|
fields = read_schema_fields(name) if fields.empty?
|
|
30
30
|
|
|
31
31
|
new(name, fields).generate
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def self.read_schema_fields(name)
|
|
35
|
-
schema_file = [
|
|
35
|
+
schema_file = [
|
|
36
|
+
'config/contracts.rb',
|
|
37
|
+
'config/contracts.tf.rb',
|
|
38
|
+
'config/schema.tf.rb',
|
|
39
|
+
'infrastructure/schema.tf.rb'
|
|
40
|
+
].find { |f| File.exist?(f) }
|
|
36
41
|
return [] unless schema_file
|
|
37
42
|
|
|
38
43
|
content = File.read(schema_file)
|
data/lib/belt/cli.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative 'cli/backup_config'
|
|
|
18
18
|
require_relative 'cli/backup_runner'
|
|
19
19
|
require_relative 'cli/server_command'
|
|
20
20
|
require_relative 'cli/routes_command'
|
|
21
|
+
require_relative 'cli/contracts_command'
|
|
21
22
|
require_relative 'cli/lambda_config_command'
|
|
22
23
|
require_relative 'cli/tasks_command'
|
|
23
24
|
require_relative 'cli/console_command'
|
|
@@ -31,6 +32,7 @@ module Belt
|
|
|
31
32
|
%w[generate g] => Belt::CLI::GenerateCommand,
|
|
32
33
|
%w[destroy d] => Belt::CLI::DestroyCommand,
|
|
33
34
|
'routes' => Belt::CLI::RoutesCommand,
|
|
35
|
+
'contracts' => Belt::CLI::ContractsCommand,
|
|
34
36
|
'lambda-config' => Belt::CLI::LambdaConfigCommand,
|
|
35
37
|
%w[console c] => Belt::CLI::ConsoleCommand,
|
|
36
38
|
%w[tasks --tasks -T] => Belt::CLI::TasksCommand,
|
|
@@ -109,6 +111,7 @@ module Belt
|
|
|
109
111
|
deploy frontend <env> Build and deploy frontend to AWS
|
|
110
112
|
frontend env <env> Write frontend/.env from terraform outputs
|
|
111
113
|
routes [-g PATTERN] [-f json] Show route definitions
|
|
114
|
+
contracts [-g PATTERN] [-f json] Show API request/response contracts
|
|
112
115
|
lambda-config [-e ENV] [-f json|terraform] Show merged lambda configuration
|
|
113
116
|
|
|
114
117
|
console Start an interactive console (IRB)
|
data/lib/belt/root.rb
CHANGED
|
@@ -9,24 +9,32 @@ module Belt
|
|
|
9
9
|
@root = path
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
# Resolves the path to routes.
|
|
12
|
+
# Resolves the path to routes.rb, checking config/ first then legacy paths.
|
|
13
13
|
def self.routes_file
|
|
14
14
|
candidates = [
|
|
15
|
+
File.join(root, 'config/routes.rb'),
|
|
15
16
|
File.join(root, 'config/routes.tf.rb'),
|
|
16
17
|
File.join(root, 'infrastructure/routes.tf.rb')
|
|
17
18
|
]
|
|
18
19
|
candidates.find { |f| File.exist?(f) }
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
# Resolves the path to
|
|
22
|
-
def self.
|
|
22
|
+
# Resolves the path to contracts.rb, checking config/ first then legacy paths.
|
|
23
|
+
def self.contracts_file
|
|
23
24
|
candidates = [
|
|
25
|
+
File.join(root, 'config/contracts.rb'),
|
|
26
|
+
File.join(root, 'config/contracts.tf.rb'),
|
|
24
27
|
File.join(root, 'config/schema.tf.rb'),
|
|
25
28
|
File.join(root, 'infrastructure/schema.tf.rb')
|
|
26
29
|
]
|
|
27
30
|
candidates.find { |f| File.exist?(f) }
|
|
28
31
|
end
|
|
29
32
|
|
|
33
|
+
# Legacy alias for backward compatibility
|
|
34
|
+
def self.schema_file
|
|
35
|
+
contracts_file
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
# Resolves the lambda config directory.
|
|
31
39
|
def self.lambda_config_dir
|
|
32
40
|
File.join(root, 'config/lambda')
|
|
@@ -35,6 +43,7 @@ module Belt
|
|
|
35
43
|
def self.detect_root
|
|
36
44
|
dir = Dir.pwd
|
|
37
45
|
loop do
|
|
46
|
+
return dir if File.exist?(File.join(dir, 'config/routes.rb'))
|
|
38
47
|
return dir if File.exist?(File.join(dir, 'config/routes.tf.rb'))
|
|
39
48
|
return dir if File.exist?(File.join(dir, 'infrastructure/routes.tf.rb'))
|
|
40
49
|
|
data/lib/belt/route_dsl.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative 'inflector'
|
|
|
5
5
|
module Belt
|
|
6
6
|
# DSL for defining API Gateway routes.
|
|
7
7
|
# Ported from terraform-provider-conveyor-belt/scripts/lib/route_dsl.rb
|
|
8
|
-
# so that `belt routes` can parse routes.
|
|
8
|
+
# so that `belt routes` can parse routes.rb without external dependencies.
|
|
9
9
|
|
|
10
10
|
class Route
|
|
11
11
|
attr_reader :method, :path, :auth, :lambda, :cors, :tables, :route_type,
|
|
@@ -518,7 +518,7 @@ module Belt
|
|
|
518
518
|
end
|
|
519
519
|
end
|
|
520
520
|
|
|
521
|
-
# SchemaBuilder captures request and response model definitions from
|
|
521
|
+
# SchemaBuilder captures request and response model definitions from contracts.rb
|
|
522
522
|
class SchemaBuilder
|
|
523
523
|
SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
|
|
524
524
|
|
data/lib/belt/version.rb
CHANGED
|
@@ -9,7 +9,8 @@ resource "random_string" "frontend_suffix" {
|
|
|
9
9
|
|
|
10
10
|
# S3 bucket for frontend static assets
|
|
11
11
|
resource "aws_s3_bucket" "frontend" {
|
|
12
|
-
bucket
|
|
12
|
+
bucket = "<%= s3_safe_name(@app_name) %>-frontend-${var.environment}-${random_string.frontend_suffix.result}"
|
|
13
|
+
force_destroy = true
|
|
13
14
|
|
|
14
15
|
lifecycle {
|
|
15
16
|
ignore_changes = [bucket]
|
|
@@ -9,7 +9,8 @@ resource "random_string" "frontend_suffix" {
|
|
|
9
9
|
|
|
10
10
|
# S3 bucket for frontend static assets
|
|
11
11
|
resource "aws_s3_bucket" "frontend" {
|
|
12
|
-
bucket
|
|
12
|
+
bucket = "${var.app_name}-frontend-${var.environment}-${random_string.frontend_suffix.result}"
|
|
13
|
+
force_destroy = true
|
|
13
14
|
|
|
14
15
|
lifecycle {
|
|
15
16
|
ignore_changes = [bucket]
|
|
@@ -22,7 +22,7 @@ resource "conveyor_belt" "main" {
|
|
|
22
22
|
provider = conveyor-belt
|
|
23
23
|
|
|
24
24
|
# path.module is infrastructure/modules/app — climb three levels to project root
|
|
25
|
-
source = "${path.module}/../../../config/routes.
|
|
25
|
+
source = "${path.module}/../../../config/routes.rb"
|
|
26
26
|
app_name = var.app_name
|
|
27
27
|
lambda_source_dir = "${path.module}/../../../lambda"
|
|
28
28
|
lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "views"]
|
|
@@ -6,7 +6,7 @@ This file explains the project structure, tooling, and conventions for AI agents
|
|
|
6
6
|
|
|
7
7
|
- **Belt** — CLI and runtime framework (like Rails for serverless). Provides Lambda handler, action router, controller base class, and CLI tooling.
|
|
8
8
|
- **ActiveItem** — ActiveRecord-like ORM for DynamoDB. Models inherit from `ActiveItem::Base`.
|
|
9
|
-
- **Conveyor Belt** — Terraform provider that reads a Ruby DSL (`routes.
|
|
9
|
+
- **Conveyor Belt** — Terraform provider that reads a Ruby DSL (`routes.rb`) and creates API Gateway + Lambda + IAM infrastructure.
|
|
10
10
|
- **Lambda Loadout** — Lambda cold-start optimizer (auto-required by Belt).
|
|
11
11
|
|
|
12
12
|
## Project Structure
|
|
@@ -23,8 +23,8 @@ This file explains the project structure, tooling, and conventions for AI agents
|
|
|
23
23
|
│ ├── lib/routes/ # Route manifests (auto-generated)
|
|
24
24
|
│ └── Gemfile # Lambda-specific dependencies
|
|
25
25
|
├── config/
|
|
26
|
-
│ ├── routes.
|
|
27
|
-
│ ├──
|
|
26
|
+
│ ├── routes.rb # API routes (Conveyor Belt DSL)
|
|
27
|
+
│ ├── contracts.rb # API request/response contracts
|
|
28
28
|
│ └── lambda/ # Per-lambda config (like database.yml)
|
|
29
29
|
│ └── api.yml # Lambda timeout, memory, env vars
|
|
30
30
|
├── infrastructure/
|
|
@@ -53,7 +53,7 @@ belt output <env> # terraform output
|
|
|
53
53
|
|
|
54
54
|
## How Routing Works
|
|
55
55
|
|
|
56
|
-
1. `config/routes.
|
|
56
|
+
1. `config/routes.rb` defines routes using a DSL:
|
|
57
57
|
```ruby
|
|
58
58
|
Belt.application.routes.draw do
|
|
59
59
|
namespace :<%= @app_name %> do
|
|
@@ -6,8 +6,8 @@ A serverless application built with [Belt](https://github.com/stowzilla/belt) an
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
├── config/
|
|
9
|
-
│ ├── routes.
|
|
10
|
-
│ ├──
|
|
9
|
+
│ ├── routes.rb # API route definitions
|
|
10
|
+
│ ├── contracts.rb # API request/response contracts
|
|
11
11
|
│ └── lambda/ # Per-lambda config (timeout, memory, env vars)
|
|
12
12
|
│ └── api.yml
|
|
13
13
|
├── infrastructure/ # Terraform environments
|
|
@@ -67,7 +67,7 @@ When fleshing out the generator, typically install:
|
|
|
67
67
|
1. **Terraform module** → `infrastructure/modules/<%= plugin_name %>/` (`main.tf`, `variables.tf`, `outputs.tf`)
|
|
68
68
|
2. **Lambda config** → `config/lambda/<%= plugin_name %>.yml` (timeout, memory, env, triggers)
|
|
69
69
|
3. **Lambda entrypoint** → `lambda/<%= plugin_name %>.rb` using `Belt::LambdaHandler`
|
|
70
|
-
4. **Routes / schema** → inject into `config/routes.
|
|
70
|
+
4. **Routes / schema** → inject into `config/routes.rb` or `config/contracts.rb` when needed
|
|
71
71
|
5. **Optional overrides** → `--controllers` (or similar) for app-local subclasses — keep defaults in the gem
|
|
72
72
|
6. **Destroy path** → `belt destroy <%= plugin_name %>` removes generated artifacts
|
|
73
73
|
7. **Help** → `.description` plus `--help` / `-h` explaining files and next steps
|
|
@@ -42,7 +42,7 @@ Typical plugin generators create some combination of:
|
|
|
42
42
|
- **Terraform module** — `infrastructure/modules/<%= plugin_name %>/`
|
|
43
43
|
- **Lambda entry point** — `lambda/<%= plugin_name %>.rb`
|
|
44
44
|
- **Lambda config** — `config/lambda/<%= plugin_name %>.yml`
|
|
45
|
-
- **Route / schema injection** — updates to `routes.
|
|
45
|
+
- **Route / schema injection** — updates to `config/routes.rb` / `config/contracts.rb`
|
|
46
46
|
- **Optional controller overrides** — `belt g <%= plugin_name %> --controllers`
|
|
47
47
|
|
|
48
48
|
## Development
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: belt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
@@ -74,6 +74,7 @@ files:
|
|
|
74
74
|
- lib/belt/cli/backup_runner.rb
|
|
75
75
|
- lib/belt/cli/bucket_security.rb
|
|
76
76
|
- lib/belt/cli/console_command.rb
|
|
77
|
+
- lib/belt/cli/contracts_command.rb
|
|
77
78
|
- lib/belt/cli/deploy_command.rb
|
|
78
79
|
- lib/belt/cli/destroy_command.rb
|
|
79
80
|
- lib/belt/cli/doctor_command.rb
|
|
@@ -144,9 +145,9 @@ files:
|
|
|
144
145
|
- lib/templates/new_app/Gemfile.erb
|
|
145
146
|
- lib/templates/new_app/README.md.erb
|
|
146
147
|
- lib/templates/new_app/Rakefile.erb
|
|
148
|
+
- lib/templates/new_app/config/contracts.rb.erb
|
|
147
149
|
- lib/templates/new_app/config/lambda/api.yml.erb
|
|
148
|
-
- lib/templates/new_app/config/routes.
|
|
149
|
-
- lib/templates/new_app/config/schema.tf.rb.erb
|
|
150
|
+
- lib/templates/new_app/config/routes.rb.erb
|
|
150
151
|
- lib/templates/new_app/gitignore.erb
|
|
151
152
|
- lib/templates/new_app/lambda/api.rb.erb
|
|
152
153
|
- lib/templates/new_app/lambda/config/environment.rb.erb
|
|
File without changes
|
|
File without changes
|