faraday 0.6.0 → 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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/.document +6 -0
  3. data/CHANGELOG.md +15 -0
  4. data/CONTRIBUTING.md +36 -0
  5. data/Gemfile +25 -15
  6. data/{LICENSE → LICENSE.md} +1 -1
  7. data/README.md +187 -137
  8. data/Rakefile +38 -101
  9. data/faraday.gemspec +26 -81
  10. data/lib/faraday/adapter/em_http.rb +237 -0
  11. data/lib/faraday/adapter/em_http_ssl_patch.rb +56 -0
  12. data/lib/faraday/adapter/em_synchrony/parallel_manager.rb +66 -0
  13. data/lib/faraday/adapter/em_synchrony.rb +65 -38
  14. data/lib/faraday/adapter/excon.rb +60 -9
  15. data/lib/faraday/adapter/httpclient.rb +106 -0
  16. data/lib/faraday/adapter/net_http.rb +89 -38
  17. data/lib/faraday/adapter/net_http_persistent.rb +47 -0
  18. data/lib/faraday/adapter/patron.rb +46 -9
  19. data/lib/faraday/adapter/rack.rb +58 -0
  20. data/lib/faraday/adapter/test.rb +68 -28
  21. data/lib/faraday/adapter/typhoeus.rb +94 -16
  22. data/lib/faraday/adapter.rb +33 -25
  23. data/lib/faraday/autoload.rb +85 -0
  24. data/lib/faraday/connection.rb +342 -134
  25. data/lib/faraday/error.rb +44 -29
  26. data/lib/faraday/middleware.rb +18 -10
  27. data/lib/faraday/options.rb +350 -0
  28. data/lib/faraday/parameters.rb +193 -0
  29. data/lib/faraday/rack_builder.rb +212 -0
  30. data/lib/faraday/request/authorization.rb +42 -0
  31. data/lib/faraday/request/basic_authentication.rb +13 -0
  32. data/lib/faraday/request/instrumentation.rb +36 -0
  33. data/lib/faraday/request/multipart.rb +15 -15
  34. data/lib/faraday/request/retry.rb +118 -0
  35. data/lib/faraday/request/token_authentication.rb +15 -0
  36. data/lib/faraday/request/url_encoded.rb +8 -9
  37. data/lib/faraday/request.rb +46 -45
  38. data/lib/faraday/response/logger.rb +4 -4
  39. data/lib/faraday/response/raise_error.rb +8 -3
  40. data/lib/faraday/response.rb +20 -25
  41. data/lib/faraday/upload_io.rb +51 -7
  42. data/lib/faraday/utils.rb +240 -44
  43. data/lib/faraday.rb +218 -38
  44. data/script/console +7 -0
  45. data/script/generate_certs +42 -0
  46. data/script/package +7 -0
  47. data/script/proxy-server +42 -0
  48. data/script/release +17 -0
  49. data/script/server +36 -0
  50. data/script/test +172 -0
  51. data/test/adapters/default_test.rb +14 -0
  52. data/test/adapters/em_http_test.rb +20 -0
  53. data/test/adapters/em_synchrony_test.rb +20 -0
  54. data/test/adapters/excon_test.rb +20 -0
  55. data/test/adapters/httpclient_test.rb +21 -0
  56. data/test/adapters/integration.rb +254 -0
  57. data/test/adapters/logger_test.rb +3 -3
  58. data/test/adapters/net_http_persistent_test.rb +20 -0
  59. data/test/adapters/net_http_test.rb +6 -25
  60. data/test/adapters/patron_test.rb +20 -0
  61. data/test/adapters/rack_test.rb +31 -0
  62. data/test/adapters/test_middleware_test.rb +74 -3
  63. data/test/adapters/typhoeus_test.rb +28 -0
  64. data/test/authentication_middleware_test.rb +65 -0
  65. data/test/composite_read_io_test.rb +111 -0
  66. data/test/connection_test.rb +381 -104
  67. data/test/env_test.rb +116 -40
  68. data/test/helper.rb +61 -25
  69. data/test/live_server.rb +55 -29
  70. data/test/middleware/instrumentation_test.rb +88 -0
  71. data/test/middleware/retry_test.rb +109 -0
  72. data/test/middleware_stack_test.rb +87 -5
  73. data/test/multibyte.txt +1 -0
  74. data/test/options_test.rb +252 -0
  75. data/test/request_middleware_test.rb +78 -21
  76. data/test/response_middleware_test.rb +32 -7
  77. data/test/strawberry.rb +2 -0
  78. data/test/utils_test.rb +58 -0
  79. metadata +116 -117
  80. data/lib/faraday/adapter/action_dispatch.rb +0 -30
  81. data/lib/faraday/builder.rb +0 -137
  82. data/lib/faraday/request/json.rb +0 -31
  83. data/test/adapters/live_test.rb +0 -186
