faraday 0.6.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.
Files changed (58) hide show
  1. data/Gemfile +22 -15
  2. data/{LICENSE → LICENSE.md} +1 -1
  3. data/README.md +216 -146
  4. data/Rakefile +14 -58
  5. data/config.ru +6 -0
  6. data/faraday.gemspec +31 -38
  7. data/lib/faraday/adapter/em_http.rb +204 -0
  8. data/lib/faraday/adapter/em_synchrony/parallel_manager.rb +66 -0
  9. data/lib/faraday/adapter/em_synchrony.rb +67 -16
  10. data/lib/faraday/adapter/excon.rb +1 -2
  11. data/lib/faraday/adapter/net_http.rb +82 -39
  12. data/lib/faraday/adapter/net_http_persistent.rb +37 -0
  13. data/lib/faraday/adapter/patron.rb +42 -8
  14. data/lib/faraday/adapter/rack.rb +59 -0
  15. data/lib/faraday/adapter/test.rb +49 -9
  16. data/lib/faraday/adapter/typhoeus.rb +70 -15
  17. data/lib/faraday/adapter.rb +41 -18
  18. data/lib/faraday/builder.rb +34 -16
  19. data/lib/faraday/connection.rb +177 -115
  20. data/lib/faraday/error.rb +4 -2
  21. data/lib/faraday/middleware.rb +18 -10
  22. data/lib/faraday/request/authorization.rb +40 -0
  23. data/lib/faraday/request/basic_authentication.rb +13 -0
  24. data/lib/faraday/request/multipart.rb +5 -6
  25. data/lib/faraday/request/retry.rb +21 -0
  26. data/lib/faraday/request/token_authentication.rb +15 -0
  27. data/lib/faraday/request/url_encoded.rb +1 -3
  28. data/lib/faraday/request.rb +48 -38
  29. data/lib/faraday/response/raise_error.rb +1 -1
  30. data/lib/faraday/response.rb +6 -5
  31. data/lib/faraday/upload_io.rb +1 -1
  32. data/lib/faraday/utils.rb +196 -29
  33. data/lib/faraday.rb +69 -13
  34. data/test/adapters/default_test.rb +14 -0
  35. data/test/adapters/em_http_test.rb +14 -0
  36. data/test/adapters/em_synchrony_test.rb +14 -0
  37. data/test/adapters/excon_test.rb +23 -0
  38. data/test/adapters/integration.rb +190 -0
  39. data/test/adapters/logger_test.rb +2 -2
  40. data/test/adapters/net_http_persistent_test.rb +11 -0
  41. data/test/adapters/net_http_test.rb +37 -21
  42. data/test/adapters/patron_test.rb +17 -0
  43. data/test/adapters/rack_test.rb +26 -0
  44. data/test/adapters/test_middleware_test.rb +28 -1
  45. data/test/adapters/typhoeus_test.rb +14 -0
  46. data/test/authentication_middleware_test.rb +65 -0
  47. data/test/connection_test.rb +211 -81
  48. data/test/env_test.rb +80 -31
  49. data/test/helper.rb +23 -13
  50. data/test/live_server.rb +38 -29
  51. data/test/middleware/retry_test.rb +25 -0
  52. data/test/middleware_stack_test.rb +85 -4
  53. data/test/request_middleware_test.rb +42 -19
  54. data/test/response_middleware_test.rb +30 -3
  55. metadata +104 -110
  56. data/lib/faraday/adapter/action_dispatch.rb +0 -30
  57. data/lib/faraday/request/json.rb +0 -31
  58. data/test/adapters/live_test.rb +0 -186
@@ -1,16 +1,17 @@
1
- require 'addressable/uri'
1
+ require 'cgi'
2
2
  require 'set'
3
- require 'base64'
3
+ require 'forwardable'
4
+ require 'uri'
5
+
6
+ Faraday.require_libs 'builder', 'request', 'response', 'utils'
4
7
 
5
8
  module Faraday
6
9
  class Connection
