agiley-faraday_middleware 0.8.3.2 → 0.9.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.
Files changed (40) hide show
  1. data/.gitignore +2 -2
  2. data/.rspec +2 -1
  3. data/.travis.yml +6 -3
  4. data/Gemfile +5 -1
  5. data/Rakefile +16 -4
  6. data/faraday_middleware.gemspec +1 -1
  7. data/lib/faraday_middleware.rb +17 -13
  8. data/lib/faraday_middleware/instrumentation.rb +2 -2
  9. data/lib/faraday_middleware/rack_compatible.rb +14 -9
  10. data/lib/faraday_middleware/request/encode_json.rb +4 -2
  11. data/lib/faraday_middleware/request/method_override.rb +51 -0
  12. data/lib/faraday_middleware/request/oauth.rb +28 -5
  13. data/lib/faraday_middleware/response/caching.rb +7 -2
  14. data/lib/faraday_middleware/response/chunked.rb +29 -0
  15. data/lib/faraday_middleware/response/follow_redirects.rb +100 -11
  16. data/lib/faraday_middleware/response/mashify.rb +11 -2
  17. data/lib/faraday_middleware/response/parse_dates.rb +39 -0
  18. data/lib/faraday_middleware/response/parse_json.rb +17 -5
  19. data/lib/faraday_middleware/response/parse_marshal.rb +2 -2
  20. data/lib/faraday_middleware/response/parse_xml.rb +3 -2
  21. data/lib/faraday_middleware/response/parse_yaml.rb +3 -1
  22. data/lib/faraday_middleware/response/rashify.rb +2 -0
  23. data/lib/faraday_middleware/response_middleware.rb +2 -2
  24. data/lib/faraday_middleware/version.rb +1 -1
  25. data/spec/caching_test.rb +37 -4
  26. data/spec/chunked_spec.rb +78 -0
  27. data/spec/encode_json_spec.rb +13 -13
  28. data/spec/follow_redirects_spec.rb +203 -18
  29. data/spec/helper.rb +15 -1
  30. data/spec/mashify_spec.rb +46 -26
  31. data/spec/method_override_spec.rb +92 -0
  32. data/spec/oauth2_spec.rb +18 -18
  33. data/spec/oauth_spec.rb +67 -17
  34. data/spec/parse_dates_spec.rb +39 -0
  35. data/spec/parse_json_spec.rb +37 -19
  36. data/spec/parse_marshal_spec.rb +3 -3
  37. data/spec/parse_xml_spec.rb +13 -13
  38. data/spec/parse_yaml_spec.rb +10 -10
  39. data/spec/rashify_spec.rb +26 -23
  40. metadata +78 -21
data/.gitignore CHANGED
@@ -1,6 +1,7 @@
1
1
  # Mac OS X
2
2
  .DS_Store
3
3
 
4
+ # RVM
4
5
  .rvmrc
5
6
 
6
7
  # TextMate
@@ -21,13 +22,12 @@ rdoc
21
22
  doc
22
23
  log
23
24
  .yardoc
25
+ tmp
24
26
 
25
27
  # Bundler
26
- *.gem
27
28
  .bundle
28
29
  Gemfile.lock
29
30
  pkg
30
- *.gem
31
31
 
32
32
  # Rubinius
33
33
  *.rbc
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --color
2
- --backtrace
2
+ --fail-fast
3
+ --order random
@@ -1,7 +1,10 @@
1
+ language: ruby
1
2
  rvm:
3
+ - rbx-18mode
4
+ - rbx-19mode
5
+ - jruby-18mode
6
+ - jruby-19mode
2
7
  - 1.8.7
3
8
  - 1.9.2
4
9
  - 1.9.3
5
- - jruby
6
- - rbx
7
- - ree
10
+ - ruby-head
data/Gemfile CHANGED
@@ -1,6 +1,10 @@
1
1
  source 'http://rubygems.org'
2
2
 
3
- gem 'simplecov' unless ENV['CI']
3
+ platforms :mri_19 do
4
+ gem 'simplecov'
5
+ gem 'cane', '~> 2.2.2'
6
+ end
7
+
4
8
  gem 'json', :platforms => [:ruby_18, :jruby]