@@ -0,0 +1,106 @@
1
+ module Faraday
2
+ class Adapter
3
+ class HTTPClient < Faraday::Adapter
4
+ dependency 'httpclient'
5
+
6
+ def client
7
+ @client ||= ::HTTPClient.new
8
+ end
9
+
10
+ def call(env)
11
+ super
12
+
13
+ if req = env[:request]
14
+ if proxy = req[:proxy]
15
+ configure_proxy proxy
16
+ end
17
+
18
+ if bind = req[:bind]
19
+ configure_socket bind
20
+ end
21
+
22
+ configure_timeouts req
23
+ end
24
+
25
+ if env[:url].scheme == 'https' && ssl = env[:ssl]
26
+ configure_ssl ssl
27
+ end
28
+
29
+ # TODO Don't stream yet.
30
+ # https://github.com/nahi/httpclient/pull/90
31
+ env[:body] = env[:body].read if env[:body].respond_to? :read
32
+
33
+ resp = client.request env[:method], env[:url],
34
+ :body => env[:body],
35
+ :header => env[:request_headers]
36
+
37
+ save_response env, resp.status, resp.body, resp.headers
38
+
39
+ @app.call env
40
+ rescue ::HTTPClient::TimeoutError
41
+ raise Faraday::Error::TimeoutError, $!
42
+ rescue ::HTTPClient::BadResponseError => err
43
+ if err.message.include?('status 407')
44
+ raise Faraday::Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
45
+ else
46
+ raise Faraday::Error::ClientError, $!
47
+ end
48
+ rescue Errno::ECONNREFUSED, EOFError
49
+ raise Faraday::Error::ConnectionFailed, $!
50
+ rescue => err
51
+ if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
52
+ raise Faraday::SSLError, err
53
+ else
54
+ raise
55
+ end
56
+ end
57
+
58
+ def configure_socket(bind)
59
+ client.socket_local.host = bind[:host]
60
+ client.socket_local.port = bind[:port]
61
+ end
62
+
63
+ def configure_proxy(proxy)
64
+ client.proxy = proxy[:uri]
65
+ if proxy[:user] && proxy[:password]
66
+ client.set_proxy_auth proxy[:user], proxy[:password]
67
+ end
68
+ end
69
+
70
+ def configure_ssl(ssl)
71
+ ssl_config = client.ssl_config
72
+
73
+ ssl_config.add_trust_ca ssl[:ca_file] if ssl[:ca_file]
74
+ ssl_config.add_trust_ca ssl[:ca_path] if ssl[:ca_path]
75
+ ssl_config.cert_store = ssl[:cert_store] if ssl[:cert_store]
76
+ ssl_config.client_cert = ssl[:client_cert] if ssl[:client_cert]
77
+ ssl_config.client_key = ssl[:client_key] if ssl[:client_key]
78
+ ssl_config.verify_depth = ssl[:verify_depth] if ssl[:verify_depth]
79
+ ssl_config.verify_mode = ssl_verify_mode(ssl)
80
+ end
81
+
82
+ def configure_timeouts(req)
83
+ if req[:timeout]
84
+ client.connect_timeout = req[:timeout]
85
+ client.receive_timeout = req[:timeout]
86
+ client.send_timeout = req[:timeout]
87
+ end
88
+
89
+ if req[:open_timeout]
90
+ client.connect_timeout = req[:open_timeout]
91
+ client.send_timeout = req[:open_timeout]
92
+ end
93
+ end
94
+
95
+ def ssl_verify_mode(ssl)
96
+ ssl[:verify_mode] || begin
97
+ if ssl.fetch(:verify, true)
98
+ OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
99
+ else
100
+ OpenSSL::SSL::VERIFY_NONE
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -4,68 +4,119 @@ rescue LoadError
4
4
  warn "Warning: no such file to load -- net/https. Make sure openssl is installed if you want ssl support"
