Skip to content

Commit 329274b

Browse files
ericproulxclaude
andcommitted
Promote route metadata to Route keyword arguments
`version`, `namespace`, `prefix`, `requirements`, `anchor` and `settings` were computed in `Endpoint#prepare_default_route_attributes`, merged into the route options Hash, and read back through `Grape::Router::BaseRoute`'s `def_delegators :@options`. They are route metadata, not part of the user-declared options bag grape-swagger consumes. They are now passed to `Grape::Router::Route` as explicit keyword arguments and exposed as plain readers on `BaseRoute`. `Endpoint#to_routes` computes each as a local and passes `config.route_options` straight through, so `prepare_default_route_attributes` (and the intermediate `default_route_options`/`complete_route_options`) is gone. `Route` forwards the attributes to `super` via `**route_attributes` to keep its signature lean. `route.options` now carries only what was declared for the route; the `@options` delegation is down to `description` plus the documentation `DSL_METHODS`. The readers are unchanged, so grape-swagger (which reads `route.prefix` and `route.settings`) is unaffected. `requirements` and `anchor` can still be declared as route options, so any explicit value stays in `route.options`; the effective value comes from the reader. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e26108b commit 329274b

6 files changed

Lines changed: 82 additions & 28 deletions

File tree

UPGRADING.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ Grape::Endpoint.new(settings, http_methods: :get, path: '/foo', api: my_api)
3737

3838
The owning API is no longer carried on the endpoint's public `options` Hash — `endpoint.options[:for]` is gone. Use the new `endpoint.api` reader instead.
3939

