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 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.
@@ -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
@@ -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