belt 0.3.37 → 0.3.39
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 +128 -0
- data/README.md +47 -0
- data/lib/belt/docs/routing.md +105 -0
- data/lib/belt/lambda_handler.rb +8 -1
- data/lib/belt/route_dsl.rb +188 -10
- data/lib/belt/version.rb +1 -1
- data/lib/templates/generate/controller.rb.erb +0 -2
- 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: e529a712ad25eb12761d155d77b9a62f4342858c9b2272713edd0eef36ce7b6c
|
|
4
|
+
data.tar.gz: e1e38c997a9bcfc0f454315915412cc4911cd832771cb389e8733b64d082fac9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f2dbd95f0dfe51d250baeed09aa65d0704fbe783b67054b61f8eb6d46519c1fc0ebad6c641db6a5bd76fe75acd2f7a74a60abca16589b4e89da1dcb1d796bc2
|
|
7
|
+
data.tar.gz: 462ba513dc663920b9cdd8af7fe12632d541c99f14234e8b43c4dd6e706de16995852a72781f38a572e3aa92f78111e304a924e92e8e03eb791a80209163df2b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,133 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.39
|
|
4
|
+
|
|
5
|
+
### Enhancement
|
|
6
|
+
|
|
7
|
+
- **Auto-load `application_controller.rb` per gateway**: `Belt::LambdaHandler`
|
|
8
|
+
now requires each gateway's `application_controller.rb` before its sibling
|
|
9
|
+
controllers, mirroring the existing model convention (`application_record.rb`
|
|
10
|
+
loads first). Controllers can inherit from `ApplicationController` without an
|
|
11
|
+
explicit `require_relative 'application_controller'` — Belt handles ordering,
|
|
12
|
+
matching Rails. The `belt generate controller` template no longer emits the
|
|
13
|
+
redundant `require_relative`, keeping loading a single framework concern.
|
|
14
|
+
|
|
15
|
+
## 0.3.38
|
|
16
|
+
|
|
17
|
+
### Enhancement
|
|
18
|
+
|
|
19
|
+
- **Rails-like `namespace` inside nested resources**: The `NestedResourceBuilder`
|
|
20
|
+
now supports `namespace` blocks that add both a path prefix AND a controller
|
|
21
|
+
module prefix, just like Rails. This provides 99% Rails compatibility for
|
|
22
|
+
nested route organization.
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
resources :projects do
|
|
26
|
+
namespace :admin do
|
|
27
|
+
resources :users # → /projects/:project_id/admin/users → admin/users controller
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Namespaces can be nested and inherit auth/tables options:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
resources :projects do
|
|
36
|
+
namespace :admin, auth: :iam, tables: [:audit_log] do
|
|
37
|
+
namespace :v2 do
|
|
38
|
+
resources :settings, only: [:index] # → admin/v2/settings controller
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- **`scope module:` option inside nested resources**: The existing `scope` method
|
|
45
|
+
now supports the `module:` option for Rails-like controller module prefixing
|
|
46
|
+
without adding a path prefix.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
resources :projects do
|
|
50
|
+
scope module: 'v2' do
|
|
51
|
+
resources :users # → /projects/:project_id/users → v2/users controller
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 0.3.37
|
|
57
|
+
|
|
58
|
+
### Enhancement
|
|
59
|
+
|
|
60
|
+
- **DRYer routing DSL with Rails-like `scope` inside nested resources**: The
|
|
61
|
+
`NestedResourceBuilder` now supports `scope` blocks for grouping routes with
|
|
62
|
+
shared options (path prefix, controller, tables, auth). This enables much
|
|
63
|
+
cleaner route definitions when multiple routes share the same controller or
|
|
64
|
+
tables.
|
|
65
|
+
|
|
66
|
+
**Before:**
|
|
67
|
+
```ruby
|
|
68
|
+
resources :projects do
|
|
69
|
+
get 'billing', controller: :billing, action: :show, tables: [:memberships]
|
|
70
|
+
post 'billing/checkout', controller: :billing, action: :checkout, tables: [:memberships]
|
|
71
|
+
post 'billing/subscribe', controller: :billing, action: :subscribe, tables: [:memberships]
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**After:**
|
|
76
|
+
```ruby
|
|
77
|
+
resources :projects do
|
|
78
|
+
scope path: 'billing', controller: :billing, tables: [:memberships] do
|
|
79
|
+
get '/', action: :show
|
|
80
|
+
post :checkout
|
|
81
|
+
post :subscribe
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **Singular `resource` inside nested resources**: You can now use `resource`
|
|
87
|
+
(singular) inside a `resources` block for nested singular resources like
|
|
88
|
+
`:billing`, `:token_usage`, or `:profile`.
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
resources :projects do
|
|
92
|
+
resource :billing, only: [:show], tables: [:memberships]
|
|
93
|
+
resource :token_usage, only: [:show]
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- **Action inference from path**: When using symbol arguments or simple string
|
|
98
|
+
paths, the action is now inferred automatically. This works in `member`,
|
|
99
|
+
`collection`, and direct route definitions within resources.
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
resources :webhooks do
|
|
103
|
+
member do
|
|
104
|
+
post :test # action: :test inferred
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
resources :surfaces do
|
|
109
|
+
collection do
|
|
110
|
+
get :teams # action: :teams inferred
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
resources :projects do
|
|
115
|
+
post 'mark-complete' # action: :mark_complete inferred (hyphens → underscores)
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- **Controller inheritance in member/collection blocks**: Routes defined in
|
|
120
|
+
`member` and `collection` blocks now properly inherit the parent resource's
|
|
121
|
+
controller. This was inconsistent before and sometimes returned `nil`.
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
resources :webhooks do
|
|
125
|
+
member do
|
|
126
|
+
post :test # controller: 'webhooks' inherited
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
3
131
|
## 0.3.32
|
|
4
132
|
|
|
5
133
|
### 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
|
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/lambda_handler.rb
CHANGED
|
@@ -39,7 +39,14 @@ module Belt
|
|
|
39
39
|
Belt.controller_paths << controllers_dir
|
|
40
40
|
Dir.children(controllers_dir).each do |child|
|
|
41
41
|
subdir = File.join(controllers_dir, child)
|
|
42
|
-
|
|
42
|
+
next unless File.directory?(subdir)
|
|
43
|
+
|
|
44
|
+
Belt.controller_paths << subdir
|
|
45
|
+
|
|
46
|
+
# Auto-require application_controller.rb first (like application_record for models)
|
|
47
|
+
# so other controllers can inherit from it without explicit require_relative
|
|
48
|
+
app_controller = File.join(subdir, 'application_controller.rb')
|
|
49
|
+
require app_controller if File.exist?(app_controller)
|
|
43
50
|
end
|
|
44
51
|
|
|
45
52
|
# Auto-load all models (application_record first, then the rest)
|
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,26 +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 |
|
|
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)
|
|
99
198
|
base = options[:on] == :collection ? @collection_prefix : @prefix
|
|
100
199
|
full_path = join_path(base, path)
|
|
101
200
|
options = merge_inherited_options(options)
|
|
102
201
|
route_options = options.except(:on)
|
|
202
|
+
route_options[:action] ||= action if action
|
|
103
203
|
@gateway.send(:add_route, method, full_path, route_options)
|
|
104
204
|
end
|
|
105
205
|
end
|
|
106
206
|
|
|
107
207
|
private
|
|
108
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
|
+
|
|
109
234
|
# Join a base path with a relative path, ensuring exactly one `/` separator.
|
|
235
|
+
# Handles "/" path by returning just the base (no trailing slash).
|
|
110
236
|
def join_path(base, path)
|
|
111
237
|
path = path.to_s
|
|
112
|
-
return base if path.empty?
|
|
113
|
-
return "#{base}#{path}" if path.start_with?('/')
|
|
238
|
+
return base if path.empty? || path == '/'
|
|
239
|
+
return "#{base}#{path}".chomp('/') if path.start_with?('/')
|
|
114
240
|
|
|
115
241
|
"#{base}/#{path}"
|
|
116
242
|
end
|
|
117
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
|
+
|
|
118
254
|
def add_nested_resource_routes(resource_name, param_name, resource_options, actions)
|
|
119
255
|
if actions.include?(:index)
|
|
120
256
|
@gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}",
|
|
@@ -160,6 +296,17 @@ module Belt
|
|
|
160
296
|
result[:lambda] ||= @inherited_lambda if @inherited_lambda
|
|
161
297
|
result
|
|
162
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
|
|
163
310
|
end
|
|
164
311
|
|
|
165
312
|
class MemberCollectionBuilder
|
|
@@ -174,20 +321,49 @@ module Belt
|
|
|
174
321
|
end
|
|
175
322
|
|
|
176
323
|
%i[get post put delete patch].each do |method|
|
|
177
|
-
define_method(method) do |
|
|
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)
|
|
178
327
|
full_path = join_path(@prefix, path)
|
|
179
328
|
options = merge_inherited_options(options)
|
|
329
|
+
options[:action] ||= action if action
|
|
180
330
|
@gateway.send(:add_route, method, full_path, options)
|
|
181
331
|
end
|
|
182
332
|
end
|
|
183
333
|
|
|
184
334
|
private
|
|
185
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
|
+
|
|
186
361
|
# Join a base path with a relative path, ensuring exactly one `/` separator.
|
|
362
|
+
# Handles "/" path by returning just the base (no trailing slash).
|
|
187
363
|
def join_path(base, path)
|
|
188
364
|
path = path.to_s
|
|
189
|
-
return base if path.empty?
|
|
190
|
-
return "#{base}#{path}" if path.start_with?('/')
|
|
365
|
+
return base if path.empty? || path == '/'
|
|
366
|
+
return "#{base}#{path}".chomp('/') if path.start_with?('/')
|
|
191
367
|
|
|
192
368
|
"#{base}/#{path}"
|
|
193
369
|
end
|
|
@@ -249,9 +425,11 @@ module Belt
|
|
|
249
425
|
inherited_tables = (@default_tables + resource_tables).uniq
|
|
250
426
|
inherited_auth = options[:auth] || @default_auth
|
|
251
427
|
inherited_lambda = options[:lambda]
|
|
428
|
+
inherited_controller = resource_name
|
|
252
429
|
nested_builder = NestedResourceBuilder.new(self, member_prefix, collection_prefix,
|
|
253
430
|
inherited_tables: inherited_tables,
|
|
254
431
|
inherited_auth: inherited_auth,
|
|
432
|
+
inherited_controller: inherited_controller,
|
|
255
433
|
inherited_lambda: inherited_lambda)
|
|
256
434
|
nested_builder.instance_eval(&)
|
|
257
435
|
end
|
data/lib/belt/version.rb
CHANGED