hal-client 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +85 -0
- data/Rakefile +6 -0
- data/hal-client.gemspec +29 -0
- data/lib/hal-client.rb +1 -0
- data/lib/hal_client/representation.rb +181 -0
- data/lib/hal_client/representation_set.rb +34 -0
- data/lib/hal_client/version.rb +3 -0
- data/lib/hal_client.rb +34 -0
- data/spec/hal_client/representation_set_spec.rb +114 -0
- data/spec/hal_client/representation_spec.rb +172 -0
- data/spec/hal_client_spec.rb +44 -0
- data/spec/spec_helper.rb +5 -0
- metadata +197 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a22811cd4e0f71ae8fe9767f40314c6c070a603b
|
4
|
+
data.tar.gz: de7b7a1b94bc47dd3a1da179d2241ce85e337eb0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6d75d90c76548319814b03a18d7c8518609ef4911325d59c686a4ad63f5aba75933254fd70b850d05bb7b008504f5a8245a650841bc34046d99fb051aef048d8
|
7
|
+
data.tar.gz: 9cf40c295ddc43e1083a45e67c74a75fb901ebe793bfb0a097acbcd9f93fc5d4bfc74b50037e5a3750b890b3254780df2141776a27ec449a94c1798004889f04
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Peter Williams
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# HalClient
|
2
|
+
|
3
|
+
An easy to use interface for REST APIs that use [HAL](http://stateless.co/hal_specification.html).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'hal-client'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install hal-client
|
18
|
+
|
19
|
+
## Usage
|
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
|
30
|
+
|
31
|
+
In normal usage you will rarely use the `HalClient` instance directly. Normally, you will traverse links on a representation which uses the `HalClient` indirectly. Getting API entry points is main use for the HalClient instance.
|
32
|
+
|
33
|
+
blog = my_client.get("http://blog.me/")
|
34
|
+
|
35
|
+
`HalClient::Representation`s expose a `#property` method to retrieve properties from the HAL document.
|
36
|
+
|
37
|
+
blog.property('title')
|
38
|
+
#=> "Some Person's Blog"
|
39
|
+
|
40
|
+
### Link navigation
|
41
|
+
|
42
|
+
Once we have a representation we are going to need to navigate its links. This can be accomplished by using the `#related` method.
|
43
|
+
|
44
|
+
articles = blog.related("item")
|
45
|
+
# => #<RepresentationSet:...>
|
46
|
+
|
47
|
+
In the example above `item` is the link rel. The `#related` method looks up both embedded representations and links with the rel of `item`. Links are then dereferenced using the same `HalClient` instance used to retrieve the entry point. The dereferenced links and extracted embedded representations are converted into individual `HalClient::Representation`s 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
|
+
|
49
|
+
`RepresentationSet`s are `Enumerable` so they expose all your favorite methods like `#each`, `#map`, `#any?`, etc. Additionally, `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
|
+
|
51
|
+
authors = blog.related("author").related("item")
|
52
|
+
authors.first.property("name")
|
53
|
+
# => "Bob Smith"
|
54
|
+
|
55
|
+
### Templated links
|
56
|
+
|
57
|
+
The `#related` methods takes a `Hash` as its second argument which is used to expand any templated links that are involved in the navigation.
|
58
|
+
|
59
|
+
old_articles = blog.related("index", before: "2013-02-03T12:30:00Z")
|
60
|
+
# => #<RepresentationSet:...>
|
61
|
+
|
62
|
+
Assuming there is a templated link with a `before` variable this will result in a request being made to `http://blog.me/archive?before=2013-02-03T12:30:00Z`, the response parsed into a `HalClient::Representation` and that being wrapped in a representation set. Any options for which there is not a matching variable in the link's template will be ignored. Any links with that rel that are not templates will be dereferenced normally.
|
63
|
+
|
64
|
+
### Identity
|
65
|
+
|
66
|
+
All `HalClient::Representation`s exposed an `#href` attribute which is its identity. The value is extracted from the `self` link in the underlying HAL document.
|
67
|
+
|
68
|
+
blog.href # => "http://blog.me/"
|
69
|
+
|
70
|
+
### Hash like interface
|
71
|
+
|
72
|
+
`Representation`s expose a `Hash` like interface. Properties, and related representations can be retrieved using the `#[]` and `#fetch` method.
|
73
|
+
|
74
|
+
blog['title'] # => "Some Person's Blog"
|
75
|
+
blog['item'] # => #<RepresentationSet:...>
|
76
|
+
|
77
|
+
|
78
|
+
## Contributing
|
79
|
+
|
80
|
+
1. Fork it ( http://github.com/pezra/hal-client/fork )
|
81
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
82
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
83
|
+
3. Update `lib/hal_client/version.rb` following [semantic versioning rules](http://semver.org/)
|
84
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
85
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/hal-client.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hal_client/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "hal-client"
|
8
|
+
spec.version = HalClient::VERSION
|
9
|
+
spec.authors = ["Peter Williams"]
|
10
|
+
spec.email = ["pezra@barelyenough.org"]
|
11
|
+
spec.summary = %q{Use HAL APIs easily}
|
12
|
+
spec.description = %q{An easy to use interface for REST APIs that use HAL.}
|
13
|
+
spec.homepage = "https://github.com/pezra/hal-client"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "rest-client", "~> 1.6", '>= 1.6.0'
|
22
|
+
spec.add_dependency "addressable", "~> 2.3", '>= 2.3.0'
|
23
|
+
spec.add_dependency "multi_json", "~> 1.8", '>= 1.8.0'
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.1", '>= 10.1.0'
|
27
|
+
spec.add_development_dependency "rspec", "~> 2.14", '>= 2.14.0'
|
28
|
+
spec.add_development_dependency "webmock", "~> 1.16", '>= 1.16.0'
|
29
|
+
end
|
data/lib/hal-client.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'hal_client'
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'addressable/template'
|
3
|
+
|
4
|
+
require 'hal_client'
|
5
|
+
require 'hal_client/representation_set'
|
6
|
+
|
7
|
+
class HalClient
|
8
|
+
|
9
|
+
# HAL representation of a single resource. Provides access to
|
10
|
+
# properties, links and embedded representations.
|
11
|
+
class Representation
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
# Create a new Representation
|
15
|
+
#
|
16
|
+
# hal_client - The HalClient instance to use when navigating.
|
17
|
+
# parsed_json - A hash structure representing a single HAL
|
18
|
+
# document.
|
19
|
+
def initialize(hal_client, parsed_json)
|
20
|
+
@hal_client = hal_client
|
21
|
+
@raw = parsed_json
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns The value of the specified property or the specified
|
25
|
+
# default value.
|
26
|
+
#
|
27
|
+
# name - The name of property of interest
|
28
|
+
# default - an optional object that should be return if the
|
29
|
+
# specified property does not exist
|
30
|
+
# default_proc - an option proc that will be called with `name`
|
31
|
+
# to produce default value if the specified property does not
|
32
|
+
# exist
|
33
|
+
#
|
34
|
+
# Raises KeyError if the specified property does not exist
|
35
|
+
# and no default nor default_proc is provided.
|
36
|
+
def property(name, default=MISSING, &default_proc)
|
37
|
+
default_proc ||= ->(_){ default} if default != MISSING
|
38
|
+
|
39
|
+
raw.fetch(name.to_s, &default_proc)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns the URL of the resource this representation represents.
|
43
|
+
def href
|
44
|
+
link_section.fetch("self").fetch("href")
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the value of the specified property or representations
|
48
|
+
# of resources related via the specified link rel or the
|
49
|
+
# specified default value.
|
50
|
+
#
|
51
|
+
# name_or_rel - The name of property or link rel of interest
|
52
|
+
# default - an optional object that should be return if the
|
53
|
+
# specified property or link does not exist
|
54
|
+
# default_proc - an option proc that will be called with `name`
|
55
|
+
# to produce default value if the specified property or link does not
|
56
|
+
# exist
|
57
|
+
#
|
58
|
+
# Raises KeyError if the specified property or link does not exist
|
59
|
+
# and no default nor default_proc is provided.
|
60
|
+
def fetch(name_or_rel, default=MISSING, &default_proc)
|
61
|
+
item_key = name_or_rel
|
62
|
+
default_proc ||= ->(_){default} if default != MISSING
|
63
|
+
|
64
|
+
property(item_key) {
|
65
|
+
related(item_key, &default_proc)
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the value of the specified property or representations
|
70
|
+
# of resources related via the specified link rel or nil
|
71
|
+
#
|
72
|
+
# name_or_rel - The name of property or link rel of interest
|
73
|
+
def [](name_or_rel)
|
74
|
+
item_key = name_or_rel
|
75
|
+
fetch(item_key, nil)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns representations of resources related via the specified
|
79
|
+
# link rel or the specified default value.
|
80
|
+
#
|
81
|
+
# name_or_rel - The name of property or link rel of interest
|
82
|
+
# options - optional keys and values with which to expand any
|
83
|
+
# templated links that are encountered
|
84
|
+
# default_proc - an option proc that will be called with `name`
|
85
|
+
# to produce default value if the specified property or link does not
|
86
|
+
# exist
|
87
|
+
#
|
88
|
+
# Raises KeyError if the specified link does not exist
|
89
|
+
# and no default_proc is provided.
|
90
|
+
def related(link_rel, options = {}, &default_proc)
|
91
|
+
default_proc ||= ->(link_rel){
|
92
|
+
raise KeyError, "No resources are related via `#{link_rel}`"
|
93
|
+
}
|
94
|
+
|
95
|
+
embedded = embedded(link_rel) rescue nil
|
96
|
+
linked = linked(link_rel, options) rescue nil
|
97
|
+
|
98
|
+
if !embedded.nil? or !linked.nil?
|
99
|
+
RepresentationSet.new (Array(embedded) + Array(linked))
|
100
|
+
else
|
101
|
+
default_proc.call link_rel
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns urls of resources related via the specified
|
106
|
+
# link rel or the specified default value.
|
107
|
+
#
|
108
|
+
# name_or_rel - The name of property or link rel of interest
|
109
|
+
# options - optional keys and values with which to expand any
|
110
|
+
# templated links that are encountered
|
111
|
+
# default_proc - an option proc that will be called with `name`
|
112
|
+
# to produce default value if the specified property or link does not
|
113
|
+
# exist
|
114
|
+
#
|
115
|
+
# Raises KeyError if the specified link does not exist
|
116
|
+
# and no default_proc is provided.
|
117
|
+
def related_hrefs(link_rel, options={}, &default_proc)
|
118
|
+
default_proc ||= ->(link_rel){
|
119
|
+
raise KeyError, "No resources are related via `#{link_rel}`"
|
120
|
+
}
|
121
|
+
|
122
|
+
embedded = boxed embedded_section.fetch(link_rel, nil)
|
123
|
+
linked = boxed link_section.fetch(link_rel, nil)
|
124
|
+
|
125
|
+
if !embedded.nil? or !linked.nil?
|
126
|
+
Array(embedded).map{|it| it.fetch("_links").fetch("self").fetch("href") rescue nil} +
|
127
|
+
Array(linked).map{|it| it.fetch("href", nil) }.
|
128
|
+
compact
|
129
|
+
else
|
130
|
+
default_proc.call link_rel
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
protected
|
135
|
+
attr_reader :raw, :hal_client
|
136
|
+
|
137
|
+
MISSING = Object.new
|
138
|
+
|
139
|
+
def link_section
|
140
|
+
@link_section ||= raw.fetch("_links", {})
|
141
|
+
end
|
142
|
+
|
143
|
+
def embedded_section
|
144
|
+
@embedded_section ||= raw.fetch("_embedded", {})
|
145
|
+
end
|
146
|
+
|
147
|
+
def embedded(link_rel)
|
148
|
+
relations = boxed embedded_section.fetch(link_rel)
|
149
|
+
|
150
|
+
relations.map{|it| Representation.new hal_client, it}
|
151
|
+
end
|
152
|
+
|
153
|
+
def linked(link_rel, options)
|
154
|
+
relations = boxed link_section.fetch(link_rel)
|
155
|
+
|
156
|
+
relations.
|
157
|
+
map {|link| href_from link, options }.
|
158
|
+
map {|href| hal_client.get href }
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
def boxed(list_hash_or_nil)
|
163
|
+
if Hash === list_hash_or_nil
|
164
|
+
[list_hash_or_nil]
|
165
|
+
else
|
166
|
+
list_hash_or_nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def href_from(link, options)
|
171
|
+
raw_href = link.fetch('href')
|
172
|
+
|
173
|
+
if link.fetch('templated', false)
|
174
|
+
Addressable::Template.new(raw_href).expand(options).to_s
|
175
|
+
else
|
176
|
+
raw_href
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class HalClient
|
2
|
+
|
3
|
+
# A collection HAL representations
|
4
|
+
class RepresentationSet
|
5
|
+
include Enumerable
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def initialize(reprs)
|
9
|
+
@reprs = reprs
|
10
|
+
end
|
11
|
+
|
12
|
+
def_delegators :reprs, :each, :count, :empty?, :any?
|
13
|
+
|
14
|
+
# Returns representations of resources related via the specified
|
15
|
+
# link rel or the specified default value.
|
16
|
+
#
|
17
|
+
# name_or_rel - The name of property or link rel of interest
|
18
|
+
# options - optional keys and values with which to expand any
|
19
|
+
# templated links that are encountered
|
20
|
+
# default_proc - an option proc that will be called with `name`
|
21
|
+
# to produce default value if the specified property or link does not
|
22
|
+
# exist
|
23
|
+
#
|
24
|
+
# Raises KeyError if the specified link does not exist
|
25
|
+
# and no default_proc is provided.
|
26
|
+
def related(link_rel, options={})
|
27
|
+
RepresentationSet.new flat_map{|it| it.related(link_rel, options){[]}.to_a }
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
attr_reader :reprs
|
33
|
+
end
|
34
|
+
end
|
data/lib/hal_client.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require "hal_client/version"
|
2
|
+
require 'rest-client'
|
3
|
+
|
4
|
+
# Adapter used to access resources.
|
5
|
+
class HalClient
|
6
|
+
autoload :Representation, 'hal_client/representation'
|
7
|
+
autoload :RepresentationSet, 'hal_client/representation_set'
|
8
|
+
|
9
|
+
# Initializes a new client instance
|
10
|
+
#
|
11
|
+
# options - hash of configuration options
|
12
|
+
# :accept - one or more content types that should be
|
13
|
+
# prepended to the `Accept` header field of each request.
|
14
|
+
def initialize(options={})
|
15
|
+
@default_accept = options.fetch(:accept, 'application/hal+json')
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns a `Representation` of the resource identified by `url`.
|
19
|
+
#
|
20
|
+
# url - The URL of the resource of interest.
|
21
|
+
# options - set of options to pass to `RestClient#get`
|
22
|
+
def get(url, options={})
|
23
|
+
resp = RestClient.get url, rest_client_options(options)
|
24
|
+
Representation.new self, MultiJson.load(resp)
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
attr_reader :default_accept
|
30
|
+
|
31
|
+
def rest_client_options(overrides)
|
32
|
+
{accept: default_accept}.merge overrides
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
require 'hal_client'
|
4
|
+
require 'hal_client/representation'
|
5
|
+
require 'hal_client/representation_set'
|
6
|
+
|
7
|
+
describe HalClient::RepresentationSet do
|
8
|
+
describe "#new" do
|
9
|
+
let!(:return_val) { described_class.new([foo_repr, bar_repr]) }
|
10
|
+
it { should be_kind_of described_class }
|
11
|
+
it { should have(2).items }
|
12
|
+
end
|
13
|
+
|
14
|
+
subject(:repr_set) { described_class.new([foo_repr, bar_repr]) }
|
15
|
+
|
16
|
+
describe "#each" do
|
17
|
+
it "iterates over each item in the set" do
|
18
|
+
seen = []
|
19
|
+
subject.each {|it| seen << it}
|
20
|
+
expect(seen).to match_array [foo_repr, bar_repr]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
its(:count) { should eq 2 }
|
25
|
+
its(:empty?) { should be_false }
|
26
|
+
|
27
|
+
describe "#any?" do
|
28
|
+
it "returns true if there are any" do
|
29
|
+
expect(subject.any?{|it| it == foo_repr }).to be_true
|
30
|
+
end
|
31
|
+
it "returns false if there aren't any" do
|
32
|
+
expect(subject.any?{|it| false }).to be_false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#related" do
|
37
|
+
context "single target in each member" do
|
38
|
+
subject(:returned_val) { repr_set.related("spouse") }
|
39
|
+
it { should include_representation_of "http://example.com/foo-spouse" }
|
40
|
+
it { should include_representation_of "http://example.com/bar-spouse" }
|
41
|
+
it { should have(2).items }
|
42
|
+
end
|
43
|
+
context "multiple targets" do
|
44
|
+
subject(:returned_val) { repr_set.related("sibling") }
|
45
|
+
it { should include_representation_of "http://example.com/foo-brother" }
|
46
|
+
it { should include_representation_of "http://example.com/foo-sister" }
|
47
|
+
it { should include_representation_of "http://example.com/bar-brother" }
|
48
|
+
it { should have(3).items }
|
49
|
+
end
|
50
|
+
context "templated" do
|
51
|
+
subject(:returned_val) { repr_set.related("cousin", distance: "first") }
|
52
|
+
it { should include_representation_of "http://example.com/foo-first-cousin" }
|
53
|
+
it { should include_representation_of "http://example.com/bar-paternal-first-cousin" }
|
54
|
+
it { should include_representation_of "http://example.com/bar-maternal-first-cousin" }
|
55
|
+
it { should have(3).items }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
let(:a_client) { HalClient.new }
|
60
|
+
|
61
|
+
let(:foo_repr) { HalClient::Representation.new a_client, MultiJson.load(foo_hal)}
|
62
|
+
let(:foo_hal) { <<-HAL }
|
63
|
+
{ "_links":{
|
64
|
+
"self": { "href":"http://example.com/foo" }
|
65
|
+
,"cousin": { "href": "http://example.com/foo-{distance}-cousin"
|
66
|
+
,"templated": true }
|
67
|
+
}
|
68
|
+
,"_embedded": {
|
69
|
+
"spouse": { "_links": { "self": { "href": "http://example.com/foo-spouse"}}}
|
70
|
+
,"sibling": [{ "_links": { "self": { "href": "http://example.com/foo-brother"}}}
|
71
|
+
,{ "_links": { "self": { "href": "http://example.com/foo-sister"}}}]
|
72
|
+
}
|
73
|
+
}
|
74
|
+
HAL
|
75
|
+
|
76
|
+
let(:bar_repr) { HalClient::Representation.new a_client, MultiJson.load(bar_hal) }
|
77
|
+
let(:bar_hal) { <<-HAL }
|
78
|
+
{ "_links":{
|
79
|
+
"self": { "href":"http://example.com/bar" }
|
80
|
+
,"cousin": [{ "href": "http://example.com/bar-maternal-{distance}-cousin"
|
81
|
+
,"templated": true }
|
82
|
+
,{ "href": "http://example.com/bar-paternal-{distance}-cousin"
|
83
|
+
,"templated": true }]
|
84
|
+
}
|
85
|
+
,"_embedded": {
|
86
|
+
"spouse": { "_links": { "self": { "href": "http://example.com/bar-spouse"}}}
|
87
|
+
,"sibling": { "_links": { "self": { "href": "http://example.com/bar-brother"}}}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
HAL
|
91
|
+
|
92
|
+
let!(:foo_cousin_request) {
|
93
|
+
stub_identity_request "http://example.com/foo-first-cousin" }
|
94
|
+
let!(:bar_maternal_cousin_request) {
|
95
|
+
stub_identity_request "http://example.com/bar-maternal-first-cousin" }
|
96
|
+
let!(:bar_paternal_cousin_request) {
|
97
|
+
stub_identity_request "http://example.com/bar-paternal-first-cousin" }
|
98
|
+
|
99
|
+
def stub_identity_request(url)
|
100
|
+
stub_request(:get, url).
|
101
|
+
to_return body: %Q|{"_links":{"self":{"href":#{url.to_json}}}}|
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
RSpec::Matchers.define(:include_representation_of) do |url|
|
106
|
+
match { |repr_set|
|
107
|
+
repr_set.any?{|it| it.href == url}
|
108
|
+
}
|
109
|
+
failure_message_for_should { |repr_set|
|
110
|
+
"Expected representation of <#{url}> but found only #{repr_set.map(&:href)}"
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
require "hal_client/representation"
|
4
|
+
|
5
|
+
describe HalClient::Representation do
|
6
|
+
describe ".new" do
|
7
|
+
let!(:return_val) { described_class.new(a_client, MultiJson.load(raw_repr)) }
|
8
|
+
describe "return_val" do
|
9
|
+
subject { return_val }
|
10
|
+
it { should be_kind_of described_class }
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:raw_repr) { <<-HAL }
|
16
|
+
{ "prop1": 1
|
17
|
+
,"_links": {
|
18
|
+
"self": { "href": "http://example.com/foo" }
|
19
|
+
,"link1": { "href": "http://example.com/bar" }
|
20
|
+
,"link2": { "href": "http://example.com/people{?name}"
|
21
|
+
,"templated": true }
|
22
|
+
,"link3": [{ "href": "http://example.com/link3-a" }
|
23
|
+
,{ "href": "http://example.com/link3-b" }]
|
24
|
+
}
|
25
|
+
,"_embedded": {
|
26
|
+
"embed1": {
|
27
|
+
"_links": { "self": { "href": "http://example.com/baz" }}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
HAL
|
32
|
+
subject(:repr) { described_class.new(a_client, MultiJson.load(raw_repr)) }
|
33
|
+
|
34
|
+
describe "#property" do
|
35
|
+
context "existent" do
|
36
|
+
subject { repr.property "prop1" }
|
37
|
+
it { should eq 1 }
|
38
|
+
end
|
39
|
+
context "non-existent" do
|
40
|
+
it "raises exception" do
|
41
|
+
expect{repr.property 'wat'}.to raise_exception KeyError
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
its(:href) { should eq "http://example.com/foo" }
|
47
|
+
|
48
|
+
describe "#fetch" do
|
49
|
+
context "for existent property" do
|
50
|
+
subject { repr.fetch "prop1" }
|
51
|
+
it { should eq 1 }
|
52
|
+
end
|
53
|
+
context "for existent link" do
|
54
|
+
subject { repr.fetch "link1" }
|
55
|
+
it { should have(1).item }
|
56
|
+
it "includes related resource representation" do
|
57
|
+
expect(subject.first.href).to eq "http://example.com/bar"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
context "for existent embedded" do
|
61
|
+
subject { repr.fetch "embed1" }
|
62
|
+
it { should have(1).item }
|
63
|
+
it "includes related resource representation" do
|
64
|
+
expect(subject.first.href).to eq "http://example.com/baz"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
context "non-existent item w/o default" do
|
68
|
+
it "raises exception" do
|
69
|
+
expect{repr.fetch 'wat'}.to raise_exception KeyError
|
70
|
+
end
|
71
|
+
end
|
72
|
+
context "non-existent item w/ default value" do
|
73
|
+
subject { repr.fetch "wat", "whatevs" }
|
74
|
+
it { should eq "whatevs" }
|
75
|
+
end
|
76
|
+
context "non-existent item w/ default value generator" do
|
77
|
+
subject { repr.fetch("wat"){|key| key+"gen" } }
|
78
|
+
it { should eq "watgen" }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "#[]" do
|
83
|
+
context "for existent property" do
|
84
|
+
subject { repr["prop1"] }
|
85
|
+
it { should eq 1 }
|
86
|
+
end
|
87
|
+
context "for existent link" do
|
88
|
+
subject { repr["link1"] }
|
89
|
+
it { should have(1).item }
|
90
|
+
it { should include_representation_of "http://example.com/bar" }
|
91
|
+
end
|
92
|
+
context "for existent embedded" do
|
93
|
+
subject { repr["embed1"] }
|
94
|
+
it { should have(1).item }
|
95
|
+
it { should include_representation_of "http://example.com/baz" }
|
96
|
+
end
|
97
|
+
context "non-existent item w/o default" do
|
98
|
+
subject { repr["wat"] }
|
99
|
+
it { should be_nil }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#related" do
|
104
|
+
context "for existent link" do
|
105
|
+
subject { repr.related "link1" }
|
106
|
+
it { should have(1).item }
|
107
|
+
it { should include_representation_of "http://example.com/bar" }
|
108
|
+
end
|
109
|
+
context "for existent compound link" do
|
110
|
+
subject { repr.related "link3" }
|
111
|
+
it { should have(2).item }
|
112
|
+
it { should include_representation_of "http://example.com/link3-a" }
|
113
|
+
it { should include_representation_of "http://example.com/link3-b" }
|
114
|
+
end
|
115
|
+
context "for existent templated link" do
|
116
|
+
subject { repr.related "link2", name: "bob" }
|
117
|
+
it { should have(1).item }
|
118
|
+
it { should include_representation_of "http://example.com/people?name=bob" }
|
119
|
+
end
|
120
|
+
context "for existent embedded" do
|
121
|
+
subject { repr.related "embed1" }
|
122
|
+
it { should have(1).item }
|
123
|
+
it { should include_representation_of "http://example.com/baz" }
|
124
|
+
end
|
125
|
+
context "non-existent item w/o default" do
|
126
|
+
it "raises exception" do
|
127
|
+
expect{repr.related 'wat'}.to raise_exception KeyError
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "#related_hrefs" do
|
133
|
+
context "for existent link" do
|
134
|
+
subject { repr.related_hrefs "link1" }
|
135
|
+
it { should have(1).item }
|
136
|
+
it { should include "http://example.com/bar" }
|
137
|
+
end
|
138
|
+
context "for existent embedded" do
|
139
|
+
subject { repr.related_hrefs "embed1" }
|
140
|
+
it { should have(1).item }
|
141
|
+
it { should include "http://example.com/baz" }
|
142
|
+
end
|
143
|
+
context "non-existent item w/o default" do
|
144
|
+
it "raises exception" do
|
145
|
+
expect{repr.related_hrefs 'wat'}.to raise_exception KeyError
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
|
152
|
+
let(:a_client) { HalClient.new }
|
153
|
+
let!(:bar_request) { stub_identity_request("http://example.com/bar") }
|
154
|
+
let!(:baz_request) { stub_identity_request "http://example.com/baz" }
|
155
|
+
let!(:people_request) { stub_identity_request "http://example.com/people?name=bob" }
|
156
|
+
let!(:link3_a_request) { stub_identity_request "http://example.com/link3-a" }
|
157
|
+
let!(:link3_b_request) { stub_identity_request "http://example.com/link3-b" }
|
158
|
+
|
159
|
+
def stub_identity_request(url)
|
160
|
+
stub_request(:get, url).
|
161
|
+
to_return body: %Q|{"_links":{"self":{"href":#{url.to_json}}}}|
|
162
|
+
end
|
163
|
+
|
164
|
+
RSpec::Matchers.define(:include_representation_of) do |url|
|
165
|
+
match { |repr_set|
|
166
|
+
repr_set.any?{|it| it.href == url}
|
167
|
+
}
|
168
|
+
failure_message_for_should { |repr_set|
|
169
|
+
"Expected representation of <#{url}> but found only #{repr_set.map(&:href)}"
|
170
|
+
}
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative "./spec_helper"
|
2
|
+
require "hal_client"
|
3
|
+
|
4
|
+
describe HalClient do
|
5
|
+
describe ".new()" do
|
6
|
+
subject { HalClient.new }
|
7
|
+
it { should be_kind_of HalClient }
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.new w/ custom accept' do
|
11
|
+
subject { HalClient.new(accept: "application/vnd.myspecialmediatype") }
|
12
|
+
it { should be_kind_of HalClient }
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#get(<url>)" do
|
16
|
+
subject(:client) { HalClient.new }
|
17
|
+
let!(:return_val) { client.get "http://example.com/foo" }
|
18
|
+
|
19
|
+
it "returns a HalClient::Representation" do
|
20
|
+
expect(return_val).to be_kind_of HalClient::Representation
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "request" do
|
24
|
+
subject { request }
|
25
|
+
it("should have been made") { should have_been_made }
|
26
|
+
|
27
|
+
it "sends accept header" do
|
28
|
+
expect(request.with(headers: {'Accept' => 'application/hal+json'})).
|
29
|
+
to have_been_made
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "explicit accept" do
|
34
|
+
subject(:client) { HalClient.new accept: 'app/test' }
|
35
|
+
it "sends specified accept header" do
|
36
|
+
expect(request.with(headers: {'Accept' => 'app/test'})).
|
37
|
+
to have_been_made
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
let!(:request) { stub_request(:get, "http://example.com/foo").
|
43
|
+
to_return body: "{}" }
|
44
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hal-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Peter Williams
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-02-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rest-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.6.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.6'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.6.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: addressable
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.3'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 2.3.0
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '2.3'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 2.3.0
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: multi_json
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '1.8'
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.8.0
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.8'
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 1.8.0
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: bundler
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - "~>"
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '1.5'
|
80
|
+
type: :development
|
81
|
+
prerelease: false
|
82
|
+
version_requirements: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - "~>"
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '1.5'
|
87
|
+
- !ruby/object:Gem::Dependency
|
88
|
+
name: rake
|
89
|
+
requirement: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - "~>"
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '10.1'
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 10.1.0
|
97
|
+
type: :development
|
98
|
+
prerelease: false
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '10.1'
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: 10.1.0
|
107
|
+
- !ruby/object:Gem::Dependency
|
108
|
+
name: rspec
|
109
|
+
requirement: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - "~>"
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '2.14'
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 2.14.0
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '2.14'
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: 2.14.0
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: webmock
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - "~>"
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '1.16'
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: 1.16.0
|
137
|
+
type: :development
|
138
|
+
prerelease: false
|
139
|
+
version_requirements: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - "~>"
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '1.16'
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: 1.16.0
|
147
|
+
description: An easy to use interface for REST APIs that use HAL.
|
148
|
+
email:
|
149
|
+
- pezra@barelyenough.org
|
150
|
+
executables: []
|
151
|
+
extensions: []
|
152
|
+
extra_rdoc_files: []
|
153
|
+
files:
|
154
|
+
- ".gitignore"
|
155
|
+
- Gemfile
|
156
|
+
- LICENSE.txt
|
157
|
+
- README.md
|
158
|
+
- Rakefile
|
159
|
+
- hal-client.gemspec
|
160
|
+
- lib/hal-client.rb
|
161
|
+
- lib/hal_client.rb
|
162
|
+
- lib/hal_client/representation.rb
|
163
|
+
- lib/hal_client/representation_set.rb
|
164
|
+
- lib/hal_client/version.rb
|
165
|
+
- spec/hal_client/representation_set_spec.rb
|
166
|
+
- spec/hal_client/representation_spec.rb
|
167
|
+
- spec/hal_client_spec.rb
|
168
|
+
- spec/spec_helper.rb
|
169
|
+
homepage: https://github.com/pezra/hal-client
|
170
|
+
licenses:
|
171
|
+
- MIT
|
172
|
+
metadata: {}
|
173
|
+
post_install_message:
|
174
|
+
rdoc_options: []
|
175
|
+
require_paths:
|
176
|
+
- lib
|
177
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
requirements: []
|
188
|
+
rubyforge_project:
|
189
|
+
rubygems_version: 2.2.0
|
190
|
+
signing_key:
|
191
|
+
specification_version: 4
|
192
|
+
summary: Use HAL APIs easily
|
193
|
+
test_files:
|
194
|
+
- spec/hal_client/representation_set_spec.rb
|
195
|
+
- spec/hal_client/representation_spec.rb
|
196
|
+
- spec/hal_client_spec.rb
|
197
|
+
- spec/spec_helper.rb
|