faraday 0.1.2 → 0.2.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 (39) hide show
  1. data/LICENSE +1 -1
  2. data/README.rdoc +48 -61
  3. data/Rakefile +3 -1
  4. data/VERSION +1 -1
  5. data/faraday.gemspec +31 -14
  6. data/lib/faraday.rb +33 -26
  7. data/lib/faraday/adapter/net_http.rb +17 -31
  8. data/lib/faraday/adapter/patron.rb +33 -0
  9. data/lib/faraday/adapter/test.rb +96 -0
  10. data/lib/faraday/adapter/typhoeus.rb +43 -59
  11. data/lib/faraday/builder.rb +57 -0
  12. data/lib/faraday/connection.rb +112 -105
  13. data/lib/faraday/middleware.rb +54 -0
  14. data/lib/faraday/request.rb +77 -0
  15. data/lib/faraday/request/active_support_json.rb +20 -0
  16. data/lib/faraday/request/yajl.rb +18 -0
  17. data/lib/faraday/response.rb +41 -26
  18. data/lib/faraday/response/active_support_json.rb +22 -0
  19. data/lib/faraday/response/yajl.rb +20 -0
  20. data/test/adapters/live_test.rb +157 -0
  21. data/test/adapters/test_middleware_test.rb +28 -0
  22. data/test/adapters/typhoeus_test.rb +28 -0
  23. data/test/connection_app_test.rb +49 -0
  24. data/test/connection_test.rb +47 -18
  25. data/test/env_test.rb +35 -0
  26. data/test/helper.rb +5 -2
  27. data/test/live_server.rb +2 -15
  28. data/test/request_middleware_test.rb +19 -0
  29. data/test/response_middleware_test.rb +21 -0
  30. metadata +46 -16
  31. data/lib/faraday/adapter/mock_request.rb +0 -120
  32. data/lib/faraday/loadable.rb +0 -13
  33. data/lib/faraday/request/post_request.rb +0 -30
  34. data/lib/faraday/request/yajl_request.rb +0 -27
  35. data/lib/faraday/response/yajl_response.rb +0 -35
  36. data/lib/faraday/test_connection.rb +0 -5
  37. data/test/adapter/typhoeus_test.rb +0 -26
  38. data/test/adapter_test.rb +0 -118
  39. data/test/response_test.rb +0 -34
@@ -1,75 +1,59 @@
1
1
  module Faraday
2
2
  module Adapter
3
- module Typhoeus
4
- extend Faraday::Connection::Options
3
+ class Typhoeus < Middleware
4
+ self.supports_parallel_requests = true
5
+
6
+ def self.setup_parallel_manager(options = {})
7
+ options.empty? ? ::Typhoeus::Hydra.hydra : ::Typhoeus::Hydra.new(options)
8
+ end
5
9
 
6
10
  begin
7
11
  require 'typhoeus'
12
+ rescue LoadError => e
13
+ self.load_error = e
14
+ end
8
15
 
9
- def in_parallel?
10
- !!@parallel_manager
11
- end
12
-
13
- def in_parallel(options = {})
14
- setup_parallel_manager(options)
15
- yield
16
- run_parallel_requests
17
- end
18
-
19
- def setup_parallel_manager(options = {})
20
- @parallel_manager ||= ::Typhoeus::Hydra.new(options)
21
- end
22
-
23
- def run_parallel_requests
24
- @parallel_manager.run
25
- @parallel_manager = nil
26
- end
27
-
28
- def _post(uri, data, request_headers)
29
- request = request_class.new(data, request_headers)
30
- _perform(:post, uri, :headers => request.headers, :body => request.body)
31
- end
16
+ def call(env)
17
+ process_body_for_request(env)
32
18
 
33
- def _get(uri, request_headers)
34
- _perform(:get, uri, :headers => request_headers)
35
- end
19
+ hydra = env[:parallel_manager] || self.class.setup_parallel_manager
20
+ req = ::Typhoeus::Request.new env[:url].to_s,
21
+ :method => env[:method],
22
+ :body => env[:body],
23
+ :headers => env[:request_headers]
36
24
 
