hal-client 1.1.0 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +7 -0
- data/README.md +42 -22
- data/lib/hal_client/curie_resolver.rb +52 -0
- data/lib/hal_client/representation.rb +20 -2
- data/lib/hal_client/version.rb +1 -1
- data/lib/hal_client.rb +19 -0
- data/spec/hal_client/curie_resolver_spec.rb +38 -0
- data/spec/hal_client/representation_spec.rb +61 -0
- data/spec/hal_client_spec.rb +18 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2feba4ea1ae2adc4762239ce875c4d4db3dad541
|
4
|
+
data.tar.gz: d0854b133f4ca25cb6ee21d10e2e7ba8e87d1480
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ac5423a12091b050f6e27b93994b4a4914ab65a1702565b22ab724b45f73baa30a1896402ade0e62c024b1b2cbfacfde689143d41f9246d6900ddf6b723e4fa
|
7
|
+
data.tar.gz: be45454510e90beb073b2b9c8852e59718727ca91ff89cd1da6ef8ccb26d593b9d5a71d604fc02f7f2dad7453515340e8754dab217743bd79729763676ce52a9
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/pezra/hal-client.png?branch=master)](https://travis-ci.org/pezra/hal-client)
|
2
|
+
[![Code Climate](https://codeclimate.com/github/pezra/hal-client.png)](https://codeclimate.com/github/pezra/hal-client)
|
3
|
+
|
1
4
|
# HalClient
|
2
5
|
|
3
|
-
An easy to use interface for REST APIs that use [HAL](http://stateless.co/hal_specification.html).
|
6
|
+
An easy to use client interface for REST APIs that use [HAL](http://stateless.co/hal_specification.html).
|
4
7
|
|
5
8
|
## Installation
|
6
9
|
|
@@ -16,21 +19,13 @@ Or install it yourself as:
|
|
16
19
|
|
17
20
|
$ gem install hal-client
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
The first step to using HalClient is to create a `HalClient` instance.
|
22
|
-
|
23
|
-
my_client = HalClient.new
|
24
|
-
|
25
|
-
If the API uses one or more a custom mime types we can specify that they be included in the `Accept` header field of each request.
|
26
|
-
|
27
|
-
my_client = HalClient.new(accept: "application/vnd.myapp+hal+json")
|
28
|
-
|
29
|
-
### `GET`ting an entry point
|
22
|
+
Usage
|
23
|
+
-----
|
30
24
|
|
31
|
-
|
25
|
+
The first step in using a HAL based API is getting a representation of one of its entry point. The simplest way to do this is using the `get` class method of `HalClient`.
|
32
26
|
|
33
|
-
blog =
|
27
|
+
blog = HalClient.get("http://blog.me/")
|
28
|
+
# => #<Representation: http://blog.me/>
|
34
29
|
|
35
30
|
`HalClient::Representation`s expose a `#property` method to retrieve properties from the HAL document.
|
36
31
|
|
@@ -39,19 +34,36 @@ In normal usage you will rarely use the `HalClient` instance directly. Normally,
|
|
39
34
|
|
40
35
|
### Link navigation
|
41
36
|
|
42
|
-
Once we have a representation we
|
37
|
+
Once we have a representation we will want to navigate its links. This can be accomplished using the `#related` method.
|
43
38
|
|
44
39
|
articles = blog.related("item")
|
45
40
|
# => #<RepresentationSet:...>
|
46
41
|
|
47
|
-
In the example above `item` is the link rel. The `#related` method
|
42
|
+
In the example above `item` is the link rel. The `#related` method extracts embedded representations and dereferences links with the specified rel. The resulting representations and packaged into a `HalClient::RepresentationSet`. `HalClient` always returns `RepresentationSet`s when following links, even when there is only one result as doing so tends to result in simpler client code.
|
48
43
|
|
49
|
-
`RepresentationSet`s are `Enumerable` so they expose all your favorite methods like `#each`, `#map`, `#any?`, etc.
|
44
|
+
`RepresentationSet`s are `Enumerable` so they expose all your favorite methods like `#each`, `#map`, `#any?`, etc. `RepresentationSet`s expose a `#related` method which calls `#related` on each member of the set and then merges the results into a new representation set.
|
50
45
|
|
51
46
|
authors = blog.related("author").related("item")
|
52
47
|
authors.first.property("name")
|
53
48
|
# => "Bob Smith"
|
54
49
|
|
50
|
+
#### CURIEs
|
51
|
+
|
52
|
+
Links specified using a compact URI (or CURIE) as the rel are fully supported. They are accessed using the fully expanded version of the curie. For example, given a representations of an author:
|
53
|
+
|
54
|
+
{ "name": "Bob Smith,
|
55
|
+
"_links": {
|
56
|
+
"so:homeLocation": { "href": "http://example.com/denver" },
|
57
|
+
"curies": [{ "name": "so", "href": "http://schema.org/{rel}", "templated": true }]
|
58
|
+
}}
|
59
|
+
|
60
|
+
Bob's home location can be retrieved with
|
61
|
+
|
62
|
+
author.related("http://schema.org/homeLocation")
|
63
|
+
# => #<Representation: http://example.com/denver>
|
64
|
+
|
65
|
+
Links are always accessed using the full link relation, rather than the CURIE, because the document producer can use any arbitrary string as the prefix. This means that clients must not make any assumptions regarding what prefix will be used because it might change over time or even between documents.
|
66
|
+
|
55
67
|
### Templated links
|
56
68
|
|
57
69
|
The `#related` methods takes a `Hash` as its second argument which is used to expand any templated links that are involved in the navigation.
|
@@ -73,13 +85,21 @@ All `HalClient::Representation`s exposed an `#href` attribute which is its ident
|
|
73
85
|
|
74
86
|
blog['title'] # => "Some Person's Blog"
|
75
87
|
blog['item'] # => #<RepresentationSet:...>
|
76
|
-
|
88
|
+
|
89
|
+
### Custom media types
|
90
|
+
|
91
|
+
If the API uses one or more a custom mime types we can specify that they be included in the `Accept` header field of each request.
|
92
|
+
|
93
|
+
my_client = HalClient.new(accept: "application/vnd.myapp+hal+json")
|
94
|
+
my_client.get("http://blog.me/")
|
95
|
+
# => #<Representation: http://blog.me/>
|
77
96
|
|
78
97
|
## Contributing
|
79
98
|
|
80
99
|
1. Fork it ( http://github.com/pezra/hal-client/fork )
|
81
100
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
82
|
-
3.
|
83
|
-
|
84
|
-
|
85
|
-
|
101
|
+
3. Implement your improvement
|
102
|
+
4. Update `lib/hal_client/version.rb` following [semantic versioning rules](http://semver.org/)
|
103
|
+
5. Commit your changes (`git commit -am 'Add some feature'`)
|
104
|
+
6. Push to the branch (`git push origin my-new-feature`)
|
105
|
+
7. Create new Pull Request
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'addressable/template'
|
2
|
+
|
3
|
+
class HalClient
|
4
|
+
|
5
|
+
# Expands CURIEs to fully qualified URLs using a set curie
|
6
|
+
# definitions.
|
7
|
+
class CurieResolver
|
8
|
+
|
9
|
+
# Initialize new CurieResolver
|
10
|
+
#
|
11
|
+
# curie_defs - Array of curie definition links (per the HAL spec)
|
12
|
+
def initialize(curie_defs)
|
13
|
+
curie_defs = [curie_defs].flatten
|
14
|
+
@namespaces = interpret curie_defs
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns a an expanded version of `curie_or_uri` or the
|
18
|
+
# input. The input is returned when `curie_or_uri` is not a curie
|
19
|
+
# or is a curie whose namespace is not recognized.
|
20
|
+
#
|
21
|
+
# curie_or_uri - the (potential) curie to resolve
|
22
|
+
def resolve(curie_or_uri)
|
23
|
+
ns, short_name = split_curie curie_or_uri
|
24
|
+
|
25
|
+
if ns && (namespaces.has_key? ns)
|
26
|
+
namespaces[ns].expand(rel: short_name).to_s
|
27
|
+
else
|
28
|
+
curie_or_uri
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
attr_reader :namespaces
|
34
|
+
|
35
|
+
def split_curie(a_curie)
|
36
|
+
curie_parts = /(?<ns>[^:]+):(?<short_name>.+)/.match(a_curie)
|
37
|
+
|
38
|
+
if curie_parts
|
39
|
+
[curie_parts[:ns], curie_parts[:short_name]]
|
40
|
+
else
|
41
|
+
[nil,nil]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def interpret(curie_defs)
|
46
|
+
Hash[curie_defs.map{|it|
|
47
|
+
[it["name"], Addressable::Template.new(it["href"])]
|
48
|
+
}]
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -131,17 +131,23 @@ class HalClient
|
|
131
131
|
end
|
132
132
|
end
|
133
133
|
|
134
|
+
# Returns a short human readable description of this
|
135
|
+
# representation.
|
136
|
+
def to_s
|
137
|
+
"#<" + self.class.name + ": " + href + ">"
|
138
|
+
end
|
139
|
+
|
134
140
|
protected
|
135
141
|
attr_reader :raw, :hal_client
|
136
142
|
|
137
143
|
MISSING = Object.new
|
138
144
|
|
139
145
|
def link_section
|
140
|
-
@link_section ||= raw.fetch("_links", {})
|
146
|
+
@link_section ||= fully_qualified raw.fetch("_links", {})
|
141
147
|
end
|
142
148
|
|
143
149
|
def embedded_section
|
144
|
-
@embedded_section ||= raw.fetch("_embedded", {})
|
150
|
+
@embedded_section ||= fully_qualified raw.fetch("_embedded", {})
|
145
151
|
end
|
146
152
|
|
147
153
|
def embedded(link_rel)
|
@@ -177,5 +183,17 @@ class HalClient
|
|
177
183
|
end
|
178
184
|
end
|
179
185
|
|
186
|
+
def fully_qualified(relations_section)
|
187
|
+
Hash[relations_section.map {|rel, link_info|
|
188
|
+
[(namespaces.resolve rel), link_info]
|
189
|
+
}]
|
190
|
+
end
|
191
|
+
|
192
|
+
def namespaces
|
193
|
+
@namespaces ||= CurieResolver.new raw.fetch("_links", {}).fetch("curies", [])
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
|
180
198
|
end
|
181
199
|
end
|
data/lib/hal_client/version.rb
CHANGED
data/lib/hal_client.rb
CHANGED
@@ -5,6 +5,7 @@ require 'rest-client'
|
|
5
5
|
class HalClient
|
6
6
|
autoload :Representation, 'hal_client/representation'
|
7
7
|
autoload :RepresentationSet, 'hal_client/representation_set'
|
8
|
+
autoload :CurieResolver, 'hal_client/curie_resolver'
|
8
9
|
|
9
10
|
# Initializes a new client instance
|
10
11
|
#
|
@@ -31,4 +32,22 @@ class HalClient
|
|
31
32
|
def rest_client_options(overrides)
|
32
33
|
{accept: default_accept}.merge overrides
|
33
34
|
end
|
35
|
+
|
36
|
+
module EntryPointCovenienceMethods
|
37
|
+
# Returns a `Representation` of the resource identified by `url`.
|
38
|
+
#
|
39
|
+
# url - The URL of the resource of interest.
|
40
|
+
# options - set of options to pass to `RestClient#get`
|
41
|
+
def get(url, options={})
|
42
|
+
default_client.get(url, options)
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def default_client
|
48
|
+
@default_client ||= self.new
|
49
|
+
end
|
50
|
+
end
|
51
|
+
extend EntryPointCovenienceMethods
|
52
|
+
|
34
53
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
require 'hal_client/curie_resolver'
|
4
|
+
|
5
|
+
describe HalClient::CurieResolver do
|
6
|
+
describe "#new" do
|
7
|
+
it "takes an array of curie definitions" do
|
8
|
+
expect(described_class.new([f_ns, b_ns])).to be_kind_of described_class
|
9
|
+
end
|
10
|
+
|
11
|
+
it "takes a single curie definition" do
|
12
|
+
expect(described_class.new(f_ns)).to be_kind_of described_class
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
subject(:resolver) { described_class.new([f_ns, b_ns]) }
|
17
|
+
|
18
|
+
describe "#resolve" do
|
19
|
+
it "returns rel name given a standard rel name" do
|
20
|
+
expect(resolver.resolve("item")).to eq "item"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns url given a fully qualified url" do
|
24
|
+
expect(resolver.resolve("http://example.com/foo")).to eq "http://example.com/foo"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns expanded url given a curie in known namespace" do
|
28
|
+
expect(resolver.resolve("f:yer")).to eq "foo:yer"
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns unexpanded curie given a curie in unknown namespace" do
|
32
|
+
expect(resolver.resolve("ex:yer")).to eq "ex:yer"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:f_ns) { {"name" => "f", "href" => "foo:{rel}", "templated" => true} }
|
37
|
+
let(:b_ns) { {"name" => "b", "href" => "bar:{rel}", "templated" => true} }
|
38
|
+
end
|
@@ -31,6 +31,12 @@ describe HalClient::Representation do
|
|
31
31
|
HAL
|
32
32
|
subject(:repr) { described_class.new(a_client, MultiJson.load(raw_repr)) }
|
33
33
|
|
34
|
+
describe "#to_s" do
|
35
|
+
subject(:return_val) { repr.to_s }
|
36
|
+
|
37
|
+
it { should eq "#<HalClient::Representation: http://example.com/foo>" }
|
38
|
+
end
|
39
|
+
|
34
40
|
describe "#property" do
|
35
41
|
context "existent" do
|
36
42
|
subject { repr.property "prop1" }
|
@@ -148,6 +154,61 @@ HAL
|
|
148
154
|
end
|
149
155
|
|
150
156
|
|
157
|
+
context "curie links" do
|
158
|
+
let(:raw_repr) { <<-HAL }
|
159
|
+
{ "_links": {
|
160
|
+
"self": { "href": "http://example.com/foo" }
|
161
|
+
,"ex:bar": { "href": "http://example.com/bar" }
|
162
|
+
,"curies": [{"name": "ex", "href": "http://example.com/rels/{rel}", "templated": true}]
|
163
|
+
}
|
164
|
+
}
|
165
|
+
HAL
|
166
|
+
|
167
|
+
describe "#related return value " do
|
168
|
+
subject(:return_val) { repr.related("http://example.com/rels/bar") }
|
169
|
+
it { should include_representation_of "http://example.com/bar" }
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "#[] return value " do
|
173
|
+
subject(:return_val) { repr["http://example.com/rels/bar"] }
|
174
|
+
it { should include_representation_of "http://example.com/bar" }
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "#related_hrefs return value " do
|
178
|
+
subject(:return_val) { repr.related_hrefs("http://example.com/rels/bar") }
|
179
|
+
it { should include "http://example.com/bar" }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
context "curie embedded" do
|
184
|
+
let(:raw_repr) { <<-HAL }
|
185
|
+
{ "_links": {
|
186
|
+
"self": { "href": "http://example.com/foo" }
|
187
|
+
,"curies": {"name": "ex", "href": "http://example.com/rels/{rel}", "templated": true}
|
188
|
+
}
|
189
|
+
,"_embedded": {
|
190
|
+
"ex:embed1": { "_links": { "self": { "href": "http://example.com/embed1" } } }
|
191
|
+
}
|
192
|
+
}
|
193
|
+
HAL
|
194
|
+
|
195
|
+
describe "#related return value " do
|
196
|
+
subject(:return_val) { repr.related("http://example.com/rels/embed1") }
|
197
|
+
it { should include_representation_of "http://example.com/embed1" }
|
198
|
+
end
|
199
|
+
|
200
|
+
describe "#[] return value " do
|
201
|
+
subject(:return_val) { repr["http://example.com/rels/embed1"] }
|
202
|
+
it { should include_representation_of "http://example.com/embed1" }
|
203
|
+
end
|
204
|
+
|
205
|
+
describe "#related_hrefs return value " do
|
206
|
+
subject(:return_val) { repr.related_hrefs("http://example.com/rels/embed1") }
|
207
|
+
it { should include "http://example.com/embed1" }
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
|
151
212
|
|
152
213
|
let(:a_client) { HalClient.new }
|
153
214
|
let!(:bar_request) { stub_identity_request("http://example.com/bar") }
|
data/spec/hal_client_spec.rb
CHANGED
@@ -39,6 +39,24 @@ describe HalClient do
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
+
describe ".get(<url>)" do
|
43
|
+
let!(:return_val) { HalClient.get "http://example.com/foo" }
|
44
|
+
|
45
|
+
it "returns a HalClient::Representation" do
|
46
|
+
expect(return_val).to be_kind_of HalClient::Representation
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "request" do
|
50
|
+
subject { request }
|
51
|
+
it("should have been made") { should have_been_made }
|
52
|
+
|
53
|
+
it "sends accept header" do
|
54
|
+
expect(request.with(headers: {'Accept' => 'application/hal+json'})).
|
55
|
+
to have_been_made
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
42
60
|
let!(:request) { stub_request(:get, "http://example.com/foo").
|
43
61
|
to_return body: "{}" }
|
44
62
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hal-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1
|
4
|
+
version: 1.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-02-
|
11
|
+
date: 2014-02-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rest-client
|
@@ -152,6 +152,7 @@ extensions: []
|
|
152
152
|
extra_rdoc_files: []
|
153
153
|
files:
|
154
154
|
- ".gitignore"
|
155
|
+
- ".travis.yml"
|
155
156
|
- Gemfile
|
156
157
|
- LICENSE.txt
|
157
158
|
- README.md
|
@@ -159,9 +160,11 @@ files:
|
|
159
160
|
- hal-client.gemspec
|
160
161
|
- lib/hal-client.rb
|
161
162
|
- lib/hal_client.rb
|
163
|
+
- lib/hal_client/curie_resolver.rb
|
162
164
|
- lib/hal_client/representation.rb
|
163
165
|
- lib/hal_client/representation_set.rb
|
164
166
|
- lib/hal_client/version.rb
|
167
|
+
- spec/hal_client/curie_resolver_spec.rb
|
165
168
|
- spec/hal_client/representation_set_spec.rb
|
166
169
|
- spec/hal_client/representation_spec.rb
|
167
170
|
- spec/hal_client_spec.rb
|
@@ -191,6 +194,7 @@ signing_key:
|
|
191
194
|
specification_version: 4
|
192
195
|
summary: Use HAL APIs easily
|
193
196
|
test_files:
|
197
|
+
- spec/hal_client/curie_resolver_spec.rb
|
194
198
|
- spec/hal_client/representation_set_spec.rb
|
195
199
|
- spec/hal_client/representation_spec.rb
|
196
200
|
- spec/hal_client_spec.rb
|