dcu-typhoeus 0.4.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.
@@ -0,0 +1,310 @@
1
+ module Typhoeus
2
+ USER_AGENT = "Typhoeus - http://github.com/dbalatero/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.follow_location = options[:follow_location] if options[:follow_location]
199
+ easy.max_redirects = options[:max_redirects] if options[:max_redirects]
200
+ easy.set_headers
201
+
202
+ proxy = Typhoeus::RemoteProxyObject.new(clear_memoized_proxy_objects, easy, options)
203
+ set_memoized_proxy_object(method, url, options, proxy)
204
+ end
205
+
206
+ def remote_defaults(options)
207
+ @remote_defaults ||= {}
208
+ @remote_defaults.merge!(options) if options
209
+ @remote_defaults
210
+ end
211
+
212
+ # If we get subclassed, make sure that child inherits the remote defaults
213
+ # of the parent class.
214
+ def inherited(child)
215
+ child.__send__(:remote_defaults, @remote_defaults)
216
+ end
217
+
218
+ def call_remote_method(method_name, args)
219
+ m = @remote_methods[method_name]
220
+
221
+ base_uri = args.delete(:base_uri) || m.base_uri || ""
222
+
223
+ if args.has_key? :path
224
+ path = args.delete(:path)
225
+ else
226
+ path = m.interpolate_path_with_arguments(args)
227
+ end
228
+ path ||= ""
229
+
230
+ http_method = m.http_method
231
+ url = base_uri + path
232
+ options = m.merge_options(args)
233
+
234
+ # proxy_object = memoized_proxy_object(http_method, url, options)
235
+ # return proxy_object unless proxy_object.nil?
236
+ #
237
+ # if m.cache_responses?
238
+ # object = @cache.get(get_memcache_response_key(method_name, args))
239
+ # if object
240
+ # set_memoized_proxy_object(http_method, url, options, object)
241
+ # return object
242
+ # end
243
+ # end
244
+
245
+ proxy = memoized_proxy_object(http_method, url, options)
246
+ unless proxy
247
+ if m.cache_responses?
248
+ options[:cache] = @cache
249
+ options[:cache_key] = get_memcache_response_key(method_name, args)
250
+ options[:cache_timeout] = m.cache_ttl
251
+ end
252
+ proxy = send(http_method, url, options)
253
+ end
254
+ proxy
255
+ end
256
+
257
+ def set_memoized_proxy_object(http_method, url, options, object)
258
+ @memoized_proxy_objects ||= {}
259
+ @memoized_proxy_objects["#{http_method}_#{url}_#{options.to_s}"] = object
260
+ end
261
+
262
+ def memoized_proxy_object(http_method, url, options)
263
+ @memoized_proxy_objects ||= {}
264
+ @memoized_proxy_objects["#{http_method}_#{url}_#{options.to_s}"]
265
+ end
266
+
267
+ def clear_memoized_proxy_objects
268
+ lambda { @memoized_proxy_objects = {} }
269
+ end
270
+
271
+ def get_memcache_response_key(remote_method_name, args)
272
+ result = "#{remote_method_name.to_s}-#{args.to_s}"
273
+ (Digest::SHA2.new << result).to_s
274
+ end
275
+
276
+ def cache=(cache)
277
+ @cache = cache
278
+ end
279
+
280
+ def define_remote_method(name, args = {})
281
+ @remote_defaults ||= {}
282
+ args[:method] ||= @remote_defaults[:method]
283
+ args[:on_success] ||= @remote_defaults[:on_success]
284
+ args[:on_failure] ||= @remote_defaults[:on_failure]
285
+ args[:base_uri] ||= @remote_defaults[:base_uri]
286
+ args[:path] ||= @remote_defaults[:path]
287
+ args[:follow_location] ||= @remote_defaults[:follow_location]
288
+ args[:follow_location] ||= @remote_defaults[:max_redirects]
289
+ m = RemoteMethod.new(args)
290
+
291
+ @remote_methods ||= {}
292
+ @remote_methods[name] = m
293
+
294
+ class_eval <<-SRC
295
+ def self.#{name.to_s}(args = {})
296
+ call_remote_method(:#{name.to_s}, args)
297
+ end
298
+ SRC
299
+ end
300
+
301
+ private
302
+ def encode_nil_response(response)
303
+ response == nil ? :__nil__ : response
304
+ end
305
+
306
+ def decode_nil_response(response)
307
+ response == :__nil__ ? nil : response
308
+ end
309
+ end # ClassMethods
310
+ end
@@ -0,0 +1,108 @@
1
+ module Typhoeus
2
+ class RemoteMethod
3
+ attr_accessor :http_method, :options, :base_uri, :path, :on_success, :on_failure, :cache_ttl
4
+
5
+ def initialize(options = {})
6
+ @http_method = options.delete(:method) || :get
7
+ @options = options.dup
8
+ @base_uri = options.delete(:base_uri)
9
+ @path = options.delete(:path)
10
+ @on_success = options[:on_success]
11
+ @on_failure = options[:on_failure]
12
+ @cache_responses = options.delete(:cache_responses)
13
+ @memoize_responses = options.delete(:memoize_responses) || @cache_responses
14
+ @cache_ttl = @cache_responses == true ? 0 : @cache_responses
15
+ @keys = nil
16
+
17
+ clear_cache
18
+ end
19
+
20
+ def cache_responses?
21
+ @cache_responses
22
+ end
23
+
24
+ def memoize_responses?
25
+ @memoize_responses
26
+ end
27
+
28
+ def args_options_key(args, options)
29
+ "#{args.to_s}+#{options.to_s}"
30
+ end
31
+
32
+ def calling(args, options)
33
+ @called_methods[args_options_key(args, options)] = true
34
+ end
35
+
36
+ def already_called?(args, options)
37
+ @called_methods.has_key? args_options_key(args, options)
38
+ end
39
+
40
+ def add_response_block(block, args, options)
41
+ @response_blocks[args_options_key(args, options)] << block
42
+ end
43
+
44
+ def call_response_blocks(result, args, options)
45
+ key = args_options_key(args, options)
46
+ @response_blocks[key].each {|block| block.call(result)}
47
+ @response_blocks.delete(key)
48
+ @called_methods.delete(key)
49
+ end
50
+
51
+ def clear_cache
52
+ @response_blocks = Hash.new {|h, k| h[k] = []}
53
+ @called_methods = {}
54
+ end
55
+
56
+ def merge_options(new_options)
57
+ merged = options.merge(new_options)
58
+ if options.has_key?(:params) && new_options.has_key?(:params)
59
+ merged[:params] = options[:params].merge(new_options[:params])
60
+ end
61
+ argument_names.each {|a| merged.delete(a)}
62
+ merged.delete(:on_success) if merged[:on_success].nil?
63
+ merged.delete(:on_failure) if merged[:on_failure].nil?
64
+ merged
65
+ end
66
+
67
+ def interpolate_path_with_arguments(args)
68
+ interpolated_path = @path
69
+ argument_names.each do |arg|
70
+ interpolated_path = interpolated_path.gsub(":#{arg}", args[arg].to_s)
71
+ end
72
+ interpolated_path
73
+ end
74
+
75
+ def argument_names
76
+ return @keys if @keys
77
+ pattern, keys = compile(@path)
78
+ @keys = keys.collect {|k| k.to_sym}
79
+ end
80
+
81
+ # rippped from Sinatra. clean up stuff we don't need later
82
+ def compile(path)
83
+ path ||= ""
84
+ keys = []
85
+ if path.respond_to? :to_str
86
+ special_chars = %w{. + ( )}
87
+ pattern =
88
+ path.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match|
89
+ case match
90
+ when "*"
91
+ keys << 'splat'
92
+ "(.*?)"
93
+ when *special_chars
94
+ Regexp.escape(match)
95
+ else
96
+ keys << $2[1..-1]
97
+ "([^/?&#]+)"
98
+ end
99
+ end
100
+ [/^#{pattern}$/, keys]
101
+ elsif path.respond_to? :match
102
+ [path, keys]
103
+ else
104
+ raise TypeError, path
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,50 @@
1
+ module Typhoeus
2
+ class RemoteProxyObject
3
+ instance_methods.each { |m| undef_method m unless m =~ /^__|object_id/ }
4
+
5
+ def initialize(clear_memoized_store_proc, easy, options = {})
6
+ @clear_memoized_store_proc = clear_memoized_store_proc
7
+ @easy = easy
8
+ @success = options[:on_success]
9
+ @failure = options[:on_failure]
10
+ @cache = options.delete(:cache)
11
+ @cache_key = options.delete(:cache_key)
12
+ @timeout = options.delete(:cache_timeout)
13
+ Typhoeus.add_easy_request(@easy)
14
+ end
15
+
16
+ def method_missing(sym, *args, &block)
17
+ unless @proxied_object
18
+ if @cache && @cache_key
19
+ @proxied_object = @cache.get(@cache_key) rescue nil
20
+ end
21
+
22
+ unless @proxied_object
23
+ Typhoeus.perform_easy_requests
24
+ response = Response.new(:code => @easy.response_code,
25
+ :curl_return_code => @easy.curl_return_code,
26
+ :curl_error_message => @easy.curl_error_message,
27
+ :headers => @easy.response_header,
28
+ :body => @easy.response_body,
29
+ :time => @easy.total_time_taken,
30
+ :requested_url => @easy.url,
31
+ :requested_http_method => @easy.method,
32
+ :start_time => @easy.start_time)
33
+ if @easy.response_code >= 200 && @easy.response_code < 300
34
+ Typhoeus.release_easy_object(@easy)
35
+ @proxied_object = @success.nil? ? response : @success.call(response)
36
+
37
+ if @cache && @cache_key
38
+ @cache.set(@cache_key, @proxied_object, @timeout)
39
+ end
40
+ else
41
+ @proxied_object = @failure.nil? ? response : @failure.call(response)
42
+ end
43
+ @clear_memoized_store_proc.call
44
+ end
45
+ end
46
+
47
+ @proxied_object.__send__(sym, *args, &block)
48
+ end
49
+ end
50
+ end