5
9
  gem 'jruby-openssl', '~> 0.7', :platforms => :jruby
6
10
 
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
- #!/usr/bin/env rake
2
-
3
- task :default => [:enable_coverage, :spec, :test]
1
+ if defined? RUBY_ENGINE and 'ruby' == RUBY_ENGINE and RUBY_VERSION.index('1.9') == 0
2
+ task :default => [:enable_coverage, :spec, :test, :quality]
3
+ else
4
+ task :default => [:spec, :test]
5
+ end
4
6
 
5
7
  require 'bundler'
6
8
  Bundler::GemHelper.install_tasks
@@ -9,9 +11,19 @@ require 'rspec/core/rake_task'
9
11
  RSpec::Core::RakeTask.new(:spec)
10
12
 
11
13
  task :enable_coverage do
12
- ENV['COVERAGE'] = 'yes' unless ENV['CI']
14
+ ENV['COVERAGE'] = 'yes'
13
15
  end
14
16
 
17
+ desc %(Run Test::Unit tests)
15
18
  task :test do
16
19
  sh 'ruby', '-Ilib', 'spec/caching_test.rb'
17
20
  end
21
+
22
+ desc %(Check code quality metrics with Cane)
23
+ task :quality do
24
+ sh 'cane',
25
+ '--abc-max=15',
26
+ '--style-measure=110',
27
+ '--gte=coverage/covered_percent,99',
28
+ '--max-violations=0'
29
+ end
@@ -21,4 +21,4 @@ Gem::Specification.new do |gem|
21
21
  gem.summary = gem.description
22
22
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
23
  gem.version = FaradayMiddleware::VERSION
24
- end
24
+ end
@@ -4,16 +4,18 @@ module FaradayMiddleware
4
4
  autoload :OAuth, 'faraday_middleware/request/oauth'
5
5
  autoload :OAuth2, 'faraday_middleware/request/oauth2'
6
6
  autoload :EncodeJson, 'faraday_middleware/request/encode_json'
7
+ autoload :MethodOverride, 'faraday_middleware/request/method_override'
7
8
  autoload :Mashify, 'faraday_middleware/response/mashify'
8
9
  autoload :Rashify, 'faraday_middleware/response/rashify'
9
10
  autoload :ParseJson, 'faraday_middleware/response/parse_json'
10
11
  autoload :ParseXml, 'faraday_middleware/response/parse_xml'
11
12
  autoload :ParseNokogiriXml, 'faraday_middleware/response/parse_nokogiri_xml'
12
13
  autoload :ParseNokogiriHtml, 'faraday_middleware/response/parse_nokogiri_html'
13
- autoload :ForceUtf, 'faraday_middleware/response/force_utf'
14
14
  autoload :ParseMarshal, 'faraday_middleware/response/parse_marshal'
15
15
  autoload :ParseYaml, 'faraday_middleware/response/parse_yaml'
16
+ autoload :ParseDates, 'faraday_middleware/response/parse_dates'
16
17
  autoload :Caching, 'faraday_middleware/response/caching'
18
+ autoload :Chunked, 'faraday_middleware/response/chunked'
17
19
  autoload :RackCompatible, 'faraday_middleware/rack_compatible'
18
20
  autoload :FollowRedirects, 'faraday_middleware/response/follow_redirects'
19
21
  autoload :Instrumentation, 'faraday_middleware/instrumentation'
@@ -22,25 +24,27 @@ module FaradayMiddleware
22
24
  Faraday.register_middleware :request,
23
25
  :oauth => lambda { OAuth },
24
26
  :oauth2 => lambda { OAuth2 },
25
- :json => lambda { EncodeJson }
27
+ :json => lambda { EncodeJson },
28
+ :method_override => lambda { MethodOverride }
26
29
 
27
30
  Faraday.register_middleware :response,
28
- :mashify => lambda { Mashify },
29
- :rashify => lambda { Rashify },
30
- :json => lambda { ParseJson },
31
- :json_fix => lambda { ParseJson::MimeTypeFix },
32
- :xml => lambda { ParseXml },
31
+ :mashify => lambda { Mashify },
32
+ :rashify => lambda { Rashify },
33
+ :json => lambda { ParseJson },
34
+ :json_fix => lambda { ParseJson::MimeTypeFix },
35
+ :xml => lambda { ParseXml },
33
36
  :nokogiri_xml => lambda { ParseNokogiriXml },
