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.
- data/LICENSE +22 -0
- data/README.md +264 -0
- data/lib/giant_client.rb +105 -0
- data/lib/giant_client/abstract_adapter.rb +71 -0
- data/lib/giant_client/curb_adapter.rb +56 -0
- data/lib/giant_client/error.rb +8 -0
- data/lib/giant_client/excon_adapter.rb +42 -0
- data/lib/giant_client/mock_adapter.rb +57 -0
- data/lib/giant_client/mock_request.rb +11 -0
- data/lib/giant_client/net_http_adapter.rb +55 -0
- data/lib/giant_client/patron_adapter.rb +38 -0
- data/lib/giant_client/response.rb +12 -0
- data/lib/giant_client/typhoeus_adapter.rb +40 -0
- data/lib/giant_client/version.rb +3 -0
- data/spec/examples/giant_client_spec.rb +299 -0
- data/spec/examples/mock_adapter_spec.rb +276 -0
- data/spec/examples/spec_helper.rb +7 -0
- metadata +178 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Zack Reneau-Wedeen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
##Giant Client
|
2
|
+
|
3
|
+
Giant Client is a ruby library which exposes a simple and uniform API for
|
4
|
+
using a variety of http clients. Advantages of Giant Client:
|
5
|
+
* Clean, easy-to-use, cruftless API
|
6
|
+
* Testing as simple as it gets with a built in test framework that uses the exact same API
|
7
|
+
* Take advantage of 5+ http clients while only learning one API
|
8
|
+
* Seamlessly swap between http clients with one line of configuration (rather than erasing all of your code)
|
9
|
+
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
*Note: not yet available*
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
gem 'giant_client'
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install giant_client
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
*In this guide, I am very exhaustive with options, so it seems long winded, but this is just to show all configuration options, most are optional.*
|
29
|
+
|
30
|
+
####Initialize a new Giant Client
|
31
|
+
options = {
|
32
|
+
:host => "www.example.com", # optional, defaults to ""
|
33
|
+
:ssl => true, # optional, defaults to false
|
34
|
+
:port => 443, # optional, defaults to 443 when ssl == true,
|
35
|
+
# and to 80 when ssl == false
|
36
|
+
:adapter => :typhoeus # optional, defaults to :net_http, options detailed below
|
37
|
+
}
|
38
|
+
|
39
|
+
client = GiantClient.new(options)
|
40
|
+
|
41
|
+
######Passing a symbol argument as the first argument to initialize is assumed to be the adapter
|
42
|
+
######short hand
|
43
|
+
client = GiantClient.new :patron, :host => 'example.com'
|
44
|
+
|
45
|
+
####Adapters
|
46
|
+
*There are currently five adapters supported. They use the libraries they are named after.*
|
47
|
+
:net_http
|
48
|
+
:patron
|
49
|
+
:excon
|
50
|
+
:curb
|
51
|
+
:typhoeus
|
52
|
+
|
53
|
+
####The API
|
54
|
+
Giant Client supports the HTTP 'GET', 'POST', 'PUT', 'DELETE', and 'HEAD' methods
|
55
|
+
*The 'GET', 'DELETE', and 'HEAD' methods do not support a request body. Attempting to pass one will cause Giant Client to throw a NotImplementedError*
|
56
|
+
#####All methods support ssl, host, port, path, query, headers, and body as options.
|
57
|
+
|
58
|
+
#####Note:
|
59
|
+
*ssl defaults to ssl provided in new (true here)*
|
60
|
+
*host defaults to host provided in new ('www.example.com' here)*
|
61
|
+
*port defaults to port provided in new ('443' here)*
|
62
|
+
|
63
|
+
|
64
|
+
######GET
|
65
|
+
client.get({
|
66
|
+
:path => '/path/to/resource', # optional, defaults to '/'
|
67
|
+
:headers => { 'Accepts' => 'application/json' }, # optional, defaults to {}
|
68
|
+
:query => { 'post_id' => '29' } # optional, defaults to {}
|
69
|
+
})
|
70
|
+
######POST
|
71
|
+
client.post({
|
72
|
+
:path => '/path/to/resource', # optional, defaults to '/'
|
73
|
+
:body => 'hey, how\'s it going?', # optional, defaults to ''
|
74
|
+
})
|
75
|
+
######PUT
|
76
|
+
client.put({
|
77
|
+
:path => '/path/to/resource', # optional, defaults to '/'
|
78
|
+
:body => 'hey, how\'s it going?' # optional, defaults to ''
|
79
|
+
})
|
80
|
+
######DELETE
|
81
|
+
client.delete({
|
82
|
+
:path => '/path/to/resource?for_good=true'
|
83
|
+
})
|
84
|
+
######HEAD
|
85
|
+
client.head({
|
86
|
+
:path => '/path/to/resource'
|
87
|
+
})
|
88
|
+
######Giant Client also exposes a general `request` method, which each method calls under the hood.
|
89
|
+
client.request(:get, {
|
90
|
+
:path => '/search?q=ahoy',
|
91
|
+
})
|
92
|
+
|
93
|
+
####Return Value
|
94
|
+
All of Giant Client's request methods return a uniform response object. This object has accessors for `status_code`, `headers`, and `body`
|
95
|
+
In general, `status_code` is a number, `headers` is a Hash, and `body` is a String.
|
96
|
+
######Example
|
97
|
+
response = client.request(:get, {
|
98
|
+
:path => '/search?q=ahoy',
|
99
|
+
})
|
100
|
+
puts response.status_code # e.g. 200
|
101
|
+
puts response.headers # e.g. {'Content-Type' => 'application/json' }
|
102
|
+
puts response.body # e.g. 'ahoy matee'
|
103
|
+
|
104
|
+
######Giant Client provides getters and setters to default port, host, and ssl.
|
105
|
+
# one request over http on port 80
|
106
|
+
g_client = GiantClient.new( :adapter => :typhoeus, :host => 'www.google.com' )
|
107
|
+
g_client.get(:path => '/')
|
108
|
+
|
109
|
+
# the next on port 8080
|
110
|
+
# 2 ways to do this:
|
111
|
+
# way 1
|
112
|
+
g_client.port = 8080
|
113
|
+
g_client.get(:path => '/')
|
114
|
+
# way 2
|
115
|
+
g_client.get(:path => '/', :port => 8080)
|
116
|
+
|
117
|
+
######Timeouts
|
118
|
+
In GiantClient timeouts can be configured on initialization or request by request.
|
119
|
+
If you just leave it alone, the default timeout is 30 seconds. This is roughly the average of the clients supported.
|
120
|
+
Setting a timeout sets both the read timeout and the connect timeout.
|
121
|
+
*Setting a Timeout*
|
122
|
+
# on configuration
|
123
|
+
client = GiantClient.new( :adapter => :typhoeus, :timeout => 5 ) # five second timeout
|
124
|
+
client.get '/' # will raise a GiantClient::TimeoutError if it takes longer than 5 seconds.
|
125
|
+
|
126
|
+
# request specific timeout
|
127
|
+
client = GiantClient.new :typhoeus # five second timeout
|
128
|
+
client.get '/', :timeout => 1 # will raise a GiantClient::TimeoutError if it takes longer than 1 second.
|
129
|
+
|
130
|
+
######Passing a string as the options will set path = to that string and all other options to their defaults
|
131
|
+
client.get('/') # same as :path => '/'
|
132
|
+
|
133
|
+
######Passing a string and options will set path = to that string and all other options to those specified in the options
|
134
|
+
client.get('/', { :query => "foo=bar" }) # same as :path => '/', :query => "foo=bar"
|
135
|
+
|
136
|
+
######The string argument takes precedence
|
137
|
+
client.get('/', {:path => '/this/does/not/matter'}) # same as :path => '/'
|
138
|
+
|
139
|
+
######Passing more than 2 arguments or fewer than one argument will raise an ArgumentError
|
140
|
+
client.get('I', 'skipped', 'the', 'README') # ArgumentError
|
141
|
+
|
142
|
+
*Note: this is true with initialize too*
|
143
|
+
|
144
|
+
####Testing
|
145
|
+
|
146
|
+
Testing is easier than ever if you use GiantClient. Just call `GiantClient.new :adapter => :mock` and GiantClient will stub out all your requests.
|
147
|
+
When the adapter is set to `:mock` GiantClient stores all of your requests on a stack: `client.requests`. This is also true for responses.
|
148
|
+
`last_request` is shorthand (or longhand, actually) for `requests[0]` Check it out.
|
149
|
+
|
150
|
+
describe 'Mock Adapter' do
|
151
|
+
let(:client){ GiantClient.new :host => 'example.com', :adapter => :mock }
|
152
|
+
|
153
|
+
context 'super simple request' do
|
154
|
+
before do
|
155
|
+
client.get('/')
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'should record the url' do
|
159
|
+
client.last_request.url.should == 'http://example.com/'
|
160
|
+
end
|
161
|
+
end
|
162
|
+
context 'request with all options set' do
|
163
|
+
before do
|
164
|
+
opts = {
|
165
|
+
:ssl => true,
|
166
|
+
:port => 9292,
|
167
|
+
:path => '/hey/there',
|
168
|
+
:query => { 'howya' => 'doing' },
|
169
|
+
:headers => { 'Content-Type' => 'application/awesome' },
|
170
|
+
:body => 'test body',
|
171
|
+
:timeout => 27
|
172
|
+
}
|
173
|
+
client.get(opts)
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'should record the (correct) url' do
|
177
|
+
url = 'https://example.com:9292/hey/there?howya=doing'
|
178
|
+
client.last_request.url.should == url
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'should record the (correct) ssl' do
|
182
|
+
client.last_request.ssl.should == true
|
183
|
+
end
|
184
|
+
it 'should record the (correct) port' do
|
185
|
+
client.last_request.port.should == 9292
|
186
|
+
end
|
187
|
+
it 'should record the (correct) path' do
|
188
|
+
client.last_request.path.should == '/hey/there'
|
189
|
+
end
|
190
|
+
it 'should record the (correct) query' do
|
191
|
+
client.last_request.query.should == { 'howya' => 'doing' }
|
192
|
+
end
|
193
|
+
it 'should record the (correct) query_string' do
|
194
|
+
client.last_request.querystring.should == 'howya=doing'
|
195
|
+
end
|
196
|
+
it 'should record the (correct) headers' do
|
197
|
+
client.last_request.headers.should ==
|
198
|
+
{ 'Content-Type' => 'application/awesome' }
|
199
|
+
end
|
200
|
+
it 'should record the (correct) body' do
|
201
|
+
client.last_request.body.should == 'test body'
|
202
|
+
end
|
203
|
+
it 'should record the (correct) timeout' do
|
204
|
+
client.last_request.timeout.should == 27
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
Giant Client creates a response hash for the mock requests. By default it is this:
|
210
|
+
|
211
|
+
{
|
212
|
+
:status_code => 200,
|
213
|
+
:headers => {},
|
214
|
+
:body => nil
|
215
|
+
}
|
216
|
+
|
217
|
+
You can manipulate the response to a specific request with the `respond_with` method, which merges in your settings:
|
218
|
+
client.get('/').respond_with(:body => 'hey')
|
219
|
+
client.last_response.should == {
|
220
|
+
:status_code => 200,
|
221
|
+
:headers => {},
|
222
|
+
:body => 'hey'
|
223
|
+
}
|
224
|
+
You can set arbitrary fields (although there usually isn't reason to)
|
225
|
+
|
226
|
+
client.get('/').respond_with(:headers => {}:body => 'hey')
|
227
|
+
client.last_response.should == {
|
228
|
+
:status_code => 200,
|
229
|
+
:headers => {},
|
230
|
+
:body => 'hey'
|
231
|
+
}
|
232
|
+
|
233
|
+
As many as you want
|
234
|
+
|
235
|
+
client.get('/').respond_with(:headers => {
|
236
|
+
'Content-Type' => 'application/json',
|
237
|
+
'X-Powered-By' => 'Internet'
|
238
|
+
},
|
239
|
+
:body => 'hey',
|
240
|
+
:foo => 'bar'
|
241
|
+
)
|
242
|
+
client.last_response.should == {
|
243
|
+
:status_code => 200,
|
244
|
+
:headers => {
|
245
|
+
'Content-Type' => 'application/json',
|
246
|
+
'X-Powered-By' => 'Internet'
|
247
|
+
},
|
248
|
+
:body => 'hey',
|
249
|
+
:foo => 'bar'
|
250
|
+
}
|
251
|
+
|
252
|
+
# in practice you'll usually just test a property at a time e.g.
|
253
|
+
client.last_response[:body].should == 'hey' # much cleaner
|
254
|
+
|
255
|
+
|
256
|
+
*For tests with multiple requests / responses, visit spec/examples/mock_adapter_spec.rb*
|
257
|
+
|
258
|
+
## Contributing
|
259
|
+
|
260
|
+
1. Fork it
|
261
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
262
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
263
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
264
|
+
5. Create new Pull Request
|
data/lib/giant_client.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'giant_client/response'
|
2
|
+
require 'giant_client/error'
|
3
|
+
|
4
|
+
class GiantClient
|
5
|
+
BODYLESS_METHODS = [:get, :delete, :head]
|
6
|
+
|
7
|
+
attr_accessor :host, :ssl, :port
|
8
|
+
attr_reader :adapter
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
|
12
|
+
unless args.length.between?(1, 2)
|
13
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 1..2)";
|
14
|
+
end
|
15
|
+
|
16
|
+
opts = Hash === args.last ? args.last : { :adapter => args.last }
|
17
|
+
if String === args.first
|
18
|
+
opts[:adapter] = args.first
|
19
|
+
end
|
20
|
+
|
21
|
+
@host = opts[:host]
|
22
|
+
@ssl = !!opts[:ssl]
|
23
|
+
default_port = @ssl ? 443 : 80
|
24
|
+
@port = opts[:port] || default_port
|
25
|
+
@timeout = opts[:timeout] || 2
|
26
|
+
|
27
|
+
@default_opts = {
|
28
|
+
:host => @host,
|
29
|
+
:ssl => @ssl,
|
30
|
+
:port => @port,
|
31
|
+
:path => '/',
|
32
|
+
:query => {},
|
33
|
+
:headers => {},
|
34
|
+
:body => "",
|
35
|
+
:timeout => 30
|
36
|
+
}
|
37
|
+
|
38
|
+
# default timeouts
|
39
|
+
# patron: 5
|
40
|
+
# net/http: 60
|
41
|
+
# curb: none
|
42
|
+
# excon: 60
|
43
|
+
# typhoeus:
|
44
|
+
|
45
|
+
self.adapter = opts[:adapter] || :net_http
|
46
|
+
@client = @adapter.new
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def adapter=(new_adapter)
|
51
|
+
require "giant_client/#{new_adapter}_adapter"
|
52
|
+
normalized = new_adapter.to_s.split('_').map{ |s| s.capitalize }.join
|
53
|
+
@adapter = GiantClient.const_get("#{normalized}Adapter")
|
54
|
+
@client = @adapter.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def method_missing(method, *args)
|
58
|
+
|
59
|
+
unless args.length.between?(1, 2)
|
60
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 1..2)";
|
61
|
+
end
|
62
|
+
|
63
|
+
opts = Hash === args.last ? args.last : { :path => args.last }
|
64
|
+
if String === args.first
|
65
|
+
opts[:path] = args.first
|
66
|
+
end
|
67
|
+
|
68
|
+
opts = @default_opts.merge(opts)
|
69
|
+
@client.__send__(method, opts)
|
70
|
+
end
|
71
|
+
|
72
|
+
# for the mock adapter only
|
73
|
+
def last_request
|
74
|
+
if MockAdapter === @client
|
75
|
+
@client.last_request
|
76
|
+
else
|
77
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 1..2)";
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def requests
|
82
|
+
if MockAdapter === @client
|
83
|
+
@client.requests
|
84
|
+
else
|
85
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 1..2)";
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def last_response
|
90
|
+
if MockAdapter === @client
|
91
|
+
@client.last_response
|
92
|
+
else
|
93
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 1..2)";
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def responses
|
98
|
+
if MockAdapter === @client
|
99
|
+
@client.responses
|
100
|
+
else
|
101
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 1..2)";
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
class GiantClient
|
2
|
+
class AbstractAdapter
|
3
|
+
|
4
|
+
def url_from_opts(opts)
|
5
|
+
query = encode_query(opts[:query])
|
6
|
+
|
7
|
+
if opts[:ssl]
|
8
|
+
scheme = 'https://'
|
9
|
+
port = opts[:port] == 443 ? '' : ":#{opts[:port]}"
|
10
|
+
else
|
11
|
+
scheme = 'http://'
|
12
|
+
port = opts[:port] == 80 ? '' : ":#{opts[:port]}"
|
13
|
+
end
|
14
|
+
|
15
|
+
"#{scheme}#{opts[:host]}#{port}#{opts[:path]}#{query}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def encode_query(query)
|
19
|
+
query = stringify_query(query)
|
20
|
+
query = prepend_question_mark(query) unless query == ''
|
21
|
+
query
|
22
|
+
end
|
23
|
+
|
24
|
+
def stringify_query(query)
|
25
|
+
if Hash === query
|
26
|
+
query = URI.encode_www_form(query)
|
27
|
+
elsif query.nil?
|
28
|
+
query = ''
|
29
|
+
end
|
30
|
+
query
|
31
|
+
end
|
32
|
+
|
33
|
+
def prepend_question_mark(str)
|
34
|
+
"?#{str}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def normalize_header_hash(headers)
|
38
|
+
normalized_headers = {}
|
39
|
+
headers.each do |header, value|
|
40
|
+
normalized = normalize_header(header)
|
41
|
+
normalized_headers[normalized] = value
|
42
|
+
end
|
43
|
+
normalized_headers
|
44
|
+
end
|
45
|
+
|
46
|
+
def normalize_header(header)
|
47
|
+
header.split('-').map{|h| h.capitalize}.join('-')
|
48
|
+
end
|
49
|
+
|
50
|
+
def get(opts)
|
51
|
+
request(:get, opts)
|
52
|
+
end
|
53
|
+
|
54
|
+
def post(opts)
|
55
|
+
request(:post, opts)
|
56
|
+
end
|
57
|
+
|
58
|
+
def put(opts)
|
59
|
+
request(:put, opts)
|
60
|
+
end
|
61
|
+
|
62
|
+
def delete(opts)
|
63
|
+
request(:delete, opts)
|
64
|
+
end
|
65
|
+
|
66
|
+
def head(opts)
|
67
|
+
request(:head, opts)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|