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 +4 -0
- data/hal-client.gemspec +2 -2
- data/lib/hal_client/errors.rb +19 -0
- data/lib/hal_client/version.rb +1 -1
- data/lib/hal_client.rb +109 -25
- data/spec/hal_client_spec.rb +54 -4
- metadata +15 -9
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 "
|
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.
|
28
|
+
spec.add_development_dependency "webmock", ["~> 1.17", ">= 1.17.4"]
|
29
29
|
spec.add_development_dependency "rspec-collection_matchers"
|
30
30
|
end
|
data/lib/hal_client/errors.rb
CHANGED
@@ -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
|
data/lib/hal_client/version.rb
CHANGED
data/lib/hal_client.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require "hal_client/version"
|
2
|
-
require '
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
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
|
-
#
|
35
|
-
def get(url,
|
36
|
-
|
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
|
-
#
|
45
|
-
def post(url, data,
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
148
|
+
def default_entity_request_headers
|
149
|
+
@default_entity_request_headers
|
66
150
|
end
|
67
151
|
|
68
|
-
def
|
69
|
-
|
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
|
data/spec/hal_client_spec.rb
CHANGED
@@ -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' =>
|
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' =>
|
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' =>
|
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' =>
|
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:
|
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-
|
12
|
+
date: 2014-05-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
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:
|
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:
|
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.
|
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.
|
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:
|
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:
|
202
|
+
hash: -442637899348440832
|
197
203
|
requirements: []
|
198
204
|
rubyforge_project:
|
199
205
|
rubygems_version: 1.8.23
|