hal-client 1.1.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.
- 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
|