faraday 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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