hugs 2.5.2 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,2 +1,4 @@
1
- pkg/
2
- .bundle/
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Hugs
2
2
 
3
- Hugs net-http-persistent with convenient get, delete, post, and put methods.
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
- The XML usage of this gem will probably change. In the next couple of weeks Hugs
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
- * To objectify the returned JSON or hand back a Nokogiri::XML::Document.
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 "net/http/post/multipart"
4
+ require "uri"
5
5
  require "nokogiri"
6
6
  require "yajl"
7
7
 
8
8
  module Hugs
9
9
  class Client
10
- attr_accessor :headers
11
- attr_accessor :raise_4xx, :raise_5xx, :debug, :http
12
- attr_writer :raise_4xx, :raise_5xx, :debug, :http
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
- Classes = [
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+: A String containing the username for use in HTTP Basic auth.
32
- # +password+: A String containing the password for use in HTTP Basic auth.
33
- # +port+: An Integer containing the port to connect.
34
- # +scheme+: A String containing the HTTP 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 = options[:user]
38
- @password = options[:password]
39
- @host = options[:host]
40
- @port = options[:port] || 80
41
- @scheme = options[:scheme] || "http"
42
- @type = options[:type] || :json
43
- @headers = options[:headers] || {}
44
-
45
- @http = Net::HTTP::Persistent.new
46
- @url = URI.parse "#{@scheme}://#{@host}:#{@port}"
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 get, delete, post, or put.
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
- # - +:query+: Query String in the format "foo=bar"
54
- # - +:body+: A sub Hash to be JSON encoded, and posted in
55
- # the message body.
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
- Classes.each do |clazz|
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: get
77
+ # :method: delete
70
78
 
71
79
  ##
72
- # :method: delete
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, #delete, #post, #put.
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 request, path, params
86
- @http.debug_output = $stdout if debug
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
- query = params[:query] && params.delete(:query)
89
- body = params[:body] && params.delete(:body)
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
- if upload && request.class === Net::HTTP::Post
94
- parts = upload[:parts] || {}
95
- parts[:file] = UploadIO.new(parts[:file], upload[:content_type]) if parts[:file]
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
- request = Net::HTTP::Post::Multipart.new full_path, parts
98
- else
99
- request = request.new full_path
100
- request.body = encode body
110
+ def handle_request request, upload
111
+ uploads request, upload
112
+ headers request, upload
101
113
 
102
- add_headers request
103
- end
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
- request.basic_auth(@user, @password) if requires_authentication?
106
- handle_response request
119
+ @errors.errors response
107
120
  end
108
121
 
109
- def handle_response request
110
- resp = @http.request @url, request
111
- Hugs::Errors::status_error resp, @raise_4xx, @raise_5xx
112
- resp.body = parse resp.body if resp.body
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
- resp
115
- end
128
+ def uploads request, upload
129
+ return unless upload
116
130
 
117
- def path_with_query path, query
118
- [path, query].compact.join "?"
131
+ @headers.merge! chunked_headers upload if upload
132
+
133
+ request.body_stream = File.open upload[:file]
119
134
  end
120
135
 
121
- def add_headers request
122
- request.add_field "Accept", Headers[@type]
123
- if [Net::HTTP::Post, Net::HTTP::Put].include? request.class
124
- request.add_field "Content-Type", Headers[@type]
125
- end
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.add_field header, value
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 && @password
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
- def self.status_error response, raise_4xx, raise_5xx
44
- @errors ||= {
45
- 400 => [Hugs::Errors::BadRequest, 'Bad Request'],
46
- 401 => [Hugs::Errors::Unauthorized, 'Unauthorized'],
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
- case response.code
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
- private
78
- def self.raise_for response
79
- error, message = @errors[response.code.to_i]
57
+ private
58
+ def raise_for response
59
+ error, message = @errors[response.code.to_i]
80
60
 
81
- raise error.new message, response
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,3 +1,3 @@
1
1
  module Hugs
2
- VERSION = "2.5.2"
2
+ VERSION = "2.6.0"
3
3
  end
@@ -1,270 +1,279 @@
1
1
  require "test_helper"
2
2
 
3
- require "base64"
3
+ require "uri"
4
4
 
5
5
  describe Hugs::Client do
6
6
  before do
7
- @instance = Hugs::Client.new valid_options
8
- @request = Net::HTTP::Get
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 "path" do
14
- it "is valid" do
15
- stub_request :get, url
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 "is valid when an invalid :query is supplied" do
23
- stub_request :get, url
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 "also has a query string" do
31
- stub_request(:get, url).with:query => {"foo" => "bar"}
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
- describe "multi-part" do
40
- Content_Type_Matcher = %r{multipart/form-data}
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 "uploads a file" do
47
- stub_request :post, url
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
- it "has parts" do
63
- stub_request :post, url
64
- upload = {
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
- @instance.send :response_for, @request, "/", upload
39
+ @client.get @uri.path, :query => "foo=bar"
72
40
 
73
- ### wtf can't use mx together.
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 "body" do
82
- describe "parses response" do
83
- describe "json" do
84
- it "objectifies and returns a hash" do
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
- describe "xml" do
95
- it "parses and returns a Nokogiri object" do
96
- stub_request(:get, url).to_return :body => "<STORAGE></STORAGE>"
97
- instance = Hugs::Client.new valid_options(:type => :xml)
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
- response.body.must_be_kind_of Nokogiri::XML::Document
102
- end
57
+ describe "xml" do
58
+ before do
59
+ @client = Hugs::Client.new :host => @uri.host, :type => :xml
103
60
  end
104
61
 
105
- ### TODO: webmock seems to turn nil into "" body. Cannot test a failing case.
106
- ### TODO: fix when I can.
107
- it "doesn't parse a nil body" do
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
- #@instance.send :response_for, Net::HTTP::Head, "/", {}
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 "encodes request" do
72
+ describe "custom" do
115
73
  before do
116
- stub_request :get, url
74
+ @client = Hugs::Client.new(
75
+ :host => @uri.host,
76
+ :headers => { "foo" => "bar", "baz" => "xyzzy" }
77
+ )
117
78
  end
118
79
 
119
- it "is not set when :body invalid" do
120
- @instance.send :response_for, @request, "/", :body => nil
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
- it "is not set when :body is missing" do
126
- @instance.send :response_for, @request, "/", {}
127
-
128
- assert_requested :get, url, {}
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
- describe "json" do
132
- it "is valid" do
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
- describe "xml" do
142
- it "is valid" do
143
- instance = Hugs::Client.new valid_options(:type => :xml)
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
- instance.send :response_for, @request, "/", :body => "foo bar"
109
+ it "uses Basic Authentication when initialized with :user and :password" do
110
+ stub_request :get, @uri.to_s
146
111
 
147
- assert_requested :get, url, :body => "foo bar"
148
- end
149
- end
112
+ @client.get @uri.path
113
+
114
+ assert_requested :get, @uri.to_s
150
115
  end
151
116
  end
152
117
 
153
- describe "headers" do
154
- describe "authentication" do
155
- it "uses basic auth when providing user and password" do
156
- stub_request :get, url("/", "user:credentials")
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
- instance.send :response_for, @request, "/", {}
160
- assert_requested :get, url("/", "user:credentials")
123
+ response = @client.get @uri.path
124
+
125
+ response.body['foo'].must_equal "bar"
161
126
  end
127
+ end
162
128
 
163
- it "doesn't use basic auth without a user" do
164
- assert_doesnt_use_basic_auth_without :user
129
+ describe "xml" do
130
+ before do
131
+ @client = Hugs::Client.new :host => @uri.host, :type => :xml
165
132
  end
166
133
 
167
- it "doesn't use basic auth without a password" do
168
- assert_doesnt_use_basic_auth_without :password
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
- describe "JSON" do
173
- it "supports json GET" do
174
- assert_supports_http_verb :json, :get
175
- end
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
- it "supports json DELETE" do
178
- assert_supports_http_verb :json, :delete
179
- end
149
+ @client.post @uri.path, :body => { :foo => :bar }
180
150
 
181
- it "supports json POST" do
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
- it "supports json PUT" do
186
- assert_supports_http_verb :json, :put
155
+ describe "xml" do
156
+ before do
157
+ @client = Hugs::Client.new :host => @uri.host, :type => :xml
187
158
  end
188
159
 
189
- it "supports json HEAD" do
190
- assert_supports_http_verb :json, :head
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
- describe "XML" do
195
- it "supports xml GET" do
196
- assert_supports_http_verb :xml, :get
197
- end
170
+ describe "upload" do
171
+ describe "chunked" do
172
+ before do
173
+ @upload = {
174
+ :upload => { :file => "/dev/null" }
175
+ }
198
176
 
199
- it "supports xml DELETE" do
200
- assert_supports_http_verb :xml, :delete
177
+ @upload_with_headers = {
178
+ :upload => {
179
+ :file => "/dev/null",
180
+ :headers => { "Accept" => "foo bar" }
181
+ }
182
+ }
201
183
  end
202
184
 
203
- it "supports xml POST" do
204
- assert_supports_http_verb :xml, :post
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 "supports xml PUT" do
208
- assert_supports_http_verb :xml, :put
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 "supports xml HEAD" do
212
- assert_supports_http_verb :xml, :head
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 "debug_output" do
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
- @instance.debug = true
213
+ out, _ = capture_io {
214
+ client = Hugs::Client.new :host => "gist.github.com", :debug => true
229
215
 
230
- out, err = capture_io { @instance.send :response_for, Net::HTTP::Get, "/api/v1/json/374130", {} }
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, err = capture_io { @instance.send :response_for, Net::HTTP::Get, "/api/v1/json/374130", {} }
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 assert_supports_http_verb type, verb
244
- mimetype = {:xml => 'application/xml',
245
- :json => 'application/json'}[type]
237
+ def must_support_http_get
238
+ must_support_http :get
239
+ end
246
240
 
247
- assert mimetype, "Unsupported Mimetype '#{type}'"
241
+ def must_support_http_head
242
+ must_support_http :head
243
+ end
248
244
 
249
- clazz = Net::HTTP.const_get verb.capitalize
245
+ def must_support_http_post
246
+ must_support_http :post
247
+ end
250
248
 
251
- stub_request verb, url
252
- instance = Hugs::Client.new valid_options(:type => type)
249
+ def must_support_http_put
250
+ must_support_http :put
251
+ end
253
252
 
254
- instance.send :response_for, clazz, "/", {}
253
+ def must_support_http verb
254
+ stub_request verb, @uri.to_s
255
+ @client.send verb, @uri.path
255
256
 
256
- headers = { "Accept" => ["*/*", mimetype] }
257
- headers["Content-Type"] = mimetype if [:put, :put].include? type
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
- assert_requested verb, url, :headers => headers
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 assert_doesnt_use_basic_auth_without option
263
- stub_request :get, url
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
- instance.send :response_for, @request, "/", {}
275
+ @client.send verb, @uri.path, options
267
276
 
268
- assert_requested :get, url
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
- @instance = Hugs::Client.new valid_options
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 "response codes" do
11
- Errors_4xx = (400..417).to_a << 422
12
- Errors_5xx = (500..504).to_a
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 "does not raise on #{code}" do
20
- assert_does_not_raise_on code
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
- def assert_raises_on code
25
- (code.to_s =~ %r{^4[0-9]{2}$}) ? (@instance.raise_4xx = true) : (@instance.raise_5xx = true)
26
- stub_request(:get, url).to_return :status => [code, "error #{code}"]
26
+ describe "raises" do
27
+ before do
28
+ @client = Hugs::Client.new :host => @uri.host, :raise_errors => true
29
+ end
27
30
 
28
- begin
29
- @instance.send :response_for, Net::HTTP::Get, "/", {}
30
- rescue => e
31
- e.class.superclass.must_be_same_as Hugs::Errors::HTTPStatusError
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
- def assert_does_not_raise_on code
39
- stub_request(:get, url).to_return :status => [code, "error #{code}"]
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
- response.code.must_equal code.to_s
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.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-04-07 00:00:00 -07:00
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