hhry-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,24 @@
1
+ module Typhoeus
2
+ class Hydra
3
+ module Callbacks
4
+ def self.extended(base)
5
+ class << base
6
+ attr_accessor :global_hooks
7
+ end
8
+ base.global_hooks = Hash.new { |h, k| h[k] = [] }
9
+ end
10
+
11
+ def after_request_before_on_complete(&block)
12
+ global_hooks[:after_request_before_on_complete] << block
13
+ end
14
+
15
+ def run_global_hooks_for(name, request)
16
+ global_hooks[name].each { |hook| hook.call(request) }
17
+ end
18
+
19
+ def clear_global_hooks
20
+ global_hooks.clear
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,61 @@
1
+ module Typhoeus
2
+ class Hydra
3
+ class NetConnectNotAllowedError < StandardError; end
4
+
5
+ module ConnectOptions
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ # This method checks to see if we should raise an error on
11
+ # a request.
12
+ #
13
+ # @raises NetConnectNotAllowedError
14
+ def check_allow_net_connect!(request)
15
+ return if Typhoeus::Hydra.allow_net_connect?
16
+ return if Typhoeus::Hydra.ignore_hosts.include?(request.host_domain)
17
+
18
+ raise NetConnectNotAllowedError, "Real HTTP requests are not allowed. Unregistered request: #{request.inspect}"
19
+ end
20
+ private :check_allow_net_connect!
21
+
22
+ module ClassMethods
23
+ def self.extended(base)
24
+ class << base
25
+ attr_accessor :allow_net_connect
26
+ attr_accessor :ignore_localhost
27
+ end
28
+ base.allow_net_connect = true
29
+ base.ignore_localhost = false
30
+ end
31
+
32
+ # Returns whether we allow external HTTP connections.
33
+ # Useful for mocking/tests.
34
+ #
35
+ # @return [boolean] true/false
36
+ def allow_net_connect?
37
+ allow_net_connect
38
+ end
39
+
40
+ def ignore_localhost?
41
+ ignore_localhost
42
+ end
43
+
44
+ def ignore_hosts
45
+ @ignore_hosts ||= []
46
+
47
+ if ignore_localhost?
48
+ @ignore_hosts + Typhoeus::Request::LOCALHOST_ALIASES
49
+ else
50
+ @ignore_hosts
51
+ end
52
+ end
53
+
54
+ def ignore_hosts=(hosts)
55
+ @ignore_hosts = hosts
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
@@ -0,0 +1,68 @@
1
+ module Typhoeus
2
+ class Hydra
3
+ module Stubbing
4
+ module SharedMethods
5
+ def stub(method, url, options = {})
6
+ stubs << HydraMock.new(url, method, options)
7
+ stubs.last
8
+ end
9
+
10
+ def clear_stubs
11
+ self.stubs = []
12
+ end
13
+
14
+ def register_stub_finder(&block)
15
+ stub_finders << block
16
+ end
17
+
18
+ def find_stub_from_request(request)
19
+ stub_finders.each do |finder|
20
+ if response = finder.call(request)
21
+ mock = HydraMock.new(/.*/, :any)
22
+ mock.and_return(response)
23
+ return mock
24
+ end
25
+ end
26
+
27
+ stubs.detect { |stub| stub.matches?(request) }
28
+ end
29
+
30
+ def stub_finders
31
+ @stub_finders ||= []
32
+ end
33
+
34
+ def self.extended(base)
35
+ class << base
36
+ attr_accessor :stubs
37
+ end
38
+ base.stubs = []
39
+ end
40
+ end
41
+
42
+ def self.included(base)
43
+ base.extend(SharedMethods)
44
+ base.class_eval do
45
+ attr_accessor :stubs
46
+ end
47
+ end
48
+
49
+ def assign_to_stub(request)
50
+ m = find_stub_from_request(request)
51
+
52
+ # Fallback to global stubs.
53
+ m ||= self.class.find_stub_from_request(request)
54
+
55
+ if m
56
+ m.add_request(request)
57
+ @active_stubs << m
58
+ m
59
+ else
60
+ nil
61
+ end
62
+ end
63
+ private :assign_to_stub
64
+
65
+ include SharedMethods
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,131 @@
1
+ module Typhoeus
2
+ class HydraMock
3
+ attr_reader :url, :method, :requests, :uri
4
+
5
+ def initialize(url, method, options = {})
6
+ @url = url
7
+ @uri = URI.parse(url) if url.kind_of?(String)
8
+ @method = method
9
+ @requests = []
10
+ @options = options
11
+ if @options[:headers]
12
+ @options[:headers] = Typhoeus::Header.new(@options[:headers])
13
+ end
14
+
15
+ @current_response_index = 0
16
+ end
17
+
18
+ def body
19
+ @options[:body]
20
+ end
21
+
22
+ def body?
23
+ @options.has_key?(:body)
24
+ end
25
+
26
+ def headers
27
+ @options[:headers]
28
+ end
29
+
30
+ def headers?
31
+ @options.has_key?(:headers)
32
+ end
33
+
34
+ def add_request(request)
35
+ @requests << request
36
+ end
37
+
38
+ def and_return(val)
39
+ if val.respond_to?(:each)
40
+ @responses = val
41
+ else
42
+ @responses = [val]
43
+ end
44
+
45
+ # make sure to mark them as a mock.
46
+ @responses.each { |r| r.mock = true }
47
+
48
+ val
49
+ end
50
+
51
+ def response
52
+ if @current_response_index == (@responses.length - 1)
53
+ @responses.last
54
+ else
55
+ value = @responses[@current_response_index]
56
+ @current_response_index += 1
57
+ value
58
+ end
59
+ end
60
+
61
+ def matches?(request)
62
+ if !method_matches?(request) or !url_matches?(request)
63
+ return false
64
+ end
65
+
66
+ if body?
67
+ return false unless body_matches?(request)
68
+ end
69
+
70
+ if headers?
71
+ return false unless headers_match?(request)
72
+ end
73
+
74
+ true
75
+ end
76
+
77
+ private
78
+ def method_matches?(request)
79
+ self.method == :any or self.method == request.method
80
+ end
81
+
82
+ def url_matches?(request)
83
+ if url.kind_of?(String)
84
+ request_uri = URI.parse(request.url)
85
+ request_uri == self.uri
86
+ else
87
+ self.url =~ request.url
88
+ end
89
+ end
90
+
91
+ def body_matches?(request)
92
+ !request.body.nil? && !request.body.empty? && request.body == self.body
93
+ end
94
+
95
+ def headers_match?(request)
96
+ request_headers = Header.new(request.headers)
97
+
98
+ if empty_headers?(self.headers)
99
+ empty_headers?(request_headers)
100
+ else
101
+ return false if empty_headers?(request_headers)
102
+
103
+ headers.each do |key, value|
104
+ return false unless header_value_matches?(value, request_headers[key])
105
+ end
106
+
107
+ true
108
+ end
109
+ end
110
+
111
+ def header_value_matches?(mock_value, request_value)
112
+ mock_arr = mock_value.is_a?(Array) ? mock_value : [mock_value]
113
+ request_arr = request_value.is_a?(Array) ? request_value : [request_value]
114
+
115
+ return false unless mock_arr.size == request_arr.size
116
+ mock_arr.all? do |value|
117
+ request_arr.any? { |a| value === a }
118
+ end
119
+ end
120
+
121
+ def empty_headers?(headers)
122
+ # We consider the default User-Agent header to be empty since
123
+ # Typhoeus always adds that.
124
+ headers.nil? || headers.empty? || default_typhoeus_headers?(headers)
125
+ end
126
+
127
+ def default_typhoeus_headers?(headers)
128
+ headers.size == 1 && headers['User-Agent'] == Typhoeus::USER_AGENT
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,146 @@
1
+ module Typhoeus
2
+ class Multi
3
+ attr_reader :easy_handles
4
+
5
+ def initialize
6
+ Curl.init
7
+
8
+ @handle = Curl.multi_init
9
+ @active = 0
10
+ @running = 0
11
+ @easy_handles = []
12
+
13
+ @timeout = ::FFI::MemoryPointer.new(:long)
14
+ @timeval = Curl::Timeval.new
15
+ @fd_read = Curl::FDSet.new
16
+ @fd_write = Curl::FDSet.new
17
+ @fd_excep = Curl::FDSet.new
18
+ @max_fd = ::FFI::MemoryPointer.new(:int)
19
+
20
+ ObjectSpace.define_finalizer(self, self.class.finalizer(self))
21
+ end
22
+
23
+ def self.finalizer(multi)
24
+ proc { Curl.multi_cleanup(multi.handle) }
25
+ end
26
+
27
+ def add(easy)
28
+ raise "trying to add easy handle twice" if @easy_handles.include?(easy)
29
+ easy.set_headers() if easy.headers.empty?
30
+
31
+ code = Curl.multi_add_handle(@handle, easy.handle)
32
+ raise RuntimeError.new("An error occured adding the handle: #{code}: #{Curl.multi_strerror(code)}") if code != :call_multi_perform and code != :ok
33
+
34
+ do_perform if code == :call_multi_perform
35
+
36
+ @active += 1
37
+ @easy_handles << easy
38
+ easy
39
+ end
40
+
41
+ def remove(easy)
42
+ if @easy_handles.include?(easy)
43
+ @active -= 1
44
+ Curl.multi_remove_handle(@handle, easy.handle)
45
+ @easy_handles.delete(easy)
46
+ end
47
+ end
48
+
49
+ def perform
50
+ while @active > 0
51
+ run
52
+ while @running > 0
53
+ # get the curl-suggested timeout
54
+ code = Curl.multi_timeout(@handle, @timeout)
55
+ raise RuntimeError.new("an error occured getting the timeout: #{code}: #{Curl.multi_strerror(code)}") if code != :ok
56
+ timeout = @timeout.read_long
57
+ if timeout == 0 # no delay
58
+ run
59
+ next
60
+ elsif timeout < 0
61
+ timeout = 1
62
+ end
63
+
64
+ # load the fd sets from the multi handle
65
+ @fd_read.clear
66
+ @fd_write.clear
67
+ @fd_excep.clear
68
+ code = Curl.multi_fdset(@handle, @fd_read, @fd_write, @fd_excep, @max_fd)
69
+ raise RuntimeError.new("an error occured getting the fdset: #{code}: #{Curl.multi_strerror(code)}") if code != :ok
70
+
71
+ max_fd = @max_fd.read_int
72
+ if max_fd == -1
73
+ # curl is doing something special so let it run for a moment
74
+ sleep(0.001)
75
+ else
76
+ @timeval[:sec] = timeout / 1000
77
+ @timeval[:usec] = (timeout * 1000) % 1000000
78
+
79
+ code = Curl.select(max_fd + 1, @fd_read, @fd_write, @fd_excep, @timeval)
80
+ raise RuntimeError.new("error on thread select: #{::FFI.errno}") if code < 0
81
+ end
82
+
83
+ run
84
+ end
85
+ end
86
+ reset_easy_handles
87
+ end
88
+
89
+ def fire_and_forget
90
+ run
91
+ end
92
+
93
+ # check for finished easy handles and remove from the multi handle
94
+ def read_info
95
+ msgs_left = ::FFI::MemoryPointer.new(:int)
96
+ while not (msg = Curl.multi_info_read(@handle, msgs_left)).null?
97
+ next if msg[:code] != :done
98
+
99
+ easy = @easy_handles.find {|easy| easy.handle == msg[:easy_handle] }
100
+ next if not easy
101
+
102
+ response_code = ::FFI::MemoryPointer.new(:long)
103
+ response_code.write_long(-1)
104
+ Curl.easy_getinfo(easy.handle, :response_code, response_code)
105
+ response_code = response_code.read_long
106
+ remove(easy)
107
+
108
+ easy.curl_return_code = msg[:data][:code]
109
+ if easy.curl_return_code != 0 then easy.failure
110
+ elsif (200..299).member?(response_code) or response_code == 0 then easy.success
111
+ else easy.failure
112
+ end
113
+ end
114
+ end
115
+
116
+ def cleanup
117
+ Curl.multi_cleanup(@handle)
118
+ @active = 0
119
+ @running = 0
120
+ @easy_handles = []
121
+ end
122
+
123
+ def reset_easy_handles
124
+ @easy_handles.dup.each do |easy|
125
+ remove(easy)
126
+ yield easy if block_given?
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ # called by perform and fire_and_forget
133
+ def run
134
+ begin code = do_perform end while code == :call_multi_perform
135
+ raise RuntimeError.new("an error occured while running perform: #{code}: #{Curl.multi_strerror(code)}") if code != :ok
136
+ read_info
137
+ end
138
+
139
+ def do_perform
140
+ running = ::FFI::MemoryPointer.new(:int)
141
+ code = Curl.multi_perform(@handle, running)
142
+ @running = running.read_int
143
+ code
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,43 @@
1
+ require 'tempfile'
2
+
3
+ module Typhoeus
4
+ class ParamProcessor
5
+ class << self
6
+ def traverse_params_hash(hash, result = nil, current_key = nil)
7
+ result ||= { :files => [], :params => [] }
8
+
9
+ hash.keys.sort { |a, b| a.to_s <=> b.to_s }.collect do |key|
10
+ new_key = (current_key ? "#{current_key}[#{key}]" : key).to_s
11
+ current_value = hash[key]
12
+ process_value current_value, :result => result, :new_key => new_key
13
+ end
14
+ result
15
+ end
16
+
17
+ def process_value(current_value, options)
18
+ result = options[:result]
19
+ new_key = options[:new_key]
20
+
21
+ case current_value
22
+ when Hash
23
+ traverse_params_hash(current_value, result, new_key)
24
+ when Array
25
+ current_value.each do |v|
26
+ result[:params] << [new_key, v.to_s]
27
+ end
28
+ when File, Tempfile
29
+ filename = File.basename(current_value.path)
30
+ types = MIME::Types.type_for(filename)
31
+ result[:files] << [
32
+ new_key,
33
+ filename,
34
+ types.empty? ? 'application/octet-stream' : types[0].to_s,
35
+ File.expand_path(current_value.path)
36
+ ]
37
+ else
38
+ result[:params] << [new_key, current_value.to_s]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end