7
- include Addressable, Faraday::Utils
8
-
9
- METHODS = Set.new [:get, :post, :put, :delete, :head]
10
- METHODS_WITH_BODIES = Set.new [:post, :put]
10
+ METHODS = Set.new [:get, :post, :put, :delete, :head, :patch, :options]
11
+ METHODS_WITH_BODIES = Set.new [:post, :put, :patch, :options]
11
12
 
12
- attr_accessor :host, :port, :scheme, :params, :headers, :parallel_manager
13
- attr_reader :path_prefix, :builder, :options, :ssl
13
+ attr_reader :params, :headers, :url_prefix, :builder, :options, :ssl, :parallel_manager
14
+ attr_writer :default_parallel_manager
14
15
 
15
16
  # :url
16
17
  # :params
@@ -22,90 +23,127 @@ module Faraday
22
23
  options = url
23
24
  url = options[:url]
24
25
  end
25
- @headers = Headers.new
26
- @params = {}
27
- @options = options[:request] || {}
28
- @ssl = options[:ssl] || {}
29
- @parallel_manager = options[:parallel]
30
- self.url_prefix = url if url
31
- proxy(options[:proxy])
32
- merge_params @params, options[:params] if options[:params]
33
- merge_headers @headers, options[:headers] if options[:headers]
34
-
35
- if block_given?
36
- @builder = Builder.create { |b| yield b }
37
- else
38
- @builder = options[:builder] || Builder.new
26
+ @headers = Utils::Headers.new
27
+ @params = Utils::ParamsHash.new
28
+ @options = options[:request] || {}
29
+ @ssl = options[:ssl] || {}
30
+
31
+ @parallel_manager = nil
32
+ @default_parallel_manager = options[:parallel_manager]
33
+
34
+ @builder = options[:builder] || begin
35
+ # pass an empty block to Builder so it doesn't assume default middleware
36
+ block = block_given?? Proc.new {|b| } : nil
37
+ Builder.new(&block)
39
38
  end
40
- end
41
39
 
42
- def use(klass, *args, &block)
43
- @builder.use(klass, *args, &block)
44
- end
40
+ self.url_prefix = url || 'http:/'
45
41
 
46
- def request(key, *args, &block)
47
- @builder.request(key, *args, &block)
48
- end
42
+ @params.update options[:params] if options[:params]
43
+ @headers.update options[:headers] if options[:headers]
49
44
 
50
- def response(key, *args, &block)
51
- @builder.response(key, *args, &block)
45
+ @proxy = nil
46
+ proxy(options.fetch(:proxy) { ENV['http_proxy'] })
47
+
48
+ yield self if block_given?
52
49
  end
53
50
 
54
- def adapter(key, *args, &block)
55
- @builder.adapter(key, *args, &block)
51
+ # Public: Replace default query parameters.
52
+ def params=(hash)
53
+ @params.replace hash
56
54
  end
57
55
 
58
- def build(options = {}, &block)
59
- @builder.build(options, &block)
56
+ # Public: Replace default request headers.
57
+ def headers=(hash)
58
+ @headers.replace hash
60
59
  end
61
60
 
62
- def get(url = nil, headers = nil)
63
- block = block_given? ? Proc.new : nil
64
- run_request(:get, url, nil, headers, &block)
61
+ extend Forwardable
62
+ def_delegators :builder, :build, :use, :request, :response, :adapter
63
+
64
+ # The "rack app" wrapped in middleware. All requests are sent here.
65
+ #
66
+ # The builder is responsible for creating the app object. After this,
67
+ # the builder gets locked to ensure no further modifications are made
68
+ # to the middleware stack.
69
+ #
70
+ # Returns an object that responds to `call` and returns a Response.
71
+ def app
72
+ @app ||= begin
73
+ builder.lock!
74
+ builder.to_app(lambda { |env|
75
+ # the inner app that creates and returns the Response object
76
+ response = Response.new
77
+ response.finish(env) unless env[:parallel_manager]
78
+ env[:response] = response
79
+ })
80
+ end
65
81
  end
66
82
 
