prx_hyperresource 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +263 -0
- data/lib/hyper_resource/adapter/hal_json.rb +105 -0
- data/lib/hyper_resource/adapter.rb +28 -0
- data/lib/hyper_resource/attributes.rb +73 -0
- data/lib/hyper_resource/configuration.rb +194 -0
- data/lib/hyper_resource/exceptions.rb +41 -0
- data/lib/hyper_resource/link.rb +116 -0
- data/lib/hyper_resource/links.rb +66 -0
- data/lib/hyper_resource/modules/config_attributes.rb +273 -0
- data/lib/hyper_resource/modules/data_type.rb +96 -0
- data/lib/hyper_resource/modules/deprecations.rb +51 -0
- data/lib/hyper_resource/modules/http.rb +305 -0
- data/lib/hyper_resource/modules/internal_attributes.rb +74 -0
- data/lib/hyper_resource/objects.rb +41 -0
- data/lib/hyper_resource/version.rb +3 -0
- data/lib/hyper_resource.rb +243 -0
- data/lib/hyperresource.rb +1 -0
- data/lib/prx_hyperresource.rb +1 -0
- metadata +220 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8f915cfe28034820646356966a937d0d0aa356239272d2b4c67c09bf1229fadf
|
|
4
|
+
data.tar.gz: 67725008931bf8c1333d5f06b3267a3fadeea8540d572973f1f01f31097dd344
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ebe26180a2258b3f037191612663ec06b49f5b17cfba2a33b0222cd565135eeb19f8f4ff075615243e0565be120966705f3d5dfbc6caa307f20b4dda2b039def
|
|
7
|
+
data.tar.gz: f2ed869789f03947e5ed8863db56515ab9e65308bfadbf0b9b9b0d5aaf9bfce3202d77f200d87d8aa5b50500c121812b290db02271a291088f07594aef8fbbf4
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
First release as `prx_hyperresource`, PRX's fork of
|
|
6
|
+
[hyperresource](https://github.com/gamache/hyperresource) 0.9.4. The library
|
|
7
|
+
is still loaded as `HyperResource`, and `require "hyperresource"` still works.
|
|
8
|
+
|
|
9
|
+
- Support Ruby 4: require `cgi`, which is no longer in Ruby's stdlib.
|
|
10
|
+
- Match URL masks without the `fuzzyurl` gem, but with similar logic.
|
|
11
|
+
- Treat URLs without a protocol as `https` (port 443) when matching masks.
|
|
12
|
+
- Support Faraday 2 basic auth, using the authorization middleware.
|
|
13
|
+
- Don't share cached connections between resources with different `faraday_options`.
|
|
14
|
+
- Default to `application/json` unless a Content-Type is configured.
|
|
15
|
+
- Prefer headers from more specific masks over `*`.
|
|
16
|
+
- Require Ruby 3.3 or later.
|
|
17
|
+
- Remove `HyperResource::VERSION_DATE` and the gemspec `date`.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright 2013 Pete Gamache, pete@gamache.org.
|
|
2
|
+
Copyright 2025-2026 PRX.
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
5
|
+
a copy of this software and associated documentation files (the
|
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
10
|
+
the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be
|
|
13
|
+
included in all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
20
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# HyperResource
|
|
2
|
+
|
|
3
|
+
PRX's maintained fork of
|
|
4
|
+
[gamache/hyperresource](https://github.com/gamache/hyperresource), published
|
|
5
|
+
as `prx_hyperresource`. See updates in the [CHANGELOG.md](CHANGELOG.md).
|
|
6
|
+
|
|
7
|
+
HyperResource is a Ruby client library for hypermedia web services.
|
|
8
|
+
|
|
9
|
+
HyperResource makes using a hypermedia API feel like calling plain old
|
|
10
|
+
methods on plain old objects.
|
|
11
|
+
|
|
12
|
+
It is usable with no configuration other than API root endpoint, but
|
|
13
|
+
also allows incoming data types to be extended with Ruby code.
|
|
14
|
+
|
|
15
|
+
HyperResource supports the
|
|
16
|
+
[HAL+JSON hypermedia format](http://stateless.co/hal_specification.html),
|
|
17
|
+
with support for Siren and other hypermedia formats planned.
|
|
18
|
+
|
|
19
|
+
For more insight into HyperResource's goals and design,
|
|
20
|
+
[read the paper](http://petegamache.com/wsrest2014-gamache.pdf)!
|
|
21
|
+
|
|
22
|
+
## Hypermedia in a Nutshell
|
|
23
|
+
|
|
24
|
+
Hypermedia APIs return a list of hyperlinks with each response. These
|
|
25
|
+
links, each of which has a relation name or "rel", represent everything
|
|
26
|
+
you can do to, with, or from the given response. They are URLs which
|
|
27
|
+
can take arguments. The consumer of the hypermedia API uses these
|
|
28
|
+
links to access the API's entire functionality.
|
|
29
|
+
|
|
30
|
+
A primary advantage to hypermedia APIs is that a client library can
|
|
31
|
+
write itself based on the hyperlinks coming back with each response.
|
|
32
|
+
This removes both the chore of writing a custom client library in the
|
|
33
|
+
first place, and also the process of pushing client updates to your
|
|
34
|
+
users.
|
|
35
|
+
|
|
36
|
+
## HyperResource Philosophy
|
|
37
|
+
|
|
38
|
+
An automatically-generated library can and should feel as comfortable as a
|
|
39
|
+
custom client library. Hypermedia brings many promises, but this is one
|
|
40
|
+
we can deliver on.
|
|
41
|
+
|
|
42
|
+
If you're an API user, HyperResource will help you consume a hypermedia
|
|
43
|
+
API with short, direct, elegant code.
|
|
44
|
+
If you're an API designer, HyperResource is a great default client, or a
|
|
45
|
+
smart starting point for a rich SDK.
|
|
46
|
+
|
|
47
|
+
Link-driven APIs are the future, and proper tooling can make it The Jetsons
|
|
48
|
+
instead of The Road Warrior.
|
|
49
|
+
|
|
50
|
+
## Install It
|
|
51
|
+
|
|
52
|
+
Add `gem "prx_hyperresource"` to your Gemfile, or `gem install prx_hyperresource`.
|
|
53
|
+
It is loaded as `HyperResource`, and `require "hyperresource"` still works.
|
|
54
|
+
|
|
55
|
+
HyperResource requires Ruby 3.3 or later.
|
|
56
|
+
|
|
57
|
+
HyperResource uses the
|
|
58
|
+
[uri_template](https://github.com/hannesg/uri_template)
|
|
59
|
+
and [Faraday](https://github.com/lostisland/faraday)
|
|
60
|
+
gems.
|
|
61
|
+
|
|
62
|
+
## Use It - Zero Configuration
|
|
63
|
+
|
|
64
|
+
Set up API connection:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
api = HyperResource.new(root: 'https://api.example.com',
|
|
68
|
+
headers: {'Accept' => 'application/vnd.example.com.v1+json'},
|
|
69
|
+
auth: {basic: ['username', 'password']})
|
|
70
|
+
# => #<HyperResource:0xABCD1234 @root="https://api.example.com" @href="" @namespace=nil ... >
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Now we can get the API's root resource, the gateway to everything else
|
|
74
|
+
on the API.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
api.get
|
|
78
|
+
# => #<HyperResource:0xABCD1234 @root="https://api.example.com" @href="" @namespace=nil ... >
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
What'd we get back?
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
api.body
|
|
85
|
+
# => { 'message' => 'Welcome to the Example.com API',
|
|
86
|
+
# 'version' => 1,
|
|
87
|
+
# '_links' => {
|
|
88
|
+
# 'curies' => [{
|
|
89
|
+
# 'name' => 'example',
|
|
90
|
+
# 'templated' => true,
|
|
91
|
+
# 'href' => 'https://api.example.com/rels/{rel}'
|
|
92
|
+
# }],
|
|
93
|
+
# 'self' => {'href' => '/'},
|
|
94
|
+
# 'example:users' => {'href' => '/users{?email,last_name}', 'templated' => true},
|
|
95
|
+
# 'example:forums' => {'href' => '/forums{?title}', 'templated' => true}
|
|
96
|
+
# }
|
|
97
|
+
# }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Lovely. Let's find a user by their email.
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
jdoe_user = api.users(email: "jdoe@example.com").first
|
|
104
|
+
# => #<HyperResource:0x12312312 ...>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
HyperResource has performed some behind-the-scenes expansions here.
|
|
108
|
+
|
|
109
|
+
First, the `example:users` link was
|
|
110
|
+
added to the `api` object at the time the resource was
|
|
111
|
+
loaded with `api.get`. And since the link rel has a
|
|
112
|
+
[CURIE prefix](http://tools.ietf.org/html/draft-kelly-json-hal-06#section-8.2),
|
|
113
|
+
HyperResource will allow a shortened version of its name, `users`.
|
|
114
|
+
|
|
115
|
+
Then, calling `first` on the `users` link
|
|
116
|
+
followed the link and loaded it automatically.
|
|
117
|
+
|
|
118
|
+
Finally, calling `first` on the resource containing one set of
|
|
119
|
+
embedded objects -- like this one -- delegates the method to
|
|
120
|
+
`.objects.first`, which returns the first object in the resource.
|
|
121
|
+
|
|
122
|
+
Here are some equivalent expressions to the above. HyperResource offers
|
|
123
|
+
a very short, expressive syntax as its primary interface,
|
|
124
|
+
but you can always fall back to explicit syntax if you like or need to.
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
api.users(email: "jdoe@example.com").first
|
|
129
|
+
api.get.users(email: "jdoe@example.com").first
|
|
130
|
+
api.get.links.users(email: "jdoe@example.com").first
|
|
131
|
+
api.get.links['users'].where(email: "jdoe@example.com").first
|
|
132
|
+
api.get.links['users'].where(email: "jdoe@example.com").get.first
|
|
133
|
+
api.get.links['users'].where(email: "jdoe@example.com").get.objects.first[1][0]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Use It - ActiveResource-style
|
|
137
|
+
|
|
138
|
+
If an API is returning data type information as part of the response,
|
|
139
|
+
then we can assign those data types to ruby classes so that they can
|
|
140
|
+
be extended.
|
|
141
|
+
|
|
142
|
+
For example, in our hypothetical Example API above, a user object is
|
|
143
|
+
returned with a custom media type bearing a 'type=User' modifier. We
|
|
144
|
+
will extend the User class with a few convenience methods.
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
class ExampleAPI < HyperResource
|
|
148
|
+
self.root = 'https://api.example.com'
|
|
149
|
+
self.headers = {'Accept' => 'application/vnd.example.com.v1+json'}
|
|
150
|
+
self.auth = {basic: ['username', 'password']}
|
|
151
|
+
|
|
152
|
+
class User < ExampleAPI
|
|
153
|
+
def full_name
|
|
154
|
+
first_name + ' ' + last_name
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
api = ExampleApi.new
|
|
160
|
+
|
|
161
|
+
user = api.users.where(email: 'jdoe@example.com').first
|
|
162
|
+
# => #<ExampleApi::User:0xffffffff ...>
|
|
163
|
+
|
|
164
|
+
user.full_name
|
|
165
|
+
# => "John Doe"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Don't worry if your API uses some other mechanism to indicate resource data
|
|
169
|
+
type; you can override the `.get_data_type` method and
|
|
170
|
+
implement your own logic.
|
|
171
|
+
|
|
172
|
+
## Configuration for Multiple Hosts
|
|
173
|
+
|
|
174
|
+
HyperResource supports the concept of different APIs connected in one
|
|
175
|
+
ecosystem by providing a mechanism to scope configuration parameters by
|
|
176
|
+
a URL mask. This allows a simple way to provide separate
|
|
177
|
+
authentication, headers, etc. to different services which link to each
|
|
178
|
+
other.
|
|
179
|
+
|
|
180
|
+
As a toy example, consider two servers. `http://localhost:12345/` returns:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{ "name": "Server One",
|
|
184
|
+
"_links": {
|
|
185
|
+
"self": {"href": "http://localhost:12345/"},
|
|
186
|
+
"server_two": {"href": "http://localhost:23456/"}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
And `http://localhost:23456/` returns:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{ "name": "Server Two",
|
|
195
|
+
"_links": {
|
|
196
|
+
"self": {"href": "http://localhost:23456/"},
|
|
197
|
+
"server_one": {"href": "http://localhost:12345/"}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The following configuration would ensure proper namespacing of the two
|
|
203
|
+
servers' response objects:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
class APIEcosystem < HyperResource
|
|
207
|
+
self.config(
|
|
208
|
+
"localhost:12345" => {"namespace" => "ServerOneAPI"},
|
|
209
|
+
"localhost:23456" => {"namespace" => "ServerTwoAPI"}
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
root_one = APIEcosystem.new(root: ‘http://localhost:12345’).get
|
|
214
|
+
root_one.name # => ‘Server One’
|
|
215
|
+
root_one.url # => ‘http://localhost:12345’
|
|
216
|
+
root_one.namespace # => ServerOneAPI
|
|
217
|
+
|
|
218
|
+
root_two = root_one.server_two.get
|
|
219
|
+
root_two.name # => ‘Server Two’
|
|
220
|
+
root_two.url # => ‘http://localhost:23456’
|
|
221
|
+
root_two.namespace # => ServerTwoAPI
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
URL masks are matched by HyperResource itself; it no longer uses the
|
|
225
|
+
[FuzzyURL](https://github.com/gamache/fuzzyurl/) gem. A mask may include a
|
|
226
|
+
protocol, hostname, and port, e.g. `https://api.example.com:8080`,
|
|
227
|
+
`api.example.com`, `*.example.com`, or `*`. Omitted parts match anything,
|
|
228
|
+
and more specific masks take priority. A URL without a protocol is treated
|
|
229
|
+
as `https`, so it matches `https://` and `:443` masks, but not `http://` or
|
|
230
|
+
`:80` masks.
|
|
231
|
+
|
|
232
|
+
## Error Handling
|
|
233
|
+
|
|
234
|
+
HyperResource raises a `HyperResource::ClientError` on 4xx responses,
|
|
235
|
+
and `HyperResource::ServerError` on 5xx responses. Catch one or both
|
|
236
|
+
(`HyperResource::ResponseError`). The exceptions contain as much of
|
|
237
|
+
`cause` (internal exception which led to this one), `response`
|
|
238
|
+
(`Faraday::Response` object), and `body` (the decoded response
|
|
239
|
+
as a `Hash`) as is possible at the time.
|
|
240
|
+
|
|
241
|
+
## Contributors
|
|
242
|
+
|
|
243
|
+
Many thanks to the people who've sent pull requests and improved this code:
|
|
244
|
+
|
|
245
|
+
* Jean-Charles d'Anthenaise Sisk ([@jasisk](https://github.com/jasisk))
|
|
246
|
+
* Ian Asaff ([@montague](https://github.com/montague))
|
|
247
|
+
* Julien Blanchard ([@julienXX](https://github.com/julienXX))
|
|
248
|
+
* Andrew Rollins ([@andrew311](https://github.com/andrew311))
|
|
249
|
+
* Étienne Barrié ([@etiennebarrie](https://github.com/etiennebarrie))
|
|
250
|
+
* James Martelletti ([@jmartelletti](https://github.com/jmartelletti))
|
|
251
|
+
* Frank Macreery ([@fancyremarker](https://github.com/fancyremarker))
|
|
252
|
+
* Chris Rollock ([@Sastafer](https://github.com/Sastafer))
|
|
253
|
+
* Pieter Vanmeerbeek ([@pvmeerbe](https://github.com/pvmeerbe))
|
|
254
|
+
|
|
255
|
+
## Authorship and License
|
|
256
|
+
|
|
257
|
+
Copyright 2013-2015 Pete Gamache, [pete@gamache.org](mailto:pete@gamache.org).
|
|
258
|
+
Fork maintained by [PRX](https://github.com/PRX) since 2025.
|
|
259
|
+
|
|
260
|
+
Released under the MIT License. See LICENSE.txt.
|
|
261
|
+
|
|
262
|
+
If you got this far, you should probably follow @gamache on Twitter.
|
|
263
|
+
[@gamache](https://twitter.com/gamache)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require "rubygems" if RUBY_VERSION[0..2] == "1.8"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
class HyperResource
|
|
5
|
+
class Adapter
|
|
6
|
+
## HyperResource::Adapter::HAL_JSON provides support for the HAL+JSON
|
|
7
|
+
## hypermedia format by implementing the interface defined in
|
|
8
|
+
## HyperResource::Adapter.
|
|
9
|
+
|
|
10
|
+
class HAL_JSON < Adapter # standard:disable Naming/ClassAndModuleCamelCase
|
|
11
|
+
class << self
|
|
12
|
+
def serialize(object)
|
|
13
|
+
JSON.dump(object)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def deserialize(string)
|
|
17
|
+
JSON.parse(string)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def apply(response, resource, opts = {})
|
|
21
|
+
if !response.is_a?(Hash)
|
|
22
|
+
raise ArgumentError, "'response' argument must be a Hash (got #{response.inspect})"
|
|
23
|
+
end
|
|
24
|
+
if !resource.is_a?(HyperResource)
|
|
25
|
+
raise ArgumentError, "'resource' argument must be a HyperResource (got #{resource.inspect})"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
apply_objects(response, resource)
|
|
29
|
+
apply_links(response, resource)
|
|
30
|
+
apply_attributes(response, resource)
|
|
31
|
+
resource.loaded = true
|
|
32
|
+
new_href = begin
|
|
33
|
+
response["_links"]["self"]["href"]
|
|
34
|
+
rescue
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
resource.href = new_href unless new_href.nil? && resource.href
|
|
38
|
+
resource
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def apply_objects(resp, rsrc)
|
|
44
|
+
return unless resp["_embedded"]
|
|
45
|
+
objs = rsrc.objects
|
|
46
|
+
|
|
47
|
+
resp["_embedded"].each do |name, collection|
|
|
48
|
+
if collection.is_a? Hash
|
|
49
|
+
href = begin
|
|
50
|
+
collection["_links"]["self"]["href"]
|
|
51
|
+
rescue
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
objs[name] =
|
|
55
|
+
rsrc.new_from(resource: rsrc,
|
|
56
|
+
body: collection,
|
|
57
|
+
href: href)
|
|
58
|
+
else
|
|
59
|
+
objs[name] = collection.map do |obj|
|
|
60
|
+
href = begin
|
|
61
|
+
obj["_links"]["self"]["href"]
|
|
62
|
+
rescue
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
rsrc.new_from(resource: rsrc,
|
|
66
|
+
body: obj,
|
|
67
|
+
href: href)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def apply_links(resp, rsrc)
|
|
74
|
+
return unless resp["_links"]
|
|
75
|
+
links = rsrc.links
|
|
76
|
+
|
|
77
|
+
resp["_links"].each do |rel, link_spec|
|
|
78
|
+
links[rel] = if link_spec.is_a? Array
|
|
79
|
+
link_spec.map do |link|
|
|
80
|
+
new_link_from_spec(rsrc, link)
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
new_link_from_spec(rsrc, link_spec)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def new_link_from_spec(resource, link_spec)
|
|
89
|
+
resource.class::Link.new(resource, link_spec)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def apply_attributes(resp, rsrc)
|
|
93
|
+
given_attrs = resp.except("_links", "_embedded")
|
|
94
|
+
filtered_attrs = rsrc.incoming_body_filter(given_attrs)
|
|
95
|
+
|
|
96
|
+
filtered_attrs.keys.each do |attr|
|
|
97
|
+
rsrc.attributes[attr] = filtered_attrs[attr]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
rsrc.attributes._hr_clear_changed
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class HyperResource
|
|
2
|
+
## HyperResource::Adapter is the interface/abstract base class for
|
|
3
|
+
## adapters to different hypermedia formats (e.g., HAL+JSON). New
|
|
4
|
+
## adapters must implement the public methods of this class.
|
|
5
|
+
|
|
6
|
+
class Adapter
|
|
7
|
+
class << self
|
|
8
|
+
## Serialize the given object into a string.
|
|
9
|
+
def serialize(object)
|
|
10
|
+
raise NotImplementedError, "This is an abstract method -- subclasses " \
|
|
11
|
+
"of HyperResource::Adapter must implement it."
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
## Deserialize a given string into an object (Hash).
|
|
15
|
+
def deserialize(string)
|
|
16
|
+
raise NotImplementedError, "This is an abstract method -- subclasses " \
|
|
17
|
+
"of HyperResource::Adapter must implement it."
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
## Use a given deserialized response object (Hash) to update a given
|
|
21
|
+
## resource (HyperResource), returning the updated resource.
|
|
22
|
+
def apply(response, resource, opts = {})
|
|
23
|
+
raise NotImplementedError, "This is an abstract method -- subclasses " \
|
|
24
|
+
"of HyperResource::Adapter must implement it."
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
class HyperResource
|
|
2
|
+
class Attributes < Hash
|
|
3
|
+
attr_accessor :_resource # @private
|
|
4
|
+
|
|
5
|
+
def initialize(resource = nil)
|
|
6
|
+
self._resource = resource || HyperResource.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
## Returns +true+ if the given attribute has been changed since creation
|
|
10
|
+
## time, +false+ otherwise.
|
|
11
|
+
## If no attribute is given, return whether any attributes have been
|
|
12
|
+
## changed.
|
|
13
|
+
def changed?(attr = nil)
|
|
14
|
+
@_hr_changed ||= Hash.new(false)
|
|
15
|
+
return @_hr_changed[attr.to_sym] if attr
|
|
16
|
+
@_hr_changed.keys.count > 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
## Returns a hash of the attributes and values which have been changed
|
|
20
|
+
## since creation time.
|
|
21
|
+
def changed_attributes
|
|
22
|
+
@_hr_changed.select { |k, v| v }.keys.each_with_object({}) { |k, h|
|
|
23
|
+
h[k] = self[k]
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @private
|
|
28
|
+
def []=(attr, value)
|
|
29
|
+
return if has_key?(attr.to_s) && self[attr] == value
|
|
30
|
+
_hr_mark_changed(attr)
|
|
31
|
+
super(attr.to_s, value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @private
|
|
35
|
+
def [](key)
|
|
36
|
+
return super(key.to_s) if has_key?(key.to_s)
|
|
37
|
+
return super(key.to_sym) if has_key?(key.to_sym)
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @private
|
|
42
|
+
def method_missing(method, *args) # standard:disable Style/MissingRespondToMissing
|
|
43
|
+
method = method.to_s
|
|
44
|
+
if has_key?(method)
|
|
45
|
+
self[method]
|
|
46
|
+
elsif method[-1, 1] == "="
|
|
47
|
+
self[method[0..-2]] = args.first
|
|
48
|
+
else
|
|
49
|
+
raise NoMethodError, "undefined method `#{method}' for #{inspect}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @private
|
|
54
|
+
def respond_to?(method, *args)
|
|
55
|
+
method = method.to_s
|
|
56
|
+
return true if has_key?(method)
|
|
57
|
+
return true if method[-1, 1] == "=" && has_key?(method[0..-2])
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @private
|
|
62
|
+
def _hr_clear_changed # @private
|
|
63
|
+
@_hr_changed = nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @private
|
|
67
|
+
def _hr_mark_changed(attr, is_changed = true)
|
|
68
|
+
attr = attr.to_sym
|
|
69
|
+
@_hr_changed ||= Hash.new(false)
|
|
70
|
+
@_hr_changed[attr] = is_changed
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|