34
37
  :nokogiri_html => lambda { ParseNokogiriHtml },
35
- :force_utf => lambda { ForceUtf },
36
- :marshal => lambda { ParseMarshal },
37
- :yaml => lambda { ParseYaml },
38
- :caching => lambda { Caching },
39
- :follow_redirects => lambda { FollowRedirects }
38
+ :marshal => lambda { ParseMarshal },
39
+ :yaml => lambda { ParseYaml },
40
+ :dates => lambda { ParseDates },
41
+ :caching => lambda { Caching },
42
+ :follow_redirects => lambda { FollowRedirects },
43
+ :chunked => lambda { Chunked }
40
44
 
41
45
  Faraday.register_middleware \
42
46
  :instrumentation => lambda { Instrumentation }
43
47
  end
44
48
  end
45
49
 
46
- require 'faraday_middleware/backwards_compatibility'
50
+ require 'faraday_middleware/backwards_compatibility'
@@ -7,10 +7,10 @@ module FaradayMiddleware
7
7
  #
8
8
  # Examples
9
9
  #
10
- # ActiveSupport::Notifications.subscribe('request.faraday') do |name, start_time, end_time, _, env|
10
+ # ActiveSupport::Notifications.subscribe('request.faraday') do |name, starts, ends, _, env|
11
11
  # url = env[:url]
12
12
  # http_method = env[:method].to_s.upcase
13
- # duration = end_time - start_time
13
+ # duration = ends - starts
14
14
  # $stderr.puts '[%s] %s %s (%.3f s)' % [url.host, http_method, url.request_uri, duration]
15
15
  # end
16
16
  class Instrumentation < Faraday::Middleware
@@ -26,11 +26,7 @@ module FaradayMiddleware
26
26
 
27
27
  # faraday to rack-compatible
28
28
  def prepare_env(env)
29
- env[:request_headers].each do |name, value|
30
- name = name.upcase.tr('-', '_')
31
- name = "HTTP_#{name}" unless NonPrefixedHeaders.include? name
32
- env[name] = value
33
- end
29
+ headers_to_rack(env)
34
30
 
35
31
  url = env[:url]
36
32
  env['rack.url_scheme'] = url.scheme
@@ -44,6 +40,14 @@ module FaradayMiddleware
44
40
  env
45
41
  end
46
42
 
43
+ def headers_to_rack(env)
44
+ env[:request_headers].each do |name, value|
45
+ name = name.upcase.tr('-', '_')
46
+ name = "HTTP_#{name}" unless NonPrefixedHeaders.include? name
47
+ env[name] = value
48
+ end
49
+ end
50
+
47
51
  # rack to faraday-compatible
48
52
  def restore_env(env)
49
53
  headers = env[:request_headers]
@@ -63,13 +67,14 @@ module FaradayMiddleware
63
67
 
64
68
  def finalize_response(env, rack_response)
65
69
  status, headers, body = rack_response
66
- body = body.inject('') { |str, part| str << part }
70
+ body = body.inject() { |str, part| str << part }
67
71
  headers = Faraday::Utils::Headers.new(headers) unless Faraday::Utils::Headers === headers
68
72
 
69
- response_env = { :status => status, :body => body, :response_headers => headers }
73
+ env.update :status => status.to_i,
74
+ :body => body,
75
+ :response_headers => headers
70
76
 
71
- env[:response] ||= Faraday::Response.new({})
72
- env[:response].env.update(response_env)
77
+ env[:response] ||= Faraday::Response.new(env)
73
78
  env[:response]
74
79
  end
75
80
  end
@@ -12,7 +12,9 @@ module FaradayMiddleware
12
12
  CONTENT_TYPE = 'Content-Type'.freeze
13
13
  MIME_TYPE = 'application/json'.freeze
14
14
 
15
- dependency 'json'
15
+ dependency do
16
+ require 'json' unless defined?(::JSON)
17
+ end
16
18
 
17
19
  def call(env)
18
20
  match_content_type(env) do |data|