67
- def post(url = nil, body = nil, headers = nil)
68
- block = block_given? ? Proc.new : nil
69
- run_request(:post, url, body, headers, &block)
83
+ # get/head/delete(url, params, headers)
84
+ %w[get head delete].each do |method|
85
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
86
+ def #{method}(url = nil, params = nil, headers = nil)
87
+ run_request(:#{method}, url, nil, headers) { |request|
88
+ request.params.update(params) if params
89
+ yield request if block_given?
90
+ }
91
+ end
92
+ RUBY
70
93
  end
71
94
 
72
- def put(url = nil, body = nil, headers = nil)
73
- block = block_given? ? Proc.new : nil
74
- run_request(:put, url, body, headers, &block)
95
+ # post/put/patch(url, body, headers)
96
+ %w[post put patch].each do |method|
97
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
98
+ def #{method}(url = nil, body = nil, headers = nil, &block)
99
+ run_request(:#{method}, url, body, headers, &block)
100
+ end
101
+ RUBY
75
102
  end
76
103
 
77
- def head(url = nil, headers = nil)
78
- block = block_given? ? Proc.new : nil
79
- run_request(:head, url, nil, headers, &block)
104
+ def basic_auth(login, pass)
105
+ headers[Faraday::Request::Authorization::KEY] =
106
+ Faraday::Request::BasicAuthentication.header(login, pass)
80
107
  end
81
108
 
82
- def delete(url = nil, headers = nil)
83
- block = block_given? ? Proc.new : nil
84
- run_request(:delete, url, nil, headers, &block)
109
+ def token_auth(token, options = nil)
110
+ headers[Faraday::Request::Authorization::KEY] =
111
+ Faraday::Request::TokenAuthentication.header(token, options)
85
112
  end
86
113
 
87
- def basic_auth(login, pass)
88
- auth = Base64.encode64("#{login}:#{pass}")
89
- auth.gsub!("\n", "")
90
- @headers['authorization'] = "Basic #{auth}"
114
+ def authorization(type, token)
115
+ headers[Faraday::Request::Authorization::KEY] =
116
+ Faraday::Request::Authorization.header(type, token)
91
117
  end
92
118
 
93
- def token_auth(token, options = {})
94
- values = ["token=#{token.to_s.inspect}"]
95
- options.each do |key, value|
96
- values << "#{key}=#{value.to_s.inspect}"
119
+ # Internal: Traverse the middleware stack in search of a
120
+ # parallel-capable adapter.
121
+ #
122
+ # Yields in case of not found.
123
+ #
124
+ # Returns a parallel manager or nil if not found.
125
+ def default_parallel_manager
126
+ @default_parallel_manager ||= begin
127
+ handler = @builder.handlers.detect do |h|
128
+ h.klass.respond_to?(:supports_parallel?) and h.klass.supports_parallel?
129
+ end
130
+
131
+ if handler then handler.klass.setup_parallel_manager
132
+ elsif block_given? then yield
133
+ end
97
134
  end
98
- # 21 = "Authorization: Token ".size
99
- comma = ",\n#{' ' * 21}"
100
- @headers['authorization'] = "Token #{values * comma}"
101
135
  end
102
136
 
103
137
  def in_parallel?
104
138
  !!@parallel_manager
105
139
  end
106
140
 
107
- def in_parallel(manager)
108
- @parallel_manager = manager
141
+ def in_parallel(manager = nil)
142
+ @parallel_manager = manager || default_parallel_manager {
143
+ warn "Warning: `in_parallel` called but no parallel-capable adapter on Faraday stack"
144
+ warn caller[2,10].join("\n")
145
+ nil
146
+ }
109
147
  yield
110
148
  @parallel_manager && @parallel_manager.run
111
149
  ensure
@@ -115,20 +153,29 @@ module Faraday
115
153
  def proxy(arg = nil)
116
154
  return @proxy if arg.nil?
117
155
 
118
- @proxy =
119
- case arg
120
- when String then {:uri => proxy_arg_to_uri(arg)}
121
- when URI then {:uri => arg}
122
- when Hash
123
- if arg[:uri] = proxy_arg_to_uri(arg[:uri])
124
- arg
125
- else
126
- raise ArgumentError, "no :uri option."
127
- end
128
- end
156
+ @proxy = if arg.is_a? Hash
157
+ uri = arg.fetch(:uri) { raise ArgumentError, "no :uri option" }
158
+ arg.merge :uri => self.class.URI(uri)
159
+ else
160
+ {:uri => self.class.URI(arg)}
161
+ end
129
162
  end
130
163
 
131
- # Parses the giving url with Addressable::URI and stores the individual
164
+ # normalize URI() behavior across Ruby versions
165
+ def self.URI(url)
166
+ if url.respond_to?(:host)
167
+ url
168
+ elsif url.respond_to?(:to_str)
169
+ Kernel.URI(url)
170
+ else
171
+ raise ArgumentError, "bad argument (expected URI object or URI string)"
172
+ end
173
+ end
174
+
175
+ def_delegators :url_prefix, :scheme, :scheme=, :host, :host=, :port, :port=
176
+ def_delegator :url_prefix, :path, :path_prefix
177
+
178
+ # Parses the giving url with URI and stores the individual
132
179
  # components in this connection. These components serve as defaults for
133
180
  # requests made by this connection.
134
181
  #
@@ -140,26 +187,27 @@ module Faraday
140
187
  # conn.get("nigiri?page=2") # accesses https://sushi.com/api/nigiri
141
188
  #
142
189
  def url_prefix=(url)
143
- uri = URI.parse(url)
144
- self.scheme = uri.scheme
145
- self.host = uri.host
146
- self.port = uri.port
190
+ uri = @url_prefix = self.class.URI(url)
147
191
  self.path_prefix = uri.path
148
- if uri.query && !uri.query.empty?
149
- merge_params @params, parse_query(uri.query)
150
- end
192
+
193
+ params.merge_query(uri.query)
194
+ uri.query = nil
195
+
151
196
  if uri.user && uri.password
152
- basic_auth(uri.user, uri.password)
197
+ basic_auth(CGI.unescape(uri.user), CGI.unescape(uri.password))
198
+ uri.user = uri.password = nil
153
199
  end
200
+
201
+ uri
154
202
  end
155
203
 
156
- # Ensures that the path prefix always has a leading / and no trailing /
204
+ # Ensures that the path prefix always has a leading but no trailing slash
157
205
  def path_prefix=(value)
158
- if value
159
- value.chomp! "/"
160
- value.replace "/#{value}" if value !~ /^\//
206
+ url_prefix.path = if value
207
+ value = value.chomp '/'
208
+ value = '/' + value unless value[0,1] == '/'
209
+ value
161
210
  end
162
- @path_prefix = value
163
211
  end
164
212
 
165
213
  def run_request(method, url, body, headers)
@@ -167,12 +215,27 @@ module Faraday
167
215
  raise ArgumentError, "unknown http method: #{method}"
168
216
  end
169
217
 
170
- Request.run(self, method) do |req|
218
+ request = build_request(method) do |req|
171
219
  req.url(url) if url
172
220
  req.headers.update(headers) if headers
173
221
  req.body = body if body
174
222
  yield req if block_given?
175
223
  end
224
+
225
+ env = request.to_env(self)
226
+ self.app.call(env)
227
+ end
228
+
229
+ # Internal: Creates and configures the request object.
230
+ #
231
+ # Returns the new Request.
232
+ def build_request(method)
233
+ Request.create(method) do |req|
234
+ req.params = self.params.dup
235
+ req.headers = self.headers.dup
236
+ req.options = self.options.merge(:proxy => self.proxy)
237
+ yield req if block_given?
238
+ end
176
239
  end
177
240
 
178
241
  # Takes a relative url for a request and combines it with the defaults
@@ -186,39 +249,38 @@ module Faraday
186
249
  # conn.build_url("nigiri?page=2") # => https://sushi.com/api/nigiri?token=abc&page=2
187
250
  # conn.build_url("nigiri", :page => 2) # => https://sushi.com/api/nigiri?token=abc&page=2
188
251
  #
189
- def build_url(url, params = nil)
190
- uri = URI.parse(url.to_s)
191
- if @path_prefix && uri.path !~ /^\//
192
- uri.path = "#{@path_prefix.size > 1 ? @path_prefix : nil}/#{uri.path}"
193
- end
194
- uri.host ||= @host
195
- uri.port ||= @port
196
- uri.scheme ||= @scheme
197
- replace_query(uri, params)
198
- uri
199
- end
252
+ def build_url(url, extra_params = nil)
253
+ uri = build_exclusive_url(url)
200
254
 
201
- def dup
202
- self.class.new(build_url(''), :headers => headers.dup, :params => params.dup, :builder => builder.dup)
255
+ query_values = self.params.dup.merge_query(uri.query)
256
+ query_values.update extra_params if extra_params
257
+ uri.query = query_values.empty? ? nil : query_values.to_query
258
+
259
+ uri
203
260
  end
204
261
 
205
- def replace_query(uri, params)
206
- url_params = @params.dup
207
- if uri.query && !uri.query.empty?
208
- merge_params(url_params, parse_query(uri.query))
209
- end
210
- if params && !params.empty?
211
- merge_params(url_params, params)
262
+ # Internal: Build an absolute URL based on url_prefix.
263
+ #
264
+ # url - A String or URI-like object
265
+ # params - A Faraday::Utils::ParamsHash to replace the query values
266
+ # of the resulting url (default: nil).
267
+ #
268
+ # Returns the resulting URI instance.
269
+ def build_exclusive_url(url, params = nil)
270
+ url = nil if url.respond_to?(:empty?) and url.empty?
271
+ base = url_prefix
272
+ if url and base.path and base.path !~ /\/$/
273
+ base = base.dup
274
+ base.path = base.path + '/' # ensure trailing slash
212
275
  end
213
- uri.query = url_params.empty? ? nil : build_query(url_params)
276
+ uri = url ? base + url : base
277
+ uri.query = params.to_query if params
278
+ uri.query = nil if uri.query and uri.query.empty?
214
279
  uri
215
280
  end
216
281
 
217
- def proxy_arg_to_uri(arg)
218
- case arg
219
- when String then URI.parse(arg)
220
- when URI then arg
221
- end
282
+ def dup
283
+ self.class.new(build_url(''), :headers => headers.dup, :params => params.dup, :builder => builder.dup, :ssl => ssl.dup)
222
284
  end
223
285
  end
224
286
  end
data/lib/faraday/error.rb CHANGED
@@ -3,9 +3,9 @@ module Faraday
3
3
  class ClientError < StandardError
4
4
  attr_reader :response
5
5
 
6
- def initialize(ex)
6
+ def initialize(ex, response = nil)
7
7
  @wrapped_exception = nil
8
- @response = nil
8
+ @response = response
9
9
 
10
10
  if ex.respond_to?(:backtrace)
11
11
  super(ex.message)
@@ -34,5 +34,7 @@ module Faraday
34
34
  class ConnectionFailed < ClientError; end
35
35
  class ResourceNotFound < ClientError; end
36
36
  class ParsingError < ClientError; end
37
+ class TimeoutError < ClientError; end
38
+ class MissingDependency < StandardError; end
37
39
  end
38
40
  end
@@ -1,16 +1,14 @@
1
1
  module Faraday
2
2
  class Middleware
3
- class << self
4
- attr_accessor :load_error, :supports_parallel_requests
5
- alias supports_parallel_requests? supports_parallel_requests
3
+ extend MiddlewareRegistry
6
4
 
7
- # valid parallel managers should respond to #run with no parameters.
8
- # otherwise, return a short wrapper around it.
9
- def setup_parallel_manager(options = {})
10
- nil
11
- end
5
+ class << self
6
+ attr_accessor :load_error
7
+ private :load_error=
12
8
  end
13
9
 
10
+ self.load_error = nil
11
+
14
12
  # Executes a block which should try to require and reference dependent libraries
15
13
  def self.dependency(lib = nil)
16
14
  lib ? require(lib) : yield
@@ -18,12 +16,22 @@ module Faraday
18
16
  self.load_error = error
19
17
  end
20
18
 
19
+ def self.new(*)
20
+ raise "missing dependency for #{self}: #{load_error.message}" unless loaded?
21
+ super
22
+ end
23
+
21
24
  def self.loaded?
22
- @load_error.nil?
25
+ load_error.nil?
26
+ end
27
+
28
+ def self.inherited(subclass)
29
+ super
30
+ subclass.send(:load_error=, self.load_error)
23
31
  end
24
32
 
25
33
  def initialize(app = nil)
26
34
  @app = app
27
35
  end
28
36
  end
29
- end
37
+ end
@@ -0,0 +1,40 @@
1
+ module Faraday
2
+ class Request::Authorization < Faraday::Middleware
3
+ KEY = "Authorization".freeze
4
+
5
+ # Public
6
+ def self.header(type, token)
7
+ case token
8
+ when String, Symbol then "#{type} #{token}"
9
+ when Hash then build_hash(type.to_s, token)
10
+ else
11
+ raise ArgumentError, "Can't build an Authorization #{type} header from #{token.inspect}"
12
+ end
13
+ end
14
+
15
+ # Internal
16
+ def self.build_hash(type, hash)
17
+ offset = KEY.size + type.size + 3
18
+ comma = ",\n#{' ' * offset}"
19
+ values = []
20
+ hash.each do |key, value|
21
+ values << "#{key}=#{value.to_s.inspect}"
22
+ end
23
+ "#{type} #{values * comma}"
24
+ end
25
+
26
+ def initialize(app, type, token)
27
+ @header_value = self.class.header(type, token)
28
+ super(app)
29
+ end
30
+
31
+ # Public
32
+ def call(env)
33
+ unless env[:request_headers][KEY]
34
+ env[:request_headers][KEY] = @header_value
35
+ end
36
+ @app.call(env)
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,13 @@
1
+ require 'base64'
2
+
3
+ module Faraday
4
+ class Request::BasicAuthentication < Request::Authorization
5
+ # Public
6
+ def self.header(login, pass)
7
+ value = Base64.encode64([login, pass].join(':'))
8
+ value.gsub!("\n", '')
9
+ super(:Basic, value)
10
+ end
11
+ end
12
+ end
13
+
@@ -21,12 +21,11 @@ module Faraday
21
21
  )
