excon-hypermedia 0.3.0 → 0.4.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/README.md +194 -30
- data/lib/excon/hypermedia/ext/response.rb +2 -0
- data/lib/excon/hypermedia/helpers/collection.rb +62 -0
- data/lib/excon/hypermedia/link_object.rb +153 -0
- data/lib/excon/hypermedia/middleware.rb +2 -0
- data/lib/excon/hypermedia/resource_object/embedded.rb +23 -0
- data/lib/excon/hypermedia/resource_object/links.rb +19 -0
- data/lib/excon/hypermedia/resource_object/properties.rb +17 -0
- data/lib/excon/hypermedia/resource_object.rb +69 -0
- data/lib/excon/hypermedia/response.rb +18 -11
- data/lib/excon/hypermedia/version.rb +1 -1
- data/lib/excon/hypermedia.rb +4 -2
- data/test/excon/integration_test.rb +99 -0
- data/test/excon/link_object_test.rb +91 -0
- data/test/excon/links_test.rb +43 -0
- data/test/excon/properties_test.rb +67 -0
- data/test/excon/resource_object_test.rb +49 -0
- data/test/test_helper.rb +141 -0
- metadata +13 -7
- data/lib/excon/hypermedia/link.rb +0 -40
- data/lib/excon/hypermedia/resource.rb +0 -49
- data/test/excon/hypermedia_test.rb +0 -100
- data/test/excon/link_test.rb +0 -56
- data/test/excon/resource_test.rb +0 -47
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38a442095c082e7251b1a72d8e88084833357a3d
|
4
|
+
data.tar.gz: f0ff7d16042fa3d35bfca84b216b6e145f55db2d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ce27164e467d58bd426538b59cb917761c246d36dcb744d43e807169415ca225280cbd9965d41a631c907b2abeed49794090e3800a78e51a05b2f0811f781f1
|
7
|
+
data.tar.gz: 4670c0806fa665a012b0238598cc724296e5252b79735f116954aba80649b116773560d07aeebad932cbfd0f39550bd1a21248376d2ab25ccc6f49f1615fdf9e
|
data/README.md
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
Teaches [Excon][] how to talk to [HyperMedia APIs][hypermedia].
|
4
4
|
|
5
|
+
* [Installation](#installation)
|
6
|
+
* [Quick Start](#quick-start)
|
7
|
+
* [Usage](#usage)
|
8
|
+
* [resources](#resources)
|
9
|
+
* [links](#links)
|
10
|
+
* [relations](#relations)
|
11
|
+
* [properties](#properties)
|
12
|
+
* [embedded](#embedded)
|
13
|
+
* [License](#license)
|
14
|
+
|
5
15
|
## Installation
|
6
16
|
|
7
17
|
Add this line to your application's Gemfile:
|
@@ -22,6 +32,22 @@ Or install it yourself as:
|
|
22
32
|
gem install excon-hypermedia
|
23
33
|
```
|
24
34
|
|
35
|
+
## Quick Start
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
Excon.defaults[:middlewares].push(Excon::HyperMedia::Middleware)
|
39
|
+
|
40
|
+
api = Excon.get('https://www.example.org/api.json')
|
41
|
+
api.class # => Excon::Response
|
42
|
+
|
43
|
+
product = api.rel('product', expand: { uid: 'bicycle' })
|
44
|
+
product.class # => Excon::Connection
|
45
|
+
|
46
|
+
response = product.get
|
47
|
+
response.class # => Excon::Response
|
48
|
+
response.resource.name # => 'bicycle'
|
49
|
+
```
|
50
|
+
|
25
51
|
## Usage
|
26
52
|
|
27
53
|
To let Excon know the API supports HyperMedia, simply enable the correct
|
@@ -34,65 +60,202 @@ api = Excon.get('https://www.example.org/api.json')
|
|
34
60
|
api.class # => Excon::Response
|
35
61
|
```
|
36
62
|
|
37
|
-
|
38
|
-
|
63
|
+
> NOTE: we'll use the following JSON response body in the below examples:
|
64
|
+
>
|
65
|
+
> **https://www.example.org/api.json**
|
66
|
+
>
|
67
|
+
> ```json
|
68
|
+
> {
|
69
|
+
> "_links": {
|
70
|
+
> "self": {
|
71
|
+
> "href": "https://www.example.org/api.json"
|
72
|
+
> },
|
73
|
+
> "product": {
|
74
|
+
> "href": "https://www.example.org/product/{uid}",
|
75
|
+
> "templated": true
|
76
|
+
> }
|
77
|
+
> }
|
78
|
+
> }
|
79
|
+
> ```
|
80
|
+
>
|
81
|
+
> **https://www.example.org/product/bicycle**
|
82
|
+
>
|
83
|
+
> ```json
|
84
|
+
> {
|
85
|
+
> "_links": {
|
86
|
+
> "self": {
|
87
|
+
> "href": "https://www.example.org/product/bicycle"
|
88
|
+
> }
|
89
|
+
> },
|
90
|
+
> "bike-type": "Mountain Bike",
|
91
|
+
> "BMX": false,
|
92
|
+
> "derailleurs": {
|
93
|
+
> "back": 7,
|
94
|
+
> "front": 3
|
95
|
+
> },
|
96
|
+
> "name": "bicycle",
|
97
|
+
> "reflectors": true,
|
98
|
+
> "_embedded": {
|
99
|
+
> "pump": {
|
100
|
+
> "_links": {
|
101
|
+
> "self": "https://www.example.org/product/pump"
|
102
|
+
> },
|
103
|
+
> "weight": "2kg",
|
104
|
+
> "type": "Floor Pump",
|
105
|
+
> "valve-type": "Presta"
|
106
|
+
> }
|
107
|
+
> }
|
108
|
+
> }
|
109
|
+
|
110
|
+
With this middleware injected in the stack, Excon's model is now expanded with
|
111
|
+
several key concepts:
|
112
|
+
|
113
|
+
### resources
|
114
|
+
|
115
|
+
A **resource** is the representation of the object returned by the API. Almost
|
116
|
+
all other concepts and methods originate from this object.
|
117
|
+
|
118
|
+
Use the newly available `Excon::Response#resource` method to access the resource
|
119
|
+
object:
|
39
120
|
|
40
121
|
```ruby
|
41
|
-
|
42
|
-
|
122
|
+
api.resource.class # => Excon::HyperMedia::ResourceObject
|
123
|
+
```
|
43
124
|
|
44
|
-
|
45
|
-
|
46
|
-
|
125
|
+
A resource has several methods exposed:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
api.resource.public_methods(false) # => [:_links, :_properties, :_embedded]
|
129
|
+
```
|
130
|
+
|
131
|
+
Each of these methods represents one of the following HyperMedia concepts.
|
132
|
+
|
133
|
+
### links
|
134
|
+
|
135
|
+
A resource has links, that point to related resources (and itself), these can be
|
136
|
+
accessed as well:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
api.resource._links.class # => Excon::HyperMedia::ResourceObject::Links
|
140
|
+
```
|
141
|
+
|
142
|
+
You can get a list of valid links using `keys`:
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
api.resource._links.keys # => ['self', 'product']
|
146
|
+
```
|
147
|
+
|
148
|
+
Each links is represented by a `LinkObject` instance:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
api.resource._links.product.class # => Excon::HyperMedia::LinkObject
|
152
|
+
api.resource._links.product.href # => 'https://www.example.org/product/{uid}'
|
153
|
+
api.resource._links.product.templated # => true
|
154
|
+
```
|
155
|
+
|
156
|
+
### relations
|
157
|
+
|
158
|
+
Links are the primary way to traverse between relations. This is what makes a
|
159
|
+
HyperMedia-based API "self-discoverable".
|
160
|
+
|
161
|
+
To go from one resource, to the next, you use the `rel` (short for relation)
|
162
|
+
method. This method is available on any `LinkObject` instance.
|
163
|
+
|
164
|
+
Using `rel`, returns an `Excon::Connection` object, the same as if you where to
|
165
|
+
call `Excon.new`:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
relation = api.resource._links.self.rel
|
169
|
+
relation.class # => Excon::Connection
|
170
|
+
```
|
171
|
+
|
172
|
+
Since the returned object is of type `Excon::Connection`, all
|
173
|
+
[Excon-provided options][options] are available as well:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
relation.get(idempotent: true, retry_limit: 6)
|
177
|
+
```
|
178
|
+
|
179
|
+
`Excon::Response` also has a convenient delegation to `LinkObject#rel`:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
relation = api.rel('self').get
|
183
|
+
```
|
184
|
+
|
185
|
+
Once you call `get` (or `post`, or any other valid Excon request method), you
|
186
|
+
are back where you started, with a new `Excon::Response` object, imbued with
|
187
|
+
HyperMedia powers:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
relation.resource._links.keys # => ['self', 'product']
|
191
|
+
```
|
192
|
+
|
193
|
+
In this case, we ended up back with the same type of object as before. To go
|
194
|
+
anywhere meaningful, we want to use the `product` rel:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
product = api.rel('product', expand: { uid: 'bicycle' }).get
|
47
198
|
```
|
48
199
|
|
49
200
|
As seen above, you can expand URI Template variables using the `expand` option,
|
50
201
|
provided by the [`excon-addressable` library][excon-addressable].
|
51
202
|
|
52
|
-
|
53
|
-
|
54
|
-
are
|
203
|
+
### properties
|
204
|
+
|
205
|
+
Properties are what make a resource unique, they tell us more about the state of
|
206
|
+
the resource, they are the key/value pairs that define the resource.
|
207
|
+
|
208
|
+
In HAL/JSON terms, this is everything returned by the response body, excluding
|
209
|
+
the `_links` and `_embedded` sections:
|
55
210
|
|
56
211
|
```ruby
|
57
|
-
product.
|
212
|
+
product.resource.name # => "bicycle"
|
213
|
+
product.resource.reflectors # => true
|
58
214
|
```
|
59
215
|
|
60
|
-
|
61
|
-
|
62
|
-
You can access all links in a resource using the `links` method:
|
216
|
+
Nested properties are supported as well:
|
63
217
|
|
64
218
|
```ruby
|
65
|
-
|
66
|
-
|
67
|
-
|
219
|
+
product.resource.derailleurs.class # => Excon::HyperMedia::ResourceObject::Properties
|
220
|
+
product.resource.derailleurs.front # => 3
|
221
|
+
product.resource.derailleurs.back # => 7
|
68
222
|
```
|
69
223
|
|
70
|
-
|
224
|
+
Property names that aren't valid method names can always be accessed using the
|
225
|
+
hash notation:
|
71
226
|
|
72
227
|
```ruby
|
73
|
-
|
228
|
+
product.resource['bike-type'] # => 'Mountain Bike'
|
229
|
+
product.resource['BMX'] # => false
|
230
|
+
product.resource.bmx # => false
|
74
231
|
```
|
75
232
|
|
76
|
-
|
77
|
-
|
233
|
+
Properties should be implicitly accessed on a resource, but are internally
|
234
|
+
accessed via the `_properties` method:
|
78
235
|
|
79
236
|
```ruby
|
80
|
-
|
237
|
+
product.resource._properties.class # => Excon::HyperMedia::ResourceObject::Properties
|
238
|
+
```
|
81
239
|
|
82
|
-
|
83
|
-
api.customer-orders(expand: { sort: 'desc' }) # => NoMethodError: undefined method `customer'
|
240
|
+
The `Properties` object inherits its logics from `Enumerable`:
|
84
241
|
|
85
|
-
|
86
|
-
|
242
|
+
```ruby
|
243
|
+
product.resource._properties.to_h.class # => Hash
|
244
|
+
product.resource._properties.first # => ['name', 'bicycle']
|
87
245
|
```
|
88
246
|
|
89
|
-
###
|
247
|
+
### embedded
|
248
|
+
|
249
|
+
Embedded resources are resources that are available through link relations, but
|
250
|
+
embedded in the current resource for easier access.
|
251
|
+
|
252
|
+
For more information on this concept, see the [formal specification][_embedded].
|
90
253
|
|
91
|
-
|
254
|
+
Embedded resources work the same as the top-level resource:
|
92
255
|
|
93
256
|
```ruby
|
94
|
-
product.
|
95
|
-
product.
|
257
|
+
product._embedded.pump.class # => Excon::HyperMedia::ResourceObject
|
258
|
+
product._embedded.pump.weight # => '2kg'
|
96
259
|
```
|
97
260
|
|
98
261
|
## License
|
@@ -103,3 +266,4 @@ The gem is available as open source under the terms of the [MIT License](http://
|
|
103
266
|
[hypermedia]: https://en.wikipedia.org/wiki/HATEOAS
|
104
267
|
[excon-addressable]: https://github.com/JeanMertz/excon-addressable
|
105
268
|
[options]: https://github.com/excon/excon#options
|
269
|
+
[_embedded]: https://tools.ietf.org/html/draft-kelly-json-hal-08#section-4.1.2
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Excon
|
6
|
+
module HyperMedia
|
7
|
+
# Collection
|
8
|
+
#
|
9
|
+
# Given a `Hash`, provides dot-notation properties and other helper methods.
|
10
|
+
#
|
11
|
+
module Collection
|
12
|
+
include Enumerable
|
13
|
+
|
14
|
+
def initialize(collection = {})
|
15
|
+
@collection ||= collection
|
16
|
+
to_properties
|
17
|
+
end
|
18
|
+
|
19
|
+
def each(&block)
|
20
|
+
collection.each(&block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def keys
|
24
|
+
@collection.keys
|
25
|
+
end
|
26
|
+
|
27
|
+
def key?(key)
|
28
|
+
collection.key?(key.to_s)
|
29
|
+
end
|
30
|
+
|
31
|
+
def [](key)
|
32
|
+
to_property(key)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def to_properties
|
38
|
+
collection.each do |key, value|
|
39
|
+
key = key.downcase
|
40
|
+
next unless /[@$"]/ !~ key.to_sym.inspect
|
41
|
+
|
42
|
+
singleton_class.class_eval { attr_reader key }
|
43
|
+
instance_variable_set("@#{key}", property(value))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def property(value)
|
48
|
+
value.respond_to?(:keys) ? self.class.new(value) : value
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_property(key)
|
52
|
+
key?(key) ? property(collection[key]) : nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_property!(key)
|
56
|
+
key?(key) ? to_property(key) : method_missing(key)
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :collection
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Excon
|
4
|
+
module HyperMedia
|
5
|
+
# Link
|
6
|
+
#
|
7
|
+
# Encapsulates a link pointing to a resource.
|
8
|
+
#
|
9
|
+
# @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5
|
10
|
+
#
|
11
|
+
class LinkObject
|
12
|
+
include Collection
|
13
|
+
|
14
|
+
# href
|
15
|
+
#
|
16
|
+
# The "href" property is REQUIRED.
|
17
|
+
#
|
18
|
+
# Its value is either a URI [RFC3986] or a URI Template [RFC6570].
|
19
|
+
#
|
20
|
+
# If the value is a URI Template then the Link Object SHOULD have a
|
21
|
+
# "templated" attribute whose value is true.
|
22
|
+
#
|
23
|
+
# @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.1
|
24
|
+
#
|
25
|
+
def href
|
26
|
+
to_property!(:href)
|
27
|
+
end
|
28
|
+
|
29
|
+
# The "templated" property is OPTIONAL.
|
30
|
+
#
|
31
|
+
# Its value is boolean and SHOULD be true when the Link Object's "href"
|
32
|
+
# property is a URI Template.
|
33
|
+
#
|
34
|
+
# Its value SHOULD be considered false if it is undefined or any other
|
35
|
+
# value than true.
|
36
|
+
#
|
37
|
+
# @see: https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.2
|
38
|
+
#
|
39
|
+
def templated
|
40
|
+
to_property(__method__) || false
|
41
|
+
end
|
42
|
+
|
43
|
+
# type
|
44
|
+
#
|
45
|
+
# The "type" property is OPTIONAL.
|
46
|
+
#
|
47
|
+
# Its value is a string used as a hint to indicate the media type
|
48
|
+
# expected when dereferencing the target resource.
|
49
|
+
#
|
50
|
+
# @see: https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.3
|
51
|
+
#
|
52
|
+
def type
|
53
|
+
to_property!(__method__)
|
54
|
+
end
|
55
|
+
|
56
|
+
# deprecation
|
57
|
+
#
|
58
|
+
# The "deprecation" property is OPTIONAL.
|
59
|
+
#
|
60
|
+
# Its presence indicates that the link is to be deprecated (i.e.
|
61
|
+
# removed) at a future date. Its value is a URL that SHOULD provide
|
62
|
+
# further information about the deprecation.
|
63
|
+
#
|
64
|
+
# A client SHOULD provide some notification (for example, by logging a
|
65
|
+
# warning message) whenever it traverses over a link that has this
|
66
|
+
# property. The notification SHOULD include the deprecation property's
|
67
|
+
# value so that a client manitainer can easily find information about
|
68
|
+
# the deprecation.
|
69
|
+
#
|
70
|
+
# @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.4
|
71
|
+
#
|
72
|
+
def deprecation
|
73
|
+
to_property!(__method__)
|
74
|
+
end
|
75
|
+
|
76
|
+
# name
|
77
|
+
#
|
78
|
+
# The "name" property is OPTIONAL.
|
79
|
+
#
|
80
|
+
# Its value MAY be used as a secondary key for selecting Link Objects
|
81
|
+
# which share the same relation type.
|
82
|
+
#
|
83
|
+
# @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.5
|
84
|
+
#
|
85
|
+
def name
|
86
|
+
to_property!(__method__)
|
87
|
+
end
|
88
|
+
|
89
|
+
# profile
|
90
|
+
#
|
91
|
+
# The "profile" property is OPTIONAL.
|
92
|
+
#
|
93
|
+
# Its value is a string which is a URI that hints about the profile (as
|
94
|
+
# defined by [I-D.wilde-profile-link]) of the target resource.
|
95
|
+
#
|
96
|
+
# @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.6
|
97
|
+
#
|
98
|
+
def profile
|
99
|
+
to_property!(__method__)
|
100
|
+
end
|
101
|
+
|
102
|
+
# title
|
103
|
+
#
|
104
|
+
# The "title" property is OPTIONAL.
|
105
|
+
#
|
106
|
+
# Its value is a string and is intended for labelling the link with a
|
107
|
+
# human-readable identifier (as defined by [RFC5988]).
|
108
|
+
#
|
109
|
+
# @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.7
|
110
|
+
#
|
111
|
+
def title
|
112
|
+
to_property!(__method__)
|
113
|
+
end
|
114
|
+
|
115
|
+
# hreflang
|
116
|
+
#
|
117
|
+
# The "hreflang" property is OPTIONAL.
|
118
|
+
#
|
119
|
+
# Its value is a string and is intended for indicating the language of
|
120
|
+
# the target resource (as defined by [RFC5988]).
|
121
|
+
#
|
122
|
+
# @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.8
|
123
|
+
#
|
124
|
+
def hreflang
|
125
|
+
to_property!(__method__)
|
126
|
+
end
|
127
|
+
|
128
|
+
# uri
|
129
|
+
#
|
130
|
+
# Returns a URI representation of the provided "href" property.
|
131
|
+
#
|
132
|
+
# @return [URI] URI object of the "href" property
|
133
|
+
#
|
134
|
+
def uri
|
135
|
+
::Addressable::URI.parse(href)
|
136
|
+
end
|
137
|
+
|
138
|
+
# rel
|
139
|
+
#
|
140
|
+
# Returns an `Excon::Connection` instance, based on the current link.
|
141
|
+
#
|
142
|
+
# @return [Excon::Connection] Connection object based on current link
|
143
|
+
#
|
144
|
+
def rel(params = {})
|
145
|
+
Excon.new(href, params)
|
146
|
+
end
|
147
|
+
|
148
|
+
private def property(value)
|
149
|
+
value
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
Excon.defaults[:middlewares].delete(Excon::Addressable::Middleware)
|
3
4
|
Excon.defaults[:middlewares].unshift(Excon::Addressable::Middleware)
|
4
5
|
|
5
6
|
module Excon
|
@@ -16,6 +17,7 @@ module Excon
|
|
16
17
|
def request_call(datum)
|
17
18
|
return super unless (content_type = datum.dig(:response, :headers, 'Content-Type').to_s)
|
18
19
|
|
20
|
+
datum[:response] ||= {}
|
19
21
|
datum[:response][:hypermedia] = if datum[:hypermedia].nil?
|
20
22
|
content_type.include?('hal+json')
|
21
23
|
else
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Excon
|
4
|
+
module HyperMedia
|
5
|
+
class ResourceObject
|
6
|
+
# Links
|
7
|
+
#
|
8
|
+
# Represents a collection of links part of a resource.
|
9
|
+
#
|
10
|
+
class Embedded
|
11
|
+
include Collection
|
12
|
+
|
13
|
+
private def property(value)
|
14
|
+
if value.respond_to?(:to_ary)
|
15
|
+
value.map { |v| ResourceObject.new(v) }
|
16
|
+
else
|
17
|
+
ResourceObject.new(value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Excon
|
4
|
+
module HyperMedia
|
5
|
+
class ResourceObject
|
6
|
+
# Links
|
7
|
+
#
|
8
|
+
# Represents a collection of links part of a resource.
|
9
|
+
#
|
10
|
+
class Links
|
11
|
+
include Collection
|
12
|
+
|
13
|
+
private def property(value)
|
14
|
+
value.respond_to?(:to_ary) ? value.map { |v| LinkObject.new(v) } : LinkObject.new(value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Excon
|
6
|
+
module HyperMedia
|
7
|
+
class ResourceObject
|
8
|
+
# Properties
|
9
|
+
#
|
10
|
+
# Provides consistent access to resource properties.
|
11
|
+
#
|
12
|
+
class Properties
|
13
|
+
include Collection
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'excon/hypermedia/resource_object/embedded'
|
4
|
+
require 'excon/hypermedia/resource_object/links'
|
5
|
+
require 'excon/hypermedia/resource_object/properties'
|
6
|
+
|
7
|
+
module Excon
|
8
|
+
module HyperMedia
|
9
|
+
# ResourceObject
|
10
|
+
#
|
11
|
+
# Represents a resource.
|
12
|
+
#
|
13
|
+
class ResourceObject
|
14
|
+
RESERVED_PROPERTIES = %w(_links _embedded).freeze
|
15
|
+
|
16
|
+
def initialize(data)
|
17
|
+
@data = data
|
18
|
+
|
19
|
+
_properties.each do |key, value|
|
20
|
+
key = key.downcase
|
21
|
+
next unless /[@$"]/ !~ key.to_sym.inspect
|
22
|
+
|
23
|
+
singleton_class.class_eval { attr_reader key }
|
24
|
+
instance_variable_set("@#{key}", value.respond_to?(:keys) ? Properties.new(value) : value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def _properties
|
29
|
+
@_properties ||= Properties.new(@data.reject { |k, _| RESERVED_PROPERTIES.include?(k) })
|
30
|
+
end
|
31
|
+
|
32
|
+
# _links
|
33
|
+
#
|
34
|
+
# The reserved "_links" property is OPTIONAL.
|
35
|
+
#
|
36
|
+
# It is an object whose property names are link relation types (as
|
37
|
+
# defined by [RFC5988]) and values are either a Link Object or an array
|
38
|
+
# of Link Objects. The subject resource of these links is the Resource
|
39
|
+
# Object of which the containing "_links" object is a property.
|
40
|
+
#
|
41
|
+
# @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-4.1.1
|
42
|
+
#
|
43
|
+
def _links
|
44
|
+
@_links ||= Links.new(@data['_links'])
|
45
|
+
end
|
46
|
+
|
47
|
+
# _embedded
|
48
|
+
#
|
49
|
+
# The reserved "_embedded" property is OPTIONAL
|
50
|
+
#
|
51
|
+
# It is an object whose property names are link relation types (as
|
52
|
+
# defined by [RFC5988]) and values are either a Resource Object or an
|
53
|
+
# array of Resource Objects.
|
54
|
+
#
|
55
|
+
# Embedded Resources MAY be a full, partial, or inconsistent version of
|
56
|
+
# the representation served from the target URI.
|
57
|
+
#
|
58
|
+
# @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-4.1.2
|
59
|
+
#
|
60
|
+
def _embedded
|
61
|
+
@_embedded ||= Embedded.new(@data['_embedded'])
|
62
|
+
end
|
63
|
+
|
64
|
+
def [](key)
|
65
|
+
_properties[key]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|