Skip to content

Commit 197d5b4

Browse files
authored
Merge pull request #2076 from anakinj/support-env-route-information-for-allowed-methods-endpoints
Populate the env route information when calling the automatically added endpoints
2 parents a5de051 + c532d64 commit 197d5b4

File tree

4 files changed

+107
-42
lines changed

4 files changed

+107
-42
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* [#2067](https://github.com/ruby-grape/grape/pull/2067): Coerce empty string to nil for all primitive types except String - [@petekinnecom](https://github.com/petekinnecom).
1414
* [#2064](https://github.com/ruby-grape/grape/pull/2064): Fix Ruby 2.7 deprecation warning in `Grape::Middleware::Base#initialize` - [@skarger](https://github.com/skarger).
1515
* [#2072](https://github.com/ruby-grape/grape/pull/2072): Fix `Grape.eager_load!` and `compile!` - [@stanhu](https://github.com/stanhu).
16+
* [#2076](https://github.com/ruby-grape/grape/pull/2076): Make route information available for hooks when the automatically generated endpoints are invoked - [@anakinj](https://github.com/anakinj).
1617
* Your contribution here.
1718

1819
### 1.3.3 (2020/05/23)

lib/grape/api/instance.rb

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -192,37 +192,15 @@ def cascade?
192192
# will return an HTTP 405 response for any HTTP method that the resource
193193
# cannot handle.
194194
def add_head_not_allowed_methods_and_options_methods
195-
routes_map = {}
196-
197-
self.class.endpoints.each do |endpoint|
198-
routes = endpoint.routes
199-
routes.each do |route|
200-
# using the :any shorthand produces [nil] for route methods, substitute all manually
201-
route_key = route.pattern.to_regexp
202-
routes_map[route_key] ||= {}
203-
route_settings = routes_map[route_key]
204-
route_settings[:pattern] = route.pattern
205-
route_settings[:requirements] = route.requirements
206-
route_settings[:path] = route.origin
207-
route_settings[:methods] ||= []
208-
if route.request_method == '*' || route_settings[:methods].include?('*')
209-
route_settings[:methods] = Grape::Http::Headers::SUPPORTED_METHODS
210-
else
211-
route_settings[:methods] << route.request_method
212-
end
213-
route_settings[:endpoint] = route.app
214-
end
215-
end
216-
195+
versioned_route_configs = collect_route_config_per_pattern
217196
# The paths we collected are prepared (cf. Path#prepare), so they
218197
# contain already versioning information when using path versioning.
219198
# Disable versioning so adding a route won't prepend versioning
220199
# informations again.
221200
without_root_prefix do
222201
without_versioning do
223-
routes_map.each_value do |config|
224-
methods = config[:methods]
225-
allowed_methods = methods.dup
202+
versioned_route_configs.each do |config|
203+
allowed_methods = config[:methods].dup
226204

227205
unless self.class.namespace_inheritable(:do_not_route_head)
228206
allowed_methods |= [Grape::Http::Headers::HEAD] if allowed_methods.include?(Grape::Http::Headers::GET)
@@ -241,6 +219,25 @@ def add_head_not_allowed_methods_and_options_methods
241219
end
242220
end
243221

222+
def collect_route_config_per_pattern
223+
all_routes = self.class.endpoints.map(&:routes).flatten
224+
routes_by_regexp = all_routes.group_by { |route| route.pattern.to_regexp }
225+
226+
# Build the configuration based on the first endpoint and the collection of methods supported.
227+
routes_by_regexp.values.map do |routes|
228+
last_route = routes.last # Most of the configuration is taken from the last endpoint
229+
matching_wildchar = routes.any? { |route| route.request_method == '*' }
230+
{
231+
options: {},
232+
pattern: last_route.pattern,
233+
requirements: last_route.requirements,
234+
path: last_route.origin,
235+
endpoint: last_route.app,
236+
methods: matching_wildchar ? Grape::Http::Headers::SUPPORTED_METHODS : routes.map(&:request_method)
237+
}
238+
end
239+
end
240+
244241
# Generate a route that returns an HTTP 405 response for a user defined
245242
# path on methods not specified
246243
def generate_not_allowed_method(pattern, allowed_methods: [], **attributes)
@@ -252,7 +249,6 @@ def generate_not_allowed_method(pattern, allowed_methods: [], **attributes)
252249
end
253250
not_allowed_methods = supported_methods - allowed_methods
254251
return if not_allowed_methods.empty?
255-
256252
@router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes)
257253
end
258254

lib/grape/router.rb

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,37 +99,34 @@ def transaction(env)
9999
response = yield(input, method)
100100

101101
return response if response && !(cascade = cascade?(response))
102-
neighbor = greedy_match?(input)
102+
last_neighbor_route = greedy_match?(input)
103103

104-
# If neighbor exists and request method is OPTIONS,
104+
# If last_neighbor_route exists and request method is OPTIONS,
105105
# return response by using #call_with_allow_headers.
106-
return call_with_allow_headers(
107-
env,
108-
neighbor.allow_header,
109-
neighbor.endpoint
110-
) if neighbor && method == Grape::Http::Headers::OPTIONS && !cascade
106+
return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Grape::Http::Headers::OPTIONS && !cascade
111107

112108
route = match?(input, '*')
113-
return neighbor.endpoint.call(env) if neighbor && cascade && route
109+
110+
return last_neighbor_route.endpoint.call(env) if last_neighbor_route && cascade && route
114111

115112
if route
116113
response = process_route(route, env)
117114
return response if response && !(cascade = cascade?(response))
118115
end
119116

120-
!cascade && neighbor ? call_with_allow_headers(env, neighbor.allow_header, neighbor.endpoint) : nil
117+
return call_with_allow_headers(env, last_neighbor_route) if !cascade && last_neighbor_route
118+
119+
nil
121120
end
122121

123122
def process_route(route, env)
124-
input, = *extract_input_and_method(env)
125-
routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
126-
env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
123+
prepare_env_from_route(env, route)
127124
route.exec(env)
128125
end
129126

130127
def make_routing_args(default_args, route, input)
131128
args = default_args || { route_info: route }
132-
args.merge(route.params(input))
129+
args.merge(route.params(input) || {})
133130
end
134131

135132
def extract_input_and_method(env)
@@ -160,9 +157,15 @@ def greedy_match?(input)
160157
@neutral_map.detect { |route| last_match["_#{route.index}"] }
161158
end
162159

163-
def call_with_allow_headers(env, methods, endpoint)
164-
env[Grape::Env::GRAPE_ALLOWED_METHODS] = methods.join(', ').freeze
165-
endpoint.call(env)
160+
def call_with_allow_headers(env, route)
161+
prepare_env_from_route(env, route)
162+
env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header.join(', ').freeze
163+
route.endpoint.call(env)
164+
end
165+
166+
def prepare_env_from_route(env, route)
167+
input, = *extract_input_and_method(env)
168+
env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(env[Grape::Env::GRAPE_ROUTING_ARGS], route, input)
166169
end
167170

168171
def cascade?(response)

spec/grape/api_spec.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,71 @@ class DummyFormatClass
816816
end
817817
end
818818

819+
describe 'when hook behaviour is controlled by attributes on the route ' do
820+
before do
821+
subject.before do
822+
error!('Access Denied', 401) unless route.options[:secret] == params[:secret]
823+
end
824+
825+
subject.namespace 'example' do
826+
before do
827+
error!('Access Denied', 401) unless route.options[:namespace_secret] == params[:namespace_secret]
828+
end
829+
830+
desc 'it gets with secret', secret: 'password'
831+
get { status(params[:id] == '504' ? 200 : 404) }
832+
833+
desc 'it post with secret', secret: 'password', namespace_secret: 'namespace_password'
834+
post {}
835+
end
836+
end
837+
838+
context 'when HTTP method is not defined' do
839+
let(:response) { delete('/example') }
840+
841+
it 'responds with a 405 status' do
842+
expect(response.status).to eql 405
843+
end
844+
end
845+
846+
context 'when HTTP method is defined with attribute' do
847+
let(:response) { post('/example?secret=incorrect_password') }
848+
it 'responds with the defined error in the before hook' do
849+
expect(response.status).to eql 401
850+
end
851+
end
852+
853+
context 'when HTTP method is defined and the underlying before hook expectation is not met' do
854+
let(:response) { post('/example?secret=password&namespace_secret=wrong_namespace_password') }
855+
it 'ends up in the endpoint' do
856+
expect(response.status).to eql 401
857+
end
858+
end
859+
860+
context 'when HTTP method is defined and everything is like the before hooks expect' do
861+
let(:response) { post('/example?secret=password&namespace_secret=namespace_password') }
862+
it 'ends up in the endpoint' do
863+
expect(response.status).to eql 201
864+
end
865+
end
866+
867+
context 'when HEAD is called for the defined GET' do
868+
let(:response) { head('/example?id=504') }
869+
870+
it 'responds with 401 because before expectations in before hooks are not met' do
871+
expect(response.status).to eql 401
872+
end
873+
end
874+
875+
context 'when HEAD is called for the defined GET' do
876+
let(:response) { head('/example?id=504&secret=password') }
877+
878+
it 'responds with 200 because before hooks are not called' do
879+
expect(response.status).to eql 200
880+
end
881+
end
882+
end
883+
819884
context 'allows HEAD on a GET request that' do
820885
before do
821886
subject.get 'example' do

0 commit comments

Comments
 (0)