Skip to content

Commit ec239e9

Browse files
ericproulxclaude
andcommitted
Read a route's version, anchor and requirements from its pattern
`version`, `anchor` and `requirements` are the attributes that shape the matcher — they are passed to `Grape::Router::Pattern` to build the mustermann pattern. They were also passed a second time to `Grape::Router::Route` and stored as ivars, purely to back the readers. Expose them on `Pattern` instead (`attr_reader`) and delegate the `BaseRoute` readers to `@pattern`, alongside the existing `path`/`origin` delegation. `to_routes` no longer passes them to `Route.new`. Metadata that does not shape matching — `namespace`, `prefix`, `settings` — stays as route keyword arguments. grape-swagger already passes these to `Pattern.new` and none of them to `Route.new`, so its routes' readers become correct rather than `nil`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9ece4d9 commit ec239e9

5 files changed

Lines changed: 42 additions & 18 deletions

File tree

lib/grape/endpoint.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ def to_routes
304304
version:,
305305
requirements:
306306
)
307-
Grape::Router::Route.new(self, method, pattern, route_options, forward_match:, version:, namespace:, prefix:, requirements:, anchor:, settings:)
307+
Grape::Router::Route.new(self, method, pattern, route_options, forward_match:, namespace:, prefix:, settings:)
308308
end
309309
end
310310
end

lib/grape/router/base_route.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,18 @@ class BaseRoute
77

88
delegate_missing_to :@options
99

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

12-
def_delegators :@pattern, :path, :origin
12+
# +version+, +anchor+ and +requirements+ shape the matcher, so they are
13+
# read from the pattern rather than stored again on the route.
14+
def_delegators :@pattern, :path, :origin, :version, :anchor, :requirements
1315
def_delegators :@options, :description, *Grape::Util::ApiDescription::DSL_METHODS
1416

15-
def initialize(pattern, options = {}, version: nil, namespace: nil, prefix: nil, requirements: nil, anchor: nil, settings: nil)
17+
def initialize(pattern, options = {}, namespace: nil, prefix: nil, settings: nil)
1618
@pattern = pattern
1719
@options = options.is_a?(ActiveSupport::OrderedOptions) ? options : ActiveSupport::OrderedOptions.new.update(options)
18-
@version = version
1920
@namespace = namespace
2021
@prefix = prefix
21-
@requirements = requirements
22-
@anchor = anchor
2322
@settings = settings
2423
end
2524

lib/grape/router/pattern.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ class Pattern
77

88
DEFAULT_CAPTURES = %w[format version].freeze
99

10-
attr_reader :origin, :path, :pattern, :to_regexp
10+
attr_reader :origin, :path, :pattern, :to_regexp, :anchor, :version, :requirements
1111

1212
def_delegators :pattern, :params
1313
def_delegators :to_regexp, :===
1414
alias match? ===
1515

1616
def initialize(origin:, suffix:, anchor:, params:, version:, requirements:, format: nil)
1717
@origin = origin
18+
@anchor = anchor
19+
@version = version
20+
@requirements = requirements
1821
@path = PatternCache[[build_path_from_pattern(@origin, anchor), suffix]]
1922
@pattern = MustermannPattern.new(@path, uri_decode: true, params:, capture: extract_capture(format, version, requirements))
2023
@to_regexp = @pattern.to_regexp

spec/grape/router/pattern_spec.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Grape::Router::Pattern do
4+
describe 'match-parameter readers' do
5+
subject(:pattern) do
6+
described_class.new(origin: '/x', suffix: '', anchor: false, params: {}, version: 'v1', requirements: { id: /\d+/ })
7+
end
8+
9+
it 'exposes anchor, version and requirements' do
10+
expect(pattern.anchor).to be(false)
11+
expect(pattern.version).to eq('v1')
12+
expect(pattern.requirements).to eq(id: /\d+/)
13+
end
14+
end
15+
end

spec/grape/router/route_spec.rb

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

25-
describe 'route attributes' do
25+
describe 'metadata attributes (namespace, prefix, settings)' do
2626
subject(:route) do
2727
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 })
28+
namespace: '/things', prefix: '/api', settings: { a: 1 })
3029
end
3130

3231
it 'exposes them as readers' do
33-
expect(route.version).to eq('v1')
3432
expect(route.namespace).to eq('/things')
3533
expect(route.prefix).to eq('/api')
36-
expect(route.requirements).to eq(id: /\d+/)
37-
expect(route.anchor).to be(false)
3834
expect(route.settings).to eq(a: 1)
3935
end
4036

4137
it 'does not leak them into the options Hash' do
42-
%i[version namespace prefix requirements anchor settings].each do |key|
38+
%i[namespace prefix settings].each do |key|
4339
expect(route.options).not_to have_key(key)
4440
end
4541
end
@@ -48,16 +44,27 @@
4844
subject(:route) { described_class.new(endpoint, :get, pattern, options, forward_match: false) }
4945

5046
it 'defaults to nil' do
51-
expect(route.version).to be_nil
5247
expect(route.namespace).to be_nil
5348
expect(route.prefix).to be_nil
54-
expect(route.requirements).to be_nil
55-
expect(route.anchor).to be_nil
5649
expect(route.settings).to be_nil
5750
end
5851
end
5952
end
6053

54+
describe 'match parameters delegated to the pattern (version, anchor, requirements)' do
55+
subject(:route) { described_class.new(endpoint, :get, pattern, options, forward_match: false) }
56+
57+
let(:pattern) do
58+
Grape::Router::Pattern.new(origin: '/mounty', suffix: '', anchor: false, params: {}, version: 'v1', requirements: { id: /\d+/ })
59+
end
60+
61+
it 'reads them from the pattern' do
62+
expect(route.version).to eq('v1')
63+
expect(route.anchor).to be(false)
64+
expect(route.requirements).to eq(id: /\d+/)
65+
end
66+
end
67+
6168
describe '#match?' do
6269
subject { instance.match?(input) }
6370

0 commit comments

Comments
 (0)