40+
#### Route metadata is exposed through readers, not the `options` Hash
41+
42+
A route's computed metadata — `version`, `namespace`, `prefix`, `requirements`, `anchor` and `settings` — is now passed to `Grape::Router::Route` as explicit keyword arguments and exposed as plain readers, instead of being merged into the route's `options` Hash.
43+
44+
The readers are unchanged — keep using them:
45+
46+
```ruby
47+
route.version # => 'v1'
48+
route.namespace # => '/things'
49+
route.prefix # => 'api'
50+
route.requirements # => {}
51+
route.anchor # => true
52+
route.settings # => { ... }
53+
```
54+
55+
What changed is the raw bag. `route.options` now holds only what was declared for the route, so it no longer carries the *computed* values for these keys — `route.options[:version]`, `[:namespace]`, `[:prefix]` and `[:settings]` return `nil`. Nothing in Grape or grape-swagger read them that way (grape-swagger uses the `route.prefix` and `route.settings` readers). For `requirements` and `anchor`, which can be supplied as route options (e.g. a mount's `anchor: false`), any explicitly declared value still appears in `route.options`; the effective value always comes from the reader.
56+
4057
### Upgrading to >= 3.3
4158

4259
#### Minimum required Ruby is now 3.3

lib/grape/endpoint.rb

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -285,39 +285,31 @@ def compile!
285285

286286
def to_routes
287287
route_options = config.route_options
288-
default_route_options = prepare_default_route_attributes(route_options)
289-
complete_route_options = route_options.merge(default_route_options)
290288
path_settings = prepare_default_path_settings
291289
forward_match = config.app && !config.app.respond_to?(:inheritable_setting)
290+
version = prepare_version(inheritable_setting.namespace_inheritable[:version])
291+
prefix = inheritable_setting.namespace_inheritable[:root_prefix]
292+
requirements = prepare_routes_requirements(route_options[:requirements])
293+
anchor = route_options.fetch(:anchor, true)
294+
settings = inheritable_setting.route.except(:declared_params, :saved_validations)
292295

293296
config.http_methods.flat_map do |method|
294297
config.path.map do |path|
295-
prepared_path = Path.new(path, default_route_options[:namespace], path_settings)
298+
prepared_path = Path.new(path, namespace, path_settings)
296299
pattern = Grape::Router::Pattern.new(
297300
origin: prepared_path.origin,
298301
suffix: prepared_path.suffix,
299-
anchor: default_route_options[:anchor],
302+
anchor:,
300303
params: route_options[:params],
301304
format: config.format,
302-
version: default_route_options[:version],
303-
requirements: default_route_options[:requirements]
305+
version:,
306+
requirements:
304307
)
305-
Grape::Router::Route.new(self, method, pattern, complete_route_options, forward_match:)
308+
Grape::Router::Route.new(self, method, pattern, route_options, forward_match:, version:, namespace:, prefix:, requirements:, anchor:, settings:)
306309
end
307310
end
308311
end
309312

310-
def prepare_default_route_attributes(route_options)
311-
{
312-
namespace:,
313-
version: prepare_version(inheritable_setting.namespace_inheritable[:version]),
314-
requirements: prepare_routes_requirements(route_options[:requirements]),
315-
prefix: inheritable_setting.namespace_inheritable[:root_prefix],
316-
anchor: route_options.fetch(:anchor, true),
317-
settings: inheritable_setting.route.except(:declared_params, :saved_validations)
318-
}
319-
end
320-
321313
def prepare_default_path_settings
322314
namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
323315
namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash

lib/grape/router/base_route.rb

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,20 @@ class BaseRoute
77

88
delegate_missing_to :@options
99

10-
attr_reader :options, :pattern
10+
attr_reader :options, :pattern, :version, :prefix, :requirements, :anchor, :settings, :namespace
1111

1212
def_delegators :@pattern, :path, :origin
13-
def_delegators :@options, :description, :version, :requirements, :prefix, :anchor, :settings, *Grape::Util::ApiDescription::DSL_METHODS
13+
def_delegators :@options, :description, *Grape::Util::ApiDescription::DSL_METHODS
1414

15-
def initialize(pattern, options = {})
15+
def initialize(pattern, options = {}, version: nil, namespace: nil, prefix: nil, requirements: nil, anchor: nil, settings: nil)
1616
@pattern = pattern
1717
@options = options.is_a?(ActiveSupport::OrderedOptions) ? options : ActiveSupport::OrderedOptions.new.update(options)
18-
end
19-
20-
# see https://github.com/ruby-grape/grape/issues/1348
21-
def namespace
22-
@namespace ||= @options[:namespace]
18+
@version = version
19+
@namespace = namespace
20+
@prefix = prefix
21+
@requirements = requirements
22+
@anchor = anchor
23+
@settings = settings
2324
end
2425

2526
def regexp_capture_index

lib/grape/router/route.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class Route < BaseRoute
1212

1313
def_delegators :@app, :call
1414

15-
def initialize(endpoint, method, pattern, options, forward_match:)
16-
super(pattern, options)
15+
def initialize(endpoint, method, pattern, options, forward_match:, **route_attributes)
16+
super(pattern, options, **route_attributes)
1717
@app = endpoint
1818
@request_method = upcase_method(method)
1919
@match_function = forward_match ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD

spec/grape/api_spec.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3133,6 +3133,14 @@ def self.call(object, _env)
31333133
it 'sets prefix' do
31343134
expect(subject.routes[1].prefix).to eq('p')
31353135
end
3136+
3137+
it 'does not carry computed attributes in the route options Hash' do
3138+
route = subject.routes[1]
3139+
expect(route.options).not_to have_key(:version)
3140+
expect(route.options).not_to have_key(:namespace)
3141+
expect(route.options).not_to have_key(:prefix)
3142+
expect(route.options).not_to have_key(:settings)
3143+
end
31363144
end
31373145

31383146
describe 'api structure with additional parameters' do

spec/grape/router/route_spec.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,42 @@
2222
it { is_expected.to be_a(Grape::Router::BaseRoute) }
2323
end
2424

25+
describe 'route attributes' do
26+
subject(:route) do
27+
described_class.new(endpoint, :get, pattern, options, forward_match: false,
28+
version: 'v1', namespace: '/things', prefix: '/api',
29+
requirements: { id: /\d+/ }, anchor: false, settings: { a: 1 })
30+
end
31+
32+
it 'exposes them as readers' do
33+
expect(route.version).to eq('v1')
34+
expect(route.namespace).to eq('/things')
35+
expect(route.prefix).to eq('/api')
36+
expect(route.requirements).to eq(id: /\d+/)
37+
expect(route.anchor).to be(false)
38+
expect(route.settings).to eq(a: 1)
39+
end
40+
41+
it 'does not leak them into the options Hash' do
42+
%i[version namespace prefix requirements anchor settings].each do |key|
43+
expect(route.options).not_to have_key(key)
44+
end
45+
end
46+
47+
context 'when omitted' do
48+
subject(:route) { described_class.new(endpoint, :get, pattern, options, forward_match: false) }
49+
50+
it 'defaults to nil' do
51+
expect(route.version).to be_nil
52+
expect(route.namespace).to be_nil
53+
expect(route.prefix).to be_nil
54+
expect(route.requirements).to be_nil
55+
expect(route.anchor).to be_nil
56+
expect(route.settings).to be_nil
57+
end
58+
end
59+
end
60+
2561
describe '#match?' do
2662
subject { instance.match?(input) }
2763

0 commit comments

Comments
 (0)