hal-client 2.5.0 → 3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -131,6 +131,10 @@ Or install it yourself as:
131
131
 
132
132
  $ gem install hal-client
133
133
 
134
+ ## Upgrading from 2.x to 3.x
135
+
136
+ For most uses no change to client code is required. At 3.0 the underlying HTTP library changed to <https://rubygems.org/gems/http> to better support our parallelism needs. This changes the interface of `#get` and `#post` on `HalClient` and `HalClient::Representation` in the situation where the response is not a valid HAL document. In those situations the return value is now a `HTTP::Response` object, rather than a `RestClient::Response`.
137
+
134
138
  ## Upgrading from 1.x to 2.x
135
139
 
136
140
  The signature of `HalClient::Representation#new` changed such that keyword arguments are required. Any direct uses of that method must be changed. This is the only breaking change.
data/hal-client.gemspec CHANGED
@@ -18,13 +18,13 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "rest-client", "~> 1.6"
21
+ spec.add_dependency "http", "~> 0.6.1"
22
22
  spec.add_dependency "addressable", "~> 2.3"
23
23
  spec.add_dependency "multi_json", "~> 1.9"
24
24
 
25
25
  spec.add_development_dependency "bundler", "~> 1.5"
26
26
  spec.add_development_dependency "rake", "~> 10.1"
27
27
  spec.add_development_dependency "rspec", "~> 3.0.0.beta"
28
- spec.add_development_dependency "webmock", "~> 1.16"
28
+ spec.add_development_dependency "webmock", ["~> 1.17", ">= 1.17.4"]
29
29
  spec.add_development_dependency "rspec-collection_matchers"
30
30
  end
@@ -1,4 +1,23 @@
1
1
  class HalClient
2
+ # The representation is not a valid HAL document.
2
3
  InvalidRepresentationError = Class.new(StandardError)
4
+
5
+ # The representation is not a HAL collection
3
6
  NotACollectionError = Class.new(StandardError)
7
+
8
+ # Server responded with a non-200 status code
9
+ class HttpError < StandardError
10
+ def initialize(message, response)
11
+ @response = response
12
+ super(message)
13
+ end
14
+
15
+ attr_reader :response
16
+ end
17
+
18
+ # Server response with a 4xx status code
19
+ HttpClientError = Class.new(HttpError)
20
+
21
+ # Server responded with a 5xx status code
22
+ HttpServerError = Class.new(HttpError)
4
23
  end
@@ -1,3 +1,3 @@
1
1
  class HalClient
2
- VERSION = "2.5.0"
2
+ VERSION = "3.0"
3
3
  end
data/lib/hal_client.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require "hal_client/version"
2
- require 'rest-client'
2
+ require 'http'
3
3
  require 'multi_json'
4
4
 
5
5
  # Adapter used to access resources.
@@ -11,6 +11,9 @@ class HalClient
11
11
  autoload :Collection, 'hal_client/collection'
12
12
  autoload :InvalidRepresentationError, 'hal_client/errors'
13
13
  autoload :NotACollectionError, 'hal_client/errors'
14
+ autoload :HttpError, 'hal_client/errors'
15
+ autoload :HttpClientError, 'hal_client/errors'
16
+ autoload :HttpServerError, 'hal_client/errors'
14
17
 
15
18
  # Initializes a new client instance
16
19
  #
@@ -21,52 +24,133 @@ class HalClient
21
24
  # prepended to the `Content-Type` header field of each request.
22
25
  # :headers - a hash of other headers to send on each request.
23
26
  def initialize(options={})
24
- accept = options.fetch(:accept, 'application/hal+json')
25
- content_type = options.fetch(:content_type, 'application/hal+json')
26
- headers = options.fetch(:headers, {})
27
+ @default_message_request_headers = HTTP::Headers.new
28
+ @default_entity_request_headers = HTTP::Headers.new
29
+
30
+ default_message_request_headers.set('Accept', options[:accept]) if
31
+ options[:accept]
32
+ # Explicit accept option has precedence over accepts in the
33
+ # headers option.
34
+
35
+ options.fetch(:headers, {}).each do |name, value|
36
+ if entity_header_field? name
37
+ default_entity_request_headers.add(name, value)
38
+ else
39
+ default_message_request_headers.add(name, value)
40
+ end
41
+ end
42
+
43
+ default_entity_request_headers.set('Content-Type', options[:content_type]) if
44
+ options[:content_type]
45
+ # Explicit content_content options has precedence over content
46
+ # type in the headers option.
47
+
48
+ default_entity_request_headers.set('Content-Type', 'application/hal+json') unless
49
+ default_entity_request_headers['Content-Type']
50
+ # We always want a content type. If the user doesn't explicitly
51
+ # specify one we provide a default.
27
52
 
28
- @headers = {accept: accept, content_type: content_type}.merge(headers)
53
+ accept_values = Array(default_message_request_headers.get('Accept')) +
54
+ ['application/hal+json;q=0']
55
+ default_message_request_headers.set('Accept', accept_values.join(", "))
56
+ # We can work with HAL so provide a back stop accept.
29
57
  end
