Skip to content

Commit 05bb1b4

Browse files
committed
Merge pull request #1390 from Rosa/insert-middlewares-before-after
Allow inserting middleware at arbitrary points in the middleware stack
2 parents 9a08358 + c2e41f7 commit 05bb1b4

File tree

9 files changed

+342
-53
lines changed

9 files changed

+342
-53
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#### Features
77

8+
* [#1390](https://github.com/ruby-grape/grape/pull/1390): Allow inserting middleware at arbitrary points in the middleware stack - [@Rosa](https://github.com/Rosa).
89
* [#1366](https://github.com/ruby-grape/grape/pull/1366): Store `message_key` on `Grape::Exceptions::Validation` - [@mkou](https://github.com/mkou).
910

1011
#### Fixes

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2807,7 +2807,27 @@ Your middleware can overwrite application response as follows, except error case
28072807
```ruby
28082808
class Overwriter < Grape::Middleware::Base
28092809
def after
2810-
[200, { 'Content-Type' => 'text/plain' }, ['Overwrited.']]
2810+
[200, { 'Content-Type' => 'text/plain' }, ['Overwritten.']]
2811+
end
2812+
end
2813+
```
2814+
2815+
You can add your custom middleware with `use`, that push the middleware onto the stack, and you can also control where the middleware is inserted using `insert`, `insert_before` and `insert_after`.
2816+
2817+
```ruby
2818+
class CustomOverwriter < Grape::Middleware::Base
2819+
def after
2820+
[200, { 'Content-Type' => 'text/plain' }, [@options[:message]]]
2821+
end
2822+
end
2823+
2824+
2825+
class API < Grape::API
2826+
use Overwriter
2827+
insert_before Overwriter, CustomOverwriter, message: 'Overwritten again.'
2828+
insert 0, CustomOverwriter, message: 'Overwrites all other middleware.'
2829+
2830+
get '/' do
28112831
end
28122832
end
28132833
```

lib/grape/dsl/middleware.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,21 @@ module ClassMethods
1515
# @param middleware_class [Class] The class of the middleware you'd like
1616
# to inject.
1717
def use(middleware_class, *args, &block)
18-
arr = [middleware_class, *args]
18+
arr = [:use, middleware_class, *args]
19+
arr << block if block_given?
20+
21+
namespace_stackable(:middleware, arr)
22+
end
23+
24+
def insert_before(*args, &block)
25+
arr = [:insert_before, *args]
26+
arr << block if block_given?
27+
28+
namespace_stackable(:middleware, arr)
29+
end
30+
31+
def insert_after(*args, &block)
32+
arr = [:insert_after, *args]
1933
arr << block if block_given?
2034

2135
namespace_stackable(:middleware, arr)

lib/grape/endpoint.rb

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'grape/middleware/stack'
2+
13
module Grape
24
# An Endpoint is the proxy scope in which all routing
35
# blocks are executed. In other words, any methods
@@ -243,45 +245,43 @@ def run
243245
end
244246

245247
def build_stack
246-
b = Rack::Builder.new
247-
248-
b.use Rack::Head
249-
b.use Grape::Middleware::Error,
250-
format: namespace_inheritable(:format),
251-
content_types: namespace_stackable_with_hash(:content_types),
252-
default_status: namespace_inheritable(:default_error_status),
253-
rescue_all: namespace_inheritable(:rescue_all),
254-
default_error_formatter: namespace_inheritable(:default_error_formatter),
255-
error_formatters: namespace_stackable_with_hash(:error_formatters),
256-
rescue_options: namespace_stackable_with_hash(:rescue_options) || {},
257-
rescue_handlers: namespace_stackable_with_hash(:rescue_handlers) || {},
258-
base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers) || {},
259-
all_rescue_handler: namespace_inheritable(:all_rescue_handler)
260-
261-
(namespace_stackable(:middleware) || []).each do |m|
262-
m = m.dup
263-
block = m.pop if m.last.is_a?(Proc)
264-
block ? b.use(*m, &block) : b.use(*m)
265-
end
248+
ms = Grape::Middleware::Stack.new
249+
250+
ms.use Rack::Head
251+
ms.use Grape::Middleware::Error,
252+
format: namespace_inheritable(:format),
253+
content_types: namespace_stackable_with_hash(:content_types),
254+
default_status: namespace_inheritable(:default_error_status),
255+
rescue_all: namespace_inheritable(:rescue_all),
256+
default_error_formatter: namespace_inheritable(:default_error_formatter),
257+
error_formatters: namespace_stackable_with_hash(:error_formatters),
258+
rescue_options: namespace_stackable_with_hash(:rescue_options) || {},
259+
rescue_handlers: namespace_stackable_with_hash(:rescue_handlers) || {},
260+
base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers) || {},
261+
all_rescue_handler: namespace_inheritable(:all_rescue_handler)
262+
263+
ms.merge_with(namespace_stackable(:middleware) || [])
266264

