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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
class HyperResource
|
|
2
|
+
module Modules
|
|
3
|
+
module DataType
|
|
4
|
+
def self.included(klass)
|
|
5
|
+
klass.extend(ClassMethods)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# @private
|
|
9
|
+
def get_data_type_class(args)
|
|
10
|
+
self.class.get_data_type_class(args)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @private
|
|
14
|
+
def get_data_type(args)
|
|
15
|
+
self.class.get_data_type(args)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module ClassMethods
|
|
19
|
+
## Returns the class into which a given response should be
|
|
20
|
+
## instantiated. Class name is a combination of `resource.namespace`
|
|
21
|
+
## and `get_data_type(args)'. Creates this class if necessary.
|
|
22
|
+
## Args are :resource, :link, :response, :body, :url.
|
|
23
|
+
# @private
|
|
24
|
+
def get_data_type_class(args)
|
|
25
|
+
url = args[:url] || args[:link].url
|
|
26
|
+
namespace = args[:resource].namespace_for_url(url.to_s)
|
|
27
|
+
namespace || "no namespace bish"
|
|
28
|
+
return self unless namespace
|
|
29
|
+
|
|
30
|
+
## Make sure namespace class exists
|
|
31
|
+
namespace_str = sanitize_class_name(namespace.to_s)
|
|
32
|
+
if namespace.is_a?(String)
|
|
33
|
+
ns_class = begin
|
|
34
|
+
eval(namespace_str) # standard:disable Security/Eval
|
|
35
|
+
rescue
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
if !ns_class
|
|
39
|
+
Object.module_eval("class #{namespace_str} < #{self}; end", __FILE__, __LINE__)
|
|
40
|
+
ns_class = eval(namespace_str) # standard:disable Security/Eval
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
## If there's no identifiable data type, return the namespace class.
|
|
45
|
+
type = get_data_type(args)
|
|
46
|
+
return ns_class unless type
|
|
47
|
+
|
|
48
|
+
## Make sure data type class exists
|
|
49
|
+
type = type[0, 1].upcase + type[1..] ## capitalize
|
|
50
|
+
sanitized_type = sanitize_class_name(type)
|
|
51
|
+
data_type_str = "#{namespace_str}::#{sanitized_type}"
|
|
52
|
+
unless ns_class.constants.include?(sanitized_type.to_sym)
|
|
53
|
+
Object.module_eval("class #{data_type_str} < #{namespace_str}; end", __FILE__, __LINE__)
|
|
54
|
+
end
|
|
55
|
+
eval(data_type_str) # standard:disable Security/Eval
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
## Given a body Hash and a response Faraday::Response, detect and
|
|
59
|
+
## return a string describing this response's data type.
|
|
60
|
+
## Args are :body and :response.
|
|
61
|
+
def get_data_type(args)
|
|
62
|
+
get_data_type_from_body(args[:body]) ||
|
|
63
|
+
get_data_type_from_response(args[:response])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
## Given a Faraday::Response, inspects the Content-type for data
|
|
67
|
+
## type information and returns data type as a String,
|
|
68
|
+
## for instance returning `Widget` given a media
|
|
69
|
+
## type `application/vnd.example.com+hal+json;type=Widget`.
|
|
70
|
+
## Override this method to change behavior.
|
|
71
|
+
## Returns nil on failure.
|
|
72
|
+
def get_data_type_from_response(response)
|
|
73
|
+
return nil unless response
|
|
74
|
+
return nil unless (content_type = response["content-type"])
|
|
75
|
+
return nil unless (m = content_type.match(/;\s* type=([0-9A-Za-z:]+)/x))
|
|
76
|
+
m[1]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
## Given a response body Hash, returns the response's data type as
|
|
80
|
+
## a string. By default, it looks for a `_data_type` field in the
|
|
81
|
+
## response. Override this method to change behavior.
|
|
82
|
+
def get_data_type_from_body(body)
|
|
83
|
+
return nil unless body
|
|
84
|
+
body["_data_type"] || body["type"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
## Remove all non-word, non-colon elements from a class name.
|
|
90
|
+
def sanitize_class_name(name)
|
|
91
|
+
name.gsub(/[^_0-9A-Za-z:]/, "")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
class HyperResource
|
|
2
|
+
module Modules
|
|
3
|
+
module Deprecations
|
|
4
|
+
def self.included(klass)
|
|
5
|
+
klass.extend(ClassMethods)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
## Show a deprecation message.
|
|
9
|
+
# @private
|
|
10
|
+
def _hr_deprecate(*args)
|
|
11
|
+
self.class._hr_deprecate(*args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
## Show a deprecation message.
|
|
16
|
+
# @private
|
|
17
|
+
def _hr_deprecate(message)
|
|
18
|
+
warn "#{message} (called from #{caller(3..3).first})"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
###### Deprecated stuff:
|
|
23
|
+
|
|
24
|
+
## +response_body+, +response_object+, and +deserialized_response+
|
|
25
|
+
## are deprecated in favor of +body+. (Sorry. Naming things is hard.)
|
|
26
|
+
## Deprecated at 0.2. @private
|
|
27
|
+
def response_body
|
|
28
|
+
_hr_deprecate("HyperResource#response_body is deprecated. " \
|
|
29
|
+
"Please use HyperResource#body instead.")
|
|
30
|
+
body
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @private
|
|
34
|
+
def response_object
|
|
35
|
+
_hr_deprecate("HyperResource#response_object is deprecated. " \
|
|
36
|
+
"Please use HyperResource#body instead.")
|
|
37
|
+
body
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @private
|
|
41
|
+
def deserialized_response
|
|
42
|
+
_hr_deprecate("HyperResource#deserialized_response is deprecated. " \
|
|
43
|
+
"Please use HyperResource#body instead.")
|
|
44
|
+
body
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
## Deprecated at 0.9:
|
|
48
|
+
## #create, #update, Link#create, Link#update
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "json"
|
|
5
|
+
require "digest/md5"
|
|
6
|
+
|
|
7
|
+
class HyperResource
|
|
8
|
+
## Returns this resource's fully qualified URL. Returns nil when
|
|
9
|
+
## `root` or `href` are malformed.
|
|
10
|
+
def url
|
|
11
|
+
URI.join(root, href || "").to_s
|
|
12
|
+
rescue
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
## Performs a GET request to this resource's URL, and returns a
|
|
17
|
+
## new resource representing the response.
|
|
18
|
+
def get
|
|
19
|
+
to_link.get
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
## Performs a GET request to this resource's URL, and returns a
|
|
23
|
+
## `Faraday::Response` object representing the response.
|
|
24
|
+
def get_response
|
|
25
|
+
to_link.get_response
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
## Performs a POST request to this resource's URL, sending all of
|
|
29
|
+
## `attributes` as a request body unless an `attrs` Hash is given.
|
|
30
|
+
## Returns a new resource representing the response.
|
|
31
|
+
def post(attrs = nil)
|
|
32
|
+
to_link.post(attrs)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
## Performs a POST request to this resource's URL, sending all of
|
|
36
|
+
## `attributes` as a request body unless an `attrs` Hash is given.
|
|
37
|
+
## Returns a `Faraday::Response` object representing the response.
|
|
38
|
+
def post_response(attrs = nil)
|
|
39
|
+
to_link.post_response(attrs)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
## Performs a PUT request to this resource's URL, sending all of
|
|
43
|
+
## `attributes` as a request body unless an `attrs` Hash is given.
|
|
44
|
+
## Returns a new resource representing the response.
|
|
45
|
+
def put(*args)
|
|
46
|
+
to_link.put(*args)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
## Performs a PUT request to this resource's URL, sending all of
|
|
50
|
+
## `attributes` as a request body unless an `attrs` Hash is given.
|
|
51
|
+
## Returns a `Faraday::Response` object representing the response.
|
|
52
|
+
def put_response(*args)
|
|
53
|
+
to_link.put_response(*args)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
## Performs a PATCH request to this resource's URL, sending
|
|
57
|
+
## `attributes.changed_attributes` as a request body
|
|
58
|
+
## unless an `attrs` Hash is given. Returns a new resource
|
|
59
|
+
## representing the response.
|
|
60
|
+
def patch(*args)
|
|
61
|
+
to_link.patch(*args)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
## Performs a PATCH request to this resource's URL, sending
|
|
65
|
+
## `attributes.changed_attributes` as a request body
|
|
66
|
+
## unless an `attrs` Hash is given.
|
|
67
|
+
## Returns a `Faraday::Response` object representing the response.
|
|
68
|
+
def patch_response(*args)
|
|
69
|
+
to_link.patch_response(*args)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
## Performs a DELETE request to this resource's URL. Returns a new
|
|
73
|
+
## resource representing the response.
|
|
74
|
+
def delete
|
|
75
|
+
to_link.delete
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
## Performs a DELETE request to this resource's URL.
|
|
79
|
+
## Returns a `Faraday::Response` object representing the response.
|
|
80
|
+
def delete_response
|
|
81
|
+
to_link.delete_response
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
## Creates a Link representing this resource. Used for HTTP delegation.
|
|
85
|
+
# @private
|
|
86
|
+
def to_link(args = {})
|
|
87
|
+
self.class::Link.new(self,
|
|
88
|
+
href: args[:href] || href,
|
|
89
|
+
params: args[:params] || attributes)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @private
|
|
93
|
+
def create(attrs)
|
|
94
|
+
_hr_deprecate("HyperResource#create is deprecated. Please use " \
|
|
95
|
+
"#post instead.")
|
|
96
|
+
to_link.post(attrs)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @private
|
|
100
|
+
def update(*args)
|
|
101
|
+
_hr_deprecate("HyperResource#update is deprecated. Please use " \
|
|
102
|
+
"#put or #patch instead.")
|
|
103
|
+
to_link.put(*args)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
module Modules
|
|
107
|
+
## HyperResource::Modules::HTTP is included by HyperResource::Link.
|
|
108
|
+
## It provides support for GET, POST, PUT, PATCH, and DELETE.
|
|
109
|
+
## Each method returns a new object which is a kind_of HyperResource.
|
|
110
|
+
module HTTP
|
|
111
|
+
## Loads and returns the resource pointed to by +href+. The returned
|
|
112
|
+
## resource will be blessed into its "proper" class, if
|
|
113
|
+
## +self.class.namespace != nil+.
|
|
114
|
+
def get
|
|
115
|
+
new_resource_from_response(get_response)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
## Performs a GET request on the given link, and returns the
|
|
119
|
+
## response as a `Faraday::Response` object.
|
|
120
|
+
## Does not parse the response as a `HyperResource` object.
|
|
121
|
+
def get_response
|
|
122
|
+
## Adding default_attributes to URL query params is not automatic
|
|
123
|
+
uri = URI.parse(url || "")
|
|
124
|
+
query = CGI.parse(uri.query || "")
|
|
125
|
+
str = URI.encode_www_form((resource.default_attributes || {}).merge(query))
|
|
126
|
+
uri.query = str.empty? ? nil : str
|
|
127
|
+
faraday_connection.get(uri.to_s)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
## By default, calls +post+ with the given arguments. Override to
|
|
131
|
+
## change this behavior.
|
|
132
|
+
def create(*args)
|
|
133
|
+
_hr_deprecate("HyperResource::Link#create is deprecated. Please use " \
|
|
134
|
+
"#post instead.")
|
|
135
|
+
post(*args)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
## POSTs the given attributes to this resource's href, and returns
|
|
139
|
+
## the response resource.
|
|
140
|
+
def post(attrs = nil)
|
|
141
|
+
new_resource_from_response(post_response(attrs))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
## POSTs the given attributes to this resource's href, and returns the
|
|
145
|
+
## response as a `Faraday::Response` object.
|
|
146
|
+
## Does not parse the response as a `HyperResource` object.
|
|
147
|
+
def post_response(attrs = nil)
|
|
148
|
+
attrs ||= resource.attributes
|
|
149
|
+
attrs = (resource.default_attributes || {}).merge(attrs)
|
|
150
|
+
faraday_connection.post do |req|
|
|
151
|
+
req.headers["Content-Type"] ||= "application/json"
|
|
152
|
+
req.body = resource.adapter.serialize(attrs)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
## By default, calls +put+ with the given arguments. Override to
|
|
157
|
+
## change this behavior.
|
|
158
|
+
def update(*args)
|
|
159
|
+
_hr_deprecate("HyperResource::Link#update is deprecated. Please use " \
|
|
160
|
+
"#put or #patch instead.")
|
|
161
|
+
put(*args)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
## PUTs this resource's attributes to this resource's href, and returns
|
|
165
|
+
## the response resource. If attributes are given, +put+ uses those
|
|
166
|
+
## instead.
|
|
167
|
+
def put(attrs = nil)
|
|
168
|
+
new_resource_from_response(put_response(attrs))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
## PUTs this resource's attributes to this resource's href, and returns
|
|
172
|
+
## the response as a `Faraday::Response` object.
|
|
173
|
+
## Does not parse the response as a `HyperResource` object.
|
|
174
|
+
def put_response(attrs = nil)
|
|
175
|
+
attrs ||= resource.attributes
|
|
176
|
+
attrs = (resource.default_attributes || {}).merge(attrs)
|
|
177
|
+
faraday_connection.put do |req|
|
|
178
|
+
req.headers["Content-Type"] ||= "application/json"
|
|
179
|
+
req.body = resource.adapter.serialize(attrs)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
## PATCHes this resource's changed attributes to this resource's href,
|
|
184
|
+
## and returns the response resource. If attributes are given, +patch+
|
|
185
|
+
## uses those instead.
|
|
186
|
+
def patch(attrs = nil)
|
|
187
|
+
new_resource_from_response(patch_response(attrs))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
## PATCHes this resource's changed attributes to this resource's href,
|
|
191
|
+
## and returns the response as a `Faraday::Response` object.
|
|
192
|
+
## Does not parse the response as a `HyperResource` object.
|
|
193
|
+
def patch_response(attrs = nil)
|
|
194
|
+
attrs ||= resource.attributes.changed_attributes
|
|
195
|
+
attrs = (resource.default_attributes || {}).merge(attrs)
|
|
196
|
+
faraday_connection.patch do |req|
|
|
197
|
+
req.headers["Content-Type"] ||= "application/json"
|
|
198
|
+
req.body = resource.adapter.serialize(attrs)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
## DELETEs this resource's href, and returns the response resource.
|
|
203
|
+
def delete
|
|
204
|
+
new_resource_from_response(delete_response)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
## DELETEs this resource's href, and returns the response as a
|
|
208
|
+
## `Faraday::Response` object.
|
|
209
|
+
## Does not parse the response as a `HyperResource` object.
|
|
210
|
+
def delete_response
|
|
211
|
+
faraday_connection.delete
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
## Returns a raw Faraday connection to this resource's URL, with proper
|
|
217
|
+
## headers (including auth). Threadsafe.
|
|
218
|
+
def faraday_connection(url = nil)
|
|
219
|
+
rsrc = resource
|
|
220
|
+
url ||= self.url
|
|
221
|
+
headers = (rsrc.headers_for_url(url) || {}).merge(self.headers)
|
|
222
|
+
auth = rsrc.auth_for_url(url) || {}
|
|
223
|
+
fo = rsrc.faraday_options_for_url(url) || {}
|
|
224
|
+
|
|
225
|
+
key = ::Digest::MD5.hexdigest({
|
|
226
|
+
"faraday_connection" => {
|
|
227
|
+
"url" => url,
|
|
228
|
+
"headers" => headers,
|
|
229
|
+
"ba" => auth[:basic],
|
|
230
|
+
"fo" => fo
|
|
231
|
+
}
|
|
232
|
+
}.to_json)
|
|
233
|
+
return Thread.current[key] if Thread.current[key]
|
|
234
|
+
|
|
235
|
+
fc = Faraday.new(fo.merge(url: url))
|
|
236
|
+
fc.headers["User-Agent"] = rsrc.user_agent
|
|
237
|
+
fc.headers.merge!(headers)
|
|
238
|
+
if (ba = auth[:basic])
|
|
239
|
+
if fc.respond_to?(:basic_auth)
|
|
240
|
+
fc.basic_auth(*ba)
|
|
241
|
+
else
|
|
242
|
+
fc.request :authorization, :basic, *ba
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
Thread.current[key] = fc
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
## Given a Faraday::Response object, create a new resource
|
|
249
|
+
## object to represent it. The new resource will be in its
|
|
250
|
+
## proper class according to its configured `namespace` and
|
|
251
|
+
## the response's detected data type.
|
|
252
|
+
def new_resource_from_response(response)
|
|
253
|
+
status = response.status
|
|
254
|
+
is_success = (status / 100 == 2)
|
|
255
|
+
adapter = resource.adapter || HyperResource::Adapter::HAL_JSON
|
|
256
|
+
|
|
257
|
+
body = nil
|
|
258
|
+
unless empty_body?(response.body)
|
|
259
|
+
begin
|
|
260
|
+
body = adapter.deserialize(response.body)
|
|
261
|
+
rescue => e
|
|
262
|
+
if is_success
|
|
263
|
+
raise HyperResource::ResponseError.new(
|
|
264
|
+
"Error when deserializing response body",
|
|
265
|
+
response: response,
|
|
266
|
+
cause: e
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
new_rsrc = resource.new_from(link: self,
|
|
273
|
+
body: body,
|
|
274
|
+
response: response)
|
|
275
|
+
|
|
276
|
+
if status / 100 == 2
|
|
277
|
+
new_rsrc
|
|
278
|
+
elsif status / 100 == 3
|
|
279
|
+
raise NotImplementedError,
|
|
280
|
+
"HyperResource has not implemented redirection."
|
|
281
|
+
elsif status / 100 == 4
|
|
282
|
+
raise HyperResource::ClientError.new(status.to_s,
|
|
283
|
+
response: response,
|
|
284
|
+
body: body)
|
|
285
|
+
elsif status / 100 == 5
|
|
286
|
+
raise HyperResource::ServerError.new(status.to_s,
|
|
287
|
+
response: response,
|
|
288
|
+
body: body)
|
|
289
|
+
else ## 1xx? really?
|
|
290
|
+
raise HyperResource::ResponseError.new("Unknown status #{status}",
|
|
291
|
+
response: response,
|
|
292
|
+
body: body)
|
|
293
|
+
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def empty_body?(body)
|
|
298
|
+
return true if body.nil?
|
|
299
|
+
return true if body.respond_to?(:empty?) && body.empty?
|
|
300
|
+
return true if body.instance_of?(String) && body =~ /^['"]+$/ # special case for status code with optional body, example Grape API with status 405
|
|
301
|
+
false
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module HyperResource::Modules
|
|
2
|
+
module InternalAttributes
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.extend(ClassMethods)
|
|
5
|
+
|
|
6
|
+
base._hr_class_attributes.each do |attr|
|
|
7
|
+
base._hr_class_attribute attr
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
(base._hr_attributes - base._hr_class_attributes).each do |attr|
|
|
11
|
+
base.send(:attr_accessor, attr)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
## Fallback attributes fall back from instance to class.
|
|
15
|
+
(base._hr_attributes & base._hr_class_attributes).each do |attr|
|
|
16
|
+
base._hr_fallback_attribute attr
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module ClassMethods
|
|
21
|
+
# @private
|
|
22
|
+
def _hr_class_attributes
|
|
23
|
+
[:root]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @private
|
|
27
|
+
def _hr_attributes
|
|
28
|
+
[:root,
|
|
29
|
+
:href,
|
|
30
|
+
:request,
|
|
31
|
+
:response,
|
|
32
|
+
:body,
|
|
33
|
+
:attributes,
|
|
34
|
+
:links,
|
|
35
|
+
:objects,
|
|
36
|
+
:loaded]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
## Inheritable class attribute, kinda like in Rails.
|
|
40
|
+
# @private
|
|
41
|
+
def _hr_class_attribute(*names)
|
|
42
|
+
names.map(&:to_sym).each do |name|
|
|
43
|
+
instance_eval <<-EOT, __FILE__, __LINE__ + 1
|
|
44
|
+
def #{name}=(val)
|
|
45
|
+
@#{name} = val
|
|
46
|
+
end
|
|
47
|
+
def #{name}
|
|
48
|
+
return @#{name} if defined?(@#{name})
|
|
49
|
+
return superclass.#{name} if superclass.respond_to?(:#{name})
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
EOT
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
## Instance attributes which fall back to class attributes.
|
|
57
|
+
# @private
|
|
58
|
+
def _hr_fallback_attribute(*names)
|
|
59
|
+
names.map(&:to_sym).each do |name|
|
|
60
|
+
class_eval <<-EOT, __FILE__, __LINE__ + 1
|
|
61
|
+
def #{name}=(val)
|
|
62
|
+
@#{name} = val
|
|
63
|
+
end
|
|
64
|
+
def #{name}
|
|
65
|
+
return @#{name} if defined?(@#{name})
|
|
66
|
+
return self.class.#{name} if self.class.respond_to?(:#{name})
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
EOT
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end # ClassMethods
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
class HyperResource
|
|
2
|
+
class Objects < Hash
|
|
3
|
+
attr_accessor :_resource
|
|
4
|
+
|
|
5
|
+
def initialize(resource = nil)
|
|
6
|
+
self._resource = resource || HyperResource.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# @private
|
|
10
|
+
def []=(attr, value)
|
|
11
|
+
super(attr.to_s, value)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
## When +key+ is a string, returns the array of objects under that name.
|
|
15
|
+
## When +key+ is a number, returns +ith(key)+. Returns nil on lookup
|
|
16
|
+
## failure.
|
|
17
|
+
def [](key)
|
|
18
|
+
case key
|
|
19
|
+
when String, Symbol
|
|
20
|
+
return super(key.to_s) if has_key?(key.to_s)
|
|
21
|
+
return super(key.to_sym) if has_key?(key.to_sym)
|
|
22
|
+
when Integer
|
|
23
|
+
return ith(key)
|
|
24
|
+
end
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @private
|
|
29
|
+
def method_missing(method, *args) # standard:disable Style/MissingRespondToMissing
|
|
30
|
+
return self[method] if self[method]
|
|
31
|
+
raise NoMethodError, "undefined method `#{method}' for #{inspect}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @private
|
|
35
|
+
def respond_to?(method, *args)
|
|
36
|
+
method = method.to_s
|
|
37
|
+
return true if has_key?(method)
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|