37
- def _put(uri, data, request_headers)
38
- request = request_class.new(data, request_headers)
39
- _perform(:put, uri, :headers => request.headers, :body => request.body)
25
+ req.on_complete do |resp|
26
+ env.update \
27
+ :status => resp.code,
28
+ :response_headers => parse_response_headers(resp.headers),
29
+ :body => resp.body
30
+ env[:response].finish(env)
40
31
  end
41
32
 
42
- def _delete(uri, request_headers)
43
- _perform(:delete, uri, :headers => request_headers)
44
- end
33
+ hydra.queue req
45
34
 
46
- def _perform method, uri, params
47
- response_class.new do |resp|
48
- is_async = in_parallel?
49
- setup_parallel_manager
50
- params[:method] = method
51
- req = ::Typhoeus::Request.new(uri.to_s, params)
52
- req.on_complete do |response|
53
- raise Faraday::Error::ResourceNotFound if response.code == 404
54
- resp.process!(response.body)
55
- resp.headers = parse_response_headers(response.headers)
56
- end
57
- @parallel_manager.queue(req)
58
- if !is_async then run_parallel_requests end
59
- end
60
- rescue Errno::ECONNREFUSED
61
- raise Faraday::Error::ConnectionFailed, "connection refused"
35
+ if !env[:parallel_manager]
36
+ hydra.run
62
37
  end
63
38
 
64
- def parse_response_headers(header_string)
65
- return {} unless header_string # XXX
66
- Hash[*header_string.split(/\r\n/).
67
- tap { |a| a.shift }. # drop the HTTP status line
68
- map! { |h| h.split(/:\s+/,2) }. # split key and value
69
- map! { |(k, v)| [k.downcase, v] }.flatten!]
70
- end
71
- rescue LoadError => e
72
- self.load_error = e
39
+ @app.call env
40
+ rescue Errno::ECONNREFUSED
41
+ raise Error::ConnectionFailed, "connection refused"
42
+ end
43
+
44
+ def in_parallel(options = {})
45
+ @hydra = ::Typhoeus::Hydra.new(options)
46
+ yield
47
+ @hydra.run
48
+ @hydra = nil
49
+ end
50
+
51
+ def parse_response_headers(header_string)
52
+ return {} unless header_string && !header_string.empty?
53
+ Hash[*header_string.split(/\r\n/).
54
+ tap { |a| a.shift }. # drop the HTTP status line
55
+ map! { |h| h.split(/:\s+/,2) }. # split key and value
56
+ map! { |(k, v)| [k.downcase, v] }.flatten!]
73
57
  end
74
58
  end
75
59
  end
@@ -0,0 +1,57 @@
1
+ module Faraday
2
+ # Possibly going to extend this a bit.
3
+ #
4
+ # Faraday::Connection.new(:url => 'http://sushi.com') do
5
+ # request :yajl # Faraday::Request::Yajl
6
+ # adapter :logger # Faraday::Adapter::Logger
7
+ # response :yajl # Faraday::Response::Yajl
8
+ # end
9
+ class Builder
10
+ attr_accessor :handlers
11
+
12
+ def self.create_with_inner_app(&block)
13
+ inner = lambda do |env|
14
+ if !env[:parallel_manager]
15
+ env[:response].finish(env)
16
+ else
17
+ env[:response]
18
+ end
19
+ end
20
+ Builder.new(&block).tap { |builder| builder.run(inner) }
21
+ end
22
+
23
+ def initialize
24
+ @handlers = []
25
+ yield self
26
+ end
27
+
28
+ def run app
29
+ @handlers.unshift app
30
+ end
31
+
32
+ def to_app
33
+ inner_app = @handlers.first
34
+ @handlers[1..-1].inject(inner_app) { |app, middleware| middleware.call(app) }
35
+ end
36
+
37
+ def use klass, *args, &block
38
+ @handlers.unshift(lambda { |app| klass.new(app, *args, &block) })
39
+ end
40
+
41
+ def request(key, *args, &block)
42
+ use_symbol Faraday::Request, key, *args, &block
43
+ end
44
+
45
+ def response(key, *args, &block)
46
+ use_symbol Faraday::Response, key, *args, &block
47
+ end
48
+
49
+ def adapter(key, *args, &block)
50
+ use_symbol Faraday::Adapter, key, *args, &block
51
+ end
52
+
53
+ def use_symbol(mod, key, *args, &block)
54
+ use mod.lookup_module(key), *args, &block
55
+ end
56
+ end
57
+ end
@@ -1,119 +1,115 @@
1
1
  require 'addressable/uri'
