Skip to content

Commit d4244ab

Browse files
authored
Merge pull request #2629 from ruby-grape/refactor/router-pattern-delegation
Refactor Router Architecture: Improve Encapsulation and API Design
2 parents 08a8da5 + 11bd88a commit d4244ab

File tree

14 files changed

+149
-161
lines changed

14 files changed

+149
-161
lines changed

.rubocop.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Metrics/CyclomaticComplexity:
4949
Max: 15
5050

5151
Metrics/ParameterLists:
52+
CountKeywordArgs: false
5253
MaxOptionalParameters: 4
5354

5455
Metrics/MethodLength:

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
#### Features
44

5+
* [#2629](https://github.com/ruby-grape/grape/pull/2629): Refactor Router Architecture - [@ericproulx](https://github.com/ericproulx).
56
* Your contribution here.
67

78
#### Fixes
8-
9+
910
* Your contribution here.
1011

1112
### 3.0.1 (2025-11-24)

lib/grape/api/instance.rb

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def cascade?
177177
def add_head_not_allowed_methods_and_options_methods
178178
# The paths we collected are prepared (cf. Path#prepare), so they
179179
# contain already versioning information when using path versioning.
180-
all_routes = self.class.endpoints.map(&:routes).flatten
180+
all_routes = self.class.endpoints.flat_map(&:routes)
181181

182182
# Disable versioning so adding a route won't prepend versioning
183183
# informations again.
@@ -186,23 +186,21 @@ def add_head_not_allowed_methods_and_options_methods
186186

187187
def collect_route_config_per_pattern(all_routes)
188188
routes_by_regexp = all_routes.group_by(&:pattern_regexp)
189+
namespace_inheritable = self.class.inheritable_setting.namespace_inheritable
189190

190191
# Build the configuration based on the first endpoint and the collection of methods supported.
191192
routes_by_regexp.each_value do |routes|
192-
last_route = routes.last # Most of the configuration is taken from the last endpoint
193193
next if routes.any? { |route| route.request_method == '*' }
194194

195-
namespace_inheritable = self.class.inheritable_setting.namespace_inheritable
195+
last_route = routes.last # Most of the configuration is taken from the last endpoint
196196
allowed_methods = routes.map(&:request_method)
197197
allowed_methods |= [Rack::HEAD] if !namespace_inheritable[:do_not_route_head] && allowed_methods.include?(Rack::GET)
198198

199199
allow_header = namespace_inheritable[:do_not_route_options] ? allowed_methods : [Rack::OPTIONS] | allowed_methods
200200
last_route.app.options[:options_route_enabled] = true unless namespace_inheritable[:do_not_route_options] || allowed_methods.include?(Rack::OPTIONS)
201201

202-
@router.associate_routes(last_route.pattern, {
203-
endpoint: last_route.app,
204-
allow_header: allow_header
205-
})
202+
greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header: allow_header)
203+
@router.associate_routes(greedy_route)
206204
end
207205
end
208206

lib/grape/dsl/routing.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,14 @@ def route(methods, paths = ['/'], route_options = {}, &block)
149149
all_route_options.deep_merge!(endpoint_description) if endpoint_description
150150
all_route_options.deep_merge!(route_options) if route_options&.any?
151151

152-
endpoint_options = {
152+
new_endpoint = Grape::Endpoint.new(
153+
inheritable_setting,
153154
method: method,
154155
path: paths,
155156
for: self,
156-
route_options: all_route_options
157-
}
158-
159-
new_endpoint = Grape::Endpoint.new(inheritable_setting, endpoint_options, &block)
157+
route_options: all_route_options,
158+
&block
159+
)
160160
endpoints << new_endpoint unless endpoints.any? { |e| e.equals?(new_endpoint) }
161161

162162
inheritable_setting.route_end
@@ -217,7 +217,7 @@ def reset_endpoints!
217217
#
218218
# @param param [Symbol] The name of the parameter you wish to declare.
219219
# @option options [Regexp] You may supply a regular expression that the declared parameter must meet.
220-
def route_param(param, options = {}, &block)
220+
def route_param(param, **options, &block)
221221
options = options.dup
222222