30
58
 
31
59
  # Returns a `Representation` of the resource identified by `url`.
32
60
  #
33
61
  # url - The URL of the resource of interest.
34
- # options - set of options to pass to `RestClient#get`
35
- def get(url, options={})
36
- resp = RestClient.get url, get_options(options)
37
- Representation.new hal_client: self, parsed_json: MultiJson.load(resp)
62
+ # headers - custom header fields to use for this request
63
+ def get(url, headers={})
64
+ interpret_response client_for_get(override_headers: headers).get(url)
38
65
  end
39
66
 
40
67
  # Post a `Representation` or `String` to the resource identified at `url`.
41
68
  #
42
69
  # url - The URL of the resource of interest.
43
70
  # data - a `String` or an object that responds to `#to_hal`
44
- # options - set of options to pass to `RestClient#post`
45
- def post(url, data, options={})
46
- resp = RestClient.post url, data, post_options(options)
47
-
48
- begin
49
- Representation.new hal_client: self, parsed_json: MultiJson.load(resp)
50
- rescue MultiJson::ParseError, InvalidRepresentationError => e
51
- resp
52
- end
71
+ # headers - custom header fields to use for this request
72
+ def post(url, data, headers={})
73
+ req_body = if data.respond_to? :to_hal
74
+ data.to_hal
75
+ else
76
+ data
77
+ end
78
+
79
+ interpret_response client_for_post(headers).post(url, body: req_body)
53
80
  end
54
81
 
55
82
  protected
56
83
 
57
84
  attr_reader :headers
58
85
 
59
- # Exclude headers that shouldn't go with a GET
60
- def get_options(overrides)
61
- @cleansed_get_options ||= headers.dup.tap do |get_headers|
62
- get_headers.delete(:content_type)
86
+ def interpret_response(resp)
87
+ case resp.status
88
+ when 200...300
89
+ begin
90
+ Representation.new hal_client: self, parsed_json: MultiJson.load(resp.to_s)
91
+ rescue MultiJson::ParseError, InvalidRepresentationError => e
92
+ resp
93
+ end
94
+
95
+ when 400...500
96
+ raise HttpClientError.new(nil, resp)
97
+
98
+ when 500...600
99
+ raise HttpServerError.new(nil, resp)
100
+
101
+ else
102
+ raise HttpError.new(nil, resp)
103
+
104
+ end
105
+ end
106
+
107
+ # Returns the HTTP client to be used to make get requests.
108
+ #
109
+ # options
110
+ # :override_headers -
111
+ def client_for_get(options={})
112
+ override_headers = options[:override_headers]
113
+
114
+ if !override_headers
115
+ @client_for_get ||= base_client.with_headers(default_message_request_headers)
116
+ else
117
+ client_for_get.with_headers(override_headers)
118
+ end
119
+ end
120
+
121
+ # Returns the HTTP client to be used to make post requests.
122
+ #
123
+ # options
124
+ # :override_headers -
125
+ def client_for_post(options={})
126
+ override_headers = options[:override_headers]
127
+
128
+ if !override_headers
129
+ @client_for_post ||=
130
+ base_client.with_headers(default_entity_and_message_request_headers)
131
+ else
132
+ client_for_post.with_headers(override_headers)
63
133
  end
134
+ end
135
+
136
+ # Returns an HTTP client.
137
+ def base_client
138
+ @base_client ||= HTTP::Client.new
139
+ end
140
+
141
+ attr_reader :default_entity_request_headers, :default_message_request_headers
142
+
143
+ def default_entity_and_message_request_headers
144
+ @default_entity_and_message_request_headers ||=
145
+ default_message_request_headers.merge(default_entity_request_headers)
146
+ end
64
147
 
65
- @cleansed_get_options.merge overrides
148
+ def default_entity_request_headers
149
+ @default_entity_request_headers
66
150
  end
67
151
 
68
- def post_options(overrides)
69
- headers.merge(overrides)
152
+ def entity_header_field?(field_name)
153
+ [:content_type, /^content-type$/i].any?{|pat| pat === field_name}
70
154
  end
71
155
 
72
156
  module EntryPointCovenienceMethods
@@ -7,6 +7,8 @@ describe HalClient do
7
7
  it { should be_kind_of HalClient }
8
8
  end
9
9
 
10
+ subject(:client) { HalClient.new }
11
+
10
12
  describe '.new w/ custom accept' do
11
13
  subject { HalClient.new(accept: "application/vnd.myspecialmediatype") }
12
14
  it { should be_kind_of HalClient }
@@ -25,7 +27,7 @@ describe HalClient do
25
27
  it("should have been made") { should have_been_made }
26
28
 
27
29
  it "sends accept header" do
28
- expect(request.with(headers: {'Accept' => 'application/hal+json'})).
30
+ expect(request.with(headers: {'Accept' => /application\/hal\+json/i})).
29
31
  to have_been_made
30
32
  end
31
33
  end
@@ -33,7 +35,7 @@ describe HalClient do
33
35
  context "explicit accept" do
34
36
  subject(:client) { HalClient.new accept: 'app/test' }