5
5
  require 'net/http'
6
6
  end
7
+ require 'zlib'
7
8
 
8
9
  module Faraday
9
10
  class Adapter
10
11
  class NetHttp < Faraday::Adapter
11
- def call(env)
12
- super
13
- url = env[:url]
14
- req = env[:request]
12
+ NET_HTTP_EXCEPTIONS = [
13
+ EOFError,
14
+ Errno::ECONNABORTED,
15
+ Errno::ECONNREFUSED,
16
+ Errno::ECONNRESET,
17
+ Errno::EHOSTUNREACH,
18
+ Errno::EINVAL,
19
+ Errno::ENETUNREACH,
20
+ Net::HTTPBadResponse,
21
+ Net::HTTPHeaderSyntaxError,
22
+ Net::ProtocolError,
23
+ SocketError,
24
+ Zlib::GzipFile::Error,
25
+ ]
15
26
 
16
- http = net_http_class(env).new(url.host, url.inferred_port)
27
+ NET_HTTP_EXCEPTIONS << OpenSSL::SSL::SSLError if defined?(OpenSSL)
17
28
 
18
- if http.use_ssl = (url.scheme == 'https' && env[:ssl])
19
- ssl = env[:ssl]
20
- http.verify_mode = ssl[:verify_mode] || ssl.fetch(:verify, true) ?
21
- OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
22
- http.cert = ssl[:client_cert] if ssl[:client_cert]
23
- http.key = ssl[:client_key] if ssl[:client_key]
24
- http.ca_file = ssl[:ca_file] if ssl[:ca_file]
25
- http.cert_store = ssl[:cert_store] if ssl[:cert_store]
26
- end
29
+ def call(env)
30
+ super
31
+ http = net_http_connection(env)
32
+ configure_ssl(http, env[:ssl]) if env[:url].scheme == 'https' and env[:ssl]
27
33
 
34
+ req = env[:request]
28
35
  http.read_timeout = http.open_timeout = req[:timeout] if req[:timeout]
29
36
  http.open_timeout = req[:open_timeout] if req[:open_timeout]
30
37
 
31
- if :get != env[:method]
32
- http_request = Net::HTTPGenericRequest.new \
33
- env[:method].to_s.upcase, # request method
34
- !!env[:body], # is there data
35
- true, # does net/http love you, true or false?
36
- url.request_uri, # request uri path
37
- env[:request_headers] # request headers
38
-
39
- if env[:body].respond_to?(:read)
40
- http_request.body_stream = env[:body]
41
- env[:body] = nil
42
- end
43
- end
44
-
45
38
  begin
46
- http_response = if :get == env[:method]
47
- # prefer `get` to `request` because the former handles gzip (ruby 1.9)
48
- http.get url.request_uri, env[:request_headers]
39
+ http_response = perform_request(http, env)
40
+ rescue *NET_HTTP_EXCEPTIONS => err
41
+ if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
42
+ raise Faraday::SSLError, err
49
43
  else
50
- http.request http_request, env[:body]
44
+ raise Error::ConnectionFailed, err
51
45
  end
52
- rescue Errno::ECONNREFUSED
53
- raise Error::ConnectionFailed, $!
54
46
  end
55
47
 
56
- http_response.each_header do |key, value|
57
- response_headers(env)[key] = value
48
+ save_response(env, http_response.code.to_i, http_response.body || '') do |response_headers|
49
+ http_response.each_header do |key, value|
50
+ response_headers[key] = value
51
+ end
58
52
  end
