faraday_middleware 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.rspec +0 -1
- data/.travis.yml +1 -2
- data/CHANGELOG.md +9 -9
- data/Gemfile +3 -3
- data/README.md +2 -2
- data/Rakefile +9 -2
- data/faraday_middleware.gemspec +16 -24
- data/lib/faraday_middleware.rb +51 -11
- data/lib/faraday_middleware/addressable_patch.rb +20 -0
- data/lib/faraday_middleware/backwards_compatibility.rb +30 -0
- data/lib/faraday_middleware/instrumentation.rb +30 -0
- data/lib/faraday_middleware/rack_compatible.rb +76 -0
- data/lib/faraday_middleware/request/encode_json.rb +50 -0
- data/lib/faraday_middleware/request/oauth.rb +61 -0
- data/lib/faraday_middleware/request/oauth2.rb +60 -0
- data/lib/faraday_middleware/response/caching.rb +76 -0
- data/lib/faraday_middleware/response/follow_redirects.rb +53 -0
- data/lib/{faraday → faraday_middleware}/response/mashify.rb +2 -2
- data/lib/faraday_middleware/response/parse_json.rb +35 -0
- data/lib/faraday_middleware/response/parse_marshal.rb +10 -0
- data/lib/faraday_middleware/response/parse_xml.rb +11 -0
- data/lib/faraday_middleware/response/parse_yaml.rb +10 -0
- data/lib/faraday_middleware/response/rashify.rb +9 -0
- data/lib/faraday_middleware/response_middleware.rb +78 -0
- data/lib/faraday_middleware/version.rb +1 -1
- data/spec/caching_test.rb +122 -0
- data/spec/encode_json_spec.rb +95 -0
- data/spec/follow_redirects_spec.rb +33 -0
- data/spec/helper.rb +27 -12
- data/spec/mashify_spec.rb +8 -7
- data/spec/oauth2_spec.rb +100 -32
- data/spec/oauth_spec.rb +83 -28
- data/spec/parse_json_spec.rb +71 -46
- data/spec/parse_marshal_spec.rb +9 -26
- data/spec/parse_xml_spec.rb +56 -24
- data/spec/parse_yaml_spec.rb +40 -20
- data/spec/rashify_spec.rb +4 -3
- metadata +59 -57
- data/lib/faraday/request/oauth.rb +0 -23
- data/lib/faraday/request/oauth2.rb +0 -24
- data/lib/faraday/response/parse_json.rb +0 -20
- data/lib/faraday/response/parse_marshal.rb +0 -10
- data/lib/faraday/response/parse_xml.rb +0 -11
- data/lib/faraday/response/parse_yaml.rb +0 -11
- data/lib/faraday/response/rashify.rb +0 -19
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module FaradayMiddleware
|
5
|
+
# Public: A simple middleware that adds an access token to each request.
|
6
|
+
#
|
7
|
+
# The token is added as both "access_token" query parameter and the
|
8
|
+
# "Authorization" HTTP request header. However, an explicit "access_token"
|
9
|
+
# parameter or "Authorization" header for the current request are not
|
10
|
+
# overriden.
|
11
|
+
#
|
12
|
+
# Examples
|
13
|
+
#
|
14
|
+
# # configure default token:
|
15
|
+
# OAuth2.new(app, 'abc123')
|
16
|
+
#
|
17
|
+
# # configure query parameter name:
|
18
|
+
# OAuth2.new(app, 'abc123', :param_name => 'my_oauth_token')
|
19
|
+
#
|
20
|
+
# # default token value is optional:
|
21
|
+
# OAuth2.new(app, :param_name => 'my_oauth_token')
|
22
|
+
class OAuth2 < Faraday::Middleware
|
23
|
+
dependency 'oauth2'
|
24
|
+
|
25
|
+
PARAM_NAME = 'access_token'.freeze
|
26
|
+
AUTH_HEADER = 'Authorization'.freeze
|
27
|
+
|
28
|
+
attr_reader :param_name
|
29
|
+
|
30
|
+
extend Forwardable
|
31
|
+
def_delegators :'Faraday::Utils', :parse_query, :build_query
|
32
|
+
|
33
|
+
def call(env)
|
34
|
+
params = { param_name => @token }.update query_params(env[:url])
|
35
|
+
|
36
|
+
if token = params[param_name] and !token.empty?
|
37
|
+
env[:url].query = build_query params
|
38
|
+
env[:request_headers][AUTH_HEADER] ||= %(Token token="#{token}")
|
39
|
+
end
|
40
|
+
|
41
|
+
@app.call env
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(app, token = nil, options = {})
|
45
|
+
super(app)
|
46
|
+
options, token = token, nil if token.is_a? Hash
|
47
|
+
@token = token && token.to_s
|
48
|
+
@param_name = options.fetch(:param_name, PARAM_NAME).to_s
|
49
|
+
raise ArgumentError, ":param_name can't be blank" if @param_name.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
def query_params(url)
|
53
|
+
if url.query.nil? or url.query.empty?
|
54
|
+
{}
|
55
|
+
else
|
56
|
+
parse_query url.query
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'forwardable'
|
3
|
+
# fixes normalizing query strings:
|
4
|
+
require 'faraday_middleware/addressable_patch' if defined? ::Addressable::URI
|
5
|
+
|
6
|
+
module FaradayMiddleware
|
7
|
+
# Public: Caches GET responses and pulls subsequent ones from the cache.
|
8
|
+
class Caching < Faraday::Middleware
|
9
|
+
attr_reader :cache
|
10
|
+
|
11
|
+
extend Forwardable
|
12
|
+
def_delegators :'Faraday::Utils', :parse_query, :build_query
|
13
|
+
|
14
|
+
# Public: initialize the middleware.
|
15
|
+
#
|
16
|
+
# cache - An object that responds to read, write and fetch (default: nil).
|
17
|
+
# options - An options Hash (default: {}):
|
18
|
+
# :ignore_params - String name or Array names of query params
|
19
|
+
# that should be ignored when forming the cache
|
20
|
+
# key (default: []).
|
21
|
+
#
|
22
|
+
# Yields if no cache is given. The block should return a cache object.
|
23
|
+
def initialize(app, cache = nil, options = {})
|
24
|
+
super(app)
|
25
|
+
options, cache = cache, nil if cache.is_a? Hash and block_given?
|
26
|
+
@cache = cache || yield
|
27
|
+
@options = options
|
28
|
+
end
|
29
|
+
|
30
|
+
def call(env)
|
31
|
+
if :get == env[:method]
|
32
|
+
if env[:parallel_manager]
|
33
|
+
# callback mode
|
34
|
+
cache_on_complete(env)
|
35
|
+
else
|
36
|
+
# synchronous mode
|
37
|
+
response = cache.fetch(cache_key(env)) { @app.call(env) }
|
38
|
+
finalize_response(response, env)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
@app.call(env)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def cache_key(env)
|
46
|
+
url = env[:url].dup
|
47
|
+
if url.query && params_to_ignore.any?
|
48
|
+
params = parse_query url.query
|
49
|
+
params.reject! {|k,| params_to_ignore.include? k }
|
50
|
+
url.query = build_query params
|
51
|
+
end
|
52
|
+
url.normalize!
|
53
|
+
url.request_uri
|
54
|
+
end
|
55
|
+
|
56
|
+
def params_to_ignore
|
57
|
+
@params_to_ignore ||= Array(@options[:ignore_params]).map { |p| p.to_s }
|
58
|
+
end
|
59
|
+
|
60
|
+
def cache_on_complete(env)
|
61
|
+
key = cache_key(env)
|
62
|
+
if cached_response = cache.read(key)
|
63
|
+
finalize_response(cached_response, env)
|
64
|
+
else
|
65
|
+
response = @app.call(env)
|
66
|
+
response.on_complete { cache.write(key, response) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def finalize_response(response, env)
|
71
|
+
response = env[:response] = response.dup if response.frozen?
|
72
|
+
response.apply_request env unless response.env[:method]
|
73
|
+
response
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
module FaradayMiddleware
|
4
|
+
class RedirectLimitReached < Faraday::Error::ClientError
|
5
|
+
attr_reader :response
|
6
|
+
|
7
|
+
def initialize(response)
|
8
|
+
super "too many redirects; last one to: #{response['location']}"
|
9
|
+
@response = response
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Public: Follow HTTP 30x redirects.
|
14
|
+
class FollowRedirects < Faraday::Middleware
|
15
|
+
# TODO: 307 & standards-compliant 302
|
16
|
+
REDIRECTS = [301, 302, 303]
|
17
|
+
# Default value for max redirects followed
|
18
|
+
FOLLOW_LIMIT = 3
|
19
|
+
|
20
|
+
# Public: Initialize the middleware.
|
21
|
+
#
|
22
|
+
# options - An options Hash (default: {}):
|
23
|
+
# limit - A Numeric redirect limit (default: 3)
|
24
|
+
def initialize(app, options = {})
|
25
|
+
super(app)
|
26
|
+
@options = options
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(env)
|
30
|
+
process_response(@app.call(env), follow_limit)
|
31
|
+
end
|
32
|
+
|
33
|
+
def process_response(response, follows)
|
34
|
+
response.on_complete do |env|
|
35
|
+
if redirect? response
|
36
|
+
raise RedirectLimitReached, response if follows.zero?
|
37
|
+
env[:url] += response['location']
|
38
|
+
env[:method] = :get
|
39
|
+
response = process_response(@app.call(env), follows - 1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
response
|
43
|
+
end
|
44
|
+
|
45
|
+
def redirect?(response)
|
46
|
+
REDIRECTS.include? response.status
|
47
|
+
end
|
48
|
+
|
49
|
+
def follow_limit
|
50
|
+
@options.fetch(:limit, FOLLOW_LIMIT)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'faraday_middleware/response_middleware'
|
2
|
+
|
3
|
+
module FaradayMiddleware
|
4
|
+
# Public: Parse response bodies as JSON.
|
5
|
+
class ParseJson < ResponseMiddleware
|
6
|
+
dependency 'json'
|
7
|
+
|
8
|
+
define_parser { |body|
|
9
|
+
JSON.parse body unless body.empty?
|
10
|
+
}
|
11
|
+
|
12
|
+
# Public: Override the content-type of the response with "application/json"
|
13
|
+
# if the response body looks like it might be JSON, i.e. starts with an
|
14
|
+
# open bracket.
|
15
|
+
#
|
16
|
+
# This is to fix responses from certain API providers that insist on serving
|
17
|
+
# JSON with wrong MIME-types such as "text/javascript".
|
18
|
+
class MimeTypeFix < ResponseMiddleware
|
19
|
+
MIME_TYPE = 'application/json'.freeze
|
20
|
+
|
21
|
+
def process_response(env)
|
22
|
+
old_type = env[:response_headers][CONTENT_TYPE].to_s
|
23
|
+
new_type = MIME_TYPE.dup
|
24
|
+
new_type << ';' << old_type.split(';', 2).last if old_type.index(';')
|
25
|
+
env[:response_headers][CONTENT_TYPE] = new_type
|
26
|
+
end
|
27
|
+
|
28
|
+
BRACKETS = %w- [ { -
|
29
|
+
|
30
|
+
def parse_response?(env)
|
31
|
+
super and BRACKETS.include? env[:body][0,1]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'faraday_middleware/response_middleware'
|
2
|
+
|
3
|
+
module FaradayMiddleware
|
4
|
+
# Public: Restore marshalled Ruby objects in response bodies.
|
5
|
+
class ParseMarshal < ResponseMiddleware
|
6
|
+
define_parser { |body|
|
7
|
+
::Marshal.load body unless body.empty?
|
8
|
+
}
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
module FaradayMiddleware
|
4
|
+
# Internal: The base class for middleware that parses responses.
|
5
|
+
class ResponseMiddleware < Faraday::Middleware
|
6
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :parser
|
10
|
+
end
|
11
|
+
|
12
|
+
# Store a Proc that receives the body and returns the parsed result.
|
13
|
+
def self.define_parser(parser = nil)
|
14
|
+
@parser = parser || Proc.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.inherited(subclass)
|
18
|
+
super
|
19
|
+
subclass.load_error = self.load_error
|
20
|
+
subclass.parser = self.parser
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(app = nil, options = {})
|
24
|
+
super(app)
|
25
|
+
@options = options
|
26
|
+
@content_types = Array(options[:content_type])
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(env)
|
30
|
+
@app.call(env).on_complete do
|
31
|
+
if process_response_type?(response_type(env)) and parse_response?(env)
|
32
|
+
process_response(env)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def process_response(env)
|
38
|
+
env[:raw_body] = env[:body] if preserve_raw?(env)
|
39
|
+
env[:body] = parse(env[:body])
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parse the response body.
|
43
|
+
#
|
44
|
+
# Instead of overriding this method, consider using `define_parser`.
|
45
|
+
def parse(body)
|
46
|
+
if self.class.parser
|
47
|
+
begin
|
48
|
+
self.class.parser.call(body)
|
49
|
+
rescue StandardError, SyntaxError => err
|
50
|
+
raise err if err.is_a? SyntaxError and err.class.name != 'Psych::SyntaxError'
|
51
|
+
raise Faraday::Error::ParsingError, err
|
52
|
+
end
|
53
|
+
else
|
54
|
+
body
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def response_type(env)
|
59
|
+
type = env[:response_headers][CONTENT_TYPE].to_s
|
60
|
+
type = type.split(';', 2).first if type.index(';')
|
61
|
+
type
|
62
|
+
end
|
63
|
+
|
64
|
+
def process_response_type?(type)
|
65
|
+
@content_types.empty? or @content_types.any? { |pattern|
|
66
|
+
pattern.is_a?(Regexp) ? type =~ pattern : type == pattern
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_response?(env)
|
71
|
+
env[:body].respond_to? :to_str
|
72
|
+
end
|
73
|
+
|
74
|
+
def preserve_raw?(env)
|
75
|
+
env[:request].fetch(:preserve_raw, @options[:preserve_raw])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'rack/cache'
|
5
|
+
require 'faraday_middleware/response/caching'
|
6
|
+
require 'faraday_middleware/rack_compatible'
|
7
|
+
|
8
|
+
class CachingTest < Test::Unit::TestCase
|
9
|
+
class TestCache < Hash
|
10
|
+
def read(key)
|
11
|
+
if cached = self[key]
|
12
|
+
Marshal.load(cached)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def write(key, data)
|
17
|
+
self[key] = Marshal.dump(data)
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch(key)
|
21
|
+
read(key) || yield.tap { |data| write(key, data) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def setup
|
26
|
+
@cache = TestCache.new
|
27
|
+
|
28
|
+
request_count = 0
|
29
|
+
response = lambda { |env|
|
30
|
+
[200, {'Content-Type' => 'text/plain'}, "request:#{request_count+=1}"]
|
31
|
+
}
|
32
|
+
|
33
|
+
@conn = Faraday.new do |b|
|
34
|
+
b.use FaradayMiddleware::Caching, @cache
|
35
|
+
b.adapter :test do |stub|
|
36
|
+
stub.get('/', &response)
|
37
|
+
stub.get('/?foo=bar', &response)
|
38
|
+
stub.post('/', &response)
|
39
|
+
stub.get('/other', &response)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
extend Forwardable
|
45
|
+
def_delegators :@conn, :get, :post
|
46
|
+
|
47
|
+
def test_cache_get
|
48
|
+
assert_equal 'request:1', get('/').body
|
49
|
+
assert_equal 'request:1', get('/').body
|
50
|
+
assert_equal 'request:2', get('/other').body
|
51
|
+
assert_equal 'request:2', get('/other').body
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_response_has_request_params
|
55
|
+
get('/') # make cache
|
56
|
+
response = get('/')
|
57
|
+
assert_equal :get, response.env[:method]
|
58
|
+
assert_equal '/', response.env[:url].to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_cache_query_params
|
62
|
+
assert_equal 'request:1', get('/').body
|
63
|
+
assert_equal 'request:2', get('/?foo=bar').body
|
64
|
+
assert_equal 'request:2', get('/?foo=bar').body
|
65
|
+
assert_equal 'request:1', get('/').body
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_doesnt_cache_post
|
69
|
+
assert_equal 'request:1', post('/').body
|
70
|
+
assert_equal 'request:2', post('/').body
|
71
|
+
assert_equal 'request:3', post('/').body
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# RackCompatible + Rack::Cache
|
76
|
+
class HttpCachingTest < Test::Unit::TestCase
|
77
|
+
include FileUtils
|
78
|
+
|
79
|
+
CACHE_DIR = File.expand_path('../cache', __FILE__)
|
80
|
+
|
81
|
+
def setup
|
82
|
+
rm_r CACHE_DIR if File.exists? CACHE_DIR
|
83
|
+
|
84
|
+
request_count = 0
|
85
|
+
response = lambda { |env|
|
86
|
+
[200, { 'Content-Type' => 'text/plain',
|
87
|
+
'Cache-Control' => 'public, max-age=900',
|
88
|
+
},
|
89
|
+
"request:#{request_count+=1}"]
|
90
|
+
}
|
91
|
+
|
92
|
+
@conn = Faraday.new do |b|
|
93
|
+
b.use FaradayMiddleware::RackCompatible, Rack::Cache::Context,
|
94
|
+
:metastore => "file:#{CACHE_DIR}/rack/meta",
|
95
|
+
:entitystore => "file:#{CACHE_DIR}/rack/body",
|
96
|
+
:verbose => true
|
97
|
+
|
98
|
+
b.adapter :test do |stub|
|
99
|
+
stub.get('/', &response)
|
100
|
+
stub.post('/', &response)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
extend Forwardable
|
106
|
+
def_delegators :@conn, :get, :post
|
107
|
+
|
108
|
+
def test_cache_get
|
109
|
+
assert_equal 'request:1', get('/', :user_agent => 'test').body
|
110
|
+
response = get('/', :user_agent => 'test')
|
111
|
+
assert_equal 'request:1', response.body
|
112
|
+
assert_equal 'text/plain', response['content-type']
|
113
|
+
|
114
|
+
assert_equal 'request:2', post('/').body
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_doesnt_cache_post
|
118
|
+
assert_equal 'request:1', get('/').body
|
119
|
+
assert_equal 'request:2', post('/').body
|
120
|
+
assert_equal 'request:3', post('/').body
|
121
|
+
end
|
122
|
+
end unless defined? RUBY_ENGINE and "rbx" == RUBY_ENGINE # rbx bug #1522
|