hal-client 2.5.0 → 3.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/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