59
- env.update :status => http_response.code.to_i, :body => http_response.body
60
53
 
61
54
  @app.call env
55
+ rescue Timeout::Error => err
56
+ raise Faraday::Error::TimeoutError, err
57
+ end
58
+
59
+ def create_request(env)
60
+ request = Net::HTTPGenericRequest.new \
61
+ env[:method].to_s.upcase, # request method
62
+ !!env[:body], # is there request body
63
+ :head != env[:method], # is there response body
64
+ env[:url].request_uri, # request uri path
65
+ env[:request_headers] # request headers
66
+
67
+ if env[:body].respond_to?(:read)
68
+ request.body_stream = env[:body]
69
+ else
70
+ request.body = env[:body]
71
+ end
72
+ request
73
+ end
74
+
75
+ def perform_request(http, env)
76
+ if :get == env[:method] and !env[:body]
77
+ # prefer `get` to `request` because the former handles gzip (ruby 1.9)
78
+ http.get env[:url].request_uri, env[:request_headers]
79
+ else
80
+ http.request create_request(env)
81
+ end
62
82
  end
63
83
 
64
- def net_http_class(env)
84
+ def net_http_connection(env)
65
85
  if proxy = env[:request][:proxy]
66
86
  Net::HTTP::Proxy(proxy[:uri].host, proxy[:uri].port, proxy[:user], proxy[:password])
67
87
  else
68
88
  Net::HTTP
89
+ end.new(env[:url].host, env[:url].port)
90
+ end
91
+
92
+ def configure_ssl(http, ssl)
93
+ http.use_ssl = true
94
+ http.verify_mode = ssl_verify_mode(ssl)
95
+ http.cert_store = ssl_cert_store(ssl)
96
+
97
+ http.cert = ssl[:client_cert] if ssl[:client_cert]
98
+ http.key = ssl[:client_key] if ssl[:client_key]
99
+ http.ca_file = ssl[:ca_file] if ssl[:ca_file]
100
+ http.ca_path = ssl[:ca_path] if ssl[:ca_path]
101
+ http.verify_depth = ssl[:verify_depth] if ssl[:verify_depth]
102
+ http.ssl_version = ssl[:version] if ssl[:version]
103
+ end
104
+
105
+ def ssl_cert_store(ssl)
106
+ return ssl[:cert_store] if ssl[:cert_store]
107
+ # Use the default cert store by default, i.e. system ca certs
108
+ cert_store = OpenSSL::X509::Store.new
109
+ cert_store.set_default_paths
110
+ cert_store
111
+ end
112
+
113
+ def ssl_verify_mode(ssl)
114
+ ssl[:verify_mode] || begin
115
+ if ssl.fetch(:verify, true)
116
+ OpenSSL::SSL::VERIFY_PEER
117
+ else
118
+ OpenSSL::SSL::VERIFY_NONE
119
+ end
69
120
  end
70
121
  end
71
122
  end
