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 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
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .#*
7
+ *~
8
+ .ruby-version
9
+ Gemfile.lock
10
+ InstalledFiles
11
+ _yardoc
12
+ coverage
13
+ doc/
14
+ lib/bundler/man
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ test/tmp
19
+ test/version_tmp
20
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hal-client.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -0,0 +1,3 @@
1
+ class HalClient
2
+ VERSION = "1.1.0"
3
+ 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
@@ -0,0 +1,5 @@
1
+ $LOAD_PATH << Pathname(__FILE__).dirname + "../lib"
2
+
3
+ require 'rspec'
4
+ require 'webmock/rspec'
5
+ require 'multi_json'
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