22
22
  end
23
23
 
24
- def has_multipart?(body)
25
- body.values.each do |val|
26
- if val.respond_to?(:content_type)
27
- return true
28
- elsif val.respond_to?(:values)
29
- return true if has_multipart?(val)
24
+ def has_multipart?(obj)
25
+ # string is an enum in 1.8, returning list of itself
26
+ if obj.respond_to?(:each) && !obj.is_a?(String)
27
+ (obj.respond_to?(:values) ? obj.values : obj).each do |val|
28
+ return true if (val.respond_to?(:content_type) || has_multipart?(val))
30
29
  end
31
30
  end
32
31
  false
@@ -0,0 +1,21 @@
1
+ module Faraday
2
+ class Request::Retry < Faraday::Middleware
3
+ def initialize(app, retries = 2)
4
+ @retries = retries
5
+ super(app)
6
+ end
7
+
8
+ def call(env)
9
+ retries = @retries
10
+ begin
11
+ @app.call(env)
12
+ rescue StandardError, Timeout::Error
13
+ if retries > 0
14
+ retries -= 1
15
+ retry
16
+ end
17
+ raise
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module Faraday
2
+ class Request::TokenAuthentication < Request::Authorization
3
+ # Public
4
+ def self.header(token, options = nil)
5
+ options ||= {}
6
+ options[:token] = token
7
+ super :Token, options
8
+ end
9
+
10
+ def initialize(app, token, options = nil)
11
+ super(app, token, options)
12
+ end
13
+ end
14
+ end
15
+
@@ -13,10 +13,8 @@ module Faraday
13
13
  end
14
14
  @app.call env
15
15
  end
16
-
16
+
17
17
  def match_content_type(env)
18
- type = request_type(env)
19
-
20
18
  if process_request?(env)
21
19
  env[:request_headers][CONTENT_TYPE] ||= self.class.mime_type
22
20
  yield env[:body] unless env[:body].respond_to?(:to_str)