@@ -0,0 +1,47 @@
1
+ # Rely on autoloading instead of explicit require; helps avoid the "already
2
+ # initialized constant" warning on Ruby 1.8.7 when NetHttp is refereced below.
3
+ # require 'faraday/adapter/net_http'
4
+
5
+ module Faraday
6
+ class Adapter
7
+ # Experimental adapter for net-http-persistent
8
+ class NetHttpPersistent < NetHttp
9
+ dependency 'net/http/persistent'
10
+
11
+ def net_http_connection(env)
12
+ if proxy = env[:request][:proxy]
13
+ proxy_uri = ::URI::HTTP === proxy[:uri] ? proxy[:uri].dup : ::URI.parse(proxy[:uri].to_s)
14
+ proxy_uri.user = proxy_uri.password = nil
15
+ # awful patch for net-http-persistent 2.8 not unescaping user/password
16
+ (class << proxy_uri; self; end).class_eval do
17
+ define_method(:user) { proxy[:user] }
18
+ define_method(:password) { proxy[:password] }
19
+ end if proxy[:user]
20
+ end
21
+ Net::HTTP::Persistent.new 'Faraday', proxy_uri
22
+ end
23
+
24
+ def perform_request(http, env)
25
+ http.request env[:url], create_request(env)
26
+ rescue Net::HTTP::Persistent::Error => error
27
+ if error.message.include? 'Timeout'
28
+ raise Faraday::Error::TimeoutError, error
29
+ elsif error.message.include? 'connection refused'
30
+ raise Faraday::Error::ConnectionFailed, error
31
+ else
32
+ raise
33
+ end
34
+ end
35
+
36
+ def configure_ssl(http, ssl)
37
+ http.verify_mode = ssl_verify_mode(ssl)
38
+ http.cert_store = ssl_cert_store(ssl)
39
+
40
+ http.certificate = ssl[:client_cert] if ssl[:client_cert]
41
+ http.private_key = ssl[:client_key] if ssl[:client_key]
42
+ http.ca_file = ssl[:ca_file] if ssl[:ca_file]
43
+ http.ssl_version = ssl[:version] if ssl[:version]
44
+ end
45
+ end
46
+ end
47
+ end
@@ -3,28 +3,65 @@ module Faraday
3
3
  class Patron < Faraday::Adapter
4
4
  dependency 'patron'
5
5
 
6
+ def initialize(app, &block)
7
+ super(app)
8
+ @block = block
9
+ end
10
+
6
11
  def call(env)
7
12
  super
8
13
 
9
14
  # TODO: support streaming requests
10
15
  env[:body] = env[:body].read if env[:body].respond_to? :read
11
16
 
12
- session = ::Patron::Session.new
17
+ session = @session ||= create_session
13
18
 
14
- response = begin
15
- if Connection::METHODS_WITH_BODIES.include? env[:method]
16
- session.send(env[:method], env[:url].to_s, env[:body].to_s, env[:request_headers])
17
- else
18
- session.send(env[:method], env[:url].to_s, env[:request_headers])
19
+ if req = env[:request]
20
+ session.timeout = session.connect_timeout = req[:timeout] if req[:timeout]
21
+ session.connect_timeout = req[:open_timeout] if req[:open_timeout]
22
+
23
+ if proxy = req[:proxy]
24
+ proxy_uri = proxy[:uri].dup
25
+ proxy_uri.user = proxy[:user] && Utils.escape(proxy[:user]).gsub('+', '%20')
26
+ proxy_uri.password = proxy[:password] && Utils.escape(proxy[:password]).gsub('+', '%20')
27
+ session.proxy = proxy_uri.to_s
19
28
  end
20
- rescue Errno::ECONNREFUSED
29
+ end
30
+
31
+ response = begin
32
+ data = env[:body] ? env[:body].to_s : nil
33
+ session.request(env[:method], env[:url].to_s, env[:request_headers], :data => data)
34
+ rescue Errno::ECONNREFUSED, ::Patron::ConnectionFailed
21
35
  raise Error::ConnectionFailed, $!
22
36
  end
23
37
 
24
- env.update :status => response.status, :body => response.body
25
- response_headers(env).update response.headers
38
+ save_response(env, response.status, response.body, response.headers)
26
39
 
27
40
  @app.call env
41
+ rescue ::Patron::TimeoutError => err
42
+ raise Faraday::Error::TimeoutError, err
43
+ rescue ::Patron::Error => err
44
+ if err.message.include?("code 407")
45
+ raise Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
46
+ else
47
+ raise Error::ConnectionFailed, err
48
+ end
49
+ end
50
+
51
+ if loaded? && defined?(::Patron::Request::VALID_ACTIONS)
52
+ # HAX: helps but doesn't work completely
53
+ # https://github.com/toland/patron/issues/34
54
+ ::Patron::Request::VALID_ACTIONS.tap do |actions|
55
+ actions << :patch unless actions.include? :patch
56
+ actions << :options unless actions.include? :options
57
+ end
58
+ end
59
+
60
+ def create_session
61
+ session = ::Patron::Session.new
62
+ session.insecure = true
63
+ @block.call(session) if @block
64
+ session
28
65
  end