@@ -22,7 +24,7 @@ module FaradayMiddleware
22
24
  end
23
25
 
24
26
  def encode(data)
25
- JSON.dump data
27
+ ::JSON.dump data
26
28
  end
27
29
 
28
30
  def match_content_type(env)
@@ -0,0 +1,51 @@
1
+ require 'faraday'
2
+
3
+ module FaradayMiddleware
4
+ # Public: Writes the original HTTP method to "X-Http-Method-Override" header
5
+ # and sends the request as POST.
6
+ #
7
+ # This can be used to work around technical issues with making non-POST
8
+ # requests, e.g. faulty HTTP client or server router.
9
+ #
10
+ # This header is recognized in Rack apps by default, courtesy of the
11
+ # Rack::MethodOverride module. See
12
+ # http://rack.rubyforge.org/doc/classes/Rack/MethodOverride.html
13
+ class MethodOverride < Faraday::Middleware
14
+
15
+ HEADER = "X-Http-Method-Override".freeze
16
+
17
+ # Public: Initialize the middleware.
18
+ #
19
+ # app - the Faraday app to wrap
20
+ # options - (optional)
21
+ # :rewrite - Array of HTTP methods to rewrite
22
+ # (default: all but GET and POST)
23
+ def initialize(app, options = nil)
24
+ super(app)
25
+ @methods = options && options.fetch(:rewrite).map { |method|
26
+ method = method.downcase if method.respond_to? :downcase
27
+ method.to_sym
28
+ }
29
+ end
30
+
31
+ def call(env)
32
+ method = env[:method]
33
+ rewrite_request(env, method) if rewrite_request?(method)
34
+ @app.call(env)
35
+ end
36
+
37
+ def rewrite_request?(method)
38
+ if @methods.nil? or @methods.empty?
39
+ method != :get and method != :post
40
+ else
41
+ @methods.include? method
42
+ end
43
+ end
44
+
45
+ # Internal: Write the original HTTP method to header, change method to POST.
46
+ def rewrite_request(env, original_method)
47
+ env[:request_headers][HEADER] = original_method.to_s.upcase
48
+ env[:method] = :post
49
+ end
50
+ end
51
+ end
@@ -1,4 +1,5 @@
1
1
  require 'faraday'
2
+ require 'forwardable'
2
3
 
3
4
  module FaradayMiddleware
4
5
  # Public: Uses the simple_oauth library to sign requests according the
@@ -11,14 +12,25 @@ module FaradayMiddleware
11
12
  # The signature is added to the "Authorization" HTTP request header. If the
12
13
  # value for this header already exists, it is not overriden.
13
14
  #
14
- # For requests that have parameters in the body, such as POST, this
15
- # middleware expects them to be in Hash form, i.e. not encoded to string.
16
- # This means this middleware has to be positioned on the stack before any
17
- # encoding middleware such as UrlEncoded.
15
+ # If no Content-Type header is specified, this middleware assumes that
16
+ # request body parameters should be included while signing the request.
17
+ # Otherwise, it only includes them if the Content-Type is
18
+ # "application/x-www-form-urlencoded", as per OAuth 1.0.
19
+ #
20
+ # For better performance while signing requests, this middleware should be
21
+ # positioned before UrlEncoded middleware on the stack, but after any other
22
+ # body-encoding middleware (such as EncodeJson).
18
23
  class OAuth < Faraday::Middleware
19
24
  dependency 'simple_oauth'
20
25
 
21
26
  AUTH_HEADER = 'Authorization'.freeze
27
+ CONTENT_TYPE = 'Content-Type'.freeze
28
+ TYPE_URLENCODED = 'application/x-www-form-urlencoded'.freeze
29
+
30
+ extend Forwardable
31
+ parser_method = :parse_nested_query
32
+ parser_module = ::Faraday::Utils.respond_to?(parser_method) ? 'Faraday::Utils' : 'Rack::Utils'
33
+ def_delegator parser_module, parser_method
22
34
 
23
35
  def initialize(app, options)
24
36
  super(app)
@@ -50,7 +62,18 @@ module FaradayMiddleware
50
62
  end
51
63
 
52
64
  def body_params(env)
