hyperresource 0.2.4 → 0.9.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/lib/hyper_resource.rb +79 -136
- data/lib/hyper_resource/adapter/hal_json.rb +14 -33
- data/lib/hyper_resource/attributes.rb +21 -47
- data/lib/hyper_resource/configuration.rb +159 -0
- data/lib/hyper_resource/exceptions.rb +4 -2
- data/lib/hyper_resource/link.rb +91 -50
- data/lib/hyper_resource/links.rb +42 -36
- data/lib/hyper_resource/modules/config_attributes.rb +293 -0
- data/lib/hyper_resource/modules/data_type.rb +98 -0
- data/lib/hyper_resource/modules/deprecations.rb +56 -0
- data/lib/hyper_resource/modules/http.rb +155 -46
- data/lib/hyper_resource/modules/internal_attributes.rb +7 -18
- data/lib/hyper_resource/objects.rb +10 -29
- data/lib/hyper_resource/version.rb +3 -2
- metadata +24 -6
- data/lib/hyper_resource/response.rb +0 -2
@@ -0,0 +1,98 @@
|
|
1
|
+
class HyperResource
|
2
|
+
module Modules
|
3
|
+
module DataType
|
4
|
+
|
5
|
+
def self.included(klass)
|
6
|
+
klass.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
# @private
|
10
|
+
def get_data_type_class(args)
|
11
|
+
self.class.get_data_type_class(args)
|
12
|
+
end
|
13
|
+
|
14
|
+
# @private
|
15
|
+
def get_data_type(args)
|
16
|
+
self.class.get_data_type(args)
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
## Returns the class into which a given response should be
|
22
|
+
## instantiated. Class name is a combination of `resource.namespace`
|
23
|
+
## and `get_data_type(args)'. Creates this class if necessary.
|
24
|
+
## Args are :resource, :link, :response, :body, :url.
|
25
|
+
# @private
|
26
|
+
def get_data_type_class(args)
|
27
|
+
url = args[:url] || args[:link].url
|
28
|
+
namespace = args[:resource].namespace_for_url(url.to_s)
|
29
|
+
namespace || "no namespace bish"
|
30
|
+
return self unless namespace
|
31
|
+
|
32
|
+
## Make sure namespace class exists
|
33
|
+
namespace_str = sanitize_class_name(namespace.to_s)
|
34
|
+
if namespace.kind_of?(String)
|
35
|
+
ns_class = eval(namespace_str) rescue nil
|
36
|
+
if !ns_class
|
37
|
+
Object.module_eval("class #{namespace_str} < #{self}; end")
|
38
|
+
ns_class = eval(namespace_str)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
## If there's no identifiable data type, return the namespace class.
|
43
|
+
type = get_data_type(args)
|
44
|
+
return ns_class unless type
|
45
|
+
|
46
|
+
## Make sure data type class exists
|
47
|
+
type = type[0,1].upcase + type[1..-1] ## capitalize
|
48
|
+
data_type_str = sanitize_class_name("#{namespace_str}::#{type}")
|
49
|
+
data_type_class = eval(data_type_str) rescue nil
|
50
|
+
if !data_type_class
|
51
|
+
Object.module_eval("class #{data_type_str} < #{namespace_str}; end")
|
52
|
+
data_type_class = eval(data_type_str)
|
53
|
+
end
|
54
|
+
|
55
|
+
data_type_class
|
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
|
+
type = get_data_type_from_body(args[:body])
|
63
|
+
type ||= 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
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class HyperResource
|
2
|
+
module Modules
|
3
|
+
module Deprecations
|
4
|
+
|
5
|
+
def self.included(klass)
|
6
|
+
klass.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
## Show a deprecation message.
|
10
|
+
# @private
|
11
|
+
def _hr_deprecate(*args)
|
12
|
+
self.class._hr_deprecate(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
## Show a deprecation message.
|
17
|
+
# @private
|
18
|
+
def _hr_deprecate(message)
|
19
|
+
STDERR.puts "#{message} (called from #{caller[2]})"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
###### Deprecated stuff:
|
25
|
+
|
26
|
+
## +response_body+, +response_object+, and +deserialized_response+
|
27
|
+
## are deprecated in favor of +body+. (Sorry. Naming things is hard.)
|
28
|
+
## Deprecated at 0.2. @private
|
29
|
+
def response_body
|
30
|
+
_hr_deprecate('HyperResource#response_body is deprecated. '+
|
31
|
+
'Please use HyperResource#body instead.')
|
32
|
+
body
|
33
|
+
end
|
34
|
+
|
35
|
+
# @private
|
36
|
+
def response_object
|
37
|
+
_hr_deprecate('HyperResource#response_object is deprecated. '+
|
38
|
+
'Please use HyperResource#body instead.')
|
39
|
+
body
|
40
|
+
end
|
41
|
+
|
42
|
+
# @private
|
43
|
+
def deserialized_response
|
44
|
+
_hr_deprecate('HyperResource#deserialized_response is deprecated. '+
|
45
|
+
'Please use HyperResource#body instead.')
|
46
|
+
body
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
## Deprecated at 0.9:
|
51
|
+
## #create, #update, Link#create, Link#update
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -4,36 +4,124 @@ require 'json'
|
|
4
4
|
require 'digest/md5'
|
5
5
|
|
6
6
|
class HyperResource
|
7
|
+
|
8
|
+
## Returns this resource's fully qualified URL. Returns nil when
|
9
|
+
## `root` or `href` are malformed.
|
10
|
+
def url
|
11
|
+
begin
|
12
|
+
URI.join(self.root, (self.href || '')).to_s
|
13
|
+
rescue StandardError
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
## Performs a GET request to this resource's URL, and returns a
|
20
|
+
## new resource representing the response.
|
21
|
+
def get
|
22
|
+
to_link.get
|
23
|
+
end
|
24
|
+
|
25
|
+
## Performs a POST request to this resource's URL, sending all of
|
26
|
+
## `attributes` as a request body unless an `attrs` Hash is given.
|
27
|
+
## Returns a new resource representing the response.
|
28
|
+
def post(attrs=nil)
|
29
|
+
to_link.post(attrs)
|
30
|
+
end
|
31
|
+
|
32
|
+
## Performs a PUT request to this resource's URL, sending all of
|
33
|
+
## `attributes` as a request body unless an `attrs` Hash is given.
|
34
|
+
## Returns a new resource representing the response.
|
35
|
+
def put(*args)
|
36
|
+
to_link.put(*args)
|
37
|
+
end
|
38
|
+
|
39
|
+
## Performs a PATCH request to this resource's URL, sending
|
40
|
+
## `attributes.changed_attributes` as a request body
|
41
|
+
## unless an `attrs` Hash is given. Returns a new resource
|
42
|
+
## representing the response.
|
43
|
+
def patch(*args)
|
44
|
+
self.to_link.patch(*args)
|
45
|
+
end
|
46
|
+
|
47
|
+
## Performs a DELETE request to this resource's URL. Returns a new
|
48
|
+
## resource representing the response.
|
49
|
+
def delete(*args)
|
50
|
+
to_link.delete(*args)
|
51
|
+
end
|
52
|
+
|
53
|
+
## Creates a Link representing this resource. Used for HTTP delegation.
|
54
|
+
# @private
|
55
|
+
def to_link(args={})
|
56
|
+
self.class::Link.new(self,
|
57
|
+
:href => args[:href] || self.href,
|
58
|
+
:params => args[:params] || self.attributes)
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
# @private
|
64
|
+
def create(attrs)
|
65
|
+
_hr_deprecate('HyperResource#create is deprecated. Please use '+
|
66
|
+
'#post instead.')
|
67
|
+
to_link.post(attrs)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @private
|
71
|
+
def update(*args)
|
72
|
+
_hr_deprecate('HyperResource#update is deprecated. Please use '+
|
73
|
+
'#put or #patch instead.')
|
74
|
+
to_link.put(*args)
|
75
|
+
end
|
76
|
+
|
7
77
|
module Modules
|
78
|
+
|
79
|
+
## HyperResource::Modules::HTTP is included by HyperResource::Link.
|
80
|
+
## It provides support for GET, POST, PUT, PATCH, and DELETE.
|
81
|
+
## Each method returns a new object which is a kind_of HyperResource.
|
8
82
|
module HTTP
|
9
83
|
|
10
84
|
## Loads and returns the resource pointed to by +href+. The returned
|
11
85
|
## resource will be blessed into its "proper" class, if
|
12
86
|
## +self.class.namespace != nil+.
|
13
87
|
def get
|
14
|
-
|
15
|
-
|
88
|
+
## Adding default_attributes to URL query params is not automatic
|
89
|
+
url = FuzzyURL.new(self.url || '')
|
90
|
+
query_str = url[:query] || ''
|
91
|
+
query_attrs = Hash[ query_str.split('&').map{|p| p.split('=')} ]
|
92
|
+
attrs = (self.resource.default_attributes || {}).merge(query_attrs)
|
93
|
+
attrs_str = attrs.inject([]){|pairs,(k,v)| pairs<<"#{k}=#{v}"}.join('&')
|
94
|
+
if attrs_str != ''
|
95
|
+
url = FuzzyURL.new(url.to_hash.merge(:query => attrs_str))
|
96
|
+
end
|
97
|
+
response = faraday_connection.get(url.to_s)
|
98
|
+
new_resource_from_response(response)
|
16
99
|
end
|
17
100
|
|
18
101
|
## By default, calls +post+ with the given arguments. Override to
|
19
102
|
## change this behavior.
|
20
103
|
def create(*args)
|
104
|
+
_hr_deprecate('HyperResource::Link#create is deprecated. Please use '+
|
105
|
+
'#post instead.')
|
21
106
|
post(*args)
|
22
107
|
end
|
23
108
|
|
24
109
|
## POSTs the given attributes to this resource's href, and returns
|
25
110
|
## the response resource.
|
26
111
|
def post(attrs=nil)
|
27
|
-
attrs
|
28
|
-
|
29
|
-
|
112
|
+
attrs ||= self.resource.attributes
|
113
|
+
attrs = (self.resource.default_attributes || {}).merge(attrs)
|
114
|
+
response = faraday_connection.post do |req|
|
115
|
+
req.body = self.resource.adapter.serialize(attrs)
|
30
116
|
end
|
31
|
-
|
117
|
+
new_resource_from_response(response)
|
32
118
|
end
|
33
119
|
|
34
|
-
## By default, calls +
|
120
|
+
## By default, calls +puwt+ with the given arguments. Override to
|
35
121
|
## change this behavior.
|
36
122
|
def update(*args)
|
123
|
+
_hr_deprecate('HyperResource::Link#update is deprecated. Please use '+
|
124
|
+
'#put or #patch instead.')
|
37
125
|
put(*args)
|
38
126
|
end
|
39
127
|
|
@@ -41,86 +129,107 @@ class HyperResource
|
|
41
129
|
## the response resource. If attributes are given, +put+ uses those
|
42
130
|
## instead.
|
43
131
|
def put(attrs=nil)
|
44
|
-
attrs ||= self.attributes
|
45
|
-
|
46
|
-
|
132
|
+
attrs ||= self.resource.attributes
|
133
|
+
attrs = (self.resource.default_attributes || {}).merge(attrs)
|
134
|
+
response = faraday_connection.put do |req|
|
135
|
+
req.body = self.resource.adapter.serialize(attrs)
|
47
136
|
end
|
48
|
-
|
137
|
+
new_resource_from_response(response)
|
49
138
|
end
|
50
139
|
|
51
140
|
## PATCHes this resource's changed attributes to this resource's href,
|
52
141
|
## and returns the response resource. If attributes are given, +patch+
|
53
142
|
## uses those instead.
|
54
143
|
def patch(attrs=nil)
|
55
|
-
attrs ||= self.attributes.changed_attributes
|
56
|
-
|
57
|
-
|
144
|
+
attrs ||= self.resource.attributes.changed_attributes
|
145
|
+
attrs = (self.resource.default_attributes || {}).merge(attrs)
|
146
|
+
response = faraday_connection.patch do |req|
|
147
|
+
req.body = self.resource.adapter.serialize(attrs)
|
58
148
|
end
|
59
|
-
|
149
|
+
new_resource_from_response(response)
|
60
150
|
end
|
61
151
|
|
62
152
|
## DELETEs this resource's href, and returns the response resource.
|
63
153
|
def delete
|
64
|
-
|
65
|
-
|
154
|
+
response = faraday_connection.delete
|
155
|
+
new_resource_from_response(response)
|
66
156
|
end
|
67
157
|
|
158
|
+
private
|
159
|
+
|
68
160
|
## Returns a raw Faraday connection to this resource's URL, with proper
|
69
161
|
## headers (including auth). Threadsafe.
|
70
162
|
def faraday_connection(url=nil)
|
71
|
-
|
72
|
-
|
163
|
+
rsrc = self.resource
|
164
|
+
url ||= self.url
|
165
|
+
headers = rsrc.headers_for_url(url) || {}
|
166
|
+
auth = rsrc.auth_for_url(url) || {}
|
167
|
+
|
168
|
+
key = ::Digest::MD5.hexdigest({
|
73
169
|
'faraday_connection' => {
|
74
170
|
'url' => url,
|
75
|
-
'headers' =>
|
76
|
-
'ba' =>
|
171
|
+
'headers' => headers,
|
172
|
+
'ba' => auth[:basic]
|
77
173
|
}
|
78
174
|
}.to_json)
|
79
175
|
return Thread.current[key] if Thread.current[key]
|
80
176
|
|
81
|
-
|
82
|
-
fc.
|
83
|
-
fc.headers.merge!(
|
84
|
-
|
177
|
+
fo = rsrc.faraday_options_for_url(url) || {}
|
178
|
+
fc = Faraday.new(fo.merge(:url => url))
|
179
|
+
fc.headers.merge!('User-Agent' => rsrc.user_agent)
|
180
|
+
fc.headers.merge!(headers)
|
181
|
+
if ba=auth[:basic]
|
85
182
|
fc.basic_auth(*ba)
|
86
183
|
end
|
87
184
|
Thread.current[key] = fc
|
88
185
|
end
|
89
186
|
|
90
|
-
private
|
91
187
|
|
92
|
-
|
188
|
+
## Given a Faraday::Response object, create a new resource
|
189
|
+
## object to represent it. The new resource will be in its
|
190
|
+
## proper class according to its configured `namespace` and
|
191
|
+
## the response's detected data type.
|
192
|
+
def new_resource_from_response(response)
|
193
|
+
status = response.status
|
194
|
+
is_success = (status / 100 == 2)
|
195
|
+
adapter = self.resource.adapter || HyperResource::Adapter::HAL_JSON
|
196
|
+
|
197
|
+
body = nil
|
93
198
|
begin
|
94
|
-
|
199
|
+
if response.body
|
200
|
+
body = adapter.deserialize(response.body)
|
201
|
+
end
|
95
202
|
rescue StandardError => e
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
203
|
+
if is_success
|
204
|
+
raise HyperResource::ResponseError.new(
|
205
|
+
"Error when deserializing response body",
|
206
|
+
:response => response,
|
207
|
+
:cause => e
|
208
|
+
)
|
209
|
+
end
|
101
210
|
end
|
102
211
|
|
103
|
-
|
104
|
-
|
212
|
+
new_rsrc = resource.new_from(:link => self,
|
213
|
+
:body => body,
|
214
|
+
:response => response)
|
105
215
|
|
106
|
-
status = self.response.status
|
107
216
|
if status / 100 == 2
|
108
|
-
return
|
217
|
+
return new_rsrc
|
109
218
|
elsif status / 100 == 3
|
110
|
-
|
219
|
+
raise NotImplementedError,
|
220
|
+
"HyperResource has not implemented redirection."
|
111
221
|
elsif status / 100 == 4
|
112
222
|
raise HyperResource::ClientError.new(status.to_s,
|
113
|
-
:response =>
|
114
|
-
:body =>
|
223
|
+
:response => response,
|
224
|
+
:body => body)
|
115
225
|
elsif status / 100 == 5
|
116
226
|
raise HyperResource::ServerError.new(status.to_s,
|
117
|
-
:response =>
|
118
|
-
:body =>
|
119
|
-
|
227
|
+
:response => response,
|
228
|
+
:body => body)
|
120
229
|
else ## 1xx? really?
|
121
|
-
raise HyperResource::ResponseError.new("
|
122
|
-
:response =>
|
123
|
-
:body =>
|
230
|
+
raise HyperResource::ResponseError.new("Unknown status #{status}",
|
231
|
+
:response => response,
|
232
|
+
:body => body)
|
124
233
|
|
125
234
|
end
|
126
235
|
end
|
@@ -19,39 +19,27 @@ module HyperResource::Modules
|
|
19
19
|
end
|
20
20
|
|
21
21
|
module ClassMethods
|
22
|
-
|
23
|
-
def _hr_class_attributes
|
24
|
-
[ :root
|
25
|
-
:auth, ## e.g. {:basic => ['username', 'password']}
|
26
|
-
:headers, ## e.g. {'Accept' => 'application/vnd.example+json'}
|
27
|
-
:namespace, ## e.g. 'ExampleAPI', or the class ExampleAPI itself
|
28
|
-
:adapter, ## subclass of HR::Adapter
|
29
|
-
:faraday_options ## e.g. {:request => {:timeout => 30}}
|
30
|
-
]
|
22
|
+
# @private
|
23
|
+
def _hr_class_attributes
|
24
|
+
[ :root ]
|
31
25
|
end
|
32
26
|
|
33
|
-
|
27
|
+
# @private
|
28
|
+
def _hr_attributes
|
34
29
|
[ :root,
|
35
30
|
:href,
|
36
|
-
:auth,
|
37
|
-
:headers,
|
38
|
-
:namespace,
|
39
|
-
:adapter,
|
40
|
-
:faraday_options,
|
41
|
-
|
42
31
|
:request,
|
43
32
|
:response,
|
44
33
|
:body,
|
45
|
-
|
46
34
|
:attributes,
|
47
35
|
:links,
|
48
36
|
:objects,
|
49
|
-
|
50
37
|
:loaded
|
51
38
|
]
|
52
39
|
end
|
53
40
|
|
54
41
|
## Inheritable class attribute, kinda like in Rails.
|
42
|
+
# @private
|
55
43
|
def _hr_class_attribute(*names)
|
56
44
|
names.map(&:to_sym).each do |name|
|
57
45
|
instance_eval <<-EOT
|
@@ -68,6 +56,7 @@ module HyperResource::Modules
|
|
68
56
|
end
|
69
57
|
|
70
58
|
## Instance attributes which fall back to class attributes.
|
59
|
+
# @private
|
71
60
|
def _hr_fallback_attribute(*names)
|
72
61
|
names.map(&:to_sym).each do |name|
|
73
62
|
class_eval <<-EOT
|