hyperresource 0.2.4 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|