53
- env[:body] || {}
65
+ if include_body_params?(env)
66
+ if env[:body].respond_to?(:to_str)
67
+ parse_nested_query env[:body]
68
+ else
69
+ env[:body]
70
+ end
71
+ end || {}
72
+ end
73
+
74
+ def include_body_params?(env)
75
+ # see RFC 5489, section 3.4.1.3.1 for details
76
+ !(type = env[:request_headers][CONTENT_TYPE]) or type == TYPE_URLENCODED
54
77
  end
55
78
 
56
79
  def signature_params(params)
@@ -68,8 +68,13 @@ module FaradayMiddleware
68
68
  end
69
69
 
70
70
  def finalize_response(response, env)
71
- response = env[:response] = response.dup if response.frozen?
72
- response.apply_request env unless response.env[:method]
71
+ response = response.dup if response.frozen?
72
+ env[:response] = response
73
+ unless env[:response_headers]
74
+ env.update response.env
75
+ # FIXME: omg hax
76
+ response.instance_variable_set('@env', env)
77
+ end
73
78
  response
74
79
  end
75
80
  end
@@ -0,0 +1,29 @@
1
+ require 'faraday_middleware/response_middleware'
2
+
3
+ module FaradayMiddleware
4
+ # Public: Parse a Transfer-Encoding: Chunked response to just the original data
5
+ class Chunked < FaradayMiddleware::ResponseMiddleware
6
+ TRANSFER_ENCODING = 'transfer-encoding'.freeze
7
+
8
+ define_parser do |raw_body|
9
+ decoded_body = []
10
+ until raw_body.empty?
11
+ chunk_len, raw_body = raw_body.split("\r\n", 2)
12
+ chunk_len = chunk_len.split(';',2).first.hex
13
+ break if chunk_len == 0
14
+ decoded_body << raw_body[0, chunk_len]
15
+ # The 2 is to strip the extra CRLF at the end of the chunk
16
+ raw_body = raw_body[chunk_len + 2, raw_body.length - chunk_len - 2]
17
+ end
18
+ decoded_body.join('')
19
+ end
20
+
21
+ def parse_response?(env)
22
+ super and chunked_encoding?(env[:response_headers])
23
+ end
24
+
25
+ def chunked_encoding?(headers)
26
+ encoding = headers[TRANSFER_ENCODING] and encoding.split(',').include?('chunked')
27
+ end
28
+ end
29
+ end
@@ -1,6 +1,8 @@
1
1
  require 'faraday'
2
+ require 'set'
2
3
 
3
4
  module FaradayMiddleware
5
+ # Public: Exception thrown when the maximum amount of requests is exceeded.
4
6
  class RedirectLimitReached < Faraday::Error::ClientError
5
7
  attr_reader :response
6
8
 
@@ -10,10 +12,34 @@ module FaradayMiddleware
10
12
  end
11
13
  end
12
14
 
13
- # Public: Follow HTTP 30x redirects.
15
+ # Public: Follow HTTP 301, 302, 303, and 307 redirects for GET, PATCH, POST,
16
+ # PUT, and DELETE requests.
17
+ #
18
+ # This middleware does not follow the HTTP specification for HTTP 302, by
19
+ # default, in that it follows the improper implementation used by most major
20
+ # web browsers which forces the redirected request to become a GET request
21
+ # regardless of the original request method.
22
+ #
23
+ # For HTTP 301, 302, and 303, the original request is transformed into a
24
+ # GET request to the response Location, by default. However, with standards
25
+ # compliance enabled, a 302 will instead act in accordance with the HTTP
26
+ # specification, which will replay the original request to the received
27
+ # Location, just as with a 307.
28
+ #
29
+ # For HTTP 307, the original request is replayed to the response Location,
30
+ # including original HTTP request method (GET, POST, PUT, DELETE, PATCH),
31
+ # original headers, and original body.
32
+ #
33
+ # This middleware currently only works with synchronous requests; in other
34
+ # words, it doesn't support parallelism.
14
35
  class FollowRedirects < Faraday::Middleware