2
+ require 'set'
3
+
2
4
  module Faraday
3
5
  class Connection
4
- module Options
5
- def load_error() @load_error end
6
- def load_error=(v) @load_error = v end
7
- def supports_async() @supports_async end
8
- def supports_async=(v) @supports_async = v end
9
- def loaded?() !@load_error end
10
- alias supports_async? supports_async
11
- end
6
+ include Addressable, Rack::Utils
12
7
 
13
- include Addressable
8
+ HEADERS = Hash.new { |h, k| k.respond_to?(:to_str) ? k : k.to_s.capitalize }.update \
9
+ :content_type => "Content-Type",
10
+ :content_length => "Content-Length",
11
+ :accept_charset => "Accept-Charset",
12
+ :accept_encoding => "Accept-Encoding"
13
+ HEADERS.values.each { |v| v.freeze }
14
14
 
15
- attr_accessor :host, :port, :scheme, :params, :headers
16
- attr_reader :path_prefix
15
+ METHODS = Set.new [:get, :post, :put, :delete, :head]
16
+ METHODS_WITH_BODIES = Set.new [:post, :put]
17
+
18
+ attr_accessor :host, :port, :scheme, :params, :headers, :parallel_manager
19
+ attr_reader :path_prefix, :builder
17
20
 
18
21
  # :url
19
22
  # :params
20
23
  # :headers
21
- # :response
22
- def initialize(url = nil, options = {})
24
+ def initialize(url = nil, options = {}, &block)
23
25
  if url.is_a?(Hash)
24
26
  options = url
25
27
  url = options[:url]
26
28
  end
27
- @response_class = options[:response]
28
- @params = options[:params] || {}
29
- @headers = options[:headers] || {}
29
+ @headers = HeaderHash.new
30
+ @params = {}
31
+ @parallel_manager = options[:parallel]
30
32
  self.url_prefix = url if url
33
+ merge_params @params, options[:params] if options[:params]
34
+ merge_headers @headers, options[:headers] if options[:headers]
35
+ if block
36
+ @builder = Builder.create_with_inner_app(&block)
37
+ end
31
38
  end
32
39
 
33
- def url_prefix=(url)
34
- uri = URI.parse(url)
35
- self.scheme = uri.scheme
36
- self.host = uri.host
37
- self.port = uri.port
38
- self.path_prefix = uri.path
39
- end
40
-
41
-
42
- # Override in a subclass, or include an adapter
43
- #
44
- # def _post(uri, post_params, headers)
45
- # end
46
- #
47
- def post(uri, params = {}, headers = {})
48
- _post build_uri(uri), build_params(params), build_headers(headers)
49
- end
50
-
51
- # Override in a subclass, or include an adapter
52
- #
53
- # def _put(uri, post_params, headers)
54
- # end
55
- #
56
- def put(uri, params = {}, headers = {})
57
- _put build_uri(uri), build_params(params), build_headers(headers)
40
+ def get(url = nil, headers = nil, &block)
41
+ run_request :get, url, nil, headers, &block
58
42
  end