35
37
  it "sends specified accept header" do
36
- expect(request.with(headers: {'Accept' => 'app/test'})).
38
+ expect(request.with(headers: {'Accept' => /app\/test/i})).
37
39
  to have_been_made
38
40
  end
39
41
  end
@@ -41,7 +43,7 @@ describe HalClient do
41
43
  context "explicit content type" do
42
44
  subject(:client) { HalClient.new content_type: 'custom' }
43
45
  it "does not send the content type header" do
44
- expect(request.with(headers: {'Accept' => 'application/hal+json'})).to have_been_made
46
+ expect(request.with(headers: {'Accept' => /application\/hal\+json/i})).to have_been_made
45
47
  end
46
48
  end
47
49
 
@@ -61,6 +63,54 @@ describe HalClient do
61
63
  end
62
64
  end
63
65
 
66
+ context "server responds with client error" do
67
+ let!(:request) { stub_request(:any, "http://example.com/foo").
68
+ to_return body: "Bad client! No cookie!", status: 400 }
69
+
70
+ it "#get raises HttpClientError" do
71
+ expect{client.get "http://example.com/foo"}.to raise_exception HalClient::HttpClientError
72
+ end
73
+
74
+ it "#get attaches response to the raised error" do
75
+ err = client.get("http://example.com/foo") rescue $!
76
+ expect(err.response).to be_kind_of HTTP::Response
77
+ end
78
+
79
+
80
+ it "#post raises HttpClientError" do
81
+ expect{client.post "http://example.com/foo", "foo"}.to raise_exception HalClient::HttpClientError
82
+ end
83
+
84
+ it "#post attaches response to the raise error" do
85
+ err = client.post("http://example.com/foo", "") rescue $!
86
+ expect(err.response).to be_kind_of HTTP::Response
87
+ end
88
+ end
89
+
90
+ context "server responds with server error" do
91
+ let!(:request) { stub_request(:any, "http://example.com/foo").
92
+ to_return body: "Bad server! No cookie!", status: 500 }
93
+
94
+ it "#get raises HttpServerError" do
95
+ expect{client.get "http://example.com/foo"}.to raise_exception HalClient::HttpServerError
96
+ end
97
+
98
+ it "#get attaches response to the raised error" do
99
+ err = client.get("http://example.com/foo") rescue $!
100
+ expect(err.response).to be_kind_of HTTP::Response
101
+ end
102
+
103
+
104
+ it "#post raises HttpServerError" do
105
+ expect{client.post "http://example.com/foo", "foo"}.to raise_exception HalClient::HttpServerError
106
+ end
107
+
108
+ it "#post attaches response to the raise error" do
109
+ err = client.post("http://example.com/foo", "") rescue $!
110
+ expect(err.response).to be_kind_of HTTP::Response
111
+ end
112
+ end
113
+
64
114
  describe ".get(<url>)" do
65
115
  let!(:return_val) { HalClient.get "http://example.com/foo" }
66
116
 
@@ -73,7 +123,7 @@ describe HalClient do
73
123
  it("should have been made") { should have_been_made }
74
124
 
75
125
  it "sends accept header" do
76
- expect(request.with(headers: {'Accept' => 'application/hal+json'})).
126
+ expect(request.with(headers: {'Accept' => /application\/hal\+json/})).
77
127
  to have_been_made
78
128
  end
79
129
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hal-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: '3.0'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,16 +9,16 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-04-29 00:00:00.000000000 Z
12
+ date: 2014-05-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: rest-client
15
+ name: http
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: '1.6'
21
+ version: 0.6.1
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
- version: '1.6'
29
+ version: 0.6.1
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: addressable
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -114,7 +114,10 @@ dependencies:
114
114
  requirements:
115
115
  - - ~>
116
116
  - !ruby/object:Gem::Version
117
- version: '1.16'
117
+ version: '1.17'
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: 1.17.4
118
121
  type: :development
119
122
  prerelease: false
120
123
  version_requirements: !ruby/object:Gem::Requirement
@@ -122,7 +125,10 @@ dependencies:
122
125
  requirements:
123
126
  - - ~>
124
127
  - !ruby/object:Gem::Version
125
- version: '1.16'
128
+ version: '1.17'
129
+ - - ! '>='
130
+ - !ruby/object:Gem::Version
131
+ version: 1.17.4
126
132
  - !ruby/object:Gem::Dependency
127
133
  name: rspec-collection_matchers
128
134
  requirement: !ruby/object:Gem::Requirement
@@ -184,7 +190,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
184
190
  version: '0'
185
191
  segments:
186
192
  - 0
187
- hash: 1272540946785022860
193
+ hash: -442637899348440832
188
194
  required_rubygems_version: !ruby/object:Gem::Requirement
189
195
  none: false
190
196
  requirements:
@@ -193,7 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
193
199
  version: '0'
194
200
  segments:
195
201
  - 0
196
- hash: 1272540946785022860
202
+ hash: -442637899348440832
197
203
  requirements: []
198
204
  rubyforge_project:
199
205
  rubygems_version: 1.8.23