typhoeus 0.1.2
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.
- data/ext/typhoeus/Makefile +157 -0
- data/ext/typhoeus/extconf.rb +64 -0
- data/ext/typhoeus/native.c +11 -0
- data/ext/typhoeus/native.h +14 -0
- data/ext/typhoeus/typhoeus_easy.c +207 -0
- data/ext/typhoeus/typhoeus_easy.h +19 -0
- data/ext/typhoeus/typhoeus_multi.c +213 -0
- data/ext/typhoeus/typhoeus_multi.h +16 -0
- data/lib/typhoeus.rb +55 -0
- data/lib/typhoeus/easy.rb +210 -0
- data/lib/typhoeus/filter.rb +28 -0
- data/lib/typhoeus/hydra.rb +160 -0
- data/lib/typhoeus/multi.rb +34 -0
- data/lib/typhoeus/remote.rb +306 -0
- data/lib/typhoeus/remote_method.rb +108 -0
- data/lib/typhoeus/remote_proxy_object.rb +48 -0
- data/lib/typhoeus/request.rb +88 -0
- data/lib/typhoeus/response.rb +19 -0
- data/spec/servers/delay_fixture_server.rb +66 -0
- data/spec/servers/method_server.rb +51 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/typhoeus/easy_spec.rb +148 -0
- data/spec/typhoeus/filter_spec.rb +35 -0
- data/spec/typhoeus/multi_spec.rb +82 -0
- data/spec/typhoeus/remote_method_spec.rb +141 -0
- data/spec/typhoeus/remote_proxy_object_spec.rb +73 -0
- data/spec/typhoeus/remote_spec.rb +699 -0
- data/spec/typhoeus/response_spec.rb +36 -0
- metadata +93 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class Filter
|
3
|
+
attr_reader :method_name
|
4
|
+
|
5
|
+
def initialize(method_name, options = {})
|
6
|
+
@method_name = method_name
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def apply_filter?(method_name)
|
11
|
+
if @options[:only]
|
12
|
+
if @options[:only].instance_of? Symbol
|
13
|
+
@options[:only] == method_name
|
14
|
+
else
|
15
|
+
@options[:only].include?(method_name)
|
16
|
+
end
|
17
|
+
elsif @options[:except]
|
18
|
+
if @options[:except].instance_of? Symbol
|
19
|
+
@options[:except] != method_name
|
20
|
+
else
|
21
|
+
!@options[:except].include?(method_name)
|
22
|
+
end
|
23
|
+
else
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class Hydra
|
3
|
+
def initialize
|
4
|
+
@multi = Multi.new
|
5
|
+
@easy_pool = []
|
6
|
+
@mocks = []
|
7
|
+
@memoized_requests = {}
|
8
|
+
@retrieved_from_cache = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.hydra
|
12
|
+
@hydra ||= new
|
13
|
+
end
|
14
|
+
|
15
|
+
def queue(request)
|
16
|
+
return if assign_to_mock(request)
|
17
|
+
|
18
|
+
if request.method == :get
|
19
|
+
if @memoized_requests.has_key? request.url
|
20
|
+
if response = @retrieved_from_cache[request.url]
|
21
|
+
request.response = response
|
22
|
+
request.call_handlers
|
23
|
+
else
|
24
|
+
@memoized_requests[request.url] << request
|
25
|
+
end
|
26
|
+
else
|
27
|
+
@memoized_requests[request.url] = []
|
28
|
+
get_from_cache_or_queue(request)
|
29
|
+
end
|
30
|
+
else
|
31
|
+
get_from_cache_or_queue(request)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def run
|
36
|
+
@mocks.each do |m|
|
37
|
+
m.requests.each do |request|
|
38
|
+
m.response.request = request
|
39
|
+
handle_request(request, m.response)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
@multi.perform
|
43
|
+
@memoized_requests = {}
|
44
|
+
@retrieved_from_cache = {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def cache_getter(&block)
|
48
|
+
@cache_getter = block
|
49
|
+
end
|
50
|
+
|
51
|
+
def cache_setter(&block)
|
52
|
+
@cache_setter = block
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_complete(&block)
|
56
|
+
@on_complete = block
|
57
|
+
end
|
58
|
+
|
59
|
+
def on_complete=(proc)
|
60
|
+
@on_complete = proc
|
61
|
+
end
|
62
|
+
|
63
|
+
def mock(method, url)
|
64
|
+
@mocks << HydraMock.new(url, method)
|
65
|
+
@mocks.last
|
66
|
+
end
|
67
|
+
|
68
|
+
def assign_to_mock(request)
|
69
|
+
m = @mocks.detect {|mck| mck.method == request.method && mck.url == request.url}
|
70
|
+
m && m.add_request(request)
|
71
|
+
end
|
72
|
+
private :assign_to_mock
|
73
|
+
|
74
|
+
def get_from_cache_or_queue(request)
|
75
|
+
if @cache_getter
|
76
|
+
val = @cache_getter.call(request)
|
77
|
+
if val
|
78
|
+
@retrieved_from_cache[request.url] = val
|
79
|
+
handle_request(request, val, false)
|
80
|
+
else
|
81
|
+
@multi.add(get_easy_object(request))
|
82
|
+
end
|
83
|
+
else
|
84
|
+
@multi.add(get_easy_object(request))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
private :get_from_cache_or_queue
|
88
|
+
|
89
|
+
def get_easy_object(request)
|
90
|
+
easy = @easy_pool.pop || Easy.new
|
91
|
+
easy.url = request.url
|
92
|
+
easy.method = request.method
|
93
|
+
easy.headers = request.headers if request.headers
|
94
|
+
easy.request_body = request.body if request.body
|
95
|
+
easy.timeout = request.timeout if request.timeout
|
96
|
+
easy.on_success do |easy|
|
97
|
+
handle_request(request, response_from_easy(easy, request))
|
98
|
+
release_easy_object(easy)
|
99
|
+
end
|
100
|
+
easy.on_failure do |easy|
|
101
|
+
handle_request(request, response_from_easy(easy, request))
|
102
|
+
release_easy_object(easy)
|
103
|
+
end
|
104
|
+
easy.set_headers
|
105
|
+
easy
|
106
|
+
end
|
107
|
+
private :get_easy_object
|
108
|
+
|
109
|
+
def release_easy_object(easy)
|
110
|
+
easy.reset
|
111
|
+
@easy_pool.push easy
|
112
|
+
end
|
113
|
+
private :release_easy_object
|
114
|
+
|
115
|
+
def handle_request(request, response, live_request = true)
|
116
|
+
request.response = response
|
117
|
+
|
118
|
+
if live_request && request.cache_timeout && @cache_setter
|
119
|
+
@cache_setter.call(request)
|
120
|
+
end
|
121
|
+
@on_complete.call(response) if @on_complete
|
122
|
+
|
123
|
+
request.call_handlers
|
124
|
+
if requests = @memoized_requests[request.url]
|
125
|
+
requests.each do |r|
|
126
|
+
r.response = response
|
127
|
+
r.call_handlers
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
private :handle_request
|
132
|
+
|
133
|
+
def response_from_easy(easy, request)
|
134
|
+
Response.new(:code => easy.response_code,
|
135
|
+
:headers => easy.response_header,
|
136
|
+
:body => easy.response_body,
|
137
|
+
:time => easy.total_time_taken,
|
138
|
+
:request => request)
|
139
|
+
end
|
140
|
+
private :response_from_easy
|
141
|
+
end
|
142
|
+
|
143
|
+
class HydraMock
|
144
|
+
attr_reader :url, :method, :response, :requests
|
145
|
+
|
146
|
+
def initialize(url, method)
|
147
|
+
@url = url
|
148
|
+
@method = method
|
149
|
+
@requests = []
|
150
|
+
end
|
151
|
+
|
152
|
+
def add_request(request)
|
153
|
+
@requests << request
|
154
|
+
end
|
155
|
+
|
156
|
+
def and_return(val)
|
157
|
+
@response = val
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class Multi
|
3
|
+
attr_reader :easy_handles
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
reset_easy_handles
|
7
|
+
end
|
8
|
+
|
9
|
+
def remove(easy)
|
10
|
+
multi_remove_handle(easy)
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(easy)
|
14
|
+
@easy_handles << easy
|
15
|
+
multi_add_handle(easy)
|
16
|
+
end
|
17
|
+
|
18
|
+
def perform()
|
19
|
+
while active_handle_count > 0 do
|
20
|
+
multi_perform
|
21
|
+
end
|
22
|
+
reset_easy_handles
|
23
|
+
end
|
24
|
+
|
25
|
+
def cleanup()
|
26
|
+
multi_cleanup
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def reset_easy_handles
|
31
|
+
@easy_handles = []
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,306 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
USER_AGENT = "Typhoeus - http://github.com/pauldix/typhoeus/tree/master"
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
class MockExpectedError < StandardError; end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def allow_net_connect
|
12
|
+
@allow_net_connect = true if @allow_net_connect.nil?
|
13
|
+
@allow_net_connect
|
14
|
+
end
|
15
|
+
|
16
|
+
def allow_net_connect=(value)
|
17
|
+
@allow_net_connect = value
|
18
|
+
end
|
19
|
+
|
20
|
+
def mock(method, args = {})
|
21
|
+
@remote_mocks ||= {}
|
22
|
+
@remote_mocks[method] ||= {}
|
23
|
+
args[:code] ||= 200
|
24
|
+
args[:body] ||= ""
|
25
|
+
args[:headers] ||= ""
|
26
|
+
args[:time] ||= 0
|
27
|
+
url = args.delete(:url)
|
28
|
+
url ||= :catch_all
|
29
|
+
params = args.delete(:params)
|
30
|
+
|
31
|
+
key = mock_key_for(url, params)
|
32
|
+
|
33
|
+
@remote_mocks[method][key] = args
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns a key for a given URL and passed in
|
37
|
+
# set of Typhoeus options to be used to store/retrieve
|
38
|
+
# a corresponding mock.
|
39
|
+
def mock_key_for(url, params = nil)
|
40
|
+
if url == :catch_all
|
41
|
+
url
|
42
|
+
else
|
43
|
+
key = url
|
44
|
+
if params and !params.empty?
|
45
|
+
key += flatten_and_sort_hash(params).to_s
|
46
|
+
end
|
47
|
+
key
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def flatten_and_sort_hash(params)
|
52
|
+
params = params.dup
|
53
|
+
|
54
|
+
# Flatten any sub-hashes to a single string.
|
55
|
+
params.keys.each do |key|
|
56
|
+
if params[key].is_a?(Hash)
|
57
|
+
params[key] = params[key].sort_by { |k, v| k.to_s.downcase }.to_s
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
params.sort_by { |k, v| k.to_s.downcase }
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_mock(method, url, options)
|
65
|
+
return nil unless @remote_mocks
|
66
|
+
if @remote_mocks.has_key? method
|
67
|
+
extra_response_args = { :requested_http_method => method,
|
68
|
+
:requested_url => url,
|
69
|
+
:start_time => Time.now }
|
70
|
+
mock_key = mock_key_for(url, options[:params])
|
71
|
+
if @remote_mocks[method].has_key? mock_key
|
72
|
+
get_mock_and_run_handlers(method,
|
73
|
+
@remote_mocks[method][mock_key].merge(
|
74
|
+
extra_response_args),
|
75
|
+
options)
|
76
|
+
elsif @remote_mocks[method].has_key? :catch_all
|
77
|
+
get_mock_and_run_handlers(method,
|
78
|
+
@remote_mocks[method][:catch_all].merge(
|
79
|
+
extra_response_args),
|
80
|
+
options)
|
81
|
+
else
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
else
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def enforce_allow_net_connect!(http_verb, url, params = nil)
|
90
|
+
if !allow_net_connect
|
91
|
+
message = "Real HTTP connections are disabled. Unregistered request: " <<
|
92
|
+
"#{http_verb.to_s.upcase} #{url}\n" <<
|
93
|
+
" Try: mock(:#{http_verb}, :url => \"#{url}\""
|
94
|
+
if params
|
95
|
+
message << ",\n :params => #{params.inspect}"
|
96
|
+
end
|
97
|
+
|
98
|
+
message << ")"
|
99
|
+
|
100
|
+
raise MockExpectedError, message
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def check_expected_headers!(response_args, options)
|
105
|
+
missing_headers = {}
|
106
|
+
|
107
|
+
response_args[:expected_headers].each do |key, value|
|
108
|
+
if options[:headers].nil?
|
109
|
+
missing_headers[key] = [value, nil]
|
110
|
+
elsif ((options[:headers][key] && value != :anything) &&
|
111
|
+
options[:headers][key] != value)
|
112
|
+
|
113
|
+
missing_headers[key] = [value, options[:headers][key]]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
unless missing_headers.empty?
|
118
|
+
raise headers_error_summary(response_args, options, missing_headers, 'expected')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def check_unexpected_headers!(response_args, options)
|
123
|
+
bad_headers = {}
|
124
|
+
response_args[:unexpected_headers].each do |key, value|
|
125
|
+
if (options[:headers][key] && value == :anything) ||
|
126
|
+
(options[:headers][key] == value)
|
127
|
+
bad_headers[key] = [value, options[:headers][key]]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
unless bad_headers.empty?
|
132
|
+
raise headers_error_summary(response_args, options, bad_headers, 'did not expect')
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def headers_error_summary(response_args, options, missing_headers, lead_in)
|
137
|
+
error = "#{lead_in} the following headers: #{response_args[:expected_headers].inspect}, but received: #{options[:headers].inspect}\n\n"
|
138
|
+
error << "Differences:\n"
|
139
|
+
error << "------------\n"
|
140
|
+
missing_headers.each do |key, values|
|
141
|
+
error << " - #{key}: #{lead_in} #{values[0].inspect}, got #{values[1].inspect}\n"
|
142
|
+
end
|
143
|
+
|
144
|
+
error
|
145
|
+
end
|
146
|
+
private :headers_error_summary
|
147
|
+
|
148
|
+
def get_mock_and_run_handlers(method, response_args, options)
|
149
|
+
response = Response.new(response_args)
|
150
|
+
|
151
|
+
if response_args.has_key? :expected_body
|
152
|
+
raise "#{method} expected body of \"#{response_args[:expected_body]}\" but received #{options[:body]}" if response_args[:expected_body] != options[:body]
|
153
|
+
end
|
154
|
+
|
155
|
+
if response_args.has_key? :expected_headers
|
156
|
+
check_expected_headers!(response_args, options)
|
157
|
+
end
|
158
|
+
|
159
|
+
if response_args.has_key? :unexpected_headers
|
160
|
+
check_unexpected_headers!(response_args, options)
|
161
|
+
end
|
162
|
+
|
163
|
+
if response.code >= 200 && response.code < 300 && options.has_key?(:on_success)
|
164
|
+
response = options[:on_success].call(response)
|
165
|
+
elsif options.has_key?(:on_failure)
|
166
|
+
response = options[:on_failure].call(response)
|
167
|
+
end
|
168
|
+
|
169
|
+
encode_nil_response(response)
|
170
|
+
end
|
171
|
+
|
172
|
+
[:get, :post, :put, :delete].each do |method|
|
173
|
+
line = __LINE__ + 2 # get any errors on the correct line num
|
174
|
+
code = <<-SRC
|
175
|
+
def #{method.to_s}(url, options = {})
|
176
|
+
mock_object = get_mock(:#{method.to_s}, url, options)
|
177
|
+
unless mock_object.nil?
|
178
|
+
decode_nil_response(mock_object)
|
179
|
+
else
|
180
|
+
enforce_allow_net_connect!(:#{method.to_s}, url, options[:params])
|
181
|
+
remote_proxy_object(url, :#{method.to_s}, options)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
SRC
|
185
|
+
module_eval(code, "./lib/typhoeus/remote.rb", line)
|
186
|
+
end
|
187
|
+
|
188
|
+
def remote_proxy_object(url, method, options)
|
189
|
+
easy = Typhoeus.get_easy_object
|
190
|
+
|
191
|
+
easy.url = url
|
192
|
+
easy.method = method
|
193
|
+
easy.headers = options[:headers] if options.has_key?(:headers)
|
194
|
+
easy.headers["User-Agent"] = (options[:user_agent] || Typhoeus::USER_AGENT)
|
195
|
+
easy.params = options[:params] if options[:params]
|
196
|
+
easy.request_body = options[:body] if options[:body]
|
197
|
+
easy.timeout = options[:timeout] if options[:timeout]
|
198
|
+
easy.set_headers
|
199
|
+
|
200
|
+
proxy = Typhoeus::RemoteProxyObject.new(clear_memoized_proxy_objects, easy, options)
|
201
|
+
set_memoized_proxy_object(method, url, options, proxy)
|
202
|
+
end
|
203
|
+
|
204
|
+
def remote_defaults(options)
|
205
|
+
@remote_defaults ||= {}
|
206
|
+
@remote_defaults.merge!(options) if options
|
207
|
+
@remote_defaults
|
208
|
+
end
|
209
|
+
|
210
|
+
# If we get subclassed, make sure that child inherits the remote defaults
|
211
|
+
# of the parent class.
|
212
|
+
def inherited(child)
|
213
|
+
child.__send__(:remote_defaults, @remote_defaults)
|
214
|
+
end
|
215
|
+
|
216
|
+
def call_remote_method(method_name, args)
|
217
|
+
m = @remote_methods[method_name]
|
218
|
+
|
219
|
+
base_uri = args.delete(:base_uri) || m.base_uri || ""
|
220
|
+
|
221
|
+
if args.has_key? :path
|
222
|
+
path = args.delete(:path)
|
223
|
+
else
|
224
|
+
path = m.interpolate_path_with_arguments(args)
|
225
|
+
end
|
226
|
+
path ||= ""
|
227
|
+
|
228
|
+
http_method = m.http_method
|
229
|
+
url = base_uri + path
|
230
|
+
options = m.merge_options(args)
|
231
|
+
|
232
|
+
# proxy_object = memoized_proxy_object(http_method, url, options)
|
233
|
+
# return proxy_object unless proxy_object.nil?
|
234
|
+
#
|
235
|
+
# if m.cache_responses?
|
236
|
+
# object = @cache.get(get_memcache_response_key(method_name, args))
|
237
|
+
# if object
|
238
|
+
# set_memoized_proxy_object(http_method, url, options, object)
|
239
|
+
# return object
|
240
|
+
# end
|
241
|
+
# end
|
242
|
+
|
243
|
+
proxy = memoized_proxy_object(http_method, url, options)
|
244
|
+
unless proxy
|
245
|
+
if m.cache_responses?
|
246
|
+
options[:cache] = @cache
|
247
|
+
options[:cache_key] = get_memcache_response_key(method_name, args)
|
248
|
+
options[:cache_timeout] = m.cache_ttl
|
249
|
+
end
|
250
|
+
proxy = send(http_method, url, options)
|
251
|
+
end
|
252
|
+
proxy
|
253
|
+
end
|
254
|
+
|
255
|
+
def set_memoized_proxy_object(http_method, url, options, object)
|
256
|
+
@memoized_proxy_objects ||= {}
|
257
|
+
@memoized_proxy_objects["#{http_method}_#{url}_#{options.to_s}"] = object
|
258
|
+
end
|
259
|
+
|
260
|
+
def memoized_proxy_object(http_method, url, options)
|
261
|
+
@memoized_proxy_objects ||= {}
|
262
|
+
@memoized_proxy_objects["#{http_method}_#{url}_#{options.to_s}"]
|
263
|
+
end
|
264
|
+
|
265
|
+
def clear_memoized_proxy_objects
|
266
|
+
lambda { @memoized_proxy_objects = {} }
|
267
|
+
end
|
268
|
+
|
269
|
+
def get_memcache_response_key(remote_method_name, args)
|
270
|
+
result = "#{remote_method_name.to_s}-#{args.to_s}"
|
271
|
+
(Digest::SHA2.new << result).to_s
|
272
|
+
end
|
273
|
+
|
274
|
+
def cache=(cache)
|
275
|
+
@cache = cache
|
276
|
+
end
|
277
|
+
|
278
|
+
def define_remote_method(name, args = {})
|
279
|
+
@remote_defaults ||= {}
|
280
|
+
args[:method] ||= @remote_defaults[:method]
|
281
|
+
args[:on_success] ||= @remote_defaults[:on_success]
|
282
|
+
args[:on_failure] ||= @remote_defaults[:on_failure]
|
283
|
+
args[:base_uri] ||= @remote_defaults[:base_uri]
|
284
|
+
args[:path] ||= @remote_defaults[:path]
|
285
|
+
m = RemoteMethod.new(args)
|
286
|
+
|
287
|
+
@remote_methods ||= {}
|
288
|
+
@remote_methods[name] = m
|
289
|
+
|
290
|
+
class_eval <<-SRC
|
291
|
+
def self.#{name.to_s}(args = {})
|
292
|
+
call_remote_method(:#{name.to_s}, args)
|
293
|
+
end
|
294
|
+
SRC
|
295
|
+
end
|
296
|
+
|
297
|
+
private
|
298
|
+
def encode_nil_response(response)
|
299
|
+
response == nil ? :__nil__ : response
|
300
|
+
end
|
301
|
+
|
302
|
+
def decode_nil_response(response)
|
303
|
+
response == :__nil__ ? nil : response
|
304
|
+
end
|
305
|
+
end # ClassMethods
|
306
|
+
end
|