giant_client 0.1.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,56 @@
1
+ require 'giant_client/abstract_adapter'
2
+ require 'curb'
3
+
4
+ class GiantClient
5
+ class CurbAdapter < AbstractAdapter
6
+ CRLF = /\r\n/
7
+ HEADER_SPLIT = /:\s*/
8
+
9
+ def request(method, opts)
10
+
11
+ if BODYLESS_METHODS.include?(method)
12
+ raise GiantClient::Error::NotImplemented unless opts[:body] == ''
13
+ end
14
+
15
+ url = url_from_opts(opts)
16
+
17
+ if method == :post
18
+ post_body = opts[:body]
19
+ elsif method == :put
20
+ put_data = opts[:body]
21
+ end
22
+
23
+ begin
24
+ response = Curl.http( method.upcase, url, post_body, put_data ) do |curl|
25
+ curl.headers = opts[:headers]
26
+ curl.timeout = opts[:timeout]
27
+ curl.connect_timeout = opts[:timeout]
28
+ end
29
+ rescue Curl::Err::TimeoutError
30
+ raise GiantClient::Error::Timeout, "the request timed out (timeout: #{opts[:timeout]}"
31
+ end
32
+
33
+ normalize_response(response)
34
+ end
35
+
36
+ def normalize_response(response)
37
+ status_code = response.response_code
38
+ headers = parse_out_headers(response.header_str)
39
+ body = response.body_str
40
+ Response.new(status_code, headers, body)
41
+ end
42
+
43
+ def parse_out_headers(header_string)
44
+ headers = {}
45
+ pairs = header_string.split(CRLF)
46
+ pairs.shift
47
+ pairs.each do |pair|
48
+ header, value = *pair.split(HEADER_SPLIT, 2)
49
+ header = normalize_header(header)
50
+ headers[header] = value
51
+ end
52
+ headers
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,8 @@
1
+ class GiantClient
2
+ class Error < StandardError
3
+
4
+ class NotImplemented < Error; end
5
+ class Timeout < Error; end
6
+
7
+ end
8
+ end
@@ -0,0 +1,42 @@
1
+ require 'giant_client/abstract_adapter'
2
+ require 'excon'
3
+
4
+ class GiantClient
5
+ class ExconAdapter < AbstractAdapter
6
+
7
+ def request(method, opts)
8
+ if BODYLESS_METHODS.include?(method)
9
+ raise GiantClient::Error::NotImplemented unless opts[:body] == ''
10
+ end
11
+
12
+ url = url_from_opts(opts)
13
+
14
+ params = {}
15
+ params[:method] = method.to_s
16
+ params[:headers] = opts[:headers]
17
+ params[:body] = opts[:body]
18
+ params[:read_timeout] = opts[:timeout]
19
+ params[:connect_timeout] = opts[:timeout]
20
+ params[:write_timeout] = opts[:timeout]
21
+
22
+ begin
23
+ response = Excon.new(url).request(params)
24
+ rescue Excon::Errors::Timeout
25
+ raise GiantClient::Error::Timeout, "the request timed out (timeout: #{opts[:timeout]}"
26
+ end
27
+
28
+ normalize_response(response)
29
+ end
30
+
31
+ def normalize_response(response)
32
+ status_code = response.status
33
+ headers = normalize_header_hash(response.headers)
34
+ body = response.body
35
+ Response.new(status_code, headers, body)
36
+ end
37
+ end
38
+ end
39
+
40
+
41
+
42
+
@@ -0,0 +1,57 @@
1
+ require 'giant_client/abstract_adapter'
2
+ require 'giant_client/mock_request'
3
+
4
+ class GiantClient
5
+ class MockAdapter < AbstractAdapter
6
+
7
+ attr_accessor :requests, :responses
8
+ def initialize
9
+ @requests = []
10
+ @responses = []
11
+ end
12
+
13
+ def request(method, opts)
14
+ last_request = MockRequest.new
15
+
16
+ if BODYLESS_METHODS.include?(method)
17
+ unless opts[:body] == ''
18
+ last_request.raised_error = true
19
+ last_request.error_type = GiantClient::Error::NotImplemented
20
+ end
21
+ end
22
+
23
+ last_request.host = opts[:host]
24
+ last_request.ssl = opts[:ssl]
25
+ last_request.port = opts[:port]
26
+ last_request.path = opts[:path]
27
+ last_request.query = opts[:query]
28
+ last_request.headers = opts[:headers]
29
+ last_request.body = opts[:body]
30
+ last_request.timeout = opts[:timeout]
31
+
32
+ last_request.url = url_from_opts(opts)
33
+
34
+ last_request.querystring = stringify_query(opts[:query])
35
+
36
+ last_response = { :status_code => 200, :headers => {}, :body => nil }
37
+
38
+ @requests.unshift(last_request)
39
+ @responses.unshift(last_response)
40
+
41
+ self
42
+ end
43
+
44
+ def respond_with(hash)
45
+ @responses[0] = @responses[0].merge(hash)
46
+ end
47
+
48
+ def last_request
49
+ @requests[0]
50
+ end
51
+
52
+ def last_response
53
+ @responses[0]
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,11 @@
1
+ class GiantClient
2
+ class MockRequest
3
+ attr_accessor :host, :ssl, :port, :path, :query, :headers, :body, :timeout,
4
+ :url, :querystring, :raised_error, :error_type
5
+
6
+ def raised_error?
7
+ @raised_error
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,55 @@
1
+ require 'giant_client/abstract_adapter'
2
+ require 'net/http'
3
+
4
+ class GiantClient
5
+ class NetHttpAdapter < AbstractAdapter
6
+
7
+ def request(method, opts)
8
+ if BODYLESS_METHODS.include?(method)
9
+ raise GiantClient::Error::NotImplemented unless opts[:body] == ''
10
+ end
11
+ query = encode_query(opts[:query])
12
+
13
+ http = Net::HTTP.new( opts[:host], opts[:port] )
14
+ http.use_ssl = opts[:ssl]
15
+ http.read_timeout = opts[:timeout]
16
+ http.open_timeout = opts[:timeout]
17
+
18
+ request_class =
19
+ case method
20
+ when :get then Net::HTTP::Get
21
+ when :post then Net::HTTP::Post
22
+ when :put then Net::HTTP::Put
23
+ when :delete then Net::HTTP::Delete
24
+ when :head then Net::HTTP::Head
25
+ end
26
+
27
+ request = request_class.new( opts[:path] + query, opts[:headers] )
28
+
29
+ if request.request_body_permitted?
30
+ request.body = opts[:body]
31
+ end
32
+
33
+ begin
34
+ response = http.start {|http| http.request(request)}
35
+ rescue Timeout::Error
36
+ raise GiantClient::Error::Timeout, "the request timed out (timeout: #{opts[:timeout]}"
37
+ end
38
+
39
+ # make response object
40
+ normalize_response(response)
41
+ end
42
+
43
+ def normalize_response(response)
44
+ status_code = response.code.to_i
45
+ headers = {}
46
+ response.each_header do |header, value|
47
+ header = normalize_header(header)
48
+ headers[header] = value
49
+ end
50
+ body = response.body
51
+ Response.new(status_code, headers, body)
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,38 @@
1
+ require 'giant_client/abstract_adapter'
2
+ require 'patron'
3
+
4
+ class GiantClient
5
+ class PatronAdapter < AbstractAdapter
6
+
7
+ def request(method, opts)
8
+ if BODYLESS_METHODS.include?(method)
9
+ raise GiantClient::Error::NotImplemented unless opts[:body] == ''
10
+ end
11
+
12
+ url = url_from_opts(opts)
13
+
14
+ http = Patron::Session.new
15
+ http.timeout = opts[:timeout]
16
+ http.connect_timeout = opts[:timeout]
17
+
18
+ # TODO support all extra options
19
+ extra_opts = {}
20
+ extra_opts[:data] = opts[:body]
21
+ begin
22
+ response = http.request(method, url, opts[:headers], extra_opts)
23
+ rescue Patron::TimeoutError
24
+ raise GiantClient::Error::Timeout, "the request timed out (timeout: #{opts[:timeout]}"
25
+ end
26
+
27
+ normalize_response(response)
28
+ end
29
+
30
+ def normalize_response(response)
31
+ status_code = response.status
32
+ headers = normalize_header_hash(response.headers)
33
+ body = response.body
34
+ Response.new(status_code, headers, body)
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ class GiantClient
2
+ class Response
3
+ attr_accessor :body, :headers, :status_code
4
+
5
+ def initialize(status_code, headers, body)
6
+ @status_code = status_code
7
+ @headers = headers
8
+ @body = body
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,40 @@
1
+ require 'giant_client/abstract_adapter'
2
+ require 'typhoeus'
3
+
4
+ class GiantClient
5
+ class TyphoeusAdapter < AbstractAdapter
6
+
7
+ def request(method, opts)
8
+
9
+ if BODYLESS_METHODS.include?(method)
10
+ raise GiantClient::Error::NotImplemented unless opts[:body] == ''
11
+ end
12
+
13
+ url = url_from_opts(opts)
14
+
15
+ params = {}
16
+ params[:method] = method
17
+ params[:headers] = opts[:headers]
18
+ params[:body] = opts[:body]
19
+ params[:timeout] = 1000 * opts[:timeout] # typhoeus does milliseconds
20
+ params[:connect_timeout] = 1000 * opts[:timeout]
21
+
22
+ response = Typhoeus::Request.run(url, params)
23
+
24
+ if response.curl_return_code == 28 # timeout
25
+ raise GiantClient::Error::Timeout, "the request timed out (timeout: #{opts[:timeout]}"
26
+ end
27
+
28
+ normalize_response(response)
29
+
30
+ end
31
+
32
+ def normalize_response(response)
33
+ status_code = response.code
34
+ headers = normalize_header_hash(response.headers_hash)
35
+ body = response.body
36
+ Response.new(status_code, headers, body)
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ class GiantClient
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,299 @@
1
+ require File.expand_path( '../spec_helper', __FILE__ )
2
+
3
+ describe 'GiantClient' do
4
+
5
+ shared_examples "an adapter" do
6
+
7
+ context 'GET requests' do
8
+ let(:client) { GiantClient.new( :host => 'example.com', :adapter => adapter ) }
9
+
10
+ it 'should perform a simple GET request' do
11
+ stub = stub_request(:get, 'example.com')
12
+ client.get( :path => '/' )
13
+ stub.should have_been_requested
14
+ end
15
+
16
+ it 'should treat a string argument as a path' do
17
+ stub = stub_request(:get, 'example.com')
18
+ client.get( '/' )
19
+ stub.should have_been_requested
20
+ end
21
+
22
+ it 'should respond with 200' do
23
+ stub = stub_request(:get, 'example.com')
24
+ .to_return(:status => [200, 'OK'])
25
+ client.get( :path => '/' ).status_code.should == 200
26
+ end
27
+
28
+ it 'should respond with a success header' do
29
+ stub = stub_request(:get, 'example.com')
30
+ .to_return(:headers => {'Success' => 'true'})
31
+ client.get( :path => '/' ).headers.should == {'Success' => 'true'}
32
+ end
33
+
34
+ it 'should respond with a multiple headers' do
35
+ stub = stub_request(:get, 'example.com')
36
+ .to_return(:headers =>
37
+ {'Success' => 'true', 'doodle' => 'kaboodle'})
38
+ client.get( :path => '/' ).headers.should ==
39
+ {'Success' => 'true', 'Doodle' => 'kaboodle'}
40
+ end
41
+
42
+ it 'should respond with a header and status, and should capitalize status' do
43
+ stub = stub_request(:get, 'example.com')
44
+ .to_return(:status => 1700, :headers => {'success' => 'true'})
45
+ client.get( :path => '/' ).headers.should == {'Success' => 'true'}
46
+ client.get( :path => '/' ).status_code.should == 1700
47
+ end
48
+
49
+ it 'should respond with a body' do
50
+ stub = stub_request(:get, 'example.com')
51
+ .to_return(:body => 'Chucky Cheese')
52
+ client.get( :path => '/' ).body.should == 'Chucky Cheese'
53
+ end
54
+
55
+ it 'should pass on the port' do
56
+ stub = stub_request(:get, 'example.com:8080')
57
+ client.get( :path => '/', :port => '8080' )
58
+ stub.should have_been_requested
59
+ end
60
+
61
+ it 'should pass on the request headers' do
62
+ stub = stub_request(:get, 'example.com').with( :headers => { 'Content-Type' => 'text/special' } )
63
+ client.get( :path => '/', :headers => { 'Content-Type' => 'text/special' } )
64
+ stub.should have_been_requested
65
+ end
66
+
67
+ it 'should pass on the request params' do
68
+ stub = stub_request(:get, 'example.com').with( :query => { 'a' => 'b' } )
69
+ client.get( :path => '/', :query => { 'a' => 'b' } )
70
+ stub.should have_been_requested
71
+ end
72
+
73
+ it 'should allow a query in the url path' do
74
+ stub = stub_request(:get, 'example.com').with( :query => { 'z' => 'me' } )
75
+ client.get( :path => '/?z=me' )
76
+ stub.should have_been_requested
77
+ end
78
+
79
+ it 'should raise an error if there is a request body' do
80
+ stub = stub_request(:get, 'example.com').with( :body => 'hey yo' )
81
+ expect{ client.get( :path => '/', :body => 'hey yo' ) }.to raise_error( GiantClient::Error::NotImplemented )
82
+ end
83
+
84
+ context 'timing out' do
85
+ let(:client) { GiantClient.new( :host => 'example.com', :adapter => adapter, :timeout => 1 ) }
86
+
87
+ it 'should raise a timeout error if there is a timeout' do
88
+ stub = stub_request(:get, 'example.com').to_timeout
89
+ expect{ client.get( :path => '/' ) }.to raise_error( GiantClient::Error::Timeout )
90
+ end
91
+ end
92
+
93
+ context 'ssl' do
94
+ let(:client) { GiantClient.new( :host => 'example.com', :ssl => true, :adapter => adapter ) }
95
+
96
+ it 'should perform a simple get request' do
97
+ stub = stub_request(:get, 'https://example.com')
98
+ client.get( :path => '/' )
99
+ stub.should have_been_requested
100
+ end
101
+ end
102
+
103
+ context 'custom port' do
104
+ let(:client) { GiantClient.new( :host => 'example.com', :ssl => true, :port => 8080, :adapter => adapter ) }
105
+
106
+ it 'should perform a simple GET request' do
107
+ stub = stub_request(:get, 'https://example.com:8080')
108
+ client.get( :path => '/' )
109
+ stub.should have_been_requested
110
+ end
111
+ end
112
+ end
113
+
114
+ context 'POST requests' do
115
+ let(:client) { GiantClient.new( :host => 'example.com', :adapter => adapter ) }
116
+
117
+ it 'should perform a simple POST request' do
118
+ stub = stub_request(:post, 'example.com')
119
+ client.post( :path => '/' )
120
+ stub.should have_been_requested
121
+ end
122
+
123
+ it 'should post the body as well' do
124
+ stub = stub_request(:post, 'example.com').with( :body => 'woohoo' )
125
+ client.post( :path => '/', :body => 'woohoo' )
126
+ stub.should have_been_requested
127
+ end
128
+ end
129
+
130
+ context 'PUT requests' do
131
+ let(:client) { GiantClient.new( :host => 'example.com', :adapter => adapter ) }
132
+
133
+ it 'should perform a simple PUT request' do
134
+ stub = stub_request(:put, 'example.com')
135
+ client.put( :path => '/' )
136
+ stub.should have_been_requested
137
+ end
138
+
139
+ it 'should perform a PUT request to a specific path' do
140
+ stub = stub_request(:put, 'example.com/heyyy')
141
+ client.put( :path => '/heyyy' )
142
+ stub.should have_been_requested
143
+ end
144
+
145
+ it 'should put the body as well' do
146
+ stub = stub_request(:put, 'example.com').with( :body => 'woohoo' )
147
+ client.put( :path => '/', :body => 'woohoo' )
148
+ stub.should have_been_requested
149
+ end
150
+ end
151
+
152
+ context 'DELETE requests' do
153
+ let(:client) { GiantClient.new( :host => 'example.com', :adapter => adapter ) }
154
+
155
+ it 'should perform a simple DELETE request' do
156
+ stub = stub_request(:delete, 'example.com')
157
+ client.delete( :path => '/' )
158
+ stub.should have_been_requested
159
+ end
160
+
161
+ it 'should perform a DELETE request to a specific path' do
162
+ stub = stub_request(:delete, 'example.com/heyyy/how/ya/doin')
163
+ client.delete( :path => '/heyyy/how/ya/doin' )
164
+ stub.should have_been_requested
165
+ end
166
+
167
+ it 'should raise an error if there is a request body' do
168
+ stub = stub_request(:delete, 'example.com').with( :body => 'woohoo' )
169
+ expect{ client.delete( :path => '/', :body => 'woohoo' ) }.to raise_error( GiantClient::Error::NotImplemented )
170
+ end
171
+ end
172
+
173
+ context 'HEAD requests' do
174
+ let(:client) { GiantClient.new( :host => 'example.com', :adapter => adapter ) }
175
+
176
+ it 'should perform a simple HEAD request' do
177
+ stub = stub_request(:head, 'example.com')
178
+ client.head( :path => '/' )
179
+ stub.should have_been_requested
180
+ end
181
+
182
+ it 'should perform a HEAD request to a specific path' do
183
+ stub = stub_request(:head, 'example.com/heyyy/how/ya/doin')
184
+ client.head( :path => '/heyyy/how/ya/doin' )
185
+ stub.should have_been_requested
186
+ end
187
+
188
+ it 'should raise an error if there is a request body' do
189
+ stub = stub_request(:head, 'example.com').with( :body => 'woohoo' )
190
+ expect{ client.head( :path => '/', :body => 'woohoo' ) }.to raise_error( GiantClient::Error::NotImplemented )
191
+ end
192
+ end
193
+ end
194
+
195
+ describe 'NetHttpAdapter' do
196
+ it_behaves_like 'an adapter' do
197
+ let(:adapter){ :net_http }
198
+ end
199
+ end
200
+
201
+ describe 'PatronAdapter' do
202
+ it_behaves_like 'an adapter' do
203
+ let(:adapter){ :patron }
204
+ end
205
+ end
206
+
207
+ describe 'Curb' do
208
+ it_behaves_like 'an adapter' do
209
+ let(:adapter){ :curb }
210
+ end
211
+ end
212
+
213
+ describe 'Excon' do
214
+ it_behaves_like 'an adapter' do
215
+ let(:adapter){ :excon }
216
+ end
217
+ end
218
+
219
+ describe 'Typhoeus' do
220
+ it_behaves_like 'an adapter' do
221
+ let(:adapter){ :typhoeus }
222
+ end
223
+ end
224
+
225
+
226
+ describe 'AbstractAdapter' do
227
+
228
+ let(:adapter){ GiantClient::AbstractAdapter.new }
229
+
230
+ describe '#normalize_header' do
231
+
232
+ it 'should not touch a header that is already title case' do
233
+ adapter.normalize_header('Content-Type').should == 'Content-Type'
234
+ end
235
+
236
+ it 'should convert a header to title case' do
237
+ adapter.normalize_header('content-type').should == 'Content-Type'
238
+ end
239
+
240
+ it 'should only capitalize the first letter of words' do
241
+ adapter.normalize_header('CONTENT-type').should == 'Content-Type'
242
+ end
243
+
244
+ it 'should work for one word' do
245
+ adapter.normalize_header('acCEPTS').should == 'Accepts'
246
+ end
247
+
248
+ it 'should downcase appropriately' do
249
+ adapter.normalize_header('CONTENT-LENGTH').should == 'Content-Length'
250
+ end
251
+
252
+ it 'should handle a another, similar situation' do
253
+ adapter.normalize_header('x-Forwarded-for').should == 'X-Forwarded-For'
254
+ end
255
+ end
256
+
257
+ describe '#encode_query' do
258
+
259
+ it 'should return empty string from empty hash' do
260
+ adapter.encode_query({}).should == ''
261
+ end
262
+
263
+ it 'should prefix string argument with ?' do
264
+ adapter.encode_query('parsnips').should == '?parsnips'
265
+ end
266
+
267
+ it 'should correctly convert hash to query string with ?' do
268
+ adapter.encode_query({ :tra => "lala", :ha => "za"} )
269
+ .should == '?tra=lala&ha=za'
270
+ end
271
+ end
272
+
273
+ describe '#url_from_opts' do
274
+ let(:opts){{
275
+ :path => '/', :host => 'example.com',
276
+ :ssl => false, :port => 80,
277
+ :query => {}, :body => '',
278
+ :headers => {}
279
+ }}
280
+
281
+ it 'should not have the correct url' do
282
+ adapter.url_from_opts(opts).should == "http://example.com/"
283
+ end
284
+
285
+ it 'should use ssl if we tell it to' do
286
+ opts[:ssl] = true
287
+ adapter.url_from_opts(opts)[0...8].should == "https://"
288
+ end
289
+
290
+ it 'should work with a custom port' do
291
+ opts[:ssl] = true
292
+ opts[:port] = 8080
293
+ adapter.url_from_opts(opts)[19...24].should == ":8080"
294
+ end
295
+
296
+ end
297
+ end
298
+
299
+ end