hugs 2.5.2 → 2.6.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/.gitignore +4 -2
- data/README.md +15 -6
- data/lib/hugs/client.rb +114 -67
- data/lib/hugs/errors.rb +45 -35
- data/lib/hugs/version.rb +1 -1
- data/test/lib/hugs/client_test.rb +175 -166
- data/test/lib/hugs/errors_test.rb +39 -26
- data/test/test_helper.rb +0 -18
- metadata +2 -3
- data/Gemfile.lock +0 -32
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Hugs
|
2
2
|
|
3
|
-
Hugs net-http-persistent with convenient get,
|
3
|
+
Hugs net-http-persistent with convenient delete, get, head, post, and put methods.
|
4
4
|
|
5
5
|
Opted to write this gem for four reasons:
|
6
6
|
|
@@ -15,13 +15,12 @@ Opted to write this gem for four reasons:
|
|
15
15
|
There looks to be [work on this front](https://github.com/geemus/excon/issues#issue/29).
|
16
16
|
* Wanted to learn how to handle this pattern.
|
17
17
|
|
18
|
-
|
19
|
-
will be implemented against an XML OCCI API.
|
20
|
-
|
21
|
-
## Assumptions
|
18
|
+
## Opinionated
|
22
19
|
|
23
20
|
* The webservice returns JSON or XML.
|
24
|
-
*
|
21
|
+
* It is okay to parse the response.
|
22
|
+
* A Hash is returned when response is JSON.
|
23
|
+
* A Nokogiri::XML::Document is returned when XML.
|
25
24
|
|
26
25
|
## Usage
|
27
26
|
|
@@ -33,8 +32,18 @@ will be implemented against an XML OCCI API.
|
|
33
32
|
|
34
33
|
See the 'Examples' section in the [wiki](http://github.com/retr0h/hugs/wiki/).
|
35
34
|
|
35
|
+
## Compatability
|
36
|
+
|
37
|
+
ruby 1.9.2
|
38
|
+
|
36
39
|
## Testing
|
37
40
|
|
38
41
|
Tests can run offline thanks to [webmock](https://github.com/bblimke/webmock).
|
39
42
|
|
40
43
|
$ bundle exec rake
|
44
|
+
|
45
|
+
## TODOs
|
46
|
+
|
47
|
+
* avoid the parse (eg client.raw)
|
48
|
+
* override type per request
|
49
|
+
* support hased query params
|
data/lib/hugs/client.rb
CHANGED
@@ -1,60 +1,68 @@
|
|
1
1
|
require "hugs/errors"
|
2
2
|
|
3
3
|
require "net/http/persistent"
|
4
|
-
require "
|
4
|
+
require "uri"
|
5
5
|
require "nokogiri"
|
6
6
|
require "yajl"
|
7
7
|
|
8
8
|
module Hugs
|
9
9
|
class Client
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
Headers = {
|
15
|
-
:json => "application/json",
|
16
|
-
:xml => "application/xml",
|
10
|
+
MIME_TYPES = {
|
11
|
+
:binary => "application/octet-stream",
|
12
|
+
:json => "application/json",
|
13
|
+
:xml => "application/xml",
|
17
14
|
}.freeze
|
18
15
|
|
19
|
-
|
20
|
-
Net::HTTP::Get,
|
16
|
+
CLASSES = [
|
21
17
|
Net::HTTP::Delete,
|
18
|
+
Net::HTTP::Get,
|
19
|
+
Net::HTTP::Head,
|
22
20
|
Net::HTTP::Post,
|
23
|
-
Net::HTTP::Put
|
24
|
-
Net::HTTP::Head
|
21
|
+
Net::HTTP::Put
|
25
22
|
].freeze
|
26
23
|
|
27
24
|
##
|
28
25
|
# Required options:
|
29
26
|
# +host+: A String with the host to connect.
|
30
27
|
# Optional:
|
31
|
-
# +user+:
|
32
|
-
# +
|
33
|
-
# +port+:
|
34
|
-
# +scheme+:
|
28
|
+
# +user+: A String containing the username for use in HTTP Basic Authentication.
|
29
|
+
# +pass+: A String containing the password for use in HTTP Basic Authentication.
|
30
|
+
# +port+: An Integer containing the port to connect.
|
31
|
+
# +scheme+: A String containing the HTTP scheme.
|
32
|
+
# +type+: A Symbol containing (:json or :xml) for automatic content-type parsing
|
33
|
+
# and encoding.
|
34
|
+
# +headers+: A Hash containing HTTP headers.
|
35
|
+
# +raises+: A boolean if HTTP 4xx and 5xx status codes should be raised.
|
35
36
|
|
36
37
|
def initialize options
|
37
|
-
@user
|
38
|
-
@
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
@
|
44
|
-
|
45
|
-
|
46
|
-
@
|
38
|
+
@user = options[:user]
|
39
|
+
@pass = options[:password]
|
40
|
+
host = options[:host]
|
41
|
+
raises = options[:raise_errors]
|
42
|
+
port = options[:port] || 80
|
43
|
+
scheme = options[:scheme] || "http"
|
44
|
+
@type = options[:type] || :json
|
45
|
+
@headers = options[:headers] || {}
|
46
|
+
|
47
|
+
@http = Net::HTTP::Persistent.new
|
48
|
+
@errors = Errors::HTTP.new :raise_errors => raises
|
49
|
+
@uri = URI.parse "#{scheme}://#{options[:host]}:#{port}"
|
50
|
+
|
51
|
+
@http.debug_output = $stdout if options[:debug]
|
47
52
|
end
|
48
53
|
|
49
54
|
##
|
50
|
-
# Perform an HTTP
|
55
|
+
# Perform an HTTP Delete, Head, Get, Post, or Put.
|
56
|
+
#
|
51
57
|
# +path+: A String with the path to the HTTP resource.
|
52
58
|
# +params+: A Hash with the following keys:
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
59
|
+
# +:query+: A String with the format "foo=bar".
|
60
|
+
# +:body+: A String containing the message body for Put and Post requests.
|
61
|
+
# +:upload+: A Hash with the following keys:
|
62
|
+
# +file+: The file to be HTTP chunked uploaded.
|
63
|
+
# +headers+: A Hash containing additional HTTP headers.
|
56
64
|
|
57
|
-
|
65
|
+
CLASSES.each do |clazz|
|
58
66
|
verb = clazz.to_s.split("::").last.downcase
|
59
67
|
|
60
68
|
define_method verb do |*args|
|
@@ -66,10 +74,13 @@ module Hugs
|
|
66
74
|
end
|
67
75
|
|
68
76
|
##
|
69
|
-
# :method:
|
77
|
+
# :method: delete
|
70
78
|
|
71
79
|
##
|
72
|
-
# :method:
|
80
|
+
# :method: head
|
81
|
+
|
82
|
+
##
|
83
|
+
# :method: get
|
73
84
|
|
74
85
|
##
|
75
86
|
# :method: post
|
@@ -79,57 +90,88 @@ module Hugs
|
|
79
90
|
|
80
91
|
private
|
81
92
|
##
|
82
|
-
# Worker method to be called by #get, #
|
93
|
+
# Worker method to be called by #delete, #get, #head, #post, or #put.
|
94
|
+
#
|
83
95
|
# Method arguments have been documented in the callers.
|
84
96
|
|
85
|
-
def response_for
|
86
|
-
|
97
|
+
def response_for clazz, path, params
|
98
|
+
request = clazz.new path_with_query path, params[:query]
|
99
|
+
request.body = encode params[:body]
|
87
100
|
|
88
|
-
|
89
|
-
|
90
|
-
upload = params[:upload] && params.delete(:upload)
|
91
|
-
full_path = path_with_query path, query
|
101
|
+
handle_request request, params[:upload]
|
102
|
+
end
|
92
103
|
|
93
|
-
|
94
|
-
|
95
|
-
|
104
|
+
##
|
105
|
+
# Handles setting headers, performing the HTTP connection, parsing the
|
106
|
+
# response, and checking for any response errors (if configured).
|
107
|
+
#
|
108
|
+
# +request+: A net/http request Object.
|
96
109
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
request.body = encode body
|
110
|
+
def handle_request request, upload
|
111
|
+
uploads request, upload
|
112
|
+
headers request, upload
|
101
113
|
|
102
|
-
|
103
|
-
|
114
|
+
response = @http.request @uri, request
|
115
|
+
response.body = parse response.body
|
116
|
+
|
117
|
+
request.body_stream.close if request.body_stream
|
104
118
|
|
105
|
-
|
106
|
-
handle_response request
|
119
|
+
@errors.errors response
|
107
120
|
end
|
108
121
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
122
|
+
##
|
123
|
+
# Handle chunked uploads.
|
124
|
+
#
|
125
|
+
# +request+: A net/http request Object.
|
126
|
+
# +upload+: A Hash containing :file and :headers for uploading.
|
113
127
|
|
114
|
-
|
115
|
-
|
128
|
+
def uploads request, upload
|
129
|
+
return unless upload
|
116
130
|
|
117
|
-
|
118
|
-
|
131
|
+
@headers.merge! chunked_headers upload if upload
|
132
|
+
|
133
|
+
request.body_stream = File.open upload[:file]
|
119
134
|
end
|
120
135
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
136
|
+
##
|
137
|
+
# Handles the setting of various default and custom headers.
|
138
|
+
# Headers set in initialize can override all others.
|
139
|
+
#
|
140
|
+
# +request+: A net/http request Object.
|
141
|
+
|
142
|
+
def headers request, upload
|
143
|
+
request.basic_auth(@user, @pass) if requires_authentication?
|
144
|
+
|
145
|
+
request.add_field "Accept", MIME_TYPES[@type]
|
146
|
+
request.add_field "Content-Type", MIME_TYPES[@type] if requires_content_type? request
|
126
147
|
|
127
148
|
@headers.each do |header, value|
|
128
|
-
request
|
129
|
-
end
|
149
|
+
request[header] = value
|
150
|
+
end if @headers
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Setting of chunked upload headers.
|
155
|
+
#
|
156
|
+
# +:upload+: A Hash with the following keys:
|
157
|
+
# +file+: The file to be HTTP chunked uploaded.
|
158
|
+
# +headers+: A Hash containing additional HTTP headers.
|
159
|
+
|
160
|
+
def chunked_headers upload
|
161
|
+
chunked_headers = {
|
162
|
+
"Content-Type" => MIME_TYPES[:binary],
|
163
|
+
"Content-Length" => File.size(upload[:file]),
|
164
|
+
"Transfer-Encoding" => "chunked"
|
165
|
+
}.merge upload[:headers] || {}
|
166
|
+
end
|
167
|
+
|
168
|
+
def path_with_query path, query
|
169
|
+
[path, query].compact.join "?"
|
130
170
|
end
|
131
171
|
|
132
172
|
def parse data
|
173
|
+
return unless data
|
174
|
+
|
133
175
|
if is_json?
|
134
176
|
Yajl::Parser.parse data
|
135
177
|
elsif is_xml?
|
@@ -141,11 +183,16 @@ module Hugs
|
|
141
183
|
|
142
184
|
def encode body
|
143
185
|
return unless body
|
186
|
+
|
144
187
|
is_json? ? (Yajl::Encoder.encode body) : body
|
145
188
|
end
|
146
189
|
|
147
190
|
def requires_authentication?
|
148
|
-
@user && @
|
191
|
+
@user && @pass
|
192
|
+
end
|
193
|
+
|
194
|
+
def requires_content_type? request
|
195
|
+
[Net::HTTP::Post, Net::HTTP::Put].include? request.class
|
149
196
|
end
|
150
197
|
|
151
198
|
def is_xml?
|
data/lib/hugs/errors.rb
CHANGED
@@ -39,46 +39,56 @@ module Hugs
|
|
39
39
|
class ServiceUnavailable < HTTPStatusError; end # 503
|
40
40
|
class GatewayTimeout < HTTPStatusError; end # 504
|
41
41
|
|
42
|
+
class HTTP
|
43
|
+
def initialize options
|
44
|
+
@raise_errors = options[:raise_errors]
|
45
|
+
|
46
|
+
initialize_errors
|
47
|
+
end
|
42
48
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
402 => [Hugs::Errors::PaymentRequired, 'Payment Required'],
|
48
|
-
403 => [Hugs::Errors::Forbidden, 'Forbidden'],
|
49
|
-
404 => [Hugs::Errors::NotFound, 'Not Found'],
|
50
|
-
405 => [Hugs::Errors::MethodNotAllowed, 'Method Not Allowed'],
|
51
|
-
406 => [Hugs::Errors::NotAcceptable, 'Not Acceptable'],
|
52
|
-
407 => [Hugs::Errors::ProxyAuthenticationRequired, 'Proxy Authentication Required'],
|
53
|
-
408 => [Hugs::Errors::RequestTimeout, 'Request Timeout'],
|
54
|
-
409 => [Hugs::Errors::Conflict, 'Conflict'],
|
55
|
-
410 => [Hugs::Errors::Gone, 'Gone'],
|
56
|
-
411 => [Hugs::Errors::LengthRequired, 'Length Required'],
|
57
|
-
412 => [Hugs::Errors::PreconditionFailed, 'Precondition Failed'],
|
58
|
-
413 => [Hugs::Errors::RequestEntityTooLarge, 'Request Entity Too Large'],
|
59
|
-
414 => [Hugs::Errors::RequestURITooLong, 'Request-URI Too Long'],
|
60
|
-
415 => [Hugs::Errors::UnsupportedMediaType, 'Unsupported Media Type'],
|
61
|
-
416 => [Hugs::Errors::RequestedRangeNotSatisfiable, 'Request Range Not Satisfiable'],
|
62
|
-
417 => [Hugs::Errors::ExpectationFailed, 'Expectation Failed'],
|
63
|
-
422 => [Hugs::Errors::UnprocessableEntity, 'Unprocessable Entity'],
|
64
|
-
500 => [Hugs::Errors::InternalServerError, 'InternalServerError'],
|
65
|
-
501 => [Hugs::Errors::NotImplemented, 'Not Implemented'],
|
66
|
-
502 => [Hugs::Errors::BadGateway, 'Bad Gateway'],
|
67
|
-
503 => [Hugs::Errors::ServiceUnavailable, 'Service Unavailable'],
|
68
|
-
504 => [Hugs::Errors::GatewayTimeout, 'Gateway Timeout']
|
69
|
-
}
|
49
|
+
def errors response
|
50
|
+
case response.code
|
51
|
+
when @raise_errors && %r{^[45]{1}[0-9]{2}$} ; raise_for(response)
|
52
|
+
end
|
70
53
|
|
71
|
-
|
72
|
-
when raise_4xx && %r{^4[0-9]{2}$} ; raise_for(response)
|
73
|
-
when raise_5xx && %r{^5[0-9]{2}$} ; raise_for(response)
|
54
|
+
response
|
74
55
|
end
|
75
|
-
end
|
76
56
|
|
77
|
-
|
78
|
-
|
79
|
-
|
57
|
+
private
|
58
|
+
def raise_for response
|
59
|
+
error, message = @errors[response.code.to_i]
|
80
60
|
|
81
|
-
|
61
|
+
raise error.new message, response
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize_errors
|
65
|
+
@errors ||= {
|
66
|
+
400 => [Hugs::Errors::BadRequest, 'Bad Request'],
|
67
|
+
401 => [Hugs::Errors::Unauthorized, 'Unauthorized'],
|
68
|
+
402 => [Hugs::Errors::PaymentRequired, 'Payment Required'],
|
69
|
+
403 => [Hugs::Errors::Forbidden, 'Forbidden'],
|
70
|
+
404 => [Hugs::Errors::NotFound, 'Not Found'],
|
71
|
+
405 => [Hugs::Errors::MethodNotAllowed, 'Method Not Allowed'],
|
72
|
+
406 => [Hugs::Errors::NotAcceptable, 'Not Acceptable'],
|
73
|
+
407 => [Hugs::Errors::ProxyAuthenticationRequired, 'Proxy Authentication Required'],
|
74
|
+
408 => [Hugs::Errors::RequestTimeout, 'Request Timeout'],
|
75
|
+
409 => [Hugs::Errors::Conflict, 'Conflict'],
|
76
|
+
410 => [Hugs::Errors::Gone, 'Gone'],
|
77
|
+
411 => [Hugs::Errors::LengthRequired, 'Length Required'],
|
78
|
+
412 => [Hugs::Errors::PreconditionFailed, 'Precondition Failed'],
|
79
|
+
413 => [Hugs::Errors::RequestEntityTooLarge, 'Request Entity Too Large'],
|
80
|
+
414 => [Hugs::Errors::RequestURITooLong, 'Request-URI Too Long'],
|
81
|
+
415 => [Hugs::Errors::UnsupportedMediaType, 'Unsupported Media Type'],
|
82
|
+
416 => [Hugs::Errors::RequestedRangeNotSatisfiable, 'Request Range Not Satisfiable'],
|
83
|
+
417 => [Hugs::Errors::ExpectationFailed, 'Expectation Failed'],
|
84
|
+
422 => [Hugs::Errors::UnprocessableEntity, 'Unprocessable Entity'],
|
85
|
+
500 => [Hugs::Errors::InternalServerError, 'InternalServerError'],
|
86
|
+
501 => [Hugs::Errors::NotImplemented, 'Not Implemented'],
|
87
|
+
502 => [Hugs::Errors::BadGateway, 'Bad Gateway'],
|
88
|
+
503 => [Hugs::Errors::ServiceUnavailable, 'Service Unavailable'],
|
89
|
+
504 => [Hugs::Errors::GatewayTimeout, 'Gateway Timeout']
|
90
|
+
}
|
91
|
+
end
|
82
92
|
end
|
83
93
|
end
|
84
94
|
end
|
data/lib/hugs/version.rb
CHANGED
@@ -1,270 +1,279 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "uri"
|
4
4
|
|
5
5
|
describe Hugs::Client do
|
6
6
|
before do
|
7
|
-
@
|
8
|
-
@
|
7
|
+
@uri = URI::HTTP.build :host => "example.com", :path => "/"
|
8
|
+
@client = Hugs::Client.new :host => @uri.host
|
9
9
|
|
10
10
|
WebMock.reset!
|
11
11
|
end
|
12
12
|
|
13
|
-
describe "
|
14
|
-
it "
|
15
|
-
|
16
|
-
|
17
|
-
@instance.send :response_for, @request, "/", {}
|
18
|
-
|
19
|
-
assert_requested :get, url
|
13
|
+
describe "verbs" do
|
14
|
+
it "supports HTTP Get" do
|
15
|
+
must_support_http_get
|
20
16
|
end
|
21
17
|
|
22
|
-
it "
|
23
|
-
|
24
|
-
|
25
|
-
@instance.send :response_for, @request, "/", :query => nil
|
26
|
-
|
27
|
-
assert_requested :get, url
|
18
|
+
it "supports HTTP Delete" do
|
19
|
+
must_support_http_delete
|
28
20
|
end
|
29
21
|
|
30
|
-
it "
|
31
|
-
|
32
|
-
|
33
|
-
@instance.send :response_for, @request, "/", :query => "foo=bar"
|
34
|
-
|
35
|
-
assert_requested :get, url, :query => {"foo" => "bar"}
|
22
|
+
it "supports HTTP Head" do
|
23
|
+
must_support_http_head
|
36
24
|
end
|
37
|
-
end
|
38
25
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
before do
|
43
|
-
@request = Net::HTTP::Post
|
26
|
+
it "supports HTTP Post" do
|
27
|
+
must_support_http_post
|
44
28
|
end
|
45
29
|
|
46
|
-
it "
|
47
|
-
|
48
|
-
upload = {
|
49
|
-
:upload => {
|
50
|
-
:parts => { :file => "/dev/null" },
|
51
|
-
:content_type => "type/subtype"
|
52
|
-
}
|
53
|
-
}
|
54
|
-
|
55
|
-
@instance.send :response_for, @request, "/", upload
|
56
|
-
|
57
|
-
assert_requested :post, url, :body => %r{Content-Type: type/subtype}, :headers => {
|
58
|
-
"Content-Type" => Content_Type_Matcher
|
59
|
-
}
|
30
|
+
it "supports HTTP Put" do
|
31
|
+
must_support_http_put
|
60
32
|
end
|
33
|
+
end
|
61
34
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
:upload => {
|
66
|
-
:parts => { :foo => :bar, :baz => :xyzzy },
|
67
|
-
:content_type => "foo/bar"
|
68
|
-
}
|
69
|
-
}
|
35
|
+
describe "path" do
|
36
|
+
it "supports a query string" do
|
37
|
+
stub_request(:get, @uri.to_s).with:query => { "foo" => "bar" }
|
70
38
|
|
71
|
-
@
|
39
|
+
@client.get @uri.path, :query => "foo=bar"
|
72
40
|
|
73
|
-
|
74
|
-
content_disposition_matcher = %r{^Content-Disposition: form-data; name="foo".*^bar.*^Content-Disposition: form-data; name="baz".*^xyzzy.*}m
|
75
|
-
assert_requested :post, url, :body => content_disposition_matcher, :headers => {
|
76
|
-
"Content-Type" => Content_Type_Matcher
|
77
|
-
}
|
41
|
+
assert_requested :get, @uri.to_s, :query => { "foo" => "bar" }
|
78
42
|
end
|
79
43
|
end
|
80
44
|
|
81
|
-
describe "
|
82
|
-
describe "
|
83
|
-
|
84
|
-
|
85
|
-
stub_request(:get, url).to_return :body => '{"foo":"bar"}'
|
86
|
-
instance = Hugs::Client.new valid_options(:type => :json)
|
87
|
-
|
88
|
-
response = instance.get "/"
|
89
|
-
|
90
|
-
response.body.must_be_kind_of Hash
|
91
|
-
end
|
45
|
+
describe "headers" do
|
46
|
+
describe "json" do
|
47
|
+
it "adds Accept header" do
|
48
|
+
must_have_headers_for_get :json
|
92
49
|
end
|
93
50
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
response = instance.get "/"
|
51
|
+
it "adds Content-Type header" do
|
52
|
+
must_have_headers_for_put :json
|
53
|
+
must_have_headers_for_post :json
|
54
|
+
end
|
55
|
+
end
|
100
56
|
|
101
|
-
|
102
|
-
|
57
|
+
describe "xml" do
|
58
|
+
before do
|
59
|
+
@client = Hugs::Client.new :host => @uri.host, :type => :xml
|
103
60
|
end
|
104
61
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
#stub_request(:head, url).to_return :body => nil
|
62
|
+
it "adds Accept header" do
|
63
|
+
must_have_headers_for_get :xml
|
64
|
+
end
|
109
65
|
|
110
|
-
|
66
|
+
it "adds Content-Type header" do
|
67
|
+
must_have_headers_for_put :xml
|
68
|
+
must_have_headers_for_post :xml
|
111
69
|
end
|
112
70
|
end
|
113
71
|
|
114
|
-
describe "
|
72
|
+
describe "custom" do
|
115
73
|
before do
|
116
|
-
|
74
|
+
@client = Hugs::Client.new(
|
75
|
+
:host => @uri.host,
|
76
|
+
:headers => { "foo" => "bar", "baz" => "xyzzy" }
|
77
|
+
)
|
117
78
|
end
|
118
79
|
|
119
|
-
it "
|
120
|
-
|
121
|
-
|
122
|
-
assert_requested :get, url, :body => nil
|
80
|
+
it "adds headers" do
|
81
|
+
must_have_headers_for :get, {}, "foo" => "bar", "baz" => "xyzzy"
|
123
82
|
end
|
83
|
+
end
|
124
84
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
85
|
+
describe "custom override defaults" do
|
86
|
+
before do
|
87
|
+
@client = Hugs::Client.new(
|
88
|
+
:host => @uri.host,
|
89
|
+
:headers => { "Accept" => "foo bar" }
|
90
|
+
)
|
129
91
|
end
|
130
92
|
|
131
|
-
|
132
|
-
|
133
|
-
instance = Hugs::Client.new valid_options(:type => :json)
|
134
|
-
|
135
|
-
instance.send :response_for, @request, "/", :body => {:foo => :bar}
|
136
|
-
|
137
|
-
assert_requested :get, url, :body => '{"foo":"bar"}'
|
138
|
-
end
|
93
|
+
it "replaces headers" do
|
94
|
+
must_have_headers_for :get, {}, "Accept" => "foo bar"
|
139
95
|
end
|
96
|
+
end
|
97
|
+
end
|
140
98
|
|
141
|
-
|
142
|
-
|
143
|
-
|
99
|
+
describe "authentication" do
|
100
|
+
before do
|
101
|
+
@uri.userinfo = "user:credentials"
|
102
|
+
@client = Hugs::Client.new(
|
103
|
+
:host => @uri.host,
|
104
|
+
:user => "user",
|
105
|
+
:password => "credentials"
|
106
|
+
)
|
107
|
+
end
|
144
108
|
|
145
|
-
|
109
|
+
it "uses Basic Authentication when initialized with :user and :password" do
|
110
|
+
stub_request :get, @uri.to_s
|
146
111
|
|
147
|
-
|
148
|
-
|
149
|
-
|
112
|
+
@client.get @uri.path
|
113
|
+
|
114
|
+
assert_requested :get, @uri.to_s
|
150
115
|
end
|
151
116
|
end
|
152
117
|
|
153
|
-
describe "
|
154
|
-
describe "
|
155
|
-
it "
|
156
|
-
stub_request
|
157
|
-
instance = Hugs::Client.new valid_options(:user => "user", :password => "credentials")
|
118
|
+
describe "parse body" do
|
119
|
+
describe "json" do
|
120
|
+
it "parses the body" do
|
121
|
+
stub_request(:get, @uri.to_s).to_return :body => '{"foo":"bar"}'
|
158
122
|
|
159
|
-
|
160
|
-
|
123
|
+
response = @client.get @uri.path
|
124
|
+
|
125
|
+
response.body['foo'].must_equal "bar"
|
161
126
|
end
|
127
|
+
end
|
162
128
|
|
163
|
-
|
164
|
-
|
129
|
+
describe "xml" do
|
130
|
+
before do
|
131
|
+
@client = Hugs::Client.new :host => @uri.host, :type => :xml
|
165
132
|
end
|
166
133
|
|
167
|
-
it "
|
168
|
-
|
134
|
+
it "parses the body" do
|
135
|
+
stub_request(:get, @uri.to_s).to_return :body => "<foo>bar</foo>"
|
136
|
+
|
137
|
+
response = @client.get @uri.path
|
138
|
+
|
139
|
+
response.body.xpath('foo').text.must_equal "bar"
|
169
140
|
end
|
170
141
|
end
|
142
|
+
end
|
171
143
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
144
|
+
describe "encode body" do
|
145
|
+
describe "json" do
|
146
|
+
it "encodes the body" do
|
147
|
+
stub_request :post, @uri.to_s
|
176
148
|
|
177
|
-
|
178
|
-
assert_supports_http_verb :json, :delete
|
179
|
-
end
|
149
|
+
@client.post @uri.path, :body => { :foo => :bar }
|
180
150
|
|
181
|
-
|
182
|
-
assert_supports_http_verb :json, :post
|
151
|
+
assert_requested :post, @uri.to_s, :body => '{"foo":"bar"}'
|
183
152
|
end
|
153
|
+
end
|
184
154
|
|
185
|
-
|
186
|
-
|
155
|
+
describe "xml" do
|
156
|
+
before do
|
157
|
+
@client = Hugs::Client.new :host => @uri.host, :type => :xml
|
187
158
|
end
|
188
159
|
|
189
|
-
it "
|
190
|
-
|
160
|
+
it "does not encode the body" do
|
161
|
+
stub_request :post, @uri.to_s
|
162
|
+
|
163
|
+
@client.post @uri.path, :body => "foo bar"
|
164
|
+
|
165
|
+
assert_requested :post, @uri.to_s, :body => "foo bar"
|
191
166
|
end
|
192
167
|
end
|
168
|
+
end
|
193
169
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
170
|
+
describe "upload" do
|
171
|
+
describe "chunked" do
|
172
|
+
before do
|
173
|
+
@upload = {
|
174
|
+
:upload => { :file => "/dev/null" }
|
175
|
+
}
|
198
176
|
|
199
|
-
|
200
|
-
|
177
|
+
@upload_with_headers = {
|
178
|
+
:upload => {
|
179
|
+
:file => "/dev/null",
|
180
|
+
:headers => { "Accept" => "foo bar" }
|
181
|
+
}
|
182
|
+
}
|
201
183
|
end
|
202
184
|
|
203
|
-
it "
|
204
|
-
|
185
|
+
it "has chunked headers" do
|
186
|
+
must_have_headers_for :post, @upload, {
|
187
|
+
"Content-Type" => "application/octet-stream",
|
188
|
+
###"Content-Length" => "0",
|
189
|
+
"Transfer-Encoding" => "chunked"
|
190
|
+
}
|
205
191
|
end
|
206
192
|
|
207
|
-
it "
|
208
|
-
|
193
|
+
it "upload headers override default headers" do
|
194
|
+
must_have_headers_for :post, @upload_with_headers, "Accept" => "foo bar"
|
209
195
|
end
|
210
196
|
|
211
|
-
it "
|
212
|
-
|
197
|
+
it "has chunked body" do
|
198
|
+
#stub_request :post, @uri.to_s
|
199
|
+
|
200
|
+
#@client.post @uri.path, @upload
|
201
|
+
|
202
|
+
#assert_requested verb, @uri.to_s, :headers => headers
|
213
203
|
end
|
214
204
|
end
|
215
205
|
end
|
216
206
|
|
217
|
-
describe "
|
207
|
+
describe "debug" do
|
218
208
|
before do
|
219
|
-
@instance = Hugs::Client.new valid_options(
|
220
|
-
:host => "gist.github.com",
|
221
|
-
:port => "80"
|
222
|
-
)
|
223
|
-
|
224
209
|
WebMock.allow_net_connect!
|
225
210
|
end
|
226
211
|
|
227
212
|
it "outputs net/http debug data" do
|
228
|
-
|
213
|
+
out, _ = capture_io {
|
214
|
+
client = Hugs::Client.new :host => "gist.github.com", :debug => true
|
229
215
|
|
230
|
-
|
216
|
+
client.get "/api/v1/json/374130"
|
217
|
+
}
|
231
218
|
|
232
219
|
out.wont_be_empty
|
233
220
|
end
|
234
221
|
|
235
222
|
it "does not output net/http debug data" do
|
236
|
-
out,
|
223
|
+
out, _ = capture_io {
|
224
|
+
client = Hugs::Client.new :host => "gist.github.com"
|
225
|
+
|
226
|
+
client.get "/api/v1/json/374130"
|
227
|
+
}
|
237
228
|
|
238
229
|
out.must_be_empty
|
239
230
|
end
|
240
231
|
end
|
241
232
|
|
233
|
+
def must_support_http_delete
|
234
|
+
must_support_http :delete
|
235
|
+
end
|
242
236
|
|
243
|
-
def
|
244
|
-
|
245
|
-
|
237
|
+
def must_support_http_get
|
238
|
+
must_support_http :get
|
239
|
+
end
|
246
240
|
|
247
|
-
|
241
|
+
def must_support_http_head
|
242
|
+
must_support_http :head
|
243
|
+
end
|
248
244
|
|
249
|
-
|
245
|
+
def must_support_http_post
|
246
|
+
must_support_http :post
|
247
|
+
end
|
250
248
|
|
251
|
-
|
252
|
-
|
249
|
+
def must_support_http_put
|
250
|
+
must_support_http :put
|
251
|
+
end
|
253
252
|
|
254
|
-
|
253
|
+
def must_support_http verb
|
254
|
+
stub_request verb, @uri.to_s
|
255
|
+
@client.send verb, @uri.path
|
255
256
|
|
256
|
-
|
257
|
-
|
257
|
+
assert_requested verb, @uri.to_s
|
258
|
+
end
|
259
|
+
|
260
|
+
def must_have_headers_for_get type
|
261
|
+
must_have_headers_for :get, {}, "Accept" => ["*/*", "application/#{type}"]
|
262
|
+
end
|
263
|
+
|
264
|
+
def must_have_headers_for_put type
|
265
|
+
must_have_headers_for :put, {}, "Content-Type" => "application/#{type}"
|
266
|
+
end
|
258
267
|
|
259
|
-
|
268
|
+
def must_have_headers_for_post type
|
269
|
+
must_have_headers_for :post, {}, "Content-Type" => "application/#{type}"
|
260
270
|
end
|
261
271
|
|
262
|
-
def
|
263
|
-
stub_request
|
264
|
-
instance = Hugs::Client.new valid_options(option => "value")
|
272
|
+
def must_have_headers_for verb, options, headers
|
273
|
+
stub_request verb, @uri.to_s
|
265
274
|
|
266
|
-
|
275
|
+
@client.send verb, @uri.path, options
|
267
276
|
|
268
|
-
assert_requested
|
277
|
+
assert_requested verb, @uri.to_s, :headers => headers
|
269
278
|
end
|
270
279
|
end
|
@@ -1,46 +1,59 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
|
-
describe Hugs::Errors do
|
3
|
+
describe Hugs::Errors::HTTP do
|
4
4
|
before do
|
5
|
-
@
|
5
|
+
@errors_4xx = (400..417).to_a << 422
|
6
|
+
@errors_5xx = (500..504).to_a
|
7
|
+
@errors_all = @errors_4xx + @errors_5xx
|
8
|
+
@uri = URI::HTTP.build :host => "example.com"
|
6
9
|
|
7
10
|
WebMock.reset!
|
8
11
|
end
|
9
12
|
|
10
|
-
describe "
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
(Errors_4xx + Errors_5xx).each do |code|
|
15
|
-
it "raises on #{code}" do
|
16
|
-
assert_raises_on code
|
13
|
+
describe "errors" do
|
14
|
+
describe "does not raise" do
|
15
|
+
before do
|
16
|
+
@client = Hugs::Client.new :host => @uri.host
|
17
17
|
end
|
18
18
|
|
19
|
-
it "
|
20
|
-
|
19
|
+
it "returns response" do
|
20
|
+
@errors_all.each do |code|
|
21
|
+
must_not_raise_on code
|
22
|
+
end
|
21
23
|
end
|
22
24
|
end
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
-
|
26
|
+
describe "raises" do
|
27
|
+
before do
|
28
|
+
@client = Hugs::Client.new :host => @uri.host, :raise_errors => true
|
29
|
+
end
|
27
30
|
|
28
|
-
|
29
|
-
@
|
30
|
-
|
31
|
-
|
32
|
-
e.must_respond_to :response
|
33
|
-
return
|
31
|
+
it "returns exception" do
|
32
|
+
@errors_all.each do |code|
|
33
|
+
must_raise_on code
|
34
|
+
end
|
34
35
|
end
|
35
|
-
raise StandardError.new "did not raise expected exception"
|
36
36
|
end
|
37
|
+
end
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
response = @instance.send :response_for, Net::HTTP::Get, "/", {}
|
39
|
+
def must_raise_on code
|
40
|
+
stub_request(:get, @uri.to_s).to_return :status => [code, "error #{code}"]
|
42
41
|
|
43
|
-
|
42
|
+
begin
|
43
|
+
@client.get "/"
|
44
|
+
rescue => e
|
45
|
+
e.class.superclass.must_be_same_as Hugs::Errors::HTTPStatusError
|
46
|
+
e.must_respond_to :response
|
47
|
+
return
|
44
48
|
end
|
49
|
+
raise StandardError.new "did not raise expected exception"
|
50
|
+
end
|
51
|
+
|
52
|
+
def must_not_raise_on code
|
53
|
+
stub_request(:get, @uri.to_s).to_return :status => [code, "error #{code}"]
|
54
|
+
|
55
|
+
response = @client.get "/"
|
56
|
+
|
57
|
+
response.code.must_equal code.to_s
|
45
58
|
end
|
46
59
|
end
|
data/test/test_helper.rb
CHANGED
@@ -3,28 +3,10 @@ Bundler.setup :default, :test
|
|
3
3
|
require "hugs"
|
4
4
|
|
5
5
|
require "minitest/spec"
|
6
|
-
require "uri"
|
7
6
|
require "webmock"
|
8
7
|
|
9
8
|
class MiniTest::Unit::TestCase
|
10
9
|
include WebMock::API
|
11
|
-
|
12
|
-
def valid_options opts = {}
|
13
|
-
{
|
14
|
-
:host => "example.com",
|
15
|
-
:port => 80,
|
16
|
-
:scheme => "http"
|
17
|
-
}.merge opts
|
18
|
-
end
|
19
|
-
|
20
|
-
def url path = "/", credentials = nil
|
21
|
-
URI::HTTP.build(
|
22
|
-
valid_options(
|
23
|
-
:path => path,
|
24
|
-
:userinfo => credentials
|
25
|
-
)
|
26
|
-
).to_s
|
27
|
-
end
|
28
10
|
end
|
29
11
|
|
30
12
|
MiniTest::Unit.autorun
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: hugs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 2.
|
5
|
+
version: 2.6.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- John Dewey
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-
|
13
|
+
date: 2011-05-17 00:00:00 -07:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -103,7 +103,6 @@ files:
|
|
103
103
|
- .gitignore
|
104
104
|
- .rvmrc
|
105
105
|
- Gemfile
|
106
|
-
- Gemfile.lock
|
107
106
|
- LICENSE
|
108
107
|
- README.md
|
109
108
|
- Rakefile
|
data/Gemfile.lock
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
hugs (2.5.1)
|
5
|
-
multipart-post (~> 1.0.1)
|
6
|
-
net-http-persistent (~> 1.4.1)
|
7
|
-
nokogiri (~> 1.4.4)
|
8
|
-
yajl-ruby (~> 0.7.9)
|
9
|
-
|
10
|
-
GEM
|
11
|
-
remote: http://rubygems.org/
|
12
|
-
specs:
|
13
|
-
addressable (2.2.2)
|
14
|
-
crack (0.1.8)
|
15
|
-
minitest (2.0.2)
|
16
|
-
multipart-post (1.0.1)
|
17
|
-
net-http-persistent (1.4.1)
|
18
|
-
nokogiri (1.4.4)
|
19
|
-
rake (0.8.7)
|
20
|
-
webmock (1.6.1)
|
21
|
-
addressable (>= 2.2.2)
|
22
|
-
crack (>= 0.1.7)
|
23
|
-
yajl-ruby (0.7.9)
|
24
|
-
|
25
|
-
PLATFORMS
|
26
|
-
ruby
|
27
|
-
|
28
|
-
DEPENDENCIES
|
29
|
-
hugs!
|
30
|
-
minitest
|
31
|
-
rake
|
32
|
-
webmock
|