Skip to content

Commit c2e41f7

Browse files
committed
Allow inserting middleware at arbitrary points in the middleware stack
Changes include: * Include the operation when stacking a new middleware The operation gets stored before the rest of the middleware data (class, args and block), and we use it later to determine the order in which the middleware stack will be built. Currently support `insert_after` and `insert_before`, plus `use` that does the same as before. * Implement a new `Grape::Middleware::Stack` class This class contains the functionality to build the middleware stack taking the different operations into account (`use`, `insert_before` and `insert_after`). It's mostly based on `ActionDispatch::MiddlewareStack` with some simplifications. * Build middleware stack for endpoint using `Grape::Middleware::Stack` Keep the same order for default middleware stack with `use` and use the new class's `build` method to determine the real order of the final stack. * Fix small typo in `Grape::Util::StackableValues` (replace `froozen_values` by `frozen_values`). * Update `CHANGELOG` with the PR number.
1 parent 9a08358 commit c2e41f7

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)