267265
if namespace_inheritable(:version)
268-
b.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]),
269-
versions: namespace_inheritable(:version) ? namespace_inheritable(:version).flatten : nil,
270-
version_options: namespace_inheritable(:version_options),
271-
prefix: namespace_inheritable(:root_prefix)
272-
266+
ms.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]),
267+
versions: namespace_inheritable(:version) ? namespace_inheritable(:version).flatten : nil,
268+
version_options: namespace_inheritable(:version_options),
269+
prefix: namespace_inheritable(:root_prefix)
273270
end
274271

275-
b.use Grape::Middleware::Formatter,
276-
format: namespace_inheritable(:format),
277-
default_format: namespace_inheritable(:default_format) || :txt,
278-
content_types: namespace_stackable_with_hash(:content_types),
279-
formatters: namespace_stackable_with_hash(:formatters),
280-
parsers: namespace_stackable_with_hash(:parsers)
272+
ms.use Grape::Middleware::Formatter,
273+
format: namespace_inheritable(:format),
274+
default_format: namespace_inheritable(:default_format) || :txt,
275+
content_types: namespace_stackable_with_hash(:content_types),
276+
formatters: namespace_stackable_with_hash(:formatters),
277+
parsers: namespace_stackable_with_hash(:parsers)
278+
279+
builder = Rack::Builder.new
280+
ms.build(builder)
281281

282-
b.run ->(env) { env[Grape::Env::API_ENDPOINT].run }
282+
builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run }
283283

284-
b.to_app
284+
builder.to_app
285285
end
286286

287287
def build_helpers

lib/grape/middleware/stack.rb

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
module Grape
2+
module Middleware
3+
# Class to handle the stack of middlewares based on ActionDispatch::MiddlewareStack
4+
# It allows to insert and insert after
5+
class Stack
6+
class Middleware
7+
attr_reader :args, :block, :klass
8+
9+
def initialize(klass, *args, &block)
10+
@klass = klass
11+
@args = args
12+
@block = block
13+
end
14+
15+
def name
16+
klass.name
17+
end
18+
19+
def ==(other)
20+
case other
21+
when Middleware
22+
klass == other.klass
23+
when Class
24+
klass == other
25+
end
26+
end
27+
28+
def inspect
29+
klass.to_s
30+
end
31+
end
32+
33+
include Enumerable
34+
35+
attr_accessor :middlewares
36+
37+
def initialize
38+
@middlewares = []
39+
end
40+
41+
def each
42+
@middlewares.each { |x| yield x }
43+
end
44+
45+
def size
46+
middlewares.size
47+
end
48+
49+
def last
50+
middlewares.last
51+
end
52+
53+
def [](i)
54+
middlewares[i]
55+
end
56+
57+
def insert(index, *args, &block)
58+
index = assert_index(index, :before)
59+
middleware = self.class::Middleware.new(*args, &block)
60+
middlewares.insert(index, middleware)
61+
end
62+
63+
alias insert_before insert
64+
65+
def insert_after(index, *args, &block)
66+
index = assert_index(index, :after)
67+
insert(index + 1, *args, &block)
68+
end
69+
70+
def use(*args, &block)
71+
middleware = self.class::Middleware.new(*args, &block)
72+
middlewares.push(middleware)
73+
end
74+
75+
def merge_with(other)
76+
other.each do |operation, *args|
77+
block = args.pop if args.last.is_a?(Proc)
78+
block ? send(operation, *args, &block) : send(operation, *args)
79+
end
80+
end
81+
82+
def build(builder)
83+
middlewares.each do |m|
84+
m.block ? builder.use(m.klass, *m.args, &m.block) : builder.use(m.klass, *m.args)
85+
end
86+
end
87+
88+
protected
89+
90+
def assert_index(index, where)
91+
i = index.is_a?(Integer) ? index : middlewares.index(index)
92+
raise "No such middleware to insert #{where}: #{index.inspect}" unless i
93+
i
94+
end
95+
end
96+
end
97+
end

lib/grape/util/stackable_values.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ module Util
33
class StackableValues
44
attr_accessor :inherited_values
55
attr_reader :new_values
6-
attr_reader :froozen_values
6+
attr_reader :frozen_values
77

88
def initialize(inherited_values = {})
99
@inherited_values = inherited_values
1010
@new_values = {}
11-
@froozen_values = {}
11+
@frozen_values = {}
1212
end
1313

1414
def [](name)
15-
return @froozen_values[name] if @froozen_values.key? name
15+
return @frozen_values[name] if @frozen_values.key? name
1616

1717
value = []
1818
value.concat(@inherited_values[name]) if @inherited_values[name]
@@ -21,7 +21,7 @@ def [](name)
2121
end
2222

