Skip to content

Commit e37831c

Browse files
authored
fix(#1975): Allow to use before/after/rescue_from methods in any order when using mount (#2384)
* fix(#1975): Allow to use `before/after/rescue_from` methods in any order when using `mount` * fix(#1975): Apply suggestions
1 parent 0456797 commit e37831c

File tree

7 files changed

+305
-3
lines changed

7 files changed

+305
-3
lines changed

.rubocop_todo.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Lint/ConstantDefinitionInBlock:
2727
Exclude:
2828
- 'spec/grape/api/defines_boolean_in_params_spec.rb'
2929
- 'spec/grape/api/inherited_helpers_spec.rb'
30+
- 'spec/grape/api/mount_and_helpers_order_spec.rb'
31+
- 'spec/grape/api/mount_and_rescue_from_spec.rb'
3032
- 'spec/grape/api/nested_helpers_spec.rb'
3133
- 'spec/grape/api/patch_method_helpers_spec.rb'
3234
- 'spec/grape/api_spec.rb'
@@ -235,6 +237,8 @@ RSpec/FilePath:
235237
- 'spec/grape/api/documentation_spec.rb'
236238
- 'spec/grape/api/inherited_helpers_spec.rb'
237239
- 'spec/grape/api/invalid_format_spec.rb'
240+
- 'spec/grape/api/mount_and_helpers_order_spec.rb'
241+
- 'spec/grape/api/mount_and_rescue_from_spec.rb'
238242
- 'spec/grape/api/namespace_parameters_in_route_spec.rb'
239243
- 'spec/grape/api/nested_helpers_spec.rb'
240244
- 'spec/grape/api/optional_parameters_in_route_spec.rb'
@@ -289,6 +293,7 @@ RSpec/IndexedLet:
289293
# Configuration parameters: AssignmentOnly.
290294
RSpec/InstanceVariable:
291295
Exclude:
296+
- 'spec/grape/api/mount_and_helpers_order_spec.rb'
292297
- 'spec/grape/api_spec.rb'
293298
- 'spec/grape/endpoint_spec.rb'
294299
- 'spec/grape/middleware/base_spec.rb'
@@ -301,6 +306,8 @@ RSpec/LeakyConstantDeclaration:
301306
Exclude:
302307
- 'spec/grape/api/defines_boolean_in_params_spec.rb'
303308
- 'spec/grape/api/inherited_helpers_spec.rb'
309+
- 'spec/grape/api/mount_and_helpers_order_spec.rb'
310+
- 'spec/grape/api/mount_and_rescue_from_spec.rb'
304311
- 'spec/grape/api/nested_helpers_spec.rb'
305312
- 'spec/grape/api/patch_method_helpers_spec.rb'
306313
- 'spec/grape/api_spec.rb'
@@ -343,6 +350,8 @@ RSpec/MultipleExpectations:
343350
- 'spec/grape/api/deeply_included_options_spec.rb'
344351
- 'spec/grape/api/defines_boolean_in_params_spec.rb'
345352
- 'spec/grape/api/invalid_format_spec.rb'
353+
- 'spec/grape/api/mount_and_helpers_order_spec.rb'
354+
- 'spec/grape/api/mount_and_rescue_from_spec.rb'
346355
- 'spec/grape/api/namespace_parameters_in_route_spec.rb'
347356
- 'spec/grape/api/optional_parameters_in_route_spec.rb'
348357
- 'spec/grape/api/parameters_modification_spec.rb'

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* [#2377](https://github.com/ruby-grape/grape/pull/2377): Allow to use instance variables values inside `rescue_from` - [@jcagarcia](https://github.com/jcagarcia).
77
* [#2379](https://github.com/ruby-grape/grape/pull/2379): Take into account the `route_param` type in `recognize_path` - [@jcagarcia](https://github.com/jcagarcia).
88
* [#2383](https://github.com/ruby-grape/grape/pull/2383): Use regex block instead of if - [@ericproulx](https://github.com/ericproulx).
9+
* [#2384](https://github.com/ruby-grape/grape/pull/2384): Allow to use `before/after/rescue_from` methods in any order when using `mount` - [@jcagarcia](https://github.com/jcagarcia).
910
* Your contribution here.
1011

1112
#### Fixes

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,16 +408,28 @@ class Twitter::API < Grape::API
408408
end
409409
```
410410

411-
Keep in mind such declarations as `before/after/rescue_from` must be placed before `mount` in a case where they should be inherited.
411+
Declarations as `before/after/rescue_from` can be placed before or after `mount`. In any case they will be inherited.
412412

413413
```ruby
414414
class Twitter::API < Grape::API
415415
before do
416416
header 'X-Base-Header', 'will be defined for all APIs that are mounted below'
417417
end
418418

419+
rescue_from :all do
420+
error!({ "error" => "Internal Server Error" }, 500)
421+
end
422+
419423
mount Twitter::Users
420424
mount Twitter::Search
425+
426+
after do
427+
clean_cache!
428+
end
429+
430+
rescue_from ZeroDivisionError do
431+
error!({ "error" => "Not found" }, 404)
432+
end
421433
end
422434
```
423435

lib/grape/api.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,19 @@ def add_setup(method, *args, &block)
150150
@instances.each do |instance|
151151
last_response = replay_step_on(instance, setup_step)
152152
end
153+
154+
# Updating all previously mounted classes in the case that new methods have been executed.
155+
if method != :mount && @setup.any?
156+
previous_mount_steps = @setup.select { |step| step[:method] == :mount }
157+
previous_mount_steps.each do |mount_step|
158+
refresh_mount_step = mount_step.merge(method: :refresh_mounted_api)
159+
@setup += [refresh_mount_step]
160+
@instances.each do |instance|
161+
replay_step_on(instance, refresh_mount_step)
162+
end
163+
end
164+
end
165+
153166
last_response
154167
end
155168

lib/grape/dsl/routing.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ def mount(mounts, *opts)
8585
mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
8686
mounts.each_pair do |app, path|
8787
if app.respond_to?(:mount_instance)
88-
opts_with = opts.any? ? opts.shift[:with] : {}
89-
mount({ app.mount_instance(configuration: opts_with) => path })
88+
opts_with = opts.any? ? opts.first[:with] : {}
89+
mount({ app.mount_instance(configuration: opts_with) => path }, *opts)
9090
next
9191
end
9292
in_setting = inheritable_setting
@@ -103,6 +103,15 @@ def mount(mounts, *opts)
103103
change!
104104
end
105105

106+
# When trying to mount multiple times the same endpoint, remove the previous ones
107+
# from the list of endpoints if refresh_already_mounted parameter is true
108+
refresh_already_mounted = opts.any? ? opts.first[:refresh_already_mounted] : false
109+
if refresh_already_mounted && !endpoints.empty?
110+
endpoints.delete_if do |endpoint|
111+
endpoint.options[:app].to_s == app.to_s
112+
end
113+
end
114+
106115
endpoints << Grape::Endpoint.new(
107116
in_setting,
108117
method: :any,
@@ -225,6 +234,13 @@ def route_param(param, options = {}, &block)
225234
def versions
226235
@versions ||= []
227236
end
237+
238+
private
239+
240+
def refresh_mounted_api(mounts, *opts)
241+
opts << { refresh_already_mounted: true }
242+
mount(mounts, *opts)
243+
end
228244
end
229245
end
230246
end
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# frozen_string_literal: true
2+
3+
describe Grape::API do
4+
def app
5+
subject
6+
end
7+
8+
describe 'rescue_from' do
9+
context 'when the API is mounted AFTER defining the class rescue_from handler' do
10+
class APIRescueFrom < Grape::API
11+
rescue_from :all do
12+
error!({ type: 'all' }, 404)
13+
end
14+
15+
get do
16+
{ count: 1 / 0 }
17+
end
18+
end
19+
20+
class MainRescueFromAfter < Grape::API
21+
rescue_from ZeroDivisionError do
22+
error!({ type: 'zero' }, 500)
23+
end
24+
25+
mount APIRescueFrom
26+
end
27+
28+
subject { MainRescueFromAfter }
29+
30+
it 'is rescued by the rescue_from ZeroDivisionError handler from Main class' do
31+
get '/'
32+
33+
expect(last_response.status).to eq(500)
34+
expect(last_response.body).to eq({ type: 'zero' }.to_json)
35+
end
36+
end
37+
38+
context 'when the API is mounted BEFORE defining the class rescue_from handler' do
39+
class APIRescueFrom < Grape::API
40+
rescue_from :all do
41+
error!({ type: 'all' }, 404)
42+
end
43+
44+
get do
45+
{ count: 1 / 0 }
46+
end
47+
end
48+
49+
class MainRescueFromBefore < Grape::API
50+
mount APIRescueFrom
51+
52+
rescue_from ZeroDivisionError do
53+
error!({ type: 'zero' }, 500)
54+
end
55+
end
56+
57+
subject { MainRescueFromBefore }
58+
59+
it 'is rescued by the rescue_from ZeroDivisionError handler from Main class' do
60+
get '/'
61+
62+
expect(last_response.status).to eq(500)
63+
expect(last_response.body).to eq({ type: 'zero' }.to_json)
64+
end
65+
end
66+
end
67+
68+
describe 'before' do
69+
context 'when the API is mounted AFTER defining the before helper' do
70+
class APIBeforeHandler < Grape::API
71+
get do
72+
{ count: @count }.to_json
73+
end
74+
end
75+
76+
class MainBeforeHandlerAfter < Grape::API
77+
before do
78+
@count = 1
79+
end
80+
81+
mount APIBeforeHandler
82+
end
83+
84+
subject { MainBeforeHandlerAfter }
85+
86+
it 'is able to access the variables defined in the before helper' do
87+
get '/'
88+
89+
expect(last_response.status).to eq(200)
90+
expect(last_response.body).to eq({ count: 1 }.to_json)
91+
end
92+
end
93+
94+
context 'when the API is mounted BEFORE defining the before helper' do
95+
class APIBeforeHandler < Grape::API
96+
get do
97+
{ count: @count }.to_json
98+
end
99+
end
100+
101+
class MainBeforeHandlerBefore < Grape::API
102+
mount APIBeforeHandler
103+
104+
before do
105+
@count = 1
106+
end
107+
end
108+
109+
subject { MainBeforeHandlerBefore }
110+
111+
it 'is able to access the variables defined in the before helper' do
112+
get '/'
113+
114+
expect(last_response.status).to eq(200)
115+
expect(last_response.body).to eq({ count: 1 }.to_json)
116+
end
117+
end
118+
end
119+
120+
describe 'after' do
121+
context 'when the API is mounted AFTER defining the after handler' do
122+
class APIAfterHandler < Grape::API
123+
get do
124+
{ count: 1 }.to_json
125+
end
126+
end
127+
128+
class MainAfterHandlerAfter < Grape::API
129+
after do
130+
error!({ type: 'after' }, 500)
131+
end
132+
133+
mount APIAfterHandler
134+
end
135+
136+
subject { MainAfterHandlerAfter }
137+
138+
it 'is able to access the variables defined in the after helper' do
139+
get '/'
140+
141+
expect(last_response.status).to eq(500)
142+
expect(last_response.body).to eq({ type: 'after' }.to_json)
143+
end
144+
end
145+
146+
context 'when the API is mounted BEFORE defining the after helper' do
147+
class APIAfterHandler < Grape::API
148+
get do
149+
{ count: 1 }.to_json
150+
end
151+
end
152+
153+
class MainAfterHandlerBefore < Grape::API
154+
mount APIAfterHandler
155+
156+
after do
157+
error!({ type: 'after' }, 500)
158+
end
159+
end
160+
161+
subject { MainAfterHandlerBefore }
162+
163+
it 'is able to access the variables defined in the after helper' do
164+
get '/'
165+
166+
expect(last_response.status).to eq(500)
167+
expect(last_response.body).to eq({ type: 'after' }.to_json)
168+
end
169+
end
170+
end
171+
end
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# frozen_string_literal: true
2+
3+
describe Grape::API do
4+
def app
5+
subject
6+
end
7+
8+
context 'when multiple classes defines the same rescue_from' do
9+
class AnAPI < Grape::API
10+
rescue_from ZeroDivisionError do
11+
error!({ type: 'an-api-zero' }, 404)
12+
end
13+
14+
get '/an-api' do
15+
{ count: 1 / 0 }
16+
end
17+
end
18+
19+
class AnotherAPI < Grape::API
20+
rescue_from ZeroDivisionError do
21+
error!({ type: 'another-api-zero' }, 322)
22+
end
23+
24+
get '/another-api' do
25+
{ count: 1 / 0 }
26+
end
27+
end
28+
29+
class OtherMain < Grape::API
30+
mount AnAPI
31+
mount AnotherAPI
32+
end
33+
34+
subject { OtherMain }
35+
36+
it 'is rescued by the rescue_from ZeroDivisionError handler defined inside each of the classes' do
37+
get '/an-api'
38+
39+
expect(last_response.status).to eq(404)
40+
expect(last_response.body).to eq({ type: 'an-api-zero' }.to_json)
41+
42+
get '/another-api'
43+
44+
expect(last_response.status).to eq(322)
45+
expect(last_response.body).to eq({ type: 'another-api-zero' }.to_json)
46+
end
47+
48+
context 'when some class does not define a rescue_from but it was defined in a previous mounted endpoint' do
49+
class AnAPIWithoutDefinedRescueFrom < Grape::API
50+
get '/another-api-without-defined-rescue-from' do
51+
{ count: 1 / 0 }
52+
end
53+
end
54+
55+
class OtherMainWithNotDefinedRescueFrom < Grape::API
56+
mount AnAPI
57+
mount AnotherAPI
58+
mount AnAPIWithoutDefinedRescueFrom
59+
end
60+
61+
subject { OtherMainWithNotDefinedRescueFrom }
62+
63+
it 'is not rescued by any of the previous defined rescue_from ZeroDivisionError handlers' do
64+
get '/an-api'
65+
66+
expect(last_response.status).to eq(404)
67+
expect(last_response.body).to eq({ type: 'an-api-zero' }.to_json)
68+
69+
get '/another-api'
70+
71+
expect(last_response.status).to eq(322)
72+
expect(last_response.body).to eq({ type: 'another-api-zero' }.to_json)
73+
74+
expect do
75+
get '/another-api-without-defined-rescue-from'
76+
end.to raise_error(ZeroDivisionError)
77+
end
78+
end
79+
end
80+
end

0 commit comments

Comments
 (0)