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.
@@ -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
@@ -0,0 +1,3 @@
1
+ class HyperResource
2
+ VERSION = "1.0.0"
3
+ end