2323
def []=(name, value)
24-
raise if @froozen_values.key? name
24+
raise if @frozen_values.key? name
2525
@new_values[name] ||= []
2626
@new_values[name].push value
2727
end
@@ -43,7 +43,7 @@ def to_hash
4343
end
4444

4545
def freeze_value(key)
46-
@froozen_values[key] = self[key].freeze
46+
@frozen_values[key] = self[key].freeze
4747
end
4848

4949
def initialize_copy(other)

spec/grape/api_spec.rb

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,7 +1044,7 @@ def call(env)
10441044
describe '.middleware' do
10451045
it 'includes middleware arguments from settings' do
10461046
subject.use ApiSpec::PhonyMiddleware, 'abc', 123
1047-
expect(subject.middleware).to eql [[ApiSpec::PhonyMiddleware, 'abc', 123]]
1047+
expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 'abc', 123]]
10481048
end
10491049

10501050
it 'includes all middleware from stacked settings' do
@@ -1053,17 +1053,17 @@ def call(env)
10531053
subject.use ApiSpec::PhonyMiddleware, 'foo'
10541054

10551055
expect(subject.middleware).to eql [
1056-
[ApiSpec::PhonyMiddleware, 123],
1057-
[ApiSpec::PhonyMiddleware, 'abc'],
1058-
[ApiSpec::PhonyMiddleware, 'foo']
1056+
[:use, ApiSpec::PhonyMiddleware, 123],
1057+
[:use, ApiSpec::PhonyMiddleware, 'abc'],
1058+
[:use, ApiSpec::PhonyMiddleware, 'foo']
10591059
]
10601060
end
10611061
end
10621062

10631063
describe '.use' do
10641064
it 'adds middleware' do
10651065
subject.use ApiSpec::PhonyMiddleware, 123
1066-
expect(subject.middleware).to eql [[ApiSpec::PhonyMiddleware, 123]]
1066+
expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123]]
10671067
end
10681068

10691069
it 'does not show up outside the namespace' do
@@ -1074,8 +1074,8 @@ def call(env)
10741074
inner_middleware = middleware
10751075
end
10761076

1077-
expect(subject.middleware).to eql [[ApiSpec::PhonyMiddleware, 123]]
1078-
expect(inner_middleware).to eql [[ApiSpec::PhonyMiddleware, 123], [ApiSpec::PhonyMiddleware, 'abc']]
1077+
expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123]]
1078+
expect(inner_middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123], [:use, ApiSpec::PhonyMiddleware, 'abc']]
10791079
end
10801080

10811081
it 'calls the middleware' do
@@ -1091,7 +1091,7 @@ def call(env)
10911091
it 'adds a block if one is given' do
10921092
block = -> {}
10931093
subject.use ApiSpec::PhonyMiddleware, &block
1094-
expect(subject.middleware).to eql [[ApiSpec::PhonyMiddleware, block]]
1094+
expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, block]]
10951095
end
10961096

10971097
it 'uses a block if one is given' do
@@ -1132,7 +1132,50 @@ def before
11321132
expect(last_response.body).to eq('Caught in the Net')
11331133
end
11341134
end
1135+
1136+
describe '.insert_before' do
1137+
it 'runs before a given middleware' do
1138+
m = Class.new(Grape::Middleware::Base) do
1139+
def call(env)
1140+
env['phony.args'] ||= []
1141+
env['phony.args'] << @options[:message]
1142+
@app.call(env)
1143+
end
1144+
end
1145+
1146+
subject.use ApiSpec::PhonyMiddleware, 'hello'
1147+
subject.insert_before ApiSpec::PhonyMiddleware, m, message: 'bye'
1148+
subject.get '/' do
1149+
env['phony.args'].join(' ')
1150+
end
1151+
1152+
get '/'
1153+
expect(last_response.body).to eql 'bye hello'
1154+
end
1155+
end
1156+
1157+
describe '.insert_after' do
1158+
it 'runs after a given middleware' do
1159+
m = Class.new(Grape::Middleware::Base) do
1160+
def call(env)
1161+
env['phony.args'] ||= []
1162+
env['phony.args'] << @options[:message]
1163+
@app.call(env)
1164+
end
1165+
end
1166+
1167+
subject.use ApiSpec::PhonyMiddleware, 'hello'
1168+
subject.insert_after ApiSpec::PhonyMiddleware, m, message: 'bye'
1169+
subject.get '/' do
1170+
env['phony.args'].join(' ')
1171+
end
1172+
1173+
get '/'
1174+
expect(last_response.body).to eql 'hello bye'
1175+
end
1176+
end
11351177
end
1178+
11361179
describe '.http_basic' do
11371180
it 'protects any resources on the same scope' do
11381181
subject.http_basic do |u, _p|

0 commit comments

Comments
 (0)