29
66
  end
30
67
  end
@@ -0,0 +1,58 @@
1
+ module Faraday
2
+ class Adapter
3
+ # Sends requests to a Rack app.
4
+ #
5
+ # Examples
6
+ #
7
+ # class MyRackApp
8
+ # def call(env)
9
+ # [200, {'Content-Type' => 'text/html'}, ["hello world"]]
10
+ # end
11
+ # end
12
+ #
13
+ # Faraday.new do |conn|
14
+ # conn.adapter :rack, MyRackApp.new
15
+ # end
16
+ class Rack < Faraday::Adapter
17
+ dependency 'rack/test'
18
+
19
+ # not prefixed with "HTTP_"
20
+ SPECIAL_HEADERS = %w[ CONTENT_LENGTH CONTENT_TYPE ]
21
+
22
+ def initialize(faraday_app, rack_app)
23
+ super(faraday_app)
24
+ mock_session = ::Rack::MockSession.new(rack_app)
25
+ @session = ::Rack::Test::Session.new(mock_session)
26
+ end
27
+
28
+ def call(env)
29
+ super
30
+ rack_env = {
31
+ :method => env[:method],
32
+ :input => env[:body].respond_to?(:read) ? env[:body].read : env[:body],
33
+ 'rack.url_scheme' => env[:url].scheme
34
+ }
35
+
36
+ env[:request_headers].each do |name, value|
37
+ name = name.upcase.tr('-', '_')
38
+ name = "HTTP_#{name}" unless SPECIAL_HEADERS.include? name
39
+ rack_env[name] = value
40
+ end if env[:request_headers]
41
+
42
+ timeout = env[:request][:timeout] || env[:request][:open_timeout]
43
+ response = if timeout
44
+ Timer.timeout(timeout, Faraday::Error::TimeoutError) { execute_request(env, rack_env) }
45
+ else
46
+ execute_request(env, rack_env)
47
+ end
48
+
49
+ save_response(env, response.status, response.body, response.headers)
50
+ @app.call env
51
+ end
52
+
53
+ def execute_request(env, rack_env)
54
+ @session.request(env[:url].to_s, rack_env)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -14,50 +14,59 @@ module Faraday
14
14
  class Test < Faraday::Adapter
15
15
  attr_accessor :stubs
16
16
 
17
- def self.loaded?() false end
18
-
19
17
  class Stubs
18
+ class NotFound < StandardError
19
+ end
20
+
20
21
  def initialize
21
22
  # {:get => [Stub, Stub]}
22
23
  @stack, @consumed = {}, {}
23
- yield self if block_given?
24
+ yield(self) if block_given?
24
25
  end
25
26
 
26
27
  def empty?
27
28
  @stack.empty?
28
29
  end
29
30
 
30
- def match(request_method, path, body)
31
+ def match(request_method, path, headers, body)
31
32
  return false if !@stack.key?(request_method)
32
33
  stack = @stack[request_method]
33
34
  consumed = (@consumed[request_method] ||= [])
34
35
 
35
- if stub = matches?(stack, path, body)
36
+ if stub = matches?(stack, path, headers, body)
36
37
  consumed << stack.delete(stub)
37
38
  stub
38
39
  else
39
- matches?(consumed, path, body)
40
+ matches?(consumed, path, headers, body)
40
41
  end
41
42
  end
42
43
 
43
- def get(path, &block)
44
- new_stub(:get, path, &block)
44
+ def get(path, headers = {}, &block)
45
+ new_stub(:get, path, headers, &block)
46
+ end
47
+
48
+ def head(path, headers = {}, &block)
49
+ new_stub(:head, path, headers, &block)
50
+ end
51
+
52
+ def post(path, body=nil, headers = {}, &block)
53
+ new_stub(:post, path, headers, body, &block)
45
54
  end
46
55
 
47
- def head(path, &block)
48
- new_stub(:head, path, &block)
56
+ def put(path, body=nil, headers = {}, &block)
57
+ new_stub(:put, path, headers, body, &block)
49
58
  end
50
59
 