223223
options[:requirements] = {

lib/grape/endpoint.rb

Lines changed: 54 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,7 @@ def run_before_each(endpoint)
4646
# @note This happens at the time of API definition, so in this context the
4747
# endpoint does not know if it will be mounted under a different endpoint.
4848
# @yield a block defining what your API should do when this endpoint is hit
49-
def initialize(new_settings, options = {}, &block)
50-
require_option(options, :path)
51-
require_option(options, :method)
52-
49+
def initialize(new_settings, **options, &block)
5350
self.inheritable_setting = new_settings.point_in_time_copy
5451

5552
# now +namespace_stackable(:declared_params)+ contains all params defined for
@@ -67,7 +64,6 @@ def initialize(new_settings, options = {}, &block)
6764
@options[:path] << '/' if options[:path].empty?
6865

6966
@options[:method] = Array(options[:method])
70-
@options[:route_options] ||= {}
7167

7268
@lazy_initialize_lock = Mutex.new
7369
@lazy_initialized = nil
@@ -88,10 +84,6 @@ def inherit_settings(namespace_stackable)
8884
endpoints&.each { |e| e.inherit_settings(namespace_stackable) }
8985
end
9086

91-
def require_option(options, key)
92-
raise Grape::Exceptions::MissingOption.new(key) unless options.key?(key)
93-
end
94-
9587
def routes
9688
@routes ||= endpoints&.collect(&:routes)&.flatten || to_routes
9789
end
@@ -117,53 +109,6 @@ def mount_in(router)
117109
end
118110
end
119111

120-
def to_routes
121-
default_route_options = prepare_default_route_attributes
122-
123-
map_routes do |method, raw_path|
124-
prepared_path = Path.new(raw_path, namespace, prepare_default_path_settings)
125-
params = options[:route_options].present? ? options[:route_options].merge(default_route_options) : default_route_options
126-
route = Grape::Router::Route.new(method, prepared_path.origin, prepared_path.suffix, params)
127-
route.apply(self)
128-
end.flatten
129-
end
130-
131-
def prepare_routes_requirements
132-
{}.merge!(*inheritable_setting.namespace_stackable[:namespace].map(&:requirements)).tap do |requirements|
133-
endpoint_requirements = options.dig(:route_options, :requirements)
134-
requirements.merge!(endpoint_requirements) if endpoint_requirements
135-
end
136-
end
137-
138-
def prepare_default_route_attributes
139-
{
140-
namespace: namespace,
141-
version: prepare_version,
142-
requirements: prepare_routes_requirements,
143-
prefix: inheritable_setting.namespace_inheritable[:root_prefix],
144-
anchor: options[:route_options].fetch(:anchor, true),
145-
settings: inheritable_setting.route.except(:declared_params, :saved_validations),
146-
forward_match: options[:forward_match]
147-
}
148-
end
149-
150-
def prepare_version
151-
version = inheritable_setting.namespace_inheritable[:version]
152-
return if version.blank?
153-
154-
version.length == 1 ? version.first : version
155-
end
156-
157-
def map_routes
158-
options[:method].map { |method| options[:path].map { |path| yield method, path } }
159-
end
160-
161-
def prepare_default_path_settings
162-
namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
163-
namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
164-
namespace_stackable_hash.merge!(namespace_inheritable_hash)
165-
end
166-
167112
def namespace
168113
@namespace ||= Namespace.joined_space_path(inheritable_setting.namespace_stackable[:namespace])
169114
end
@@ -311,6 +256,59 @@ def options?
311256

312257
private
313258

259+
def to_routes
260+
route_options = options[:route_options]
261+
default_route_options = prepare_default_route_attributes(route_options)
262+
complete_route_options = route_options.merge(default_route_options)
263+
path_settings = prepare_default_path_settings
264+
265+
options[:method].flat_map do |method|
266+
options[:path].map do |path|
267+
prepared_path = Path.new(path, default_route_options[:namespace], path_settings)
268+
pattern = Grape::Router::Pattern.new(
269+
origin: prepared_path.origin,
270+
suffix: prepared_path.suffix,
271+
anchor: default_route_options[:anchor],
272+
params: route_options[:params],
273+
format: options[:format],
274+
version: default_route_options[:version],
275+
requirements: default_route_options[:requirements]
276+
)
277+
Grape::Router::Route.new(self, method, pattern, complete_route_options)
278+
end
279+
end
280+
end
281+
282+
def prepare_default_route_attributes(route_options)
283+
{
284+
namespace: namespace,
285+
version: prepare_version(inheritable_setting.namespace_inheritable[:version]),
286+
requirements: prepare_routes_requirements(route_options[:requirements]),
287+
prefix: inheritable_setting.namespace_inheritable[:root_prefix],
288+
anchor: route_options.fetch(:anchor, true),
289+
settings: inheritable_setting.route.except(:declared_params, :saved_validations),
290+
forward_match: options[:forward_match]
291+
}
292+
end
293+
294+
def prepare_default_path_settings
295+
namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
296+
namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
297+
namespace_stackable_hash.merge!(namespace_inheritable_hash)
298+
end
299+
300+
def prepare_routes_requirements(route_options_requirements)
301+
namespace_requirements = inheritable_setting.namespace_stackable[:namespace].filter_map(&:requirements)
302+
namespace_requirements << route_options_requirements if route_options_requirements.present?
303+
namespace_requirements.reduce({}, :merge)
304+
end
305+
306+
def prepare_version(namespace_inheritable_version)
307+
return if namespace_inheritable_version.blank?
308+
309+
namespace_inheritable_version.length == 1 ? namespace_inheritable_version.first : namespace_inheritable_version
310+
end
311+
314312
def build_stack
315313
stack = Grape::Middleware::Stack.new
316314

lib/grape/router.rb

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,9 @@ def append(route)
5757
map[route.request_method] << route
5858
end
5959

60-
def associate_routes(pattern, options)
61-
Grape::Router::GreedyRoute.new(pattern, options).then do |greedy_route|
62-
@neutral_regexes << greedy_route.to_regexp(@neutral_map.length)
63-
@neutral_map << greedy_route
64-
end
60+
def associate_routes(greedy_route)
61+
@neutral_regexes << greedy_route.to_regexp(@neutral_map.length)
62+
@neutral_map << greedy_route
6563
end
6664

6765
def call(env)
@@ -91,7 +89,7 @@ def identity(env)
9189

9290
def rotation(env, exact_route = nil)
9391
response = nil
94-
input, method = *extract_input_and_method(env)
92+
input, method = extract_input_and_method(env)
9593
map[method].each do |route|
9694
next if exact_route == route
9795
next unless route.match?(input)
@@ -103,7 +101,7 @@ def rotation(env, exact_route = nil)
103101
end
104102

105103
def transaction(env)
106-
input, method = *extract_input_and_method(env)
104+
input, method = extract_input_and_method(env)
107105

108106
# using a Proc is important since `return` will exit the enclosing function
109107
cascade_or_return_response = proc do |response|
@@ -126,7 +124,7 @@ def transaction(env)
126124

127125
route = match?(input, '*')
128126

129-
return last_neighbor_route.options[:endpoint].call(env) if last_neighbor_route && last_response_cascade && route
127+
return last_neighbor_route.call(env) if last_neighbor_route && last_response_cascade && route
130128

131129
last_response_cascade = cascade_or_return_response.call(process_route(route, env)) if route
132130

@@ -142,7 +140,8 @@ def process_route(route, env)
142140

143141
def make_routing_args(default_args, route, input)
144142
args = default_args || { route_info: route }
145-
args.merge(route.params(input))
143+
route_params = route.params(input)
144+
route_params ? args.merge(route_params) : args
146145
end
147146

148147
def extract_input_and_method(env)
@@ -171,12 +170,12 @@ def greedy_match?(input)
171170

172171
def call_with_allow_headers(env, route)
173172
prepare_env_from_route(env, route)
174-
env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.options[:allow_header]
175-
route.options[:endpoint].call(env)
173+
env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header
174+
route.call(env)
176175
end
177176

178177
def prepare_env_from_route(env, route)
179-
input, = *extract_input_and_method(env)
178+
input, = extract_input_and_method(env)
180179
env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(env[Grape::Env::GRAPE_ROUTING_ARGS], route, input)
181180
end
182181

lib/grape/router/base_route.rb

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,31 @@
33
module Grape
44
class Router
55
class BaseRoute
6+
extend Forwardable
7+
68
delegate_missing_to :@options
79

8-
attr_reader :index, :pattern, :options
10+
attr_reader :index, :options, :pattern
11+
12+
def_delegators :@pattern, :path, :origin
13+
def_delegators :@options, :description, :version, :requirements, :prefix, :anchor, :settings, :forward_match, *Grape::Util::ApiDescription::DSL_METHODS
914

10-
def initialize(options)
15+
def initialize(pattern, options = {})
16+
@pattern = pattern
1117
@options = options.is_a?(ActiveSupport::OrderedOptions) ? options : ActiveSupport::OrderedOptions.new.update(options)
1218
end
1319

14-
alias attributes options
20+
# see https://github.com/ruby-grape/grape/issues/1348
21+
def namespace
22+
@options[:namespace]
23+
end
1524

1625
def regexp_capture_index
1726
CaptureIndexCache[index]
1827
end
1928

2029
def pattern_regexp
21-
pattern.to_regexp
30+
@pattern.to_regexp
2231
end
2332

2433
def to_regexp(index)

lib/grape/router/greedy_route.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@
66
module Grape
77
class Router
88
class GreedyRoute < BaseRoute
9-
def initialize(pattern, options)
10-
@pattern = pattern
11-
super(options)
9+
extend Forwardable
10+
11+
def_delegators :@endpoint, :call
12+
13+
attr_reader :endpoint, :allow_header
14+
15+
def initialize(pattern, endpoint:, allow_header:)
16+
super(pattern)
17+
@endpoint = endpoint
18+
@allow_header = allow_header
1219
end
1320

14-
# Grape::Router:Route defines params as a function
1521
def params(_input = nil)
16-
options[:params] || {}
22+
nil
1723
end
1824
end
1925
end

0 commit comments

Comments
 (0)