hal-client 3.18.0 → 4.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 +4 -4
- data/.travis.yml +6 -3
- data/README.md +4 -0
- data/lib/hal_client/anonymous_resource_locator.rb +12 -0
- data/lib/hal_client/collection.rb +0 -1
- data/lib/hal_client/interpreter.rb +147 -0
- data/lib/hal_client/link.rb +137 -119
- data/lib/hal_client/representation.rb +74 -129
- data/lib/hal_client/representation_editor.rb +19 -1
- data/lib/hal_client/version.rb +1 -1
- data/lib/hal_client.rb +16 -0
- data/spec/hal_client/collection_spec.rb +0 -6
- data/spec/hal_client/interpreter_spec.rb +117 -0
- data/spec/hal_client/link_spec.rb +28 -73
- data/spec/hal_client/representation_spec.rb +16 -15
- data/spec/spec_helper.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8bfaa5d8d4e7d292a1451a99fc57242f11bdea8a
|
4
|
+
data.tar.gz: 5d94cd3f5b0dbfadd72fb69500961bc3ae7de871
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 509d532e384249bfc7a2c1be053cf52c71dc04bec2d5328520cdbda229a6e637a0ce094b50f7e4e9e8fcbf064ac661c543be8ea51cd48a93fabe1a48c0322d98
|
7
|
+
data.tar.gz: b2421ace2e732764564dade36b67ebf83459877f3c001bb6afe2032c05c9a6ab290e00669f31b82e79c87d40c93938b658d142d658a8ab74bcf0e7f44190221c
|
data/.travis.yml
CHANGED
@@ -8,10 +8,9 @@ before_install:
|
|
8
8
|
- gem install bundler
|
9
9
|
|
10
10
|
rvm:
|
11
|
-
- 2.0
|
12
11
|
- 2.1
|
13
|
-
- 2.2
|
14
|
-
- 2.3
|
12
|
+
- 2.2
|
13
|
+
- 2.3
|
15
14
|
- jruby-9.1.6.0
|
16
15
|
- rbx-2.7
|
17
16
|
|
@@ -20,3 +19,7 @@ gemfile:
|
|
20
19
|
- Gemfile-http-0.8
|
21
20
|
- Gemfile-http-0.9
|
22
21
|
- Gemfile-http-1.0
|
22
|
+
|
23
|
+
matrix:
|
24
|
+
allow_failures:
|
25
|
+
- rvm: rbx-2.7
|
data/README.md
CHANGED
@@ -173,6 +173,10 @@ Or install it yourself as:
|
|
173
173
|
|
174
174
|
$ gem install hal-client
|
175
175
|
|
176
|
+
## Upgrading from 3.x to 4.x
|
177
|
+
|
178
|
+
Support for ruby 2.0 has been removed. Please upgrade to ruby 2.1 or later. No other breaking changes were made.
|
179
|
+
|
176
180
|
## Upgrading from 2.x to 3.x
|
177
181
|
|
178
182
|
For most uses no change to client code is required. At 3.0 the underlying HTTP library changed to <https://rubygems.org/gems/http> to better support our parallelism needs. This changes the interface of `#get` and `#post` on `HalClient` and `HalClient::Representation` in the situation where the response is not a valid HAL document. In those situations the return value is now a `HTTP::Response` object, rather than a `RestClient::Response`.
|
@@ -20,7 +20,6 @@ class HalClient
|
|
20
20
|
# Raises ArgumentError if `first_page` is some page other than
|
21
21
|
# the first of the collection.
|
22
22
|
def initialize(first_page)
|
23
|
-
(fail NotACollectionError) unless first_page.has_related? "item"
|
24
23
|
(fail ArgumentError, "Not the first page of the collection") if first_page.has_related? "prev"
|
25
24
|
|
26
25
|
@first_page = first_page
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'addressable/template'
|
3
|
+
require 'hal_client/representation'
|
4
|
+
require 'hal_client/link'
|
5
|
+
require 'hal_client/curie_resolver'
|
6
|
+
|
7
|
+
class HalClient
|
8
|
+
# Interprets parsed JSON
|
9
|
+
class Interpreter
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
# Collection of reserved properties
|
13
|
+
# https://tools.ietf.org/html/draft-kelly-json-hal-07#section-4.1
|
14
|
+
RESERVED_PROPERTIES = ['_links', '_embedded'].freeze
|
15
|
+
|
16
|
+
def initialize(parsed_json, hal_client)
|
17
|
+
(fail InvalidRepresentationError,
|
18
|
+
"Invalid HAL representation: #{parsed_json.inspect}") unless
|
19
|
+
hashish?(parsed_json)
|
20
|
+
|
21
|
+
@raw = parsed_json
|
22
|
+
@hal_client = hal_client
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns hash of properties from `parsed_json`
|
26
|
+
def extract_props()
|
27
|
+
raw.reject{|k,_| RESERVED_PROPERTIES.include?(k) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def extract_links()
|
31
|
+
extract_embedded_links + extract_basic_links
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
attr_reader :raw, :hal_client
|
37
|
+
|
38
|
+
def hashish?(obj)
|
39
|
+
obj.respond_to?(:[]) &&
|
40
|
+
obj.respond_to?(:map)
|
41
|
+
end
|
42
|
+
|
43
|
+
def extract_basic_links
|
44
|
+
raw
|
45
|
+
.fetch("_links") { Hash.new }
|
46
|
+
.flat_map { |rel, target_info|
|
47
|
+
build_links(rel, target_info)
|
48
|
+
}
|
49
|
+
.compact
|
50
|
+
.to_set
|
51
|
+
end
|
52
|
+
|
53
|
+
def extract_embedded_links
|
54
|
+
raw
|
55
|
+
.fetch("_embedded") { Hash.new }
|
56
|
+
.flat_map { |rel, embedded_json|
|
57
|
+
build_embedded_links(rel, embedded_json)
|
58
|
+
}
|
59
|
+
.compact
|
60
|
+
.to_set
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_links(rel, target_info)
|
64
|
+
arrayify(target_info)
|
65
|
+
.map { |info|
|
66
|
+
if info["templated"]
|
67
|
+
build_templated_link(rel, info)
|
68
|
+
else
|
69
|
+
build_direct_link(rel, info)
|
70
|
+
end
|
71
|
+
}
|
72
|
+
.compact
|
73
|
+
end
|
74
|
+
|
75
|
+
def build_embedded_links(rel, targets_json)
|
76
|
+
arrayify(targets_json)
|
77
|
+
.map{ |target_json|
|
78
|
+
target_repr = Representation.new(parsed_json: target_json, hal_client: hal_client)
|
79
|
+
|
80
|
+
SimpleLink.new(rel: rel,
|
81
|
+
target: target_repr,
|
82
|
+
curie_resolver: curie_resolver)
|
83
|
+
}
|
84
|
+
|
85
|
+
rescue InvalidRepresentationError
|
86
|
+
MalformedLink.new(rel: rel,
|
87
|
+
msg: "/_embedded/#{jpointer_esc(rel)} is invalid",
|
88
|
+
curie_resolver: curie_resolver)
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_templated_link(rel, info)
|
92
|
+
fail(InvalidRepresentationError) unless hashish?(info)
|
93
|
+
|
94
|
+
target_pattern = info.fetch("href") { fail InvalidRepresentationError }
|
95
|
+
|
96
|
+
TemplatedLink.new(rel: rel,
|
97
|
+
template: Addressable::Template.new(target_pattern),
|
98
|
+
curie_resolver: curie_resolver,
|
99
|
+
hal_client: hal_client)
|
100
|
+
|
101
|
+
rescue InvalidRepresentationError
|
102
|
+
MalformedLink.new(rel: rel,
|
103
|
+
msg: "/_links/#{jpointer_esc(rel)} is invalid",
|
104
|
+
curie_resolver: curie_resolver)
|
105
|
+
end
|
106
|
+
|
107
|
+
def build_direct_link(rel, info)
|
108
|
+
fail InvalidRepresentationError unless hashish?(info)
|
109
|
+
|
110
|
+
target_url = info.fetch("href") { fail InvalidRepresentationError }
|
111
|
+
return nil unless target_url
|
112
|
+
|
113
|
+
target_repr = Representation.new(href: target_url, hal_client: hal_client)
|
114
|
+
|
115
|
+
SimpleLink.new(rel: rel,
|
116
|
+
target: target_repr,
|
117
|
+
curie_resolver: curie_resolver)
|
118
|
+
|
119
|
+
rescue InvalidRepresentationError
|
120
|
+
MalformedLink.new(rel: rel,
|
121
|
+
msg: "/_links/#{jpointer_esc(rel)} is invalid",
|
122
|
+
curie_resolver: curie_resolver)
|
123
|
+
end
|
124
|
+
|
125
|
+
def curie_resolver
|
126
|
+
@curie_resolver ||= CurieResolver.new(raw.fetch("_links", {}).fetch("curies", []))
|
127
|
+
end
|
128
|
+
|
129
|
+
def hashish?(thing)
|
130
|
+
thing.kind_of?(Hash) ||
|
131
|
+
thing.respond_to?(:fetch) && thing.respond_to?(:key?)
|
132
|
+
end
|
133
|
+
|
134
|
+
def arrayify(obj)
|
135
|
+
if Array === obj
|
136
|
+
obj
|
137
|
+
else
|
138
|
+
[obj]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def jpointer_esc(str)
|
143
|
+
str.gsub "/", "~1"
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
data/lib/hal_client/link.rb
CHANGED
@@ -4,144 +4,162 @@ class HalClient
|
|
4
4
|
|
5
5
|
# HAL representation of a single link. Provides access to an embedded representation.
|
6
6
|
class Link
|
7
|
+
protected def initialize(rel:, curie_resolver: CurieResolver.new([]), **opts)
|
8
|
+
@literal_rel = rel
|
9
|
+
@curie_resolver = curie_resolver
|
10
|
+
|
11
|
+
post_initialize(opts)
|
12
|
+
end
|
7
13
|
|
8
|
-
# Create a new Link
|
9
|
-
#
|
10
|
-
# options - name parameters
|
11
|
-
# :rel - This Link's rel property
|
12
|
-
# :target - An instance of Representation
|
13
|
-
# :template - A URI template ( https://www.rfc-editor.org/rfc/rfc6570.txt )
|
14
|
-
# :curie_resolver - An instance of CurieResolver (used to resolve curied rels)
|
15
|
-
def initialize(options)
|
16
|
-
@literal_rel = options[:rel]
|
17
|
-
@target = options[:target]
|
18
|
-
@template = options[:template]
|
19
|
-
@curie_resolver = options[:curie_resolver] || CurieResolver.new([])
|
20
|
-
|
21
|
-
(fail ArgumentError, "A rel must be provided") if @literal_rel.nil?
|
22
|
-
|
23
|
-
if @target.nil? && @template.nil?
|
24
|
-
(fail ArgumentError, "A target or template must be provided")
|
25
|
-
end
|
26
|
-
|
27
|
-
if @target && @template
|
28
|
-
(fail ArgumentError, "Cannot provide both a target and a template")
|
29
|
-
end
|
30
|
-
|
31
|
-
if @target && !@target.kind_of?(Representation)
|
32
|
-
(fail ArgumentError, "Invalid HAL representation: #{target.inspect}")
|
33
|
-
end
|
34
|
-
|
35
|
-
if @template && !@template.kind_of?(Addressable::Template)
|
36
|
-
(fail ArgumentError, "Invalid Addressable::Template: #{template.inspect}")
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
attr_accessor :literal_rel, :target, :template, :curie_resolver
|
41
|
-
|
42
|
-
|
43
|
-
# Create a new Link using an entry from the '_links' section of a HAL document
|
44
|
-
#
|
45
|
-
# options - name parameters
|
46
|
-
# :hash_entry - a hash containing keys :rel (string) and :data (hash from a '_links' entry)
|
47
|
-
# :hal_client - an instance of HalClient
|
48
|
-
# :curie_resolver - An instance of CurieResolver (used to resolve curied rels)
|
49
|
-
# :base_url - Base url for resolving relative links in hash_entry (probably the parent
|
50
|
-
# document's "self" link)
|
51
|
-
def self.new_from_link_entry(options)
|
52
|
-
hash_entry = options[:hash_entry]
|
53
|
-
hal_client = options[:hal_client]
|
54
|
-
curie_resolver = options[:curie_resolver]
|
55
|
-
base_url = options[:base_url]
|
56
|
-
|
57
|
-
rel = hash_entry[:rel]
|
58
|
-
hash_data = hash_entry[:data]
|
59
|
-
return nil unless hash_data['href']
|
60
|
-
href = (base_url + hash_data['href']).to_s
|
61
|
-
|
62
|
-
if hash_data['templated']
|
63
|
-
Link.new(rel: rel,
|
64
|
-
template: Addressable::Template.new(href),
|
65
|
-
curie_resolver: curie_resolver)
|
66
|
-
else
|
67
|
-
Link.new(rel: rel,
|
68
|
-
target: Representation.new(hal_client: hal_client, href: href),
|
69
|
-
curie_resolver: curie_resolver)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
# Create a new Link using an entry from the '_embedded' section of a HAL document
|
75
|
-
#
|
76
|
-
# options - name parameters
|
77
|
-
# :hash_entry - a hash containing keys :rel (string) and :data (hash from a '_embedded' entry)
|
78
|
-
# :hal_client - an instance of HalClient
|
79
|
-
# :curie_resolver - An instance of CurieResolver (used to resolve curied rels)
|
80
|
-
# :base_url - Base url for resolving relative links in hash_entry (probably the parent
|
81
|
-
# document's "self" link)
|
82
|
-
def self.new_from_embedded_entry(options)
|
83
|
-
hash_entry = options[:hash_entry]
|
84
|
-
hal_client = options[:hal_client]
|
85
|
-
curie_resolver = options[:curie_resolver]
|
86
|
-
base_url = options[:base_url]
|
87
|
-
|
88
|
-
rel = hash_entry[:rel]
|
89
|
-
hash_data = hash_entry[:data]
|
90
|
-
|
91
|
-
explicit_url = self_href(hash_data)
|
92
|
-
hash_data['_links']['self']['href'] = (base_url + explicit_url).to_s if explicit_url
|
93
|
-
|
94
|
-
Link.new(rel: rel,
|
95
|
-
target: Representation.new(hal_client: hal_client, parsed_json: hash_data),
|
96
|
-
curie_resolver: curie_resolver)
|
97
|
-
end
|
98
|
-
|
99
|
-
|
100
|
-
# Returns the URL of the resource this link references.
|
101
|
-
# In the case of a templated link, this is the unresolved url template pattern.
|
102
14
|
def raw_href
|
103
|
-
|
15
|
+
raise NotImplementedError
|
104
16
|
end
|
105
17
|
|
106
|
-
def
|
107
|
-
|
18
|
+
def target_url(vars = {})
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def target(vars = {})
|
23
|
+
raise NotImplementedError
|
108
24
|
end
|
109
25
|
|
110
|
-
# Returns true for a templated link, false for an ordinary (non-templated) link
|
111
26
|
def templated?
|
112
|
-
|
27
|
+
raise NotImplementedError
|
113
28
|
end
|
114
29
|
|
115
|
-
|
116
|
-
|
30
|
+
attr_reader :literal_rel, :curie_resolver
|
31
|
+
|
32
|
+
def fully_qualified_rel
|
33
|
+
curie_resolver.resolve(literal_rel)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Links with the same href, same rel value, and the same 'templated' value
|
37
|
+
# are considered equal. Otherwise, they are considered unequal. Links
|
38
|
+
# without a href (for example anonymous embedded links, are never equal to
|
39
|
+
# one another.
|
117
40
|
def ==(other)
|
118
|
-
if
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
41
|
+
return false if raw_href.nil?
|
42
|
+
|
43
|
+
return false unless other.respond_to?(:raw_href) &&
|
44
|
+
other.respond_to?(:fully_qualified_rel) &&
|
45
|
+
other.respond_to?(:templated?)
|
46
|
+
|
47
|
+
|
48
|
+
(raw_href == other.raw_href) &&
|
49
|
+
(fully_qualified_rel == other.fully_qualified_rel) &&
|
50
|
+
(templated? == other.templated?)
|
127
51
|
end
|
128
52
|
alias :eql? :==
|
129
53
|
|
130
|
-
|
131
|
-
#
|
132
|
-
#
|
54
|
+
# Differing Representations or Addressable::Templates with matching hrefs
|
55
|
+
# will get matching hash values, since we are using raw_href and not the
|
56
|
+
# objects themselves when computing hash
|
133
57
|
def hash
|
134
|
-
[fully_qualified_rel,
|
58
|
+
[fully_qualified_rel,
|
59
|
+
raw_href,
|
60
|
+
templated?].hash
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
def post_initialize(opts)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
# Links that are not templated.
|
71
|
+
class SimpleLink < Link
|
72
|
+
|
73
|
+
protected def post_initialize(target:)
|
74
|
+
fail(ArgumentError) unless target.kind_of?(HalClient::Representation)
|
75
|
+
|
76
|
+
@target = target
|
77
|
+
end
|
78
|
+
|
79
|
+
def raw_href
|
80
|
+
target.href
|
81
|
+
end
|
82
|
+
|
83
|
+
def templated?
|
84
|
+
false
|
85
|
+
end
|
86
|
+
|
87
|
+
def target_url(_vars = {})
|
88
|
+
target.href
|
135
89
|
end
|
136
90
|
|
91
|
+
def target(_vars = {})
|
92
|
+
@target
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
# Links that are templated.
|
98
|
+
class TemplatedLink < Link
|
99
|
+
|
100
|
+
protected def post_initialize(template:, hal_client:)
|
101
|
+
fail(ArgumentError) unless template.kind_of? Addressable::Template
|
102
|
+
@tmpl = template
|
103
|
+
@hal_client = hal_client
|
104
|
+
end
|
105
|
+
|
106
|
+
def raw_href
|
107
|
+
tmpl
|
108
|
+
end
|
109
|
+
|
110
|
+
def templated?
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
def target_url(vars = {})
|
115
|
+
tmpl.expand(vars)
|
116
|
+
end
|
117
|
+
|
118
|
+
def target(vars = {})
|
119
|
+
hal_client.get(target_url(vars))
|
120
|
+
end
|
121
|
+
|
122
|
+
# Differing Representations or Addressable::Templates with matching hrefs
|
123
|
+
# will get matching hash values, since we are using raw_href and not the
|
124
|
+
# objects themselves when computing hash
|
125
|
+
def hash
|
126
|
+
[fully_qualified_rel,
|
127
|
+
tmpl.pattern,
|
128
|
+
templated?].hash
|
129
|
+
end
|
137
130
|
|
138
131
|
protected
|
139
132
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
133
|
+
attr_reader :tmpl, :hal_client
|
134
|
+
end
|
135
|
+
|
136
|
+
# A link that was malformed in the JSON. This class is used to delay
|
137
|
+
# presenting interpretation errors so that consumers can ignore malformedness
|
138
|
+
# that does not block their goal. For example, busted links that they will not
|
139
|
+
# use anyway.
|
140
|
+
class MalformedLink < Link
|
141
|
+
|
142
|
+
protected def post_initialize(msg:)
|
143
|
+
@msg = msg
|
144
|
+
end
|
145
|
+
|
146
|
+
def raise_invalid(**)
|
147
|
+
raise InvalidRepresentationError, msg
|
148
|
+
end
|
149
|
+
|
150
|
+
alias_method :raw_href, :raise_invalid
|
151
|
+
alias_method :target_url, :raise_invalid
|
152
|
+
alias_method :target, :raise_invalid
|
153
|
+
alias_method :templated?, :raise_invalid
|
154
|
+
|
155
|
+
def hash
|
156
|
+
fully_qualified_rel.hash
|
145
157
|
end
|
158
|
+
|
159
|
+
protected
|
160
|
+
|
161
|
+
attr_reader :msg
|
146
162
|
end
|
163
|
+
|
164
|
+
|
147
165
|
end
|
@@ -3,18 +3,19 @@ require 'addressable/template'
|
|
3
3
|
|
4
4
|
require 'hal_client'
|
5
5
|
require 'hal_client/representation_set'
|
6
|
+
require 'hal_client/interpreter'
|
7
|
+
require 'hal_client/anonymous_resource_locator'
|
6
8
|
|
7
9
|
class HalClient
|
8
|
-
|
9
10
|
# HAL representation of a single resource. Provides access to
|
10
11
|
# properties, links and embedded representations.
|
12
|
+
#
|
13
|
+
# Operations on a representation are not thread-safe. If you'd like to
|
14
|
+
# use representations in a threaded environment, consider using the method
|
15
|
+
# #clone_for_use_in_different_thread to create a copy for each new thread
|
11
16
|
class Representation
|
12
17
|
extend Forwardable
|
13
18
|
|
14
|
-
# Collection of reserved properties
|
15
|
-
# https://tools.ietf.org/html/draft-kelly-json-hal-07#section-4.1
|
16
|
-
RESERVED_PROPERTIES = ['_links', '_embedded'].freeze
|
17
|
-
|
18
19
|
NO_RELATED_RESOURCE = ->(link_rel) {
|
19
20
|
raise KeyError, "No resources are related via `#{link_rel}`"
|
20
21
|
}
|
@@ -37,15 +38,21 @@ class HalClient
|
|
37
38
|
# :href - The href of this representation.
|
38
39
|
# :hal_client - The HalClient instance to use when navigating.
|
39
40
|
def initialize(options)
|
40
|
-
@raw = options[:parsed_json]
|
41
41
|
@hal_client = options[:hal_client]
|
42
42
|
@href = options[:href]
|
43
43
|
|
44
|
+
interpret options[:parsed_json] if options[:parsed_json]
|
45
|
+
|
44
46
|
(fail ArgumentError, "Either parsed_json or href must be provided") if
|
45
47
|
@raw.nil? && @href.nil?
|
48
|
+
end
|
46
49
|
|
47
|
-
|
48
|
-
|
50
|
+
# Returns a copy of this instance that is safe to use in threaded
|
51
|
+
# environments
|
52
|
+
def clone_for_use_in_different_thread
|
53
|
+
clone.tap do |c|
|
54
|
+
c.hal_client = c.hal_client.clone_for_use_in_different_thread
|
55
|
+
end
|
49
56
|
end
|
50
57
|
|
51
58
|
# Posts a `Representation` or `String` to this resource. Causes
|
@@ -86,7 +93,8 @@ class HalClient
|
|
86
93
|
#
|
87
94
|
# name - the name of the property to check
|
88
95
|
def property?(name)
|
89
|
-
|
96
|
+
ensure_reified
|
97
|
+
properties.key? name
|
90
98
|
end
|
91
99
|
alias_method :has_property?, :property?
|
92
100
|
|
@@ -103,24 +111,24 @@ class HalClient
|
|
103
111
|
# Raises KeyError if the specified property does not exist
|
104
112
|
# and no default nor default_proc is provided.
|
105
113
|
def property(name, default=MISSING, &default_proc)
|
114
|
+
ensure_reified
|
115
|
+
|
106
116
|
default_proc ||= ->(_){ default} if default != MISSING
|
107
117
|
|
108
|
-
|
118
|
+
properties.fetch(name.to_s, &default_proc)
|
109
119
|
end
|
110
120
|
|
111
121
|
# Returns a Hash including the key-value pairs of all the properties
|
112
122
|
# in the resource. It does not include HAL's reserved
|
113
123
|
# properties (`_links` and `_embedded`).
|
114
|
-
|
115
|
-
raw.reject { |k, _| RESERVED_PROPERTIES.include? k }
|
116
|
-
end
|
124
|
+
attr_reader :properties
|
117
125
|
|
118
126
|
# Returns the URL of the resource this representation represents.
|
119
127
|
def href
|
120
128
|
@href ||= raw
|
121
129
|
.fetch("_links",{})
|
122
130
|
.fetch("self",{})
|
123
|
-
.fetch("href",
|
131
|
+
.fetch("href", AnonymousResourceLocator.new)
|
124
132
|
end
|
125
133
|
|
126
134
|
# Returns the value of the specified property or representations
|
@@ -159,7 +167,7 @@ class HalClient
|
|
159
167
|
#
|
160
168
|
# link_rel - The link rel of interest
|
161
169
|
def related?(link_rel)
|
162
|
-
|
170
|
+
links_by_rel.key?(link_rel)
|
163
171
|
end
|
164
172
|
alias_method :has_related?, :related?
|
165
173
|
|
@@ -178,33 +186,13 @@ class HalClient
|
|
178
186
|
def related(link_rel, options = {}, &default_proc)
|
179
187
|
default_proc ||= NO_RELATED_RESOURCE
|
180
188
|
|
181
|
-
|
182
|
-
linked = linked(link_rel, options) { nil }
|
183
|
-
return default_proc.call(link_rel) if embedded.nil? and linked.nil?
|
189
|
+
ensure_reified
|
184
190
|
|
185
|
-
|
186
|
-
|
191
|
+
related = links_by_rel
|
192
|
+
.fetch(link_rel) { return default_proc.call(link_rel) }
|
193
|
+
.map { |l| l.target(options) }
|
187
194
|
|
188
|
-
|
189
|
-
result = Set.new
|
190
|
-
base_url = Addressable::URI.parse(href || "")
|
191
|
-
|
192
|
-
embedded_entries = flatten_section(raw.fetch("_embedded", {}))
|
193
|
-
result.merge(embedded_entries.map do |entry|
|
194
|
-
Link.new_from_embedded_entry(hash_entry: entry,
|
195
|
-
hal_client: hal_client,
|
196
|
-
curie_resolver: namespaces,
|
197
|
-
base_url: base_url)
|
198
|
-
end)
|
199
|
-
|
200
|
-
link_entries = flatten_section(raw.fetch("_links", {}))
|
201
|
-
result.merge(link_entries.map { |entry|
|
202
|
-
Link.new_from_link_entry(hash_entry: entry,
|
203
|
-
hal_client: hal_client,
|
204
|
-
curie_resolver: namespaces,
|
205
|
-
base_url: base_url) })
|
206
|
-
|
207
|
-
result
|
195
|
+
RepresentationSet.new(related)
|
208
196
|
end
|
209
197
|
|
210
198
|
# Returns urls of resources related via the specified
|
@@ -241,11 +229,11 @@ class HalClient
|
|
241
229
|
def raw_related_hrefs(link_rel, &default_proc)
|
242
230
|
default_proc ||= NO_RELATED_RESOURCE
|
243
231
|
|
244
|
-
|
245
|
-
linked = links.hrefs(link_rel) { nil }
|
246
|
-
return default_proc.call(link_rel) if embedded.nil? and linked.nil?
|
232
|
+
ensure_reified
|
247
233
|
|
248
|
-
|
234
|
+
links_by_rel
|
235
|
+
.fetch(link_rel) { return default_proc.call(link_rel) }
|
236
|
+
.map { |l| l.raw_href }
|
249
237
|
end
|
250
238
|
|
251
239
|
# Returns an Enumerable of the items in this collection resource
|
@@ -266,6 +254,16 @@ class HalClient
|
|
266
254
|
as_enum.to_enum(method, *args, &blk)
|
267
255
|
end
|
268
256
|
|
257
|
+
# Returns set of all links in this representation.
|
258
|
+
def all_links
|
259
|
+
links_by_rel
|
260
|
+
.reduce(Set.new) { |result, kv|
|
261
|
+
_,links = *kv
|
262
|
+
links.each { |l| result << l }
|
263
|
+
result
|
264
|
+
}
|
265
|
+
end
|
266
|
+
|
269
267
|
# Resets this representation such that it will be requested from
|
270
268
|
# the upstream on it's next use.
|
271
269
|
def reset
|
@@ -276,7 +274,7 @@ class HalClient
|
|
276
274
|
# Returns a short human readable description of this
|
277
275
|
# representation.
|
278
276
|
def to_s
|
279
|
-
"#<" + self.class.name + ": " +
|
277
|
+
"#<" + self.class.name + ": " + href.to_s + ">"
|
280
278
|
end
|
281
279
|
|
282
280
|
# Returns the raw json representation of this representation
|
@@ -304,109 +302,56 @@ class HalClient
|
|
304
302
|
end
|
305
303
|
alias :eql? :==
|
306
304
|
|
307
|
-
#
|
305
|
+
# Returns raw parsed json.
|
308
306
|
def raw
|
309
|
-
|
310
|
-
(fail "unable to make requests due to missing hal client") unless hal_client
|
311
|
-
|
312
|
-
response = hal_client.get(@href)
|
313
|
-
|
314
|
-
unless response.is_a?(Representation)
|
315
|
-
error_message = "Response body wasn't a valid HAL document:\n\n"
|
316
|
-
error_message += response.body
|
317
|
-
raise InvalidRepresentationError.new(error_message)
|
318
|
-
end
|
319
|
-
|
320
|
-
@raw ||= response.raw
|
321
|
-
end
|
307
|
+
ensure_reified
|
322
308
|
|
323
309
|
@raw
|
324
310
|
end
|
325
311
|
|
326
|
-
#
|
327
|
-
# representation
|
312
|
+
# Return the HalClient used to retrieve this representation
|
328
313
|
attr_reader :hal_client
|
329
314
|
|
330
315
|
protected
|
331
316
|
|
332
|
-
|
317
|
+
attr_reader :links_by_rel
|
318
|
+
attr_writer :hal_client
|
333
319
|
|
334
|
-
|
335
|
-
section_hash
|
336
|
-
.each_pair
|
337
|
-
.flat_map { |rel, some_link_info|
|
338
|
-
[some_link_info].flatten
|
339
|
-
.map { |a_link_info| { rel: rel, data: a_link_info } }
|
340
|
-
}
|
341
|
-
end
|
342
|
-
|
343
|
-
def links
|
344
|
-
@links ||= LinksSection.new((raw.fetch("_links"){{}}),
|
345
|
-
base_url: Addressable::URI.parse(href || ""))
|
346
|
-
end
|
347
|
-
|
348
|
-
def embedded_section
|
349
|
-
embedded = raw.fetch("_embedded", {})
|
350
|
-
|
351
|
-
@embedded_section ||= embedded.merge fully_qualified(embedded)
|
352
|
-
end
|
353
|
-
|
354
|
-
def embedded(link_rel, &default_proc)
|
355
|
-
default_proc ||= NO_EMBED_FOUND
|
356
|
-
|
357
|
-
relations = embedded_section.fetch(link_rel) { MISSING }
|
358
|
-
return default_proc.call(link_rel) if relations == MISSING
|
359
|
-
|
360
|
-
(boxed relations).map{|it| Representation.new hal_client: hal_client, parsed_json: it}
|
361
|
-
|
362
|
-
rescue InvalidRepresentationError
|
363
|
-
fail InvalidRepresentationError, "/_embedded/#{jpointer_esc(link_rel)} is not a valid representation"
|
364
|
-
end
|
320
|
+
MISSING = Object.new
|
365
321
|
|
366
|
-
|
367
|
-
|
322
|
+
# Fetch the representation from origin server if that has not already
|
323
|
+
# happened.
|
324
|
+
def ensure_reified
|
325
|
+
return if @raw
|
326
|
+
(fail "unable to make requests due to missing hal client") unless hal_client
|
327
|
+
(fail "unable to make requests due to missing href") unless @href
|
368
328
|
|
369
|
-
|
370
|
-
return default_proc.call(link_rel, options) if relations == MISSING || relations.compact.empty?
|
329
|
+
response = hal_client.get(@href)
|
371
330
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
url_or_tmpl
|
378
|
-
end }
|
379
|
-
.map {|href| Representation.new href: href, hal_client: hal_client }
|
331
|
+
unless response.is_a?(Representation)
|
332
|
+
error_message = "Response body wasn't a valid HAL document:\n\n"
|
333
|
+
error_message += response.body
|
334
|
+
raise InvalidRepresentationError.new(error_message)
|
335
|
+
end
|
380
336
|
|
381
|
-
|
382
|
-
fail InvalidRepresentationError, "/_links/#{jpointer_esc(link_rel)} is not a valid link"
|
337
|
+
interpret response.raw
|
383
338
|
end
|
384
339
|
|
385
|
-
def
|
386
|
-
|
387
|
-
end
|
340
|
+
def interpret(parsed_json)
|
341
|
+
@raw = parsed_json
|
388
342
|
|
389
|
-
|
390
|
-
if hashish? list_hash_or_nil
|
391
|
-
[list_hash_or_nil]
|
392
|
-
elsif list_hash_or_nil.respond_to? :map
|
393
|
-
list_hash_or_nil
|
394
|
-
else
|
395
|
-
# The only valid values for a link/embedded set are hashes or
|
396
|
-
# array-ish things.
|
343
|
+
interpreter = HalClient::Interpreter.new(parsed_json, hal_client)
|
397
344
|
|
398
|
-
|
399
|
-
end
|
400
|
-
end
|
401
|
-
|
402
|
-
def fully_qualified(relations_section)
|
403
|
-
Hash[relations_section.map {|rel, link_info|
|
404
|
-
[(namespaces.resolve rel), link_info]
|
405
|
-
}]
|
406
|
-
end
|
345
|
+
@properties = interpreter.extract_props
|
407
346
|
|
408
|
-
|
409
|
-
|
347
|
+
@links_by_rel =
|
348
|
+
interpreter
|
349
|
+
.extract_links
|
350
|
+
.reduce(Hash.new { |h,k| h[k] = Set.new }) { |links_tab, link|
|
351
|
+
links_tab[link.literal_rel] << link
|
352
|
+
links_tab[link.fully_qualified_rel] << link
|
353
|
+
links_tab
|
354
|
+
}
|
410
355
|
end
|
411
356
|
|
412
357
|
def_delegators :links, :namespaces
|
@@ -35,11 +35,17 @@ class HalClient
|
|
35
35
|
|
36
36
|
# Returns true if this, or any previous, editor actually changed the hal
|
37
37
|
# representation.
|
38
|
+
#
|
39
|
+
# ---
|
40
|
+
#
|
41
|
+
# Anonymous entries are hard to deal with in a logically clean way. We fudge
|
42
|
+
# it a bit by treating anonymous resources with the same raw value as equal.
|
38
43
|
def dirty?
|
39
44
|
new_repr = Representation.new(parsed_json: raw)
|
40
45
|
|
41
46
|
orig_repr.properties != new_repr.properties ||
|
42
|
-
orig_repr.all_links != new_repr.all_links
|
47
|
+
sans_anon(orig_repr.all_links) != sans_anon(new_repr.all_links) ||
|
48
|
+
raw_anons(orig_repr.all_links) != raw_anons(new_repr.all_links)
|
43
49
|
end
|
44
50
|
|
45
51
|
# Returns the raw json representation of this representation
|
@@ -140,6 +146,18 @@ class HalClient
|
|
140
146
|
|
141
147
|
attr_reader :orig_repr
|
142
148
|
|
149
|
+
def sans_anon(links)
|
150
|
+
links.reject { |l| AnonymousResourceLocator === l.raw_href}
|
151
|
+
.to_set
|
152
|
+
end
|
153
|
+
|
154
|
+
def raw_anons(links)
|
155
|
+
links
|
156
|
+
.select { |l| AnonymousResourceLocator === l.raw_href}
|
157
|
+
.map!{ |l| l.target.raw }
|
158
|
+
.to_set
|
159
|
+
end
|
160
|
+
|
143
161
|
def Array(thing)
|
144
162
|
if Hash === thing
|
145
163
|
[thing]
|
data/lib/hal_client/version.rb
CHANGED
data/lib/hal_client.rb
CHANGED
@@ -4,6 +4,11 @@ require 'multi_json'
|
|
4
4
|
require 'benchmark'
|
5
5
|
|
6
6
|
# Adapter used to access resources.
|
7
|
+
#
|
8
|
+
# Operations on a HalClient instance are not thread-safe. If you'd like to
|
9
|
+
# use a HalClient instance in a threaded environment, consider using the
|
10
|
+
# method #clone_for_use_in_different_thread to create a copy for each new
|
11
|
+
# thread
|
7
12
|
class HalClient
|
8
13
|
autoload :Representation, 'hal_client/representation'
|
9
14
|
autoload :RepresentationSet, 'hal_client/representation_set'
|
@@ -76,6 +81,11 @@ class HalClient
|
|
76
81
|
end
|
77
82
|
protected :initialize
|
78
83
|
|
84
|
+
# Returns a copy of this instance that is safe to use in threaded environments
|
85
|
+
def clone_for_use_in_different_thread
|
86
|
+
clone.tap { |c| c.clear_clients! }
|
87
|
+
end
|
88
|
+
|
79
89
|
# Returns a `Representation` of the resource identified by `url`.
|
80
90
|
#
|
81
91
|
# url - The URL of the resource of interest.
|
@@ -229,6 +239,12 @@ class HalClient
|
|
229
239
|
base_client_with_headers(headers)
|
230
240
|
end
|
231
241
|
|
242
|
+
# Resets memoized HTTP clients
|
243
|
+
def clear_clients!
|
244
|
+
@base_client = nil
|
245
|
+
@base_client_with_headers = {}
|
246
|
+
end
|
247
|
+
|
232
248
|
# Returns an HTTP client.
|
233
249
|
def base_client
|
234
250
|
@base_client ||= begin
|
@@ -37,17 +37,11 @@ RSpec.describe HalClient::Collection do
|
|
37
37
|
.not_to raise_error
|
38
38
|
end
|
39
39
|
|
40
|
-
specify do
|
41
|
-
expect { described_class.new(non_collection_repr) }
|
42
|
-
.to raise_error HalClient::NotACollectionError
|
43
|
-
end
|
44
|
-
|
45
40
|
specify do
|
46
41
|
expect { described_class.new(non_first_page) }
|
47
42
|
.to raise_error ArgumentError, /first page/
|
48
43
|
end
|
49
44
|
|
50
|
-
let(:non_collection_repr) { repr({}) }
|
51
45
|
let(:non_first_page) { collection_page(prev_href: "http://example.com/p1") }
|
52
46
|
end
|
53
47
|
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'hal_client/interpreter'
|
2
|
+
|
3
|
+
RSpec.describe HalClient::Interpreter do
|
4
|
+
let(:hal_client) { HalClient.new }
|
5
|
+
|
6
|
+
describe ".new" do
|
7
|
+
specify { expect(described_class.new({}, hal_client)).to be_kind_of described_class }
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
describe "#extract_props" do
|
12
|
+
subject { described_class.new(parsed_json, hal_client) }
|
13
|
+
let(:parsed_json) { {"num" => 1,
|
14
|
+
"_links" => {},
|
15
|
+
"_embedded" => {}
|
16
|
+
} }
|
17
|
+
|
18
|
+
specify { expect(subject.extract_props).to be_kind_of Hash }
|
19
|
+
specify { expect(subject.extract_props).to include("num") }
|
20
|
+
specify { expect(subject.extract_props).not_to include("_links") }
|
21
|
+
specify { expect(subject.extract_props).not_to include("_embedded") }
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
describe "#extract_links" do
|
26
|
+
subject { described_class.new({}, hal_client) }
|
27
|
+
|
28
|
+
specify { expect(subject.extract_links).to be_kind_of Enumerable }
|
29
|
+
|
30
|
+
context "with out links" do
|
31
|
+
subject { described_class.new({}, hal_client) }
|
32
|
+
|
33
|
+
specify { expect(subject.extract_links).to be_empty }
|
34
|
+
end
|
35
|
+
|
36
|
+
context "with links" do
|
37
|
+
subject { described_class.new(links_json, hal_client) }
|
38
|
+
let(:links_json) { {"_links" => {
|
39
|
+
"foo" => { "href" => "http://example.com/foo" },
|
40
|
+
"bar" => [ { "href" => "http://example.com/bar1" },
|
41
|
+
{ "href" => "http://example.com/bar2" } ],
|
42
|
+
"tmpl" => { "href" => "http://example.com/foo{?q}",
|
43
|
+
"templated" => true},
|
44
|
+
"mixed" => [ { "href" => "http://example.com/foo" },
|
45
|
+
{ "href" => "http://example.com/foo{?q}",
|
46
|
+
"templated" => true } ]
|
47
|
+
} } }
|
48
|
+
|
49
|
+
specify { expect(subject.extract_links).not_to be_empty }
|
50
|
+
specify { expect(subject.extract_links)
|
51
|
+
.to include link_matching("foo", "http://example.com/foo") }
|
52
|
+
specify { expect(subject.extract_links)
|
53
|
+
.to include link_matching("bar", "http://example.com/bar1") }
|
54
|
+
specify { expect(subject.extract_links)
|
55
|
+
.to include link_matching("bar", "http://example.com/bar2") }
|
56
|
+
specify { expect(subject.extract_links)
|
57
|
+
.to include link_matching("mixed", "http://example.com/foo") }
|
58
|
+
|
59
|
+
specify { expect(subject.extract_links)
|
60
|
+
.to include templated_link_matching("tmpl", "http://example.com/foo{?q}") }
|
61
|
+
specify { expect(subject.extract_links)
|
62
|
+
.to include templated_link_matching("mixed", "http://example.com/foo{?q}") }
|
63
|
+
end
|
64
|
+
|
65
|
+
context "with embedded" do
|
66
|
+
subject { described_class.new(embedded_json, hal_client) }
|
67
|
+
let(:embedded_json) { {"_links" => {
|
68
|
+
"foo" => { "href" => "http://example.com/foo" }
|
69
|
+
},
|
70
|
+
"_embedded" => {
|
71
|
+
"bar" => { "_links" => { "self" => { "href" => "http://example.com/bar" } } },
|
72
|
+
"baz" => [ { "_links" => { "self" => { "href" => "http://example.com/baz1" } } },
|
73
|
+
{ "_links" => { "self" => { "href" => "http://example.com/baz2" } } } ]
|
74
|
+
}
|
75
|
+
} }
|
76
|
+
|
77
|
+
specify { expect(subject.extract_links).not_to be_empty }
|
78
|
+
specify { expect(subject.extract_links).to include link_matching("foo", "http://example.com/foo") }
|
79
|
+
specify { expect(subject.extract_links).to include link_matching("bar", "http://example.com/bar") }
|
80
|
+
specify { expect(subject.extract_links).to include link_matching("baz", "http://example.com/baz1") }
|
81
|
+
specify { expect(subject.extract_links).to include link_matching("baz", "http://example.com/baz2") }
|
82
|
+
end
|
83
|
+
|
84
|
+
context "curies" do
|
85
|
+
let(:raw_repr) { <<-HAL }
|
86
|
+
{ "_links": {
|
87
|
+
"self": { "href": "http://example.com/foo" }
|
88
|
+
,"ex:bar": { "href": "http://example.com/bar" }
|
89
|
+
,"curies": [{"name": "ex", "href": "http://example.com/rels/{rel}", "templated": true}]
|
90
|
+
}
|
91
|
+
}
|
92
|
+
HAL
|
93
|
+
|
94
|
+
subject { described_class.new(MultiJson.load(raw_repr), hal_client) }
|
95
|
+
|
96
|
+
specify { expect(subject.extract_links).to include link_matching("ex:bar", "http://example.com/bar") }
|
97
|
+
specify { expect(subject.extract_links).to include link_matching("http://example.com/rels/bar", "http://example.com/bar") }
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
matcher :link_matching do |rel, target_url|
|
103
|
+
match { |actual_link|
|
104
|
+
(actual_link.literal_rel == rel || actual_link.fully_qualified_rel == rel) &&
|
105
|
+
actual_link.target_url == target_url
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
matcher :templated_link_matching do |rel, target_url|
|
110
|
+
match { |actual_link|
|
111
|
+
(actual_link.literal_rel == rel || actual_link.fully_qualified_rel == rel) &&
|
112
|
+
actual_link.raw_href == Addressable::Template.new(target_url) &&
|
113
|
+
actual_link.templated?
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
@@ -4,89 +4,37 @@ require "hal_client/link"
|
|
4
4
|
|
5
5
|
RSpec.describe HalClient::Link do
|
6
6
|
|
7
|
-
subject(:link) {
|
7
|
+
subject(:link) { HalClient::SimpleLink.new(rel: rel_1, target: repr_1) }
|
8
8
|
|
9
9
|
describe "#initialize" do
|
10
10
|
it "requires a target" do
|
11
|
-
expect { HalClient::
|
11
|
+
expect { HalClient::SimpleLink.new(rel: rel_1) }.to raise_error(ArgumentError)
|
12
12
|
end
|
13
13
|
|
14
14
|
it "doesn't allow both target and template" do
|
15
15
|
expect {
|
16
|
-
HalClient::
|
16
|
+
HalClient::SimpleLink.new(rel: rel_1, target: repr_1, template: template_1)
|
17
17
|
}.to raise_error(ArgumentError)
|
18
18
|
end
|
19
19
|
|
20
20
|
it "requires target to be a Representation" do
|
21
21
|
expect {
|
22
|
-
HalClient::
|
22
|
+
HalClient::SimpleLink.new(rel: rel_1, target: template_1)
|
23
23
|
}.to raise_error(ArgumentError)
|
24
24
|
end
|
25
25
|
|
26
26
|
it "requires template to be an Addressable::Template" do
|
27
27
|
expect {
|
28
|
-
HalClient::
|
28
|
+
HalClient::TemplatedLink.new(rel: rel_1, template: repr_1)
|
29
29
|
}.to raise_error(ArgumentError)
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
-
describe ".new_from_link_entry" do
|
34
|
-
it "creates an instance of Link" do
|
35
|
-
my_link = described_class.new_from_link_entry(hash_entry: link_entry_hash(href: href_1),
|
36
|
-
hal_client: a_client,
|
37
|
-
curie_resolver: curie_resolver,
|
38
|
-
base_url: href_1)
|
39
|
-
expect(my_link).to be_a(HalClient::Link)
|
40
|
-
end
|
41
|
-
|
42
|
-
it "handles relative hrefs" do
|
43
|
-
input_hash = link_entry_hash(href: relative_href_1)
|
44
|
-
base_url = Addressable::URI.parse(href_1)
|
45
|
-
|
46
|
-
my_link = described_class.new_from_link_entry(hash_entry: input_hash,
|
47
|
-
hal_client: a_client,
|
48
|
-
curie_resolver: curie_resolver,
|
49
|
-
base_url: base_url)
|
50
|
-
expect(my_link.raw_href).to eq((base_url + relative_href_1).to_s)
|
51
|
-
end
|
52
|
-
|
53
|
-
it "handles hrefs with a nil value" do
|
54
|
-
input_hash = link_entry_hash(href: nil)
|
55
|
-
base_url = Addressable::URI.parse(href_1)
|
56
|
-
|
57
|
-
my_link = described_class.new_from_link_entry(hash_entry: input_hash,
|
58
|
-
hal_client: a_client,
|
59
|
-
curie_resolver: curie_resolver,
|
60
|
-
base_url: base_url)
|
61
|
-
|
62
|
-
expect(my_link.raw_href).to eq(base_url.to_s)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
describe ".new_from_embedded_entry" do
|
67
|
-
it "creates an instance of Link" do
|
68
|
-
my_link = described_class.new_from_embedded_entry(hash_entry: embedded_entry_hash,
|
69
|
-
hal_client: a_client,
|
70
|
-
curie_resolver: curie_resolver,
|
71
|
-
base_url: href_1)
|
72
|
-
expect(my_link).to be_a(HalClient::Link)
|
73
|
-
end
|
74
|
-
|
75
|
-
it "handles relative hrefs" do
|
76
|
-
input_hash = embedded_entry_hash(href: relative_href_1)
|
77
|
-
base_url = Addressable::URI.parse(href_1)
|
78
|
-
|
79
|
-
my_link = described_class.new_from_embedded_entry(hash_entry: input_hash,
|
80
|
-
hal_client: a_client,
|
81
|
-
curie_resolver: curie_resolver,
|
82
|
-
base_url: base_url)
|
83
|
-
expect(my_link.raw_href).to eq((base_url + relative_href_1).to_s)
|
84
|
-
end
|
85
|
-
end
|
86
33
|
|
87
34
|
describe "#href" do
|
88
35
|
specify { expect(link.raw_href).to eq('http://example.com/href_1') }
|
89
|
-
specify { expect(templated_link1.raw_href)
|
36
|
+
specify { expect(templated_link1.raw_href)
|
37
|
+
.to eq Addressable::Template.new('http://example.com/people{?name}') }
|
90
38
|
end
|
91
39
|
|
92
40
|
describe "#templated?" do
|
@@ -95,27 +43,33 @@ RSpec.describe HalClient::Link do
|
|
95
43
|
end
|
96
44
|
|
97
45
|
context "equality and hash" do
|
98
|
-
let(:link_same_target_same_rel) { HalClient::
|
46
|
+
let(:link_same_target_same_rel) { HalClient::SimpleLink.new(target: repr_1, rel: rel_1) }
|
99
47
|
|
100
|
-
let(:link_same_target_diff_rel) { HalClient::
|
101
|
-
let(:link_diff_target_same_rel) { HalClient::
|
102
|
-
let(:link_diff_target_diff_rel) { HalClient::
|
48
|
+
let(:link_same_target_diff_rel) { HalClient::SimpleLink.new(target: repr_1, rel: rel_2) }
|
49
|
+
let(:link_diff_target_same_rel) { HalClient::SimpleLink.new(target: repr_2, rel: rel_1) }
|
50
|
+
let(:link_diff_target_diff_rel) { HalClient::SimpleLink.new(target: repr_2, rel: rel_2) }
|
103
51
|
|
104
|
-
let(:link_same_non_fetched) { HalClient::
|
52
|
+
let(:link_same_non_fetched) { HalClient::SimpleLink.new(target: href_only_repr, rel: rel_1) }
|
105
53
|
|
106
54
|
let(:same_as_templated_link1) do
|
107
|
-
HalClient::
|
108
|
-
|
55
|
+
HalClient::TemplatedLink
|
56
|
+
.new(rel: 'templated_link',
|
57
|
+
template: Addressable::Template.new('http://example.com/people{?name}'),
|
58
|
+
hal_client: a_client)
|
109
59
|
end
|
110
60
|
|
111
61
|
let(:templated_link2) do
|
112
|
-
HalClient::
|
113
|
-
|
62
|
+
HalClient::TemplatedLink
|
63
|
+
.new(rel: 'templated_link',
|
64
|
+
template: Addressable::Template.new('http://example.com/places{?name}'),
|
65
|
+
hal_client: a_client)
|
114
66
|
end
|
115
67
|
|
116
68
|
let(:template_but_not_a_template) do
|
117
|
-
HalClient::
|
118
|
-
|
69
|
+
HalClient::TemplatedLink
|
70
|
+
.new(rel: 'rel_1',
|
71
|
+
template: Addressable::Template.new('http://example.com/href_1'),
|
72
|
+
hal_client: a_client)
|
119
73
|
end
|
120
74
|
|
121
75
|
describe "#==" do
|
@@ -181,7 +135,7 @@ RSpec.describe HalClient::Link do
|
|
181
135
|
let(:rel_2) { 'rel_2' }
|
182
136
|
|
183
137
|
let(:full_uri_rel_1) { 'http://example.com/rels/rel_1' }
|
184
|
-
let(:full_uri_link_1) { HalClient::
|
138
|
+
let(:full_uri_link_1) { HalClient::SimpleLink.new(rel: full_uri_rel_1, target: repr_1) }
|
185
139
|
|
186
140
|
let(:curie_resolver) do
|
187
141
|
HalClient::CurieResolver.new({
|
@@ -194,7 +148,7 @@ RSpec.describe HalClient::Link do
|
|
194
148
|
let(:curied_rel_1) { 'ex:rel_1' }
|
195
149
|
|
196
150
|
let(:curied_link_1) do
|
197
|
-
HalClient::
|
151
|
+
HalClient::SimpleLink.new(rel: curied_rel_1, target: repr_1, curie_resolver: curie_resolver)
|
198
152
|
end
|
199
153
|
|
200
154
|
let(:href_1) { 'http://example.com/href_1' }
|
@@ -281,6 +235,7 @@ RSpec.describe HalClient::Link do
|
|
281
235
|
|
282
236
|
let(:template_1) { Addressable::Template.new('http://example.com/people{?name}') }
|
283
237
|
|
284
|
-
let(:templated_link1) { HalClient::
|
238
|
+
let(:templated_link1) { HalClient::TemplatedLink.new(rel: 'templated_link', template: template_1, hal_client: a_client) }
|
285
239
|
|
240
|
+
let(:a_client) { HalClient.new }
|
286
241
|
end
|
@@ -107,13 +107,15 @@ HAL
|
|
107
107
|
describe "#to_s" do
|
108
108
|
subject(:return_val) { repr.to_s }
|
109
109
|
|
110
|
-
it { is_expected.to
|
110
|
+
it { is_expected.to match %{#<HalClient::Representation:} }
|
111
|
+
it { is_expected.to match %r{http://example.com/foo} }
|
111
112
|
|
112
113
|
context "anonymous" do
|
113
114
|
let(:repr) { described_class.new(hal_client: a_client,
|
114
115
|
parsed_json: MultiJson.load("{}")) }
|
115
116
|
|
116
|
-
it { is_expected.to
|
117
|
+
it { is_expected.to match %{#<HalClient::Representation:} }
|
118
|
+
it { is_expected.to match /ANONYMOUS/i }
|
117
119
|
end
|
118
120
|
end
|
119
121
|
|
@@ -152,7 +154,7 @@ HAL
|
|
152
154
|
|
153
155
|
describe "hash" do
|
154
156
|
specify{ expect(repr.hash).to eq repr.href.hash }
|
155
|
-
specify{ expect(repr_no_href.hash).
|
157
|
+
specify{ expect(repr_no_href.hash).not_to eq repr_no_href.raw.hash }
|
156
158
|
end
|
157
159
|
end
|
158
160
|
|
@@ -279,7 +281,10 @@ HAL
|
|
279
281
|
specify { expect(subject).to include(link3a_link) }
|
280
282
|
specify { expect(subject).to include(link3b_link) }
|
281
283
|
|
282
|
-
specify { expect(subject
|
284
|
+
specify { expect(subject
|
285
|
+
.find { |l| l.literal_rel == "dup" }
|
286
|
+
.target['dupProperty'])
|
287
|
+
.to eq "foo" }
|
283
288
|
end
|
284
289
|
|
285
290
|
specify { expect(repr.related_hrefs "link1")
|
@@ -408,11 +413,6 @@ HAL
|
|
408
413
|
specify { expect( repr.to_enum ).to have(2).items }
|
409
414
|
end
|
410
415
|
|
411
|
-
context "non-collection" do
|
412
|
-
specify { expect{repr.as_enum}.to raise_error(HalClient::NotACollectionError) }
|
413
|
-
specify { expect{repr.to_enum}.to raise_error(HalClient::NotACollectionError) }
|
414
|
-
end
|
415
|
-
|
416
416
|
# Background
|
417
417
|
|
418
418
|
let(:link1_repr) do
|
@@ -429,24 +429,25 @@ HAL
|
|
429
429
|
|
430
430
|
|
431
431
|
let(:link1_link) do
|
432
|
-
HalClient::
|
432
|
+
HalClient::SimpleLink.new(rel: 'link1', target: link1_repr)
|
433
433
|
end
|
434
434
|
|
435
435
|
let(:link2_link) do
|
436
|
-
HalClient::
|
436
|
+
HalClient::SimpleLink.new(rel: 'link2', target: link1_repr)
|
437
437
|
end
|
438
438
|
|
439
439
|
let(:templated_link) do
|
440
|
-
HalClient::
|
441
|
-
|
440
|
+
HalClient::TemplatedLink.new(rel: 'templated',
|
441
|
+
template: Addressable::Template.new('http://example.com/people{?name}'),
|
442
|
+
hal_client: a_client)
|
442
443
|
end
|
443
444
|
|
444
445
|
let(:link3a_link) do
|
445
|
-
HalClient::
|
446
|
+
HalClient::SimpleLink.new(rel: 'link3', target: link3a_repr)
|
446
447
|
end
|
447
448
|
|
448
449
|
let(:link3b_link) do
|
449
|
-
HalClient::
|
450
|
+
HalClient::SimpleLink.new(rel: 'link3', target: link3b_repr)
|
450
451
|
end
|
451
452
|
|
452
453
|
|
data/spec/spec_helper.rb
CHANGED
@@ -39,7 +39,7 @@ RSpec.configure do |config|
|
|
39
39
|
|
40
40
|
# This setting enables warnings. It's recommended, but in some cases may
|
41
41
|
# be too noisy due to issues in dependencies.
|
42
|
-
config.warnings =
|
42
|
+
config.warnings = false
|
43
43
|
|
44
44
|
# Many RSpec users commonly either run the entire suite or an individual
|
45
45
|
# file, and it's useful to allow more verbose output when running an
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hal-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-05-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http
|
@@ -168,9 +168,11 @@ files:
|
|
168
168
|
- hal-client.gemspec
|
169
169
|
- lib/hal-client.rb
|
170
170
|
- lib/hal_client.rb
|
171
|
+
- lib/hal_client/anonymous_resource_locator.rb
|
171
172
|
- lib/hal_client/collection.rb
|
172
173
|
- lib/hal_client/curie_resolver.rb
|
173
174
|
- lib/hal_client/errors.rb
|
175
|
+
- lib/hal_client/interpreter.rb
|
174
176
|
- lib/hal_client/link.rb
|
175
177
|
- lib/hal_client/links_section.rb
|
176
178
|
- lib/hal_client/representation.rb
|
@@ -179,6 +181,7 @@ files:
|
|
179
181
|
- lib/hal_client/version.rb
|
180
182
|
- spec/hal_client/collection_spec.rb
|
181
183
|
- spec/hal_client/curie_resolver_spec.rb
|
184
|
+
- spec/hal_client/interpreter_spec.rb
|
182
185
|
- spec/hal_client/link_spec.rb
|
183
186
|
- spec/hal_client/links_section_spec.rb
|
184
187
|
- spec/hal_client/representation_editor_spec.rb
|
@@ -214,6 +217,7 @@ summary: Use HAL APIs easily
|
|
214
217
|
test_files:
|
215
218
|
- spec/hal_client/collection_spec.rb
|
216
219
|
- spec/hal_client/curie_resolver_spec.rb
|
220
|
+
- spec/hal_client/interpreter_spec.rb
|
217
221
|
- spec/hal_client/link_spec.rb
|
218
222
|
- spec/hal_client/links_section_spec.rb
|
219
223
|
- spec/hal_client/representation_editor_spec.rb
|