51
- def post(path, body=nil, &block)
52
- new_stub(:post, path, body, &block)
60
+ def patch(path, body=nil, headers = {}, &block)
61
+ new_stub(:patch, path, headers, body, &block)
53
62
  end
54
63
 
55
- def put(path, body=nil, &block)
56
- new_stub(:put, path, body, &block)
64
+ def delete(path, headers = {}, &block)
65
+ new_stub(:delete, path, headers, &block)
57
66
  end
58
67
 
59
- def delete(path, &block)
60
- new_stub(:delete, path, &block)
68
+ def options(path, headers = {}, &block)
69
+ new_stub(:options, path, headers, &block)
61
70
  end
62
71
 
63
72
  # Raises an error if any of the stubbed calls have not been made.
@@ -75,18 +84,46 @@ module Faraday
75
84
 
76
85
  protected
77
86
 
78
- def new_stub(request_method, path, body=nil, &block)
79
- (@stack[request_method] ||= []) << Stub.new(path, body, block)
87
+ def new_stub(request_method, path, headers = {}, body=nil, &block)
88
+ normalized_path = Faraday::Utils.normalize_path(path)
89
+ (@stack[request_method] ||= []) << Stub.new(normalized_path, headers, body, block)
80
90
  end
81
91
 
82
- def matches?(stack, path, body)
83
- stack.detect { |stub| stub.matches?(path, body) }
92
+ def matches?(stack, path, headers, body)
93
+ stack.detect { |stub| stub.matches?(path, headers, body) }
84
94
  end
85
95
  end
86
96
 
87
- class Stub < Struct.new(:path, :body, :block)
88
- def matches?(request_path, request_body)
89
- request_path == path && (body.to_s.size.zero? || request_body == body)
97
+ class Stub < Struct.new(:path, :params, :headers, :body, :block)
98
+ def initialize(full, headers, body, block)
99
+ path, query = full.split('?')
100
+ params = query ?
101
+ Faraday::Utils.parse_nested_query(query) :
102
+ {}
103
+ super(path, params, headers, body, block)
104
+ end
105
+
106
+ def matches?(request_uri, request_headers, request_body)
107
+ request_path, request_query = request_uri.split('?')
108
+ request_params = request_query ?
109
+ Faraday::Utils.parse_nested_query(request_query) :
110
+ {}
111
+ request_path == path &&
112
+ params_match?(request_params) &&
113
+ (body.to_s.size.zero? || request_body == body) &&
114
+ headers_match?(request_headers)
115
+ end
116
+
117
+ def params_match?(request_params)
118
+ params.keys.all? do |key|
119
+ request_params[key] == params[key]
120
+ end
121
+ end
122
+
123
+ def headers_match?(request_headers)
124
+ headers.keys.all? do |key|
125
+ request_headers[key] == headers[key]
126
+ end
90
127
  end
91
128
 
92
129
  def to_s
@@ -101,19 +138,22 @@ module Faraday
101
138
  end
102
139
 
103
140
  def configure
104
- yield stubs
141
+ yield(stubs)
105
142
  end
106
143
 
107
144
  def call(env)
108
145
  super
109
146
  normalized_path = Faraday::Utils.normalize_path(env[:url])
147
+ params_encoder = env.request.params_encoder || Faraday::Utils.default_params_encoder
110
148
 
111
- if stub = stubs.match(env[:method], normalized_path, env[:body])
149
+ if stub = stubs.match(env[:method], normalized_path, env.request_headers, env[:body])
150
+ env[:params] = (query = env[:url].query) ?
151
+ params_encoder.decode(query) :
152
+ {}
112
153
  status, headers, body = stub.block.call(env)
113
- env.update :status => status, :body => body
114
- response_headers(env).update headers
154
+ save_response(env, status, body, headers)
115
155
  else
116
- raise "no stubbed request for #{env[:method]} #{normalized_path} #{env[:body]}"
156
+ raise Stubs::NotFound, "no stubbed request for #{env[:method]} #{normalized_path} #{env[:body]}"
117
157
  end
118
158
  @app.call(env)
119
159
  end