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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0308512f255c22a7fbc31ff42a20947ee8c155c4c9ceaea3a4824a1e5a50c618'
4
- data.tar.gz: e071128ae46f66256e4f52c5a0b3eb096e49faf2a0cde9e6c3d962c6ed54af93
3
+ metadata.gz: e529a712ad25eb12761d155d77b9a62f4342858c9b2272713edd0eef36ce7b6c
4
+ data.tar.gz: e1e38c997a9bcfc0f454315915412cc4911cd832771cb389e8733b64d082fac9
5
5
  SHA512:
6
- metadata.gz: ec4a7be24ec7cb6961ed6c06cbc7690eb05a92406cf10cedf8760ef5cde8d464bd062652524ca74e41fdd6045f495b77e1c252823d1d18e14090ed986073f1ac
7
- data.tar.gz: '08f0980d98b9c868c7e53c23236acc3c72be619f3b361dea0a1520a3d5e71549ad95758d7cc8cdbdd21186dfcbd7f6e7d7acff822b1b12d00225d2c35711f6e3'
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
@@ -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
@@ -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
- Belt.controller_paths << subdir if File.directory?(subdir)
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)
@@ -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
- options = merge_inherited_options(options)
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: @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 |path, options = {}|
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 |path, options = {}|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.3.37'
4
+ VERSION = '0.3.39'
5
5
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'application_controller'
4
-
5
3
  module <%= @module_name %>Controllers
6
4
  class <%= @class_name %>sController < ApplicationController
7
5
  <% if @references.any? -%>
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.37
4
+ version: 0.3.39
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla