agiley-faraday_middleware 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gemtest +0 -0
  2. data/.gitignore +31 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +7 -0
  5. data/CHANGELOG.md +10 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.md +20 -0
  8. data/README.md +54 -0
  9. data/Rakefile +17 -0
  10. data/faraday_middleware.gemspec +24 -0
  11. data/lib/faraday_middleware.rb +42 -0
  12. data/lib/faraday_middleware/addressable_patch.rb +20 -0
  13. data/lib/faraday_middleware/backwards_compatibility.rb +15 -0
  14. data/lib/faraday_middleware/instrumentation.rb +30 -0
  15. data/lib/faraday_middleware/rack_compatible.rb +76 -0
  16. data/lib/faraday_middleware/request/encode_json.rb +50 -0
  17. data/lib/faraday_middleware/request/oauth.rb +64 -0
  18. data/lib/faraday_middleware/request/oauth2.rb +62 -0
  19. data/lib/faraday_middleware/response/caching.rb +76 -0
  20. data/lib/faraday_middleware/response/follow_redirects.rb +53 -0
  21. data/lib/faraday_middleware/response/mashify.rb +28 -0
  22. data/lib/faraday_middleware/response/parse_json.rb +38 -0
  23. data/lib/faraday_middleware/response/parse_marshal.rb +13 -0
  24. data/lib/faraday_middleware/response/parse_nokogiri_xml.rb +14 -0
  25. data/lib/faraday_middleware/response/parse_xml.rb +14 -0
  26. data/lib/faraday_middleware/response/parse_yaml.rb +13 -0
  27. data/lib/faraday_middleware/response/rashify.rb +13 -0
  28. data/lib/faraday_middleware/response_middleware.rb +78 -0
  29. data/lib/faraday_middleware/version.rb +3 -0
  30. data/spec/caching_test.rb +122 -0
  31. data/spec/encode_json_spec.rb +95 -0
  32. data/spec/follow_redirects_spec.rb +33 -0
  33. data/spec/helper.rb +33 -0
  34. data/spec/mashify_spec.rb +79 -0
  35. data/spec/oauth2_spec.rb +118 -0
  36. data/spec/oauth_spec.rb +101 -0
  37. data/spec/parse_json_spec.rb +94 -0
  38. data/spec/parse_marshal_spec.rb +16 -0
  39. data/spec/parse_xml_spec.rb +71 -0
  40. data/spec/parse_yaml_spec.rb +53 -0
  41. data/spec/rashify_spec.rb +69 -0
  42. metadata +202 -0
@@ -0,0 +1,62 @@
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
+
24
+ PARAM_NAME = 'access_token'.freeze
25
+ AUTH_HEADER = 'Authorization'.freeze
26
+
27
+ attr_reader :param_name
28
+
29
+ extend Forwardable
30
+ def_delegators :'Faraday::Utils', :parse_query, :build_query
31
+
32
+ def call(env)
33
+ params = { param_name => @token }.update query_params(env[:url])
34
+
35
+ if token = params[param_name] and !token.empty?
36
+ env[:url].query = build_query params
37
+ env[:request_headers][AUTH_HEADER] ||= %(Token token="#{token}")
38
+ end
39
+
40
+ @app.call env
41
+ end
42
+
43
+ def initialize(app, token = nil, options = {})
44
+ super(app)
45
+ options, token = token, nil if token.is_a? Hash
46
+ @token = token && token.to_s
47
+ @param_name = options.fetch(:param_name, PARAM_NAME).to_s
48
+ raise ArgumentError, ":param_name can't be blank" if @param_name.empty?
49
+ end
50
+
51
+ def query_params(url)
52
+ if url.query.nil? or url.query.empty?
53
+ {}
54
+ else
55
+ parse_query url.query
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # deprecated alias
62
+ Faraday::Request::OAuth2 = FaradayMiddleware::OAuth2
@@ -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,28 @@
1
+ require 'faraday'
2
+
3
+ module FaradayMiddleware
4
+ class Mashify < Faraday::Response::Middleware
5
+ class << self
6
+ attr_accessor :mash_class
7
+ end
8
+
9
+ dependency do
10
+ require 'hashie/mash'
11
+ self.mash_class = ::Hashie::Mash
12
+ end
13
+
14
+ def parse(body)
15
+ case body
16
+ when Hash
17
+ self.class.mash_class.new(body)
18
+ when Array
19
+ body.map { |item| item.is_a?(Hash) ? self.class.mash_class.new(item) : item }
20
+ else
21
+ body
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # deprecated alias
28
+ Faraday::Response::Mashify = FaradayMiddleware::Mashify
@@ -0,0 +1,38 @@
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
36
+
37
+ # deprecated alias
38
+ Faraday::Response::ParseJson = FaradayMiddleware::ParseJson
@@ -0,0 +1,13 @@
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
11
+
12
+ # deprecated alias
13
+ Faraday::Response::ParseMarshal = FaradayMiddleware::ParseMarshal
@@ -0,0 +1,14 @@
1
+ require 'faraday_middleware/response_middleware'
2
+
3
+ module FaradayMiddleware
4
+ class ParseNokogiriXml < ResponseMiddleware
5
+ dependency 'nokogiri'
6
+
7
+ define_parser { |body|
8
+ ::Nokogiri::XML(body, nil, "utf-8")
9
+ }
10
+ end
11
+ end
12
+
13
+ # deprecated alias
14
+ Faraday::Response::ParseNokogiriXml = FaradayMiddleware::ParseNokogiriXml
@@ -0,0 +1,14 @@
1
+ require 'faraday_middleware/response_middleware'
2
+
3
+ module FaradayMiddleware
4
+ class ParseXml < ResponseMiddleware
5
+ dependency 'multi_xml'
6
+
7
+ define_parser { |body|
8
+ ::MultiXml.parse(body)
9
+ }
10
+ end
11
+ end
12
+
13
+ # deprecated alias
14
+ Faraday::Response::ParseXml = FaradayMiddleware::ParseXml
@@ -0,0 +1,13 @@
1
+ require 'faraday_middleware/response_middleware'
2
+
3
+ module FaradayMiddleware
4
+ # Public: Parse response bodies as YAML.
5
+ class ParseYaml < ResponseMiddleware
6
+ dependency 'yaml'
7
+
8
+ define_parser { |body| ::YAML.load body }
9
+ end
10
+ end
11
+
12
+ # deprecated alias
13
+ Faraday::Response::ParseYaml = FaradayMiddleware::ParseYaml
@@ -0,0 +1,13 @@
1
+ require 'faraday_middleware/response/mashify'
2
+
3
+ module FaradayMiddleware
4
+ class Rashify < Mashify
5
+ dependency do
6
+ require 'rash'
7
+ self.mash_class = ::Hashie::Rash
8
+ end
9
+ end
10
+ end
11
+
12
+ # deprecated alias
13
+ Faraday::Response::Rashify = FaradayMiddleware::Rashify
@@ -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 if subclass.respond_to? :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,3 @@
1
+ module FaradayMiddleware
2
+ VERSION = "0.8.3"
3
+ 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