Skip to content

Commit d83a281

Browse files
authored
Introduce Middleware DEFAULT_OPTIONS with Application and Instance Configurability (#1572)
1 parent 1958cb1 commit d83a281

File tree

7 files changed

+308
-12
lines changed

7 files changed

+308
-12
lines changed

docs/middleware/index.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,49 @@ conn = Faraday.new do |f|
114114
end
115115
```
116116

117+
### DEFAULT_OPTIONS
118+
119+
`DEFAULT_OPTIONS` improve the flexibility and customizability of new and existing middleware. Class-level `DEFAULT_OPTIONS` and the ability to set these defaults at the application level compliment existing functionality in which options can be passed into middleware on a per-instance basis.
120+
121+
#### Using DEFAULT_OPTIONS
122+
123+
Using `RaiseError` as an example, you can see that `DEFAULT_OPTIONS` have been defined at the top of the class:
124+
125+
```ruby
126+
DEFAULT_OPTIONS = { include_request: true }.freeze
127+
```
128+
129+
These options will be set at the class level upon instantiation and referenced as needed within the class. From our same example:
130+
131+
```ruby
132+
def response_values(env)
133+
...
134+
return response unless options[:include_request]
135+
...
136+
```
137+
138+
If the default value provides the desired functionality, no further consideration is needed.
139+
140+
#### Setting Alternative Options per Application
141+
142+
In the case where it is desirable to change the default option for all instances within an application, it can be done by configuring the options in a `/config/initializers` file. For example:
143+
144+
```ruby
145+
# config/initializers/faraday_config.rb
146+
147+
Faraday::Response::RaiseError.default_options = { include_request: false }
148+
```
149+
150+
After app initialization, all instances of the middleware will have the newly configured option(s). They can still be overriden on a per-instance bases (if handled in the middleware), like this:
151+
152+
```ruby
153+
Faraday.new do |f|
154+
...
155+
f.response :raise_error, include_request: true
156+
...
157+
end
158+
```
159+
117160
### Available Middleware
118161

119162
The following pages provide detailed configuration for the middleware that ships with Faraday:

lib/faraday/error.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,8 @@ class SSLError < Error
158158
# Raised by middlewares that parse the response, like the JSON response middleware.
159159
class ParsingError < Error
160160
end
161+
162+
# Raised by Faraday::Middleware and subclasses when invalid default_options are used
163+
class InitializationError < Error
164+
end
161165
end

lib/faraday/middleware.rb

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,57 @@
11
# frozen_string_literal: true
22

3+
require 'monitor'
4+
35
module Faraday
46
# Middleware is the basic base class of any Faraday middleware.
57
class Middleware
68
extend MiddlewareRegistry
79

810
attr_reader :app, :options
911

12+
DEFAULT_OPTIONS = {}.freeze
13+
1014
def initialize(app = nil, options = {})
1115
@app = app
12-
@options = options
16+
@options = self.class.default_options.merge(options)
17+
end
18+
19+
class << self
20+
# Faraday::Middleware::default_options= allows user to set default options at the Faraday::Middleware
21+
# class level.
22+
#
23+
# @example Set the Faraday::Response::RaiseError option, `include_request` to `false`
24+
# my_app/config/initializers/my_faraday_middleware.rb
25+
#
26+
# Faraday::Response::RaiseError.default_options = { include_request: false }
27+
#
28+
def default_options=(options = {})
29+
validate_default_options(options)
30+
lock.synchronize do
31+
@default_options = default_options.merge(options)
32+
end
33+
end
34+
35+
# default_options attr_reader that initializes class instance variable
36+
# with the values of any Faraday::Middleware defaults, and merges with
37+
# subclass defaults
38+
def default_options
39+
@default_options ||= DEFAULT_OPTIONS.merge(self::DEFAULT_OPTIONS)
40+
end
41+
42+
private
43+
44+
def lock
45+
@lock ||= Monitor.new
46+
end
47+
48+
def validate_default_options(options)
49+
invalid_keys = options.keys.reject { |opt| self::DEFAULT_OPTIONS.key?(opt) }
50+
return unless invalid_keys.any?
51+
52+
raise(Faraday::InitializationError,
53+
"Invalid options provided. Keys not found in #{self}::DEFAULT_OPTIONS: #{invalid_keys.join(', ')}")
54+
end
1355
end
1456

1557
def call(env)

lib/faraday/response/raise_error.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class RaiseError < Middleware
1010
ServerErrorStatuses = (500...600)
1111
# rubocop:enable Naming/ConstantName
1212

13+
DEFAULT_OPTIONS = { include_request: true }.freeze
14+
1315
def on_complete(env)
1416
case env[:status]
1517
when 400
@@ -58,7 +60,7 @@ def response_values(env)
5860

5961
# Include the request data by default. If the middleware was explicitly
6062
# configured to _not_ include request data, then omit it.
61-
return response unless options.fetch(:include_request, true)
63+
return response unless options[:include_request]
6264

6365
response.merge(
6466
request: {

spec/faraday/middleware_spec.rb

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,147 @@ def on_error(error)
6767
end
6868
end
6969
end
70+
71+
describe '::default_options' do
72+
let(:subclass_no_options) { FaradayMiddlewareSubclasses::SubclassNoOptions }
73+
let(:subclass_one_option) { FaradayMiddlewareSubclasses::SubclassOneOption }
74+
let(:subclass_two_options) { FaradayMiddlewareSubclasses::SubclassTwoOptions }
75+
76+
def build_conn(resp_middleware)
77+
Faraday.new do |c|
78+
c.adapter :test do |stub|
79+
stub.get('/success') { [200, {}, 'ok'] }
80+
end
81+
c.response resp_middleware
82+
end
83+
end
84+
85+
RSpec.shared_context 'reset @default_options' do
86+
before(:each) do
87+
FaradayMiddlewareSubclasses::SubclassNoOptions.instance_variable_set(:@default_options, nil)
88+
FaradayMiddlewareSubclasses::SubclassOneOption.instance_variable_set(:@default_options, nil)
89+
FaradayMiddlewareSubclasses::SubclassTwoOptions.instance_variable_set(:@default_options, nil)
90+
Faraday::Middleware.instance_variable_set(:@default_options, nil)
91+
end
92+
end
93+
94+
after(:all) do
95+
FaradayMiddlewareSubclasses::SubclassNoOptions.instance_variable_set(:@default_options, nil)
96+
FaradayMiddlewareSubclasses::SubclassOneOption.instance_variable_set(:@default_options, nil)
97+
FaradayMiddlewareSubclasses::SubclassTwoOptions.instance_variable_set(:@default_options, nil)
98+
Faraday::Middleware.instance_variable_set(:@default_options, nil)
99+
end
100+
101+
context 'with subclass DEFAULT_OPTIONS defined' do
102+
include_context 'reset @default_options'
103+
104+
context 'and without application options configured' do
105+
let(:resp1) { build_conn(:one_option).get('/success') }
106+
107+
it 'has only subclass defaults' do
108+
expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
109+
expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS)
110+
expect(subclass_one_option.default_options).to eq(subclass_one_option::DEFAULT_OPTIONS)
111+
expect(subclass_two_options.default_options).to eq(subclass_two_options::DEFAULT_OPTIONS)
112+
end
113+
114+
it { expect(resp1.body).to eq('ok') }
115+
end
116+
117+
context "and with one application's options changed" do
118+
let(:resp2) { build_conn(:two_options).get('/success') }
119+
120+
before(:each) do
121+
FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false }
122+
end
123+
124+
it 'only updates default options of target subclass' do
125+
expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
126+
expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS)
127+
expect(subclass_one_option.default_options).to eq(subclass_one_option::DEFAULT_OPTIONS)
128+
expect(subclass_two_options.default_options).to eq({ some_option: false, some_other_option: false })
129+
end
130+
131+
it { expect(resp2.body).to eq('ok') }
132+
end
133+
134+
context "and with two applications' options changed" do
135+
let(:resp1) { build_conn(:one_option).get('/success') }
136+
let(:resp2) { build_conn(:two_options).get('/success') }
137+
138+
before(:each) do
139+
FaradayMiddlewareSubclasses::SubclassOneOption.default_options = { some_other_option: true }
140+
FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false }
141+
end
142+
143+
it 'updates subclasses and parent independent of each other' do
144+
expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
145+
expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS)
146+
expect(subclass_one_option.default_options).to eq({ some_other_option: true })
147+
expect(subclass_two_options.default_options).to eq({ some_option: false, some_other_option: false })
148+
end
149+
150+
it { expect(resp1.body).to eq('ok') }
151+
it { expect(resp2.body).to eq('ok') }
152+
end
153+
end
154+
155+
context 'with FARADAY::MIDDLEWARE DEFAULT_OPTIONS and with Subclass DEFAULT_OPTIONS' do
156+
before(:each) do
157+
stub_const('Faraday::Middleware::DEFAULT_OPTIONS', { its_magic: false })
158+
end
159+
160+
# Must stub Faraday::Middleware::DEFAULT_OPTIONS before resetting default options
161+
include_context 'reset @default_options'
162+
163+
context 'and without application options configured' do
164+
let(:resp1) { build_conn(:one_option).get('/success') }
165+
166+
it 'has only subclass defaults' do
167+
expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
168+
expect(FaradayMiddlewareSubclasses::SubclassNoOptions.default_options).to eq({ its_magic: false })
169+
expect(FaradayMiddlewareSubclasses::SubclassOneOption.default_options).to eq({ its_magic: false, some_other_option: false })
170+
expect(FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options).to eq({ its_magic: false, some_option: true, some_other_option: false })
171+
end
172+
173+
it { expect(resp1.body).to eq('ok') }
174+
end
175+
176+
context "and with two applications' options changed" do
177+
let(:resp1) { build_conn(:one_option).get('/success') }
178+
let(:resp2) { build_conn(:two_options).get('/success') }
179+
180+
before(:each) do
181+
FaradayMiddlewareSubclasses::SubclassOneOption.default_options = { some_other_option: true }
182+
FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false }
183+
end
184+
185+
it 'updates subclasses and parent independent of each other' do
186+
expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
187+
expect(FaradayMiddlewareSubclasses::SubclassNoOptions.default_options).to eq({ its_magic: false })
188+
expect(FaradayMiddlewareSubclasses::SubclassOneOption.default_options).to eq({ its_magic: false, some_other_option: true })
189+
expect(FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options).to eq({ its_magic: false, some_option: false, some_other_option: false })
190+
end
191+
192+
it { expect(resp1.body).to eq('ok') }
193+
it { expect(resp2.body).to eq('ok') }
194+
end
195+
end
196+
197+
describe 'default_options input validation' do
198+
include_context 'reset @default_options'
199+
200+
it 'raises error if Faraday::Middleware option does not exist' do
201+
expect { Faraday::Middleware.default_options = { something_special: true } }.to raise_error(Faraday::InitializationError) do |e|
202+
expect(e.message).to eq('Invalid options provided. Keys not found in Faraday::Middleware::DEFAULT_OPTIONS: something_special')
203+
end
204+
end
205+
206+
it 'raises error if subclass option does not exist' do
207+
expect { subclass_one_option.default_options = { this_is_a_typo: true } }.to raise_error(Faraday::InitializationError) do |e|
208+
expect(e.message).to eq('Invalid options provided. Keys not found in FaradayMiddlewareSubclasses::SubclassOneOption::DEFAULT_OPTIONS: this_is_a_typo')
209+
end
210+
end
211+
end
212+
end
70213
end

spec/faraday/response/raise_error_spec.rb

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -194,16 +194,60 @@
194194
end
195195
end
196196

197-
context 'when the include_request option is set to false' do
198-
let(:middleware_options) { { include_request: false } }
199-
200-
it 'does not include request info in the exception' do
201-
expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex|
202-
expect(ex.response.keys).to contain_exactly(
203-
:status,
204-
:headers,
205-
:body
206-
)
197+
describe 'DEFAULT_OPTION: include_request' do
198+
before(:each) do
199+
Faraday::Response::RaiseError.instance_variable_set(:@default_options, nil)
200+
Faraday::Middleware.instance_variable_set(:@default_options, nil)
201+
end
202+
203+
after(:all) do
204+
Faraday::Response::RaiseError.instance_variable_set(:@default_options, nil)
205+
Faraday::Middleware.instance_variable_set(:@default_options, nil)
206+
end
207+
208+
context 'when RaiseError DEFAULT_OPTION (include_request: true) is used' do
209+
it 'includes request info in the exception' do
210+
expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex|
211+
expect(ex.response.keys).to contain_exactly(
212+
:status,
213+
:headers,
214+
:body,
215+
:request
216+
)
217+
end
218+
end
219+
end
220+
221+
context 'when application sets default_options `include_request: false`' do
222+
before(:each) do
223+
Faraday::Response::RaiseError.default_options = { include_request: false }
224+
end
225+
226+
context 'and when include_request option is omitted' do
227+
it 'does not include request info in the exception' do
228+
expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex|
229+
expect(ex.response.keys).to contain_exactly(
230+
:status,
231+
:headers,
232+
:body
233+
)
234+
end
235+
end
236+
end
237+
238+
context 'and when include_request option is explicitly set for instance' do
239+
let(:middleware_options) { { include_request: true } }
240+
241+
it 'includes request info in the exception' do
242+
expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex|
243+
expect(ex.response.keys).to contain_exactly(
244+
:status,
245+
:headers,
246+
:body,
247+
:request
248+
)
249+
end
250+
end
207251
end
208252
end
209253
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
module FaradayMiddlewareSubclasses
4+
class SubclassNoOptions < Faraday::Middleware
5+
end
6+
7+
class SubclassOneOption < Faraday::Middleware
8+
DEFAULT_OPTIONS = { some_other_option: false }.freeze
9+
end
10+
11+
class SubclassTwoOptions < Faraday::Middleware
12+
DEFAULT_OPTIONS = { some_option: true, some_other_option: false }.freeze
13+
end
14+
end
15+
16+
Faraday::Response.register_middleware(no_options: FaradayMiddlewareSubclasses::SubclassNoOptions)
17+
Faraday::Response.register_middleware(one_option: FaradayMiddlewareSubclasses::SubclassOneOption)
18+
Faraday::Response.register_middleware(two_options: FaradayMiddlewareSubclasses::SubclassTwoOptions)

0 commit comments

Comments
 (0)