15
- # TODO: 307 & standards-compliant 302
16
- REDIRECTS = [301, 302, 303]
36
+ # HTTP methods for which 30x redirects can be followed
37
+ ALLOWED_METHODS = Set.new [:head, :options, :get, :post, :put, :patch, :delete]
38
+ # HTTP redirect status codes that this middleware implements
39
+ REDIRECT_CODES = Set.new [301, 302, 303, 307]
40
+ # Keys in env hash which will get cleared between requests
41
+ ENV_TO_CLEAR = Set.new [:status, :response, :response_headers]
42
+
17
43
  # Default value for max redirects followed
18
44
  FOLLOW_LIMIT = 3
19
45
 
@@ -21,33 +47,96 @@ module FaradayMiddleware
21
47
  #
22
48
  # options - An options Hash (default: {}):
23
49
  # limit - A Numeric redirect limit (default: 3)
50
+ # standards_compliant - A Boolean indicating whether to respect
51
+ # the HTTP spec when following 302
52
+ # (default: false)
53
+ # cookie - Use either an array of strings
54
+ # (e.g. ['cookie1', 'cookie2']) to choose kept cookies
55
+ # or :all to keep all cookies.
24
56
  def initialize(app, options = {})
25
57
  super(app)
26
58
  @options = options
59
+
60
+ @replay_request_codes = Set.new [307]
61
+ @replay_request_codes << 302 if standards_compliant?
27
62
  end
28
63
 
29
64
  def call(env)
30
- process_response(@app.call(env), follow_limit)
65
+ perform_with_redirection(env, follow_limit)
66
+ end
67
+
68
+ private
69
+
70
+ def transform_into_get?(response)
71
+ return false if [:head, :options].include? response.env[:method]
72
+ # Never convert head or options to a get. That would just be silly.
73
+
74
+ !@replay_request_codes.include? response.status
31
75
  end
32
76
 
33
- def process_response(response, follows)
77
+ def perform_with_redirection(env, follows)
78
+ request_body = env[:body]
79
+ response = @app.call(env)
80
+
34
81
  response.on_complete do |env|
35
- if redirect? response
82
+ if follow_redirect?(env, response)
36
83
  raise RedirectLimitReached, response if follows.zero?
37
- env[:url] += response['location']
38
- env[:method] = :get
39
- response = process_response(@app.call(env), follows - 1)
84
+ env = update_env(env, request_body, response)
85
+ response = perform_with_redirection(env, follows - 1)
40
86
  end
41
87
  end
42
88
  response
43
89
  end
44
90
 
45
- def redirect?(response)
46
- REDIRECTS.include? response.status
91
+ def update_env(env, request_body, response)
92
+ env[:url] += response['location']
93
+ if @options[:cookies]
94
+ cookies = keep_cookies(env)
95
+ env[:request_headers][:cookies] = cookies unless cookies.nil?
96
+ end
97
+
98
+ if transform_into_get?(response)
99
+ env[:method] = :get
100
+ env[:body] = nil
101
+ else
102
+ env[:body] = request_body
103
+ end
104
+
105
+ ENV_TO_CLEAR.each {|key| env.delete key }
106
+
107
+ env
108
+ end
109
+
110
+ def follow_redirect?(env, response)
111
+ ALLOWED_METHODS.include? env[:method] and
112
+ REDIRECT_CODES.include? response.status
47
113
  end
48
114
 
49
115
  def follow_limit
50
116
  @options.fetch(:limit, FOLLOW_LIMIT)
51
117
  end
118
+
119
+ def keep_cookies(env)
120
+ cookies = @options.fetch(:cookies, [])
121
+ response_cookies = env[:response_headers][:cookies]
122
+ cookies == :all ? response_cookies : selected_request_cookies(response_cookies)
123
+ end
124
+
125
+ def selected_request_cookies(cookies)
126
+ selected_cookies(cookies)[0...-1]
127
+ end
128
+
129
+ def selected_cookies(cookies)
130
+ "".tap do |cookie_string|
131
+ @options[:cookies].each do |cookie|
132
+ string = /#{cookie}=?[^;]*/.match(cookies)[0] + ';'
133
+ cookie_string << string
134
+ end
135
+ end
136
+ end
137
+
138
+ def standards_compliant?
139
+ @options.fetch(:standards_compliant, false)
140
+ end
52
141
  end
53
142
  end