hal-client 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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