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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43ae84461595be08ca6caada3afd148ccfa0db94bfc2e2bd864a9b466d41f087
4
- data.tar.gz: 1f33826b8c7a7b068458db8ca38aa9910079e16f04c3c5c727550f084ed85075
3
+ metadata.gz: 10e92ad2ab3fe79f48f07cca6495ce8b9ccdad09755c7a6517e651deadcc118d
4
+ data.tar.gz: 6cd8a8ee7ad1023fb949183122c3aca465c216cf91d175e8f9395e8f413ad880
5
5
  SHA512:
6
- metadata.gz: 81532edca53a084bdd6aabf5e7027fd1653ec0fc4c592b0bdb60374283c7c59512a2217405d3a19f32a975bcbcbbebdf9f9a97d952a25b1567e05af8e2cb879d
7
- data.tar.gz: d5ff609d6526f8cc59964df6da091a43150eca9d1c6f625dee27b070db0c56bd90ef58ef11c42f57e27259f169c477a4c1c548f9ca0f4c4f916c8bea540763bb
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.tf.rb checking config/ first, then infrastructure/ (legacy).
43
+ # Finds routes file checking config/routes.rb first, then legacy paths.
44
44
  def find_routes_file_path
45
- candidates = ['config/routes.tf.rb', 'infrastructure/routes.tf.rb']
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 schema.tf.rb checking config/ first, then infrastructure/ (legacy).
50
- def find_schema_file_path
51
- candidates = ['config/schema.tf.rb', 'infrastructure/schema.tf.rb']
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.tf.rb Route entry added
42
- config/schema.tf.rb API response contract added
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/schema.tf.rb API response contract added
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.tf.rb'} (updated)"
337
- puts " #{find_schema_file_path || 'config/schema.tf.rb'} (updated)"
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
@@ -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.tf.rb.erb' => "#{@app_name}/config/routes.tf.rb",
171
- 'config/schema.tf.rb.erb' => "#{@app_name}/config/schema.tf.rb",
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 = resolve_schema_file(routes_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 schema file #{schema_file}: #{e.message}"
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 resolve_schema_file(routes_file)
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
- schema_file = File.join(routes_dir, 'schema.tf.rb')
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 File.exist?(schema_file)
33
- alt = 'infrastructure/schema.tf.rb'
34
- schema_file = alt if File.exist?(alt)
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.tf.rb (or infrastructure/routes.tf.rb)'
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 schema.tf.rb for model definitions') do |file|
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 = ['config/routes.tf.rb', 'infrastructure/routes.tf.rb']
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 schema.tf.rb'
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 schema.tf.rb
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 = ['config/schema.tf.rb', 'infrastructure/schema.tf.rb'].find { |f| File.exist?(f) }
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.tf.rb, checking config/ first then infrastructure/ (legacy).
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 schema.tf.rb, checking config/ first then infrastructure/ (legacy).
22
- def self.schema_file
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
 
@@ -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.tf.rb without external dependencies.
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 schema.tf.rb
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.2.11'
4
+ VERSION = '0.2.12'
5
5
  end
@@ -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 = "<%= s3_safe_name(@app_name) %>-frontend-${var.environment}-${random_string.frontend_suffix.result}"
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 = "${var.app_name}-frontend-${var.environment}-${random_string.frontend_suffix.result}"
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.tf.rb"
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.tf.rb`) and creates API Gateway + Lambda + IAM infrastructure.
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.tf.rb # API routes (Conveyor Belt DSL)
27
- │ ├── schema.tf.rb # Model schema definitions
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.tf.rb` defines routes using a DSL:
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.tf.rb # API route definitions
10
- │ ├── schema.tf.rb # Model schema definitions
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.tf.rb` or schema files when needed
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.tf.rb` / `schema.tf.rb`
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.11
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.tf.rb.erb
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