belt 0.3.5 → 0.3.7
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 +35 -1
- data/lib/belt/cli/app_detection.rb +26 -1
- data/lib/belt/cli/deploy_command.rb +13 -0
- data/lib/belt/cli/routes_command/request_model_inference.rb +131 -0
- data/lib/belt/cli/routes_command.rb +7 -0
- data/lib/belt/route_dsl.rb +113 -23
- data/lib/belt/version.rb +1 -1
- data/lib/templates/new_app/AGENTS.md.erb +29 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0be7ca644efba0ca7b1e95b076bbae96873ced55d3251e5de235208169bbcce9
|
|
4
|
+
data.tar.gz: d5f0b277442b1b139fb48c306b4f2c0ca13ce30d6b35637a67749b549f6a7182
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cac4e33287828876e5f248dcf3536f2e4709b2637481453e9d5015711b8a1de4bfc854edfc429060c7929a17f3ec46b17dc15127474ce9bd0312f064c23d77ee
|
|
7
|
+
data.tar.gz: eca9b584458ff63559fe3e4fad915be300628ec0659ae4e3d7d7cac66bba8d8d2e2eb9dd9010174f3d6bce6d7e688356e1325070f7aec0150e518348ab8be50d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.3.7
|
|
4
|
+
|
|
5
|
+
### Bug Fix
|
|
6
|
+
|
|
7
|
+
- **App name detection in worktrees**: `detect_app_name` now checks
|
|
8
|
+
`infrastructure/*/variables.tf` for `variable "app_name" { default = "..." }`
|
|
9
|
+
before falling back to the directory name. Fixes incorrect Lambda function
|
|
10
|
+
lookups (e.g. `belt logs`) when running from a git worktree whose directory
|
|
11
|
+
name doesn't match the actual app name.
|
|
12
|
+
|
|
13
|
+
## 0.3.6
|
|
14
|
+
|
|
15
|
+
### Enhancement
|
|
16
|
+
|
|
17
|
+
- **Per-action request_model**: `request_model` on `resources` now accepts a Hash
|
|
18
|
+
to specify different validation schemas per action:
|
|
19
|
+
```ruby
|
|
20
|
+
resources :items, request_model: { create: :create_item, update: :update_item }
|
|
21
|
+
```
|
|
22
|
+
Symbol style (uniform for all body verbs) still works unchanged.
|
|
23
|
+
|
|
24
|
+
- **Convention-based request_model inference**: When no explicit `request_model` is
|
|
25
|
+
set on a resource route, `belt routes` auto-discovers matching contracts from
|
|
26
|
+
`contracts.rb` using a naming cascade:
|
|
27
|
+
1. `:<verb>_<gateway>_<singular_resource>` (e.g. `:create_customer_item`)
|
|
28
|
+
2. `:<verb>_<singular_resource>` (e.g. `:create_item`)
|
|
29
|
+
|
|
30
|
+
Only applies to body-accepting verbs (POST/PUT/PATCH). Explicit `request_model:`
|
|
31
|
+
always takes priority. No matching contract = no validation (same as before).
|
|
32
|
+
|
|
33
|
+
- **Convention-based response_model inference**: When no explicit `response_model` is
|
|
34
|
+
set on a resource route, `belt routes` auto-discovers a matching response model
|
|
35
|
+
from `contracts.rb` using the singular resource name.
|
|
36
|
+
e.g. `resources :items` → looks for `model :item` → auto-wires `response_model: :item`.
|
|
37
|
+
Applies to all verbs. Explicit `response_model:` always takes priority.
|
|
4
38
|
|
|
5
39
|
## 0.3.5
|
|
6
40
|
|
|
@@ -17,7 +17,7 @@ module Belt
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# Detects the application name.
|
|
20
|
-
#
|
|
20
|
+
# Priority: terraform.tfvars app_name > variables.tf default > directory name.
|
|
21
21
|
def detect_app_name
|
|
22
22
|
# Check any environment tfvars for the app_name
|
|
23
23
|
Dir.glob('infrastructure/*/terraform.tfvars').each do |f|
|
|
@@ -26,6 +26,10 @@ module Belt
|
|
|
26
26
|
return match[1] if match
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Check variables.tf for a default app_name value
|
|
30
|
+
name = detect_app_name_from_variables_tf
|
|
31
|
+
return name if name
|
|
32
|
+
|
|
29
33
|
# Fall back to directory name
|
|
30
34
|
File.basename(Dir.pwd)
|
|
31
35
|
end
|
|
@@ -67,6 +71,27 @@ module Belt
|
|
|
67
71
|
def find_schema_file_path
|
|
68
72
|
find_contracts_file_path
|
|
69
73
|
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Parses `variable "app_name" { default = "..." }` from any environment's variables.tf.
|
|
78
|
+
# This handles the common pattern where app_name is a Terraform variable with a default
|
|
79
|
+
# rather than being set directly in terraform.tfvars.
|
|
80
|
+
def detect_app_name_from_variables_tf
|
|
81
|
+
Dir.glob('infrastructure/*/variables.tf').each do |f|
|
|
82
|
+
content = File.read(f)
|
|
83
|
+
# Match the app_name variable block and extract its default value
|
|
84
|
+
next unless content.match?(/variable\s+"app_name"/)
|
|
85
|
+
|
|
86
|
+
# Extract default from within the variable block
|
|
87
|
+
block_match = content.match(/variable\s+"app_name"\s*\{([^}]*)\}/m)
|
|
88
|
+
next unless block_match
|
|
89
|
+
|
|
90
|
+
default_match = block_match[1].match(/default\s*=\s*"([^"]+)"/)
|
|
91
|
+
return default_match[1] if default_match
|
|
92
|
+
end
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
70
95
|
end
|
|
71
96
|
end
|
|
72
97
|
end
|
|
@@ -321,6 +321,19 @@ module Belt
|
|
|
321
321
|
return match[1] if match
|
|
322
322
|
end
|
|
323
323
|
|
|
324
|
+
# Try variables.tf for a default app_name value
|
|
325
|
+
vars_file = File.join(@infra_dir, @env, 'variables.tf')
|
|
326
|
+
if File.exist?(vars_file)
|
|
327
|
+
content = File.read(vars_file)
|
|
328
|
+
if content.match?(/variable\s+"app_name"/)
|
|
329
|
+
block_match = content.match(/variable\s+"app_name"\s*\{([^}]*)\}/m)
|
|
330
|
+
if block_match
|
|
331
|
+
default_match = block_match[1].match(/default\s*=\s*"([^"]+)"/)
|
|
332
|
+
return default_match[1] if default_match
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
324
337
|
# Fall back to directory name
|
|
325
338
|
File.basename(@project_root)
|
|
326
339
|
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module CLI
|
|
5
|
+
class RoutesCommand
|
|
6
|
+
# Infers request_model and response_model from contracts using naming conventions.
|
|
7
|
+
#
|
|
8
|
+
# Request model convention cascade (first match wins):
|
|
9
|
+
# 1. :<verb>_<gateway>_<singular_resource> (e.g. :create_customer_item)
|
|
10
|
+
# 2. :<verb>_<singular_resource> (e.g. :create_item)
|
|
11
|
+
#
|
|
12
|
+
# Response model convention:
|
|
13
|
+
# Singular of the resource name matches a `model` in contracts.
|
|
14
|
+
# e.g. `resources :items` → looks for `model :item` in contracts.rb
|
|
15
|
+
#
|
|
16
|
+
# Only request_model inference applies to body-accepting verbs (POST/PUT/PATCH).
|
|
17
|
+
# Response model inference applies to all verbs on resource routes.
|
|
18
|
+
# Explicit values always win — inference only fills in blanks.
|
|
19
|
+
module RequestModelInference
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Applies convention-based request_model inference to all routes.
|
|
23
|
+
# Requires the set of known contract names from contracts.rb.
|
|
24
|
+
def infer_request_models!(routes, contracts_file)
|
|
25
|
+
contract_names = load_contract_names(contracts_file)
|
|
26
|
+
return if contract_names[:request].empty? && contract_names[:response].empty?
|
|
27
|
+
|
|
28
|
+
routes.each do |route|
|
|
29
|
+
infer_request_model_for!(route, contract_names[:request])
|
|
30
|
+
infer_response_model_for!(route, contract_names[:response])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def load_contract_names(contracts_file)
|
|
35
|
+
empty_result = { request: Set.new, response: Set.new }
|
|
36
|
+
return empty_result unless contracts_file && File.exist?(contracts_file)
|
|
37
|
+
|
|
38
|
+
# Reset application state for clean contract loading
|
|
39
|
+
Belt.instance_variable_set(:@application, nil)
|
|
40
|
+
begin
|
|
41
|
+
eval(File.read(contracts_file), binding, contracts_file) # rubocop:disable Security/Eval
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
warn "Warning: Failed to load contracts for inference: #{e.message}"
|
|
44
|
+
return empty_result
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
schema = Belt.application.schema.to_h
|
|
48
|
+
request_names = Set.new
|
|
49
|
+
response_names = Set.new
|
|
50
|
+
(schema[:request_models] || {}).each_key { |name| request_names << name.to_s }
|
|
51
|
+
(schema[:response_models] || {}).each_key { |name| response_names << name.to_s }
|
|
52
|
+
{ request: request_names, response: response_names }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def infer_request_model_for!(route, request_contracts)
|
|
56
|
+
return if request_contracts.empty?
|
|
57
|
+
return unless route[:request_model].to_s.empty?
|
|
58
|
+
return unless body_accepting_verb?(route[:verb])
|
|
59
|
+
|
|
60
|
+
inferred = infer_request_model_for_route(route, request_contracts)
|
|
61
|
+
route[:request_model] = inferred if inferred
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def infer_response_model_for!(route, response_contracts)
|
|
65
|
+
return if response_contracts.empty?
|
|
66
|
+
return unless route[:response_model].to_s.empty?
|
|
67
|
+
|
|
68
|
+
inferred = infer_response_model_for_route(route, response_contracts)
|
|
69
|
+
route[:response_model] = inferred if inferred
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def infer_request_model_for_route(route, contract_names)
|
|
73
|
+
verb_prefix = infer_verb_prefix(route[:verb], route[:action])
|
|
74
|
+
return nil unless verb_prefix
|
|
75
|
+
|
|
76
|
+
resource_name = extract_singular_resource(route)
|
|
77
|
+
return nil unless resource_name
|
|
78
|
+
|
|
79
|
+
gateway = route[:gateway]
|
|
80
|
+
|
|
81
|
+
# Cascade: gateway-scoped first, then generic
|
|
82
|
+
candidates = [
|
|
83
|
+
"#{verb_prefix}_#{gateway}_#{resource_name}",
|
|
84
|
+
"#{verb_prefix}_#{resource_name}"
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
candidates.find { |candidate| contract_names.include?(candidate) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def infer_response_model_for_route(route, response_contracts)
|
|
91
|
+
resource_name = extract_singular_resource(route)
|
|
92
|
+
return nil unless resource_name
|
|
93
|
+
|
|
94
|
+
# Convention: singular resource name matches a response model
|
|
95
|
+
return resource_name if response_contracts.include?(resource_name)
|
|
96
|
+
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def infer_verb_prefix(verb, action)
|
|
101
|
+
# Map HTTP verb + action to the contract naming prefix
|
|
102
|
+
case action
|
|
103
|
+
when 'create' then 'create'
|
|
104
|
+
when 'update' then 'update'
|
|
105
|
+
else
|
|
106
|
+
# For non-standard actions on body verbs, don't infer
|
|
107
|
+
case verb
|
|
108
|
+
when 'POST' then 'create'
|
|
109
|
+
when 'PUT', 'PATCH' then 'update'
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def extract_singular_resource(route)
|
|
115
|
+
# Extract the resource name from the path
|
|
116
|
+
segments = route[:path].split('/').reject(&:empty?)
|
|
117
|
+
# Find the last non-param segment that looks like a resource
|
|
118
|
+
resource_segments = segments.reject { |s| s.start_with?('{', ':') }
|
|
119
|
+
return nil if resource_segments.empty?
|
|
120
|
+
|
|
121
|
+
resource = resource_segments.last
|
|
122
|
+
Belt::Inflector.singularize(resource.gsub('-', '_'))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def body_accepting_verb?(verb)
|
|
126
|
+
%w[POST PUT PATCH].include?(verb)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -7,12 +7,14 @@ require_relative '../route_dsl'
|
|
|
7
7
|
require_relative '../table_inference'
|
|
8
8
|
require_relative 'routes_command/schema_loader'
|
|
9
9
|
require_relative 'routes_command/route_inference'
|
|
10
|
+
require_relative 'routes_command/request_model_inference'
|
|
10
11
|
|
|
11
12
|
module Belt
|
|
12
13
|
module CLI
|
|
13
14
|
class RoutesCommand
|
|
14
15
|
include SchemaLoader
|
|
15
16
|
include RouteInference
|
|
17
|
+
include RequestModelInference
|
|
16
18
|
|
|
17
19
|
def self.run(args)
|
|
18
20
|
new(args).run
|
|
@@ -33,6 +35,11 @@ module Belt
|
|
|
33
35
|
dsl = load_routes(routes_file)
|
|
34
36
|
@table_inference = TableInference.new(@options[:tables_file])
|
|
35
37
|
routes = collect_routes(dsl)
|
|
38
|
+
|
|
39
|
+
# Convention-based request_model inference from contracts
|
|
40
|
+
contracts_file = resolve_contracts_file(routes_file)
|
|
41
|
+
infer_request_models!(routes, contracts_file) if contracts_file
|
|
42
|
+
|
|
36
43
|
routes = apply_grep(routes) if @options[:grep]
|
|
37
44
|
|
|
38
45
|
warn 'Warning: --output-dir has no effect without --namespace' if @options[:output_dir] && !@options[:namespace]
|
data/lib/belt/route_dsl.rb
CHANGED
|
@@ -106,17 +106,37 @@ module Belt
|
|
|
106
106
|
private
|
|
107
107
|
|
|
108
108
|
def add_nested_resource_routes(resource_name, param_name, resource_options, actions)
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
if actions.include?(:index)
|
|
110
|
+
@gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}",
|
|
111
|
+
resolve_request_model_for(resource_options, :index))
|
|
112
|
+
end
|
|
113
|
+
if actions.include?(:create)
|
|
114
|
+
@gateway.send(:add_route, :post, "#{@prefix}/#{resource_name}",
|
|
115
|
+
resolve_request_model_for(resource_options, :create))
|
|
116
|
+
end
|
|
111
117
|
if actions.include?(:show)
|
|
112
|
-
@gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}/{#{param_name}}",
|
|
118
|
+
@gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}/{#{param_name}}",
|
|
119
|
+
resolve_request_model_for(resource_options, :show))
|
|
113
120
|
end
|
|
114
121
|
if actions.include?(:update)
|
|
115
|
-
@gateway.send(:add_route, :put, "#{@prefix}/#{resource_name}/{#{param_name}}",
|
|
122
|
+
@gateway.send(:add_route, :put, "#{@prefix}/#{resource_name}/{#{param_name}}",
|
|
123
|
+
resolve_request_model_for(resource_options, :update))
|
|
116
124
|
end
|
|
117
125
|
return unless actions.include?(:destroy)
|
|
118
126
|
|
|
119
|
-
@gateway.send(:add_route, :delete, "#{@prefix}/#{resource_name}/{#{param_name}}",
|
|
127
|
+
@gateway.send(:add_route, :delete, "#{@prefix}/#{resource_name}/{#{param_name}}",
|
|
128
|
+
resolve_request_model_for(resource_options, :destroy))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def resolve_request_model_for(options, action)
|
|
132
|
+
rm = options[:request_model]
|
|
133
|
+
case rm
|
|
134
|
+
when Hash
|
|
135
|
+
resolved = rm[action] || rm[action.to_s]
|
|
136
|
+
options.merge(request_model: resolved)
|
|
137
|
+
else
|
|
138
|
+
options
|
|
139
|
+
end
|
|
120
140
|
end
|
|
121
141
|
|
|
122
142
|
def merge_inherited_options(options)
|
|
@@ -200,11 +220,7 @@ module Belt
|
|
|
200
220
|
resource_options = options.merge(route_type: :resources)
|
|
201
221
|
actions = determine_actions(options)
|
|
202
222
|
|
|
203
|
-
|
|
204
|
-
add_route(:post, "/#{resource_name}", resource_options) if actions.include?(:create)
|
|
205
|
-
add_route(:get, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:show)
|
|
206
|
-
add_route(:put, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:update)
|
|
207
|
-
add_route(:delete, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:destroy)
|
|
223
|
+
add_resource_routes(resource_name, param_name, resource_options, actions)
|
|
208
224
|
|
|
209
225
|
return unless block_given?
|
|
210
226
|
|
|
@@ -226,10 +242,22 @@ module Belt
|
|
|
226
242
|
actions = determine_actions(options, default: %i[show update destroy])
|
|
227
243
|
resource_options = options.merge(route_type: :resource)
|
|
228
244
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
245
|
+
if actions.include?(:show)
|
|
246
|
+
add_route(:get, "/#{resource_name}",
|
|
247
|
+
resolve_request_model_for(resource_options, :show))
|
|
248
|
+
end
|
|
249
|
+
if actions.include?(:update)
|
|
250
|
+
add_route(:put, "/#{resource_name}",
|
|
251
|
+
resolve_request_model_for(resource_options, :update))
|
|
252
|
+
end
|
|
253
|
+
if actions.include?(:destroy)
|
|
254
|
+
add_route(:delete, "/#{resource_name}",
|
|
255
|
+
resolve_request_model_for(resource_options, :destroy))
|
|
256
|
+
end
|
|
257
|
+
return unless actions.include?(:create)
|
|
258
|
+
|
|
259
|
+
add_route(:post, "/#{resource_name}",
|
|
260
|
+
resolve_request_model_for(resource_options, :create))
|
|
233
261
|
end
|
|
234
262
|
|
|
235
263
|
private
|
|
@@ -271,6 +299,39 @@ module Belt
|
|
|
271
299
|
options.merge(tables: [resource_name.to_sym])
|
|
272
300
|
end
|
|
273
301
|
|
|
302
|
+
def add_resource_routes(resource_name, param_name, resource_options, actions)
|
|
303
|
+
if actions.include?(:index)
|
|
304
|
+
add_route(:get, "/#{resource_name}",
|
|
305
|
+
resolve_request_model_for(resource_options, :index))
|
|
306
|
+
end
|
|
307
|
+
if actions.include?(:create)
|
|
308
|
+
add_route(:post, "/#{resource_name}",
|
|
309
|
+
resolve_request_model_for(resource_options, :create))
|
|
310
|
+
end
|
|
311
|
+
if actions.include?(:show)
|
|
312
|
+
add_route(:get, "/#{resource_name}/{#{param_name}}",
|
|
313
|
+
resolve_request_model_for(resource_options, :show))
|
|
314
|
+
end
|
|
315
|
+
if actions.include?(:update)
|
|
316
|
+
add_route(:put, "/#{resource_name}/{#{param_name}}",
|
|
317
|
+
resolve_request_model_for(resource_options, :update))
|
|
318
|
+
end
|
|
319
|
+
return unless actions.include?(:destroy)
|
|
320
|
+
|
|
321
|
+
add_route(:delete, "/#{resource_name}/{#{param_name}}", resolve_request_model_for(resource_options, :destroy))
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def resolve_request_model_for(options, action)
|
|
325
|
+
rm = options[:request_model]
|
|
326
|
+
case rm
|
|
327
|
+
when Hash
|
|
328
|
+
resolved = rm[action] || rm[action.to_s]
|
|
329
|
+
options.merge(request_model: resolved)
|
|
330
|
+
else
|
|
331
|
+
options
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
274
335
|
def determine_actions(options, default: %i[index create show update destroy])
|
|
275
336
|
if options[:only]
|
|
276
337
|
Array(options[:only])
|
|
@@ -540,17 +601,37 @@ module Belt
|
|
|
540
601
|
end
|
|
541
602
|
|
|
542
603
|
def add_scoped_resource_routes(resource_name, param_name, resource_options, actions)
|
|
543
|
-
|
|
544
|
-
|
|
604
|
+
if actions.include?(:index)
|
|
605
|
+
@gateway.send(:add_route, :get, build_path("/#{resource_name}"),
|
|
606
|
+
resolve_request_model_for(resource_options, :index))
|
|
607
|
+
end
|
|
608
|
+
if actions.include?(:create)
|
|
609
|
+
@gateway.send(:add_route, :post, build_path("/#{resource_name}"),
|
|
610
|
+
resolve_request_model_for(resource_options, :create))
|
|
611
|
+
end
|
|
545
612
|
if actions.include?(:show)
|
|
546
|
-
@gateway.send(:add_route, :get, build_path("/#{resource_name}/{#{param_name}}"),
|
|
613
|
+
@gateway.send(:add_route, :get, build_path("/#{resource_name}/{#{param_name}}"),
|
|
614
|
+
resolve_request_model_for(resource_options, :show))
|
|
547
615
|
end
|
|
548
616
|
if actions.include?(:update)
|
|
549
|
-
@gateway.send(:add_route, :put, build_path("/#{resource_name}/{#{param_name}}"),
|
|
617
|
+
@gateway.send(:add_route, :put, build_path("/#{resource_name}/{#{param_name}}"),
|
|
618
|
+
resolve_request_model_for(resource_options, :update))
|
|
550
619
|
end
|
|
551
620
|
return unless actions.include?(:destroy)
|
|
552
621
|
|
|
553
|
-
@gateway.send(:add_route, :delete, build_path("/#{resource_name}/{#{param_name}}"),
|
|
622
|
+
@gateway.send(:add_route, :delete, build_path("/#{resource_name}/{#{param_name}}"),
|
|
623
|
+
resolve_request_model_for(resource_options, :destroy))
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def resolve_request_model_for(options, action)
|
|
627
|
+
rm = options[:request_model]
|
|
628
|
+
case rm
|
|
629
|
+
when Hash
|
|
630
|
+
resolved = rm[action] || rm[action.to_s]
|
|
631
|
+
options.merge(request_model: resolved)
|
|
632
|
+
else
|
|
633
|
+
options
|
|
634
|
+
end
|
|
554
635
|
end
|
|
555
636
|
|
|
556
637
|
def build_scoped_resources(name, options, &block)
|
|
@@ -585,13 +666,22 @@ module Belt
|
|
|
585
666
|
resource_options = options.merge(route_type: :resource, controller: controller)
|
|
586
667
|
actions = determine_scoped_actions(options, default: %i[show update destroy create])
|
|
587
668
|
|
|
588
|
-
|
|
589
|
-
|
|
669
|
+
if actions.include?(:show)
|
|
670
|
+
@gateway.send(:add_route, :get, build_path("/#{resource_name}"),
|
|
671
|
+
resolve_request_model_for(resource_options, :show))
|
|
672
|
+
end
|
|
673
|
+
if actions.include?(:update)
|
|
674
|
+
@gateway.send(:add_route, :put, build_path("/#{resource_name}"),
|
|
675
|
+
resolve_request_model_for(resource_options, :update))
|
|
676
|
+
end
|
|
590
677
|
if actions.include?(:destroy)
|
|
591
678
|
@gateway.send(:add_route, :delete, build_path("/#{resource_name}"),
|
|
592
|
-
resource_options)
|
|
679
|
+
resolve_request_model_for(resource_options, :destroy))
|
|
593
680
|
end
|
|
594
|
-
|
|
681
|
+
return unless actions.include?(:create)
|
|
682
|
+
|
|
683
|
+
@gateway.send(:add_route, :post, build_path("/#{resource_name}"),
|
|
684
|
+
resolve_request_model_for(resource_options, :create))
|
|
595
685
|
end
|
|
596
686
|
|
|
597
687
|
def apply_scope_to_route(options)
|
data/lib/belt/version.rb
CHANGED
|
@@ -161,6 +161,35 @@ belt doctor # Check dependencies
|
|
|
161
161
|
|
|
162
162
|
4. `resources :things` generates: `GET /things`, `POST /things`, `GET /things/:id`, `PUT /things/:id`, `DELETE /things/:id`. **PUT, not PATCH.**
|
|
163
163
|
|
|
164
|
+
### Request Model Wiring (Validation)
|
|
165
|
+
|
|
166
|
+
Belt auto-wires request validation from `config/contracts.rb` using conventions:
|
|
167
|
+
|
|
168
|
+
- `POST /items` → looks for `:create_item` (or `:create_<gateway>_item`)
|
|
169
|
+
- `PUT /items/:id` → looks for `:update_item` (or `:update_<gateway>_item`)
|
|
170
|
+
|
|
171
|
+
**No `request_model:` declaration needed** if your contract names follow this pattern. Belt infers it.
|
|
172
|
+
|
|
173
|
+
### Response Model Wiring (Documentation)
|
|
174
|
+
|
|
175
|
+
Belt auto-wires response models the same way:
|
|
176
|
+
|
|
177
|
+
- `resources :items` → looks for `model :item` in contracts → auto-wires `response_model: :item`
|
|
178
|
+
|
|
179
|
+
**No `response_model:` declaration needed** if your model name matches the singular resource name.
|
|
180
|
+
|
|
181
|
+
### Overrides
|
|
182
|
+
|
|
183
|
+
For per-action overrides when conventions don't fit:
|
|
184
|
+
```ruby
|
|
185
|
+
resources :items, request_model: { create: :create_item, update: :update_item }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
For non-CRUD custom actions, use explicit wiring:
|
|
189
|
+
```ruby
|
|
190
|
+
post '/schedule', request_model: :schedule_pickup
|
|
191
|
+
```
|
|
192
|
+
|
|
164
193
|
## How Models Work
|
|
165
194
|
|
|
166
195
|
Models use ActiveItem (DynamoDB ORM):
|
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.3.
|
|
4
|
+
version: 0.3.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
@@ -132,6 +132,7 @@ files:
|
|
|
132
132
|
- lib/belt/cli/path_gem_materializer.rb
|
|
133
133
|
- lib/belt/cli/plugin_command.rb
|
|
134
134
|
- lib/belt/cli/routes_command.rb
|
|
135
|
+
- lib/belt/cli/routes_command/request_model_inference.rb
|
|
135
136
|
- lib/belt/cli/routes_command/route_inference.rb
|
|
136
137
|
- lib/belt/cli/routes_command/schema_loader.rb
|
|
137
138
|
- lib/belt/cli/server_command.rb
|