belt 0.3.36 → 0.3.38
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 +116 -0
- data/README.md +47 -0
- data/lib/belt/cli/destroy_command.rb +7 -1
- data/lib/belt/docs/routing.md +105 -0
- data/lib/belt/route_dsl.rb +205 -8
- data/lib/belt/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e5d53661a4b6d4cc3c041c12f7ba7bd68683319eaf40d8a31c160c49d832014
|
|
4
|
+
data.tar.gz: 1e0ea7568414ad29a6219ae5b1c2f3506d589df6cfedce365f3feaee6aae44a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c9abf953818eacb858d5dda98c293c752ef07aaeef977e6914ddc6bccbbacd19992f8a762de131453fc8adf3cdbd121198e83d319961dee8e6cecb285a92f28
|
|
7
|
+
data.tar.gz: 8e09702c796419dcd1359b48ab6e7d30b41e233e737d834a35447c03483e62ac5474a6d6af3260dad9e5d60984c8e5c4127a491aaa6e8a8ae7642ab40babe9e9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,121 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.38
|
|
4
|
+
|
|
5
|
+
### Enhancement
|
|
6
|
+
|
|
7
|
+
- **Rails-like `namespace` inside nested resources**: The `NestedResourceBuilder`
|
|
8
|
+
now supports `namespace` blocks that add both a path prefix AND a controller
|
|
9
|
+
module prefix, just like Rails. This provides 99% Rails compatibility for
|
|
10
|
+
nested route organization.
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
resources :projects do
|
|
14
|
+
namespace :admin do
|
|
15
|
+
resources :users # → /projects/:project_id/admin/users → admin/users controller
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Namespaces can be nested and inherit auth/tables options:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
resources :projects do
|
|
24
|
+
namespace :admin, auth: :iam, tables: [:audit_log] do
|
|
25
|
+
namespace :v2 do
|
|
26
|
+
resources :settings, only: [:index] # → admin/v2/settings controller
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **`scope module:` option inside nested resources**: The existing `scope` method
|
|
33
|
+
now supports the `module:` option for Rails-like controller module prefixing
|
|
34
|
+
without adding a path prefix.
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
resources :projects do
|
|
38
|
+
scope module: 'v2' do
|
|
39
|
+
resources :users # → /projects/:project_id/users → v2/users controller
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 0.3.37
|
|
45
|
+
|
|
46
|
+
### Enhancement
|
|
47
|
+
|
|
48
|
+
- **DRYer routing DSL with Rails-like `scope` inside nested resources**: The
|
|
49
|
+
`NestedResourceBuilder` now supports `scope` blocks for grouping routes with
|
|
50
|
+
shared options (path prefix, controller, tables, auth). This enables much
|
|
51
|
+
cleaner route definitions when multiple routes share the same controller or
|
|
52
|
+
tables.
|
|
53
|
+
|
|
54
|
+
**Before:**
|
|
55
|
+
```ruby
|
|
56
|
+
resources :projects do
|
|
57
|
+
get 'billing', controller: :billing, action: :show, tables: [:memberships]
|
|
58
|
+
post 'billing/checkout', controller: :billing, action: :checkout, tables: [:memberships]
|
|
59
|
+
post 'billing/subscribe', controller: :billing, action: :subscribe, tables: [:memberships]
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**After:**
|
|
64
|
+
```ruby
|
|
65
|
+
resources :projects do
|
|
66
|
+
scope path: 'billing', controller: :billing, tables: [:memberships] do
|
|
67
|
+
get '/', action: :show
|
|
68
|
+
post :checkout
|
|
69
|
+
post :subscribe
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **Singular `resource` inside nested resources**: You can now use `resource`
|
|
75
|
+
(singular) inside a `resources` block for nested singular resources like
|
|
76
|
+
`:billing`, `:token_usage`, or `:profile`.
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
resources :projects do
|
|
80
|
+
resource :billing, only: [:show], tables: [:memberships]
|
|
81
|
+
resource :token_usage, only: [:show]
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- **Action inference from path**: When using symbol arguments or simple string
|
|
86
|
+
paths, the action is now inferred automatically. This works in `member`,
|
|
87
|
+
`collection`, and direct route definitions within resources.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
resources :webhooks do
|
|
91
|
+
member do
|
|
92
|
+
post :test # action: :test inferred
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
resources :surfaces do
|
|
97
|
+
collection do
|
|
98
|
+
get :teams # action: :teams inferred
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
resources :projects do
|
|
103
|
+
post 'mark-complete' # action: :mark_complete inferred (hyphens → underscores)
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- **Controller inheritance in member/collection blocks**: Routes defined in
|
|
108
|
+
`member` and `collection` blocks now properly inherit the parent resource's
|
|
109
|
+
controller. This was inconsistent before and sometimes returned `nil`.
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
resources :webhooks do
|
|
113
|
+
member do
|
|
114
|
+
post :test # controller: 'webhooks' inherited
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
3
119
|
## 0.3.32
|
|
4
120
|
|
|
5
121
|
### Enhancement
|
data/README.md
CHANGED
|
@@ -209,6 +209,53 @@ end
|
|
|
209
209
|
|
|
210
210
|
**Key point:** `namespace` and `scope` are purely organizational — they affect URL paths and controller module resolution but never change which Lambda handles the request. Use `function` when you need routes to go to a different Lambda.
|
|
211
211
|
|
|
212
|
+
### Nested Resource DSL
|
|
213
|
+
|
|
214
|
+
Inside a `resources` block, you can use Rails-like `member`, `collection`, `scope`, and singular `resource` for DRYer route definitions:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
Belt.application.routes.draw do
|
|
218
|
+
gateway :api do
|
|
219
|
+
resources :projects do
|
|
220
|
+
# Singular nested resource
|
|
221
|
+
resource :billing, only: [:show], tables: [:memberships]
|
|
222
|
+
resource :token_usage, only: [:show]
|
|
223
|
+
|
|
224
|
+
# Member routes (include /:id/)
|
|
225
|
+
resources :webhooks do
|
|
226
|
+
member do
|
|
227
|
+
post :test # → POST /projects/:project_id/webhooks/:webhook_id/test
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Collection routes (no /:id/)
|
|
232
|
+
resources :surfaces do
|
|
233
|
+
collection do
|
|
234
|
+
get :teams # → GET /projects/:project_id/surfaces/teams
|
|
235
|
+
end
|
|
236
|
+
member do
|
|
237
|
+
put :assign # → PUT /projects/:project_id/surfaces/:surface_id/assign
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Scope groups routes with shared options
|
|
242
|
+
scope path: 'billing', controller: :billing, tables: [:memberships] do
|
|
243
|
+
get '/', action: :show # → GET /projects/:project_id/billing
|
|
244
|
+
post :checkout # → POST /projects/:project_id/billing/checkout
|
|
245
|
+
post :subscribe
|
|
246
|
+
post :cancel
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Key features:**
|
|
254
|
+
- **Action inference**: `:checkout` means both path segment and action name
|
|
255
|
+
- **Controller inheritance**: `member` and `collection` inherit parent resource's controller
|
|
256
|
+
- **Scope in nested context**: Group routes with shared path prefix, controller, tables, or auth
|
|
257
|
+
- **Singular `resource`**: For nested resources without an `:id` (billing, profile, etc.)
|
|
258
|
+
|
|
212
259
|
## BeltController Features
|
|
213
260
|
|
|
214
261
|
### Callbacks
|
|
@@ -148,7 +148,13 @@ module Belt
|
|
|
148
148
|
# -- keyword options for destroy flags
|
|
149
149
|
def initialize(generator, name, fields, force: false, skip_terraform: false, full: false, frontend_name: nil)
|
|
150
150
|
@generator = generator
|
|
151
|
-
|
|
151
|
+
# Environment names preserve hyphens (matching `belt g environment`);
|
|
152
|
+
# other generators normalize to underscores for valid Ruby identifiers.
|
|
153
|
+
@name = if generator == 'environment'
|
|
154
|
+
name&.downcase&.gsub(/[^a-z0-9_-]/, '')
|
|
155
|
+
else
|
|
156
|
+
name&.downcase&.gsub(/[^a-z0-9_]/, '_')
|
|
157
|
+
end
|
|
152
158
|
@fields = fields
|
|
153
159
|
@force = force
|
|
154
160
|
@skip_terraform = skip_terraform
|
data/lib/belt/docs/routing.md
CHANGED
|
@@ -57,6 +57,111 @@ resources :comments, except: [:destroy]
|
|
|
57
57
|
| POST | /profile | create |
|
|
58
58
|
| DELETE | /profile | destroy |
|
|
59
59
|
|
|
60
|
+
## Nested Resource DSL
|
|
61
|
+
|
|
62
|
+
Inside a `resources` block, you can use Rails-like `member`, `collection`, `scope`,
|
|
63
|
+
`namespace`, and singular `resource` for DRYer route definitions:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
Belt.application.routes.draw do
|
|
67
|
+
gateway :api do
|
|
68
|
+
resources :projects do
|
|
69
|
+
# Singular nested resource (no :id in path)
|
|
70
|
+
resource :billing, only: [:show], tables: [:memberships]
|
|
71
|
+
resource :token_usage, only: [:show]
|
|
72
|
+
|
|
73
|
+
# Member routes (include /:id/)
|
|
74
|
+
resources :webhooks do
|
|
75
|
+
member do
|
|
76
|
+
post :test # → POST /projects/:project_id/webhooks/:webhook_id/test
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Collection routes (no /:id/)
|
|
81
|
+
resources :surfaces do
|
|
82
|
+
collection do
|
|
83
|
+
get :teams # → GET /projects/:project_id/surfaces/teams
|
|
84
|
+
end
|
|
85
|
+
member do
|
|
86
|
+
put :assign # → PUT /projects/:project_id/surfaces/:surface_id/assign
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Scope: groups routes with shared options
|
|
91
|
+
scope path: 'billing', controller: :billing, tables: [:memberships] do
|
|
92
|
+
get '/', action: :show # → GET /projects/:project_id/billing
|
|
93
|
+
post :checkout # → POST /projects/:project_id/billing/checkout
|
|
94
|
+
post :subscribe
|
|
95
|
+
post :cancel
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Namespace: adds path prefix AND controller module
|
|
99
|
+
namespace :admin do
|
|
100
|
+
resources :users # → /projects/:project_id/admin/users → admin/users controller
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Action Inference
|
|
108
|
+
|
|
109
|
+
Symbol paths automatically become the action name:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
member do
|
|
113
|
+
post :test # path: /test, action: :test
|
|
114
|
+
end
|
|
115
|
+
post 'mark-complete' # path: /mark-complete, action: :mark_complete
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Hyphens in path segments convert to underscores in action names.
|
|
119
|
+
|
|
120
|
+
### Controller Inheritance
|
|
121
|
+
|
|
122
|
+
Routes inside `member` and `collection` blocks inherit the parent resource's controller:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
resources :surfaces do
|
|
126
|
+
member do
|
|
127
|
+
put :assign # controller: surfaces, action: assign
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Override with the `controller:` option:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
member do
|
|
136
|
+
get :billing, controller: :project_billing
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Namespace vs Scope
|
|
141
|
+
|
|
142
|
+
| Feature | `namespace` | `scope` |
|
|
143
|
+
|---------|-------------|---------|
|
|
144
|
+
| Path prefix | ✓ | Optional (`path:`) |
|
|
145
|
+
| Controller module | ✓ | Optional (`module:`) |
|
|
146
|
+
| Use case | Rails-like module nesting | Flexible grouping |
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
# Namespace: path + controller module
|
|
150
|
+
namespace :admin do
|
|
151
|
+
resources :users # → /admin/users → admin/users controller
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Scope with path only (no controller change)
|
|
155
|
+
scope path: 'v2' do
|
|
156
|
+
resources :users # → /v2/users → users controller
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Scope with module only (no path change)
|
|
160
|
+
scope module: 'legacy' do
|
|
161
|
+
resources :users # → /users → legacy/users controller
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
60
165
|
## Namespace and Scope
|
|
61
166
|
|
|
62
167
|
```ruby
|
data/lib/belt/route_dsl.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Belt
|
|
|
52
52
|
|
|
53
53
|
class NestedResourceBuilder
|
|
54
54
|
def initialize(gateway, prefix, collection_prefix, inherited_tables: [], inherited_auth: nil, # rubocop:disable Metrics/ParameterLists
|
|
55
|
-
inherited_controller: nil, inherited_lambda: nil)
|
|
55
|
+
inherited_controller: nil, inherited_lambda: nil, scope_module: nil)
|
|
56
56
|
@gateway = gateway
|
|
57
57
|
@prefix = prefix
|
|
58
58
|
@collection_prefix = collection_prefix
|
|
@@ -60,6 +60,7 @@ module Belt
|
|
|
60
60
|
@inherited_auth = inherited_auth
|
|
61
61
|
@inherited_controller = inherited_controller
|
|
62
62
|
@inherited_lambda = inherited_lambda
|
|
63
|
+
@scope_module = scope_module
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
def resources(name, options = {}, &block)
|
|
@@ -67,7 +68,10 @@ module Belt
|
|
|
67
68
|
singular = @gateway.send(:singularize, resource_name)
|
|
68
69
|
param_name = options[:param] || "#{singular}_id"
|
|
69
70
|
options = options.merge(tables: [resource_name.to_sym]) unless options.key?(:tables)
|
|
70
|
-
|
|
71
|
+
# Compute controller FIRST: explicit > scope_module/resource > resource_name
|
|
72
|
+
# Don't inherit controller from parent resource — nested resources get their own controller
|
|
73
|
+
controller_name = options[:controller] || compute_controller_name(resource_name)
|
|
74
|
+
options = merge_inherited_options(options.merge(controller: controller_name))
|
|
71
75
|
resource_options = options.merge(route_type: :resources)
|
|
72
76
|
actions = @gateway.send(:determine_actions, options)
|
|
73
77
|
|
|
@@ -76,14 +80,107 @@ module Belt
|
|
|
76
80
|
|
|
77
81
|
nested_member_prefix = "#{@prefix}/#{resource_name}/{#{param_name}}"
|
|
78
82
|
nested_collection_prefix = "#{@prefix}/#{resource_name}"
|
|
83
|
+
# Pass scope_module to nested builder so nested resources inherit it
|
|
79
84
|
nested_builder = NestedResourceBuilder.new(@gateway, nested_member_prefix, nested_collection_prefix,
|
|
80
85
|
inherited_tables: Array(options[:tables] || []),
|
|
81
86
|
inherited_auth: options[:auth] || @inherited_auth,
|
|
82
|
-
inherited_controller:
|
|
83
|
-
inherited_lambda: @inherited_lambda
|
|
87
|
+
inherited_controller: controller_name,
|
|
88
|
+
inherited_lambda: @inherited_lambda,
|
|
89
|
+
scope_module: @scope_module)
|
|
84
90
|
nested_builder.instance_eval(&block)
|
|
85
91
|
end
|
|
86
92
|
|
|
93
|
+
# Singular resource inside nested context (e.g. resource :billing inside resources :projects)
|
|
94
|
+
def resource(name, options = {})
|
|
95
|
+
resource_name = name.to_s
|
|
96
|
+
# Compute controller FIRST: explicit > scope_module/resource > resource_name
|
|
97
|
+
# Don't inherit controller from parent resource — singular resources get their own controller
|
|
98
|
+
controller_name = options[:controller] || compute_controller_name(resource_name)
|
|
99
|
+
options = merge_inherited_options(options.merge(controller: controller_name))
|
|
100
|
+
resource_options = options.merge(route_type: :resource)
|
|
101
|
+
actions = determine_actions(options, default: %i[show update destroy create])
|
|
102
|
+
|
|
103
|
+
if actions.include?(:show)
|
|
104
|
+
@gateway.send(:add_route, :get, join_path(@prefix, resource_name),
|
|
105
|
+
resolve_request_model_for(resource_options, :show))
|
|
106
|
+
end
|
|
107
|
+
if actions.include?(:update)
|
|
108
|
+
@gateway.send(:add_route, :put, join_path(@prefix, resource_name),
|
|
109
|
+
resolve_request_model_for(resource_options, :update))
|
|
110
|
+
end
|
|
111
|
+
if actions.include?(:destroy)
|
|
112
|
+
@gateway.send(:add_route, :delete, join_path(@prefix, resource_name),
|
|
113
|
+
resolve_request_model_for(resource_options, :destroy))
|
|
114
|
+
end
|
|
115
|
+
return unless actions.include?(:create)
|
|
116
|
+
|
|
117
|
+
@gateway.send(:add_route, :post, join_path(@prefix, resource_name),
|
|
118
|
+
resolve_request_model_for(resource_options, :create))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Rails-like `namespace` inside nested resource context — adds both a path prefix
|
|
122
|
+
# AND sets the controller module. Resources inside inherit the namespace module.
|
|
123
|
+
#
|
|
124
|
+
# Example:
|
|
125
|
+
# resources :projects do
|
|
126
|
+
# namespace :admin do
|
|
127
|
+
# resources :users # → /projects/:project_id/admin/users → admin/users controller
|
|
128
|
+
# end
|
|
129
|
+
# end
|
|
130
|
+
def namespace(name, options = {}, &)
|
|
131
|
+
segment = name.to_s
|
|
132
|
+
# namespace sets path AND module (controller prefix)
|
|
133
|
+
merged = { path: segment, module: segment }.merge(options)
|
|
134
|
+
scope(merged, &)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Scope inside nested resource context - groups routes with shared options
|
|
138
|
+
# Example:
|
|
139
|
+
# resources :projects do
|
|
140
|
+
# scope path: 'billing', controller: :billing, tables: [:memberships] do
|
|
141
|
+
# get '/', action: :show
|
|
142
|
+
# post :checkout
|
|
143
|
+
# end
|
|
144
|
+
# end
|
|
145
|
+
#
|
|
146
|
+
# With module option (like Rails):
|
|
147
|
+
# resources :projects do
|
|
148
|
+
# scope module: 'v2' do
|
|
149
|
+
# resources :users # → /projects/:project_id/users → v2/users controller
|
|
150
|
+
# end
|
|
151
|
+
# end
|
|
152
|
+
def scope(options = {}, &)
|
|
153
|
+
previous_prefix = @prefix
|
|
154
|
+
previous_collection_prefix = @collection_prefix
|
|
155
|
+
previous_tables = @inherited_tables
|
|
156
|
+
previous_auth = @inherited_auth
|
|
157
|
+
previous_controller = @inherited_controller
|
|
158
|
+
previous_module = @scope_module
|
|
159
|
+
|
|
160
|
+
if options.key?(:path)
|
|
161
|
+
segment = options[:path].to_s.gsub(%r{^/|/$}, '')
|
|
162
|
+
@prefix = join_path(@prefix, segment)
|
|
163
|
+
@collection_prefix = join_path(@collection_prefix, segment)
|
|
164
|
+
end
|
|
165
|
+
# Module sets the controller prefix for nested resources (like Rails)
|
|
166
|
+
if options.key?(:module)
|
|
167
|
+
mod_segment = options[:module].to_s
|
|
168
|
+
@scope_module = @scope_module.to_s.empty? ? mod_segment : "#{@scope_module}/#{mod_segment}"
|
|
169
|
+
end
|
|
170
|
+
@inherited_auth = options[:auth] || @inherited_auth
|
|
171
|
+
@inherited_tables = (@inherited_tables + Array(options[:tables] || [])).uniq
|
|
172
|
+
@inherited_controller = options[:controller]&.to_s || @inherited_controller
|
|
173
|
+
|
|
174
|
+
instance_eval(&) if block_given?
|
|
175
|
+
|
|
176
|
+
@prefix = previous_prefix
|
|
177
|
+
@collection_prefix = previous_collection_prefix
|
|
178
|
+
@inherited_tables = previous_tables
|
|
179
|
+
@inherited_auth = previous_auth
|
|
180
|
+
@inherited_controller = previous_controller
|
|
181
|
+
@scope_module = previous_module
|
|
182
|
+
end
|
|
183
|
+
|
|
87
184
|
def member(&)
|
|
88
185
|
MemberCollectionBuilder.new(@gateway, @prefix, @inherited_tables, @inherited_auth,
|
|
89
186
|
@inherited_controller, @inherited_lambda).instance_eval(&)
|
|
@@ -95,16 +192,65 @@ module Belt
|
|
|
95
192
|
end
|
|
96
193
|
|
|
97
194
|
%i[get post put delete patch].each do |method|
|
|
98
|
-
define_method(method) do |
|
|
99
|
-
|
|
195
|
+
define_method(method) do |path_or_action, options = {}|
|
|
196
|
+
# Support both symbol (action name = path) and string (explicit path)
|
|
197
|
+
path, action = normalize_path_and_action(path_or_action, options)
|
|
198
|
+
base = options[:on] == :collection ? @collection_prefix : @prefix
|
|
199
|
+
full_path = join_path(base, path)
|
|
100
200
|
options = merge_inherited_options(options)
|
|
101
201
|
route_options = options.except(:on)
|
|
202
|
+
route_options[:action] ||= action if action
|
|
102
203
|
@gateway.send(:add_route, method, full_path, route_options)
|
|
103
204
|
end
|
|
104
205
|
end
|
|
105
206
|
|
|
106
207
|
private
|
|
107
208
|
|
|
209
|
+
# Normalize path/action: symbol means path=action, string means infer action from path
|
|
210
|
+
def normalize_path_and_action(path_or_action, options)
|
|
211
|
+
return [options[:path] || '', options[:action]] if path_or_action.nil?
|
|
212
|
+
|
|
213
|
+
if path_or_action.is_a?(Symbol)
|
|
214
|
+
# Symbol: use as both path segment and action name (e.g. :checkout → path: 'checkout', action: :checkout)
|
|
215
|
+
[path_or_action.to_s, options[:action] || path_or_action]
|
|
216
|
+
else
|
|
217
|
+
path = path_or_action.to_s
|
|
218
|
+
# Infer action from last non-param segment of path (e.g. 'checkout' → :checkout, '/' → nil)
|
|
219
|
+
inferred_action = options[:action] || infer_action_from_path(path)
|
|
220
|
+
[path, inferred_action]
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Infer action name from path: 'checkout' → :checkout, 'billing/checkout' → :checkout
|
|
225
|
+
def infer_action_from_path(path)
|
|
226
|
+
return nil if path.nil? || path.empty? || path == '/'
|
|
227
|
+
|
|
228
|
+
segments = path.gsub(%r{^/|/$}, '').split('/')
|
|
229
|
+
# Find last segment that's not a parameter (doesn't contain {})
|
|
230
|
+
last_segment = segments.reverse.find { |s| !s.include?('{') }
|
|
231
|
+
last_segment&.tr('-', '_')&.to_sym
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Join a base path with a relative path, ensuring exactly one `/` separator.
|
|
235
|
+
# Handles "/" path by returning just the base (no trailing slash).
|
|
236
|
+
def join_path(base, path)
|
|
237
|
+
path = path.to_s
|
|
238
|
+
return base if path.empty? || path == '/'
|
|
239
|
+
return "#{base}#{path}".chomp('/') if path.start_with?('/')
|
|
240
|
+
|
|
241
|
+
"#{base}/#{path}"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def determine_actions(options, default: %i[index create show update destroy])
|
|
245
|
+
if options[:only]
|
|
246
|
+
Array(options[:only])
|
|
247
|
+
elsif options[:except]
|
|
248
|
+
default - Array(options[:except])
|
|
249
|
+
else
|
|
250
|
+
default
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
108
254
|
def add_nested_resource_routes(resource_name, param_name, resource_options, actions)
|
|
109
255
|
if actions.include?(:index)
|
|
110
256
|
@gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}",
|
|
@@ -150,6 +296,17 @@ module Belt
|
|
|
150
296
|
result[:lambda] ||= @inherited_lambda if @inherited_lambda
|
|
151
297
|
result
|
|
152
298
|
end
|
|
299
|
+
|
|
300
|
+
# Compute controller name from scope_module and resource name
|
|
301
|
+
# If scope_module is set, returns "module/resource_name" (Rails-like)
|
|
302
|
+
# Otherwise returns just resource_name
|
|
303
|
+
def compute_controller_name(resource_name)
|
|
304
|
+
if @scope_module && !@scope_module.empty?
|
|
305
|
+
"#{@scope_module}/#{resource_name}"
|
|
306
|
+
else
|
|
307
|
+
resource_name
|
|
308
|
+
end
|
|
309
|
+
end
|
|
153
310
|
end
|
|
154
311
|
|
|
155
312
|
class MemberCollectionBuilder
|
|
@@ -164,15 +321,53 @@ module Belt
|
|
|
164
321
|
end
|
|
165
322
|
|
|
166
323
|
%i[get post put delete patch].each do |method|
|
|
167
|
-
define_method(method) do |
|
|
168
|
-
|
|
324
|
+
define_method(method) do |path_or_action, options = {}|
|
|
325
|
+
# Support both symbol (action name = path) and string (explicit path)
|
|
326
|
+
path, action = normalize_path_and_action(path_or_action, options)
|
|
327
|
+
full_path = join_path(@prefix, path)
|
|
169
328
|
options = merge_inherited_options(options)
|
|
329
|
+
options[:action] ||= action if action
|
|
170
330
|
@gateway.send(:add_route, method, full_path, options)
|
|
171
331
|
end
|
|
172
332
|
end
|
|
173
333
|
|
|
174
334
|
private
|
|
175
335
|
|
|
336
|
+
# Normalize path/action: symbol means path=action, string means infer action from path
|
|
337
|
+
def normalize_path_and_action(path_or_action, options)
|
|
338
|
+
return ['', options[:action]] if path_or_action.nil?
|
|
339
|
+
|
|
340
|
+
if path_or_action.is_a?(Symbol)
|
|
341
|
+
# Symbol: use as both path segment and action name (e.g. :test → path: 'test', action: :test)
|
|
342
|
+
[path_or_action.to_s, options[:action] || path_or_action]
|
|
343
|
+
else
|
|
344
|
+
path = path_or_action.to_s
|
|
345
|
+
# Infer action from last non-param segment of path (e.g. 'test' → :test)
|
|
346
|
+
inferred_action = options[:action] || infer_action_from_path(path)
|
|
347
|
+
[path, inferred_action]
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Infer action name from path: 'test' → :test, 'deep/nested' → :nested
|
|
352
|
+
def infer_action_from_path(path)
|
|
353
|
+
return nil if path.nil? || path.empty? || path == '/'
|
|
354
|
+
|
|
355
|
+
segments = path.gsub(%r{^/|/$}, '').split('/')
|
|
356
|
+
# Find last segment that's not a parameter (doesn't contain {})
|
|
357
|
+
last_segment = segments.reverse.find { |s| !s.include?('{') && !s.start_with?(':') }
|
|
358
|
+
last_segment&.tr('-', '_')&.to_sym
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Join a base path with a relative path, ensuring exactly one `/` separator.
|
|
362
|
+
# Handles "/" path by returning just the base (no trailing slash).
|
|
363
|
+
def join_path(base, path)
|
|
364
|
+
path = path.to_s
|
|
365
|
+
return base if path.empty? || path == '/'
|
|
366
|
+
return "#{base}#{path}".chomp('/') if path.start_with?('/')
|
|
367
|
+
|
|
368
|
+
"#{base}/#{path}"
|
|
369
|
+
end
|
|
370
|
+
|
|
176
371
|
def merge_inherited_options(options)
|
|
177
372
|
result = options.dup
|
|
178
373
|
if @inherited_tables.any?
|
|
@@ -230,9 +425,11 @@ module Belt
|
|
|
230
425
|
inherited_tables = (@default_tables + resource_tables).uniq
|
|
231
426
|
inherited_auth = options[:auth] || @default_auth
|
|
232
427
|
inherited_lambda = options[:lambda]
|
|
428
|
+
inherited_controller = resource_name
|
|
233
429
|
nested_builder = NestedResourceBuilder.new(self, member_prefix, collection_prefix,
|
|
234
430
|
inherited_tables: inherited_tables,
|
|
235
431
|
inherited_auth: inherited_auth,
|
|
432
|
+
inherited_controller: inherited_controller,
|
|
236
433
|
inherited_lambda: inherited_lambda)
|
|
237
434
|
nested_builder.instance_eval(&)
|
|
238
435
|
end
|
data/lib/belt/version.rb
CHANGED