59
43
 
60
- # Override in a subclass, or include an adapter
61
- #
62
- # def _delete(uri, headers)
63
- # end
64
- #
65
- def delete(uri, params = {}, headers = {})
66
- _delete build_uri(uri, build_params(params)), build_headers(headers)
44
+ def post(url = nil, body = nil, headers = nil, &block)
45
+ run_request :post, url, body, headers, &block
67
46
  end
68
47
 
69
- # Override in a subclass, or include an adapter
70
- #
71
- # def _get(uri, headers)
72
- # end
73
- #
74
- def get(url, params = nil, headers = nil)
75
- uri = build_uri(url, build_params(params))
76
- _get(uri, build_headers(headers))
48
+ def put(url = nil, body = nil, headers = nil, &block)
49
+ run_request :put, url, body, headers, &block
77
50
  end
78
51
 
79
- def request_class
80
- @request_class || Request::PostRequest
52
+ def head(url = nil, headers = nil, &block)
53
+ run_request :head, url, nil, headers, &block
81
54
  end
82
55
 
83
- def response_class
84
- @response_class || Response
56
+ def delete(url = nil, headers = nil, &block)
57
+ run_request :delete, url, nil, headers, &block
85
58
  end
86
59
 
87
- def response_class=(v)
88
- if v.respond_to?(:loaded?) && !v.loaded?
89
- raise ArgumentError, "The response class: #{v.inspect} does not appear to be loaded."
60
+ def run_request(method, url, body, headers)
61
+ if !METHODS.include?(method)
62
+ raise ArgumentError, "unknown http method: #{method}"
90
63
  end
91
- @response_class = v
92
- end
93
64
 
94
- def request_class=(v)
95
- if v.respond_to?(:loaded?) && !v.loaded?
96
- raise ArgumentError, "The request class: #{v.inspect} does not appear to be loaded."
65
+ Request.run(self, method) do |req|
66
+ req.url(url) if url
67
+ req.headers.update(headers) if headers
68
+ req.body = body if body
69
+ yield req if block_given?
97
70
  end
98
- @request_class = v
99
71
  end
100
72
 
101
73
  def in_parallel?
102
74
  !!@parallel_manager
103
75
  end
104
76
 
105
- def in_parallel(options = {})
106
- @parallel_manager = true
77
+ def in_parallel(manager)
78
+ @parallel_manager = manager
107
79
  yield
108
- @parallel_manager = false
80
+ @parallel_manager && @parallel_manager.run
81
+ ensure
82
+ @parallel_manager = nil
109
83
  end
110
84
 
111
- def setup_parallel_manager(options = {})
85
+ # return the assembled Rack application for this instance.
86
+ def to_app
87
+ @builder.to_app
112
88
  end
113
89
 
114
- def run_parallel_requests
90
+ # Parses the giving url with Addressable::URI and stores the individual
91
+ # components in this connection. These components serve as defaults for
92
+ # requests made by this connection.
93
+ #
94
+ # conn = Faraday::Connection.new { ... }
95
+ # conn.url_prefix = "https://sushi.com/api"
96
+ # conn.scheme # => https
97
+ # conn.path_prefix # => "/api"
98
+ #
99
+ # conn.get("nigiri?page=2") # accesses https://sushi.com/api/nigiri
100
+ #
101
+ def url_prefix=(url)
102
+ uri = URI.parse(url)
103
+ self.scheme = uri.scheme
104
+ self.host = uri.host
105
+ self.port = uri.port
106
+ self.path_prefix = uri.path
107
+ if uri.query && !uri.query.empty?
108
+ merge_params @params, parse_query(uri.query)
109
+ end
115
110
  end
116
111
 
112
+ # Ensures that the path prefix always has a leading / and no trailing /
117
113
  def path_prefix=(value)
118
114
  if value
119
115
  value.chomp! "/"
@@ -122,54 +118,65 @@ module Faraday
122
118
  @path_prefix = value
123
119
  end
124
120
 
125
- def build_uri(url, params = nil)
126
- uri = URI.parse(url)
121
+ # Takes a relative url for a request and combines it with the defaults
122
+ # set on the connection instance.
123
+ #
124
+ # conn = Faraday::Connection.new { ... }
125
+ # conn.url_prefix = "https://sushi.com/api?token=abc"
126
+ # conn.scheme # => https
127
+ # conn.path_prefix # => "/api"
128
+ #
129
+ # conn.build_url("nigiri?page=2") # => https://sushi.com/api/nigiri?token=abc&page=2
130
+ # conn.build_url("nigiri", :page => 2) # => https://sushi.com/api/nigiri?token=abc&page=2
131
+ #
132
+ def build_url(url, params = nil)
133
+ uri = URI.parse(url.to_s)
127
134
  uri.scheme ||= @scheme
128
135
  uri.host ||= @host
129
136
  uri.port ||= @port
130
137
  if @path_prefix && uri.path !~ /^\//
131
138
  uri.path = "#{@path_prefix.size > 1 ? @path_prefix : nil}/#{uri.path}"
132
139
  end
133
- if params && !params.empty?
134
- uri.query = params_to_query(params)
135
- end
140
+ replace_query(uri, params)
136
141
  uri
137
142
  end
138
143
 
139
- def path_for(uri)
140
- uri.path.tap do |s|
141
- s << "?#{uri.query}" if uri.query
142
- s << "##{uri.fragment}" if uri.fragment
144
+ def replace_query(uri, params)
145
+ url_params = @params.dup
146
+ if uri.query && !uri.query.empty?
147
+ merge_params url_params, parse_query(uri.query)
143
148
  end
144
- end
145
-
146
- def build_params(existing)
147
- build_hash :params, existing
148
- end
149
-
150
- def build_headers(existing)
151
- build_hash(:headers, existing).tap do |headers|
152
- headers.keys.each do |key|
153
- headers[key] = headers.delete(key).to_s
154
- end
149
+ if params && !params.empty?
150
+ merge_params url_params, params
155
151
  end
152
+ uri.query = url_params.empty? ? nil : build_query(url_params)
153
+ uri
156
154
  end
157
155
 
158
- def build_hash(method, existing)
159
- existing ? send(method).merge(existing) : send(method)
156
+ # turns param keys into strings
157
+ def merge_params(existing_params, new_params)
158
+ new_params.each do |key, value|
159
+ existing_params[key.to_s] = value
160
+ end
160
161
  end
161
162
 
162
- def params_to_query(params)
163
- params.inject([]) do |memo, (key, val)|
164
- memo << "#{escape_for_querystring(key)}=#{escape_for_querystring(val)}"
165
- end.join("&")
163
+ # turns headers keys and values into strings. Look up symbol keys in the
164
+ # the HEADERS hash.
165
+ #
166
+ # h = merge_headers(HeaderHash.new, :content_type => 'text/plain')
167
+ # h['Content-Type'] # = 'text/plain'
168
+ #
169
+ def merge_headers(existing_headers, new_headers)
170
+ new_headers.each do |key, value|
171
+ existing_headers[HEADERS[key]] = value.to_s
172
+ end
166
173
  end
167
174
 
168
- # Some servers convert +'s in URL query params to spaces.
169
- # Go ahead and encode it.
170
- def escape_for_querystring(s)
171
- URI.encode_component(s.to_s, Addressable::URI::CharacterClasses::QUERY).tap do |escaped|
172
- escaped.gsub! /\+/, "%2B"
175
+ # Be sure to URI escape '+' symbols to %2B. Otherwise, they get interpreted
176
+ # as spaces.
177
+ def escape(s)
178
+ s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) do
179
+ '%' << $1.unpack('H2'*bytesize($1)).join('%').tap { |c| c.upcase! }
173
180
  end
174
181
  end
175
182
  end