hyperresource_zuhrig 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+
@@ -0,0 +1,240 @@
1
+ require 'faraday'
2
+ require 'uri'
3
+ require 'json'
4
+ require 'digest/md5'
5
+
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
+
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.
82
+ module HTTP
83
+
84
+ ## Loads and returns the resource pointed to by +href+. The returned
85
+ ## resource will be blessed into its "proper" class, if
86
+ ## +self.class.namespace != nil+.
87
+ def get
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)
99
+ end
100
+
101
+ ## By default, calls +post+ with the given arguments. Override to
102
+ ## change this behavior.
103
+ def create(*args)
104
+ _hr_deprecate('HyperResource::Link#create is deprecated. Please use '+
105
+ '#post instead.')
106
+ post(*args)
107
+ end
108
+
109
+ ## POSTs the given attributes to this resource's href, and returns
110
+ ## the response resource.
111
+ def post(attrs=nil)
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)
116
+ end
117
+ new_resource_from_response(response)
118
+ end
119
+
120
+ ## By default, calls +puwt+ with the given arguments. Override to
121
+ ## change this behavior.
122
+ def update(*args)
123
+ _hr_deprecate('HyperResource::Link#update is deprecated. Please use '+
124
+ '#put or #patch instead.')
125
+ put(*args)
126
+ end
127
+
128
+ ## PUTs this resource's attributes to this resource's href, and returns
129
+ ## the response resource. If attributes are given, +put+ uses those
130
+ ## instead.
131
+ def put(attrs=nil)
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)
136
+ end
137
+ new_resource_from_response(response)
138
+ end
139
+
140
+ ## PATCHes this resource's changed attributes to this resource's href,
141
+ ## and returns the response resource. If attributes are given, +patch+
142
+ ## uses those instead.
143
+ def patch(attrs=nil)
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)
148
+ end
149
+ new_resource_from_response(response)
150
+ end
151
+
152
+ ## DELETEs this resource's href, and returns the response resource.
153
+ def delete
154
+ response = faraday_connection.delete
155
+ new_resource_from_response(response)
156
+ end
157
+
158
+ private
159
+
160
+ ## Returns a raw Faraday connection to this resource's URL, with proper
161
+ ## headers (including auth). Threadsafe.
162
+ def faraday_connection(url=nil)
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({
169
+ 'faraday_connection' => {
170
+ 'url' => url,
171
+ 'headers' => headers,
172
+ 'ba' => auth[:basic]
173
+ }
174
+ }.to_json)
175
+ return Thread.current[key] if Thread.current[key]
176
+
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]
182
+ fc.basic_auth(*ba)
183
+ end
184
+ Thread.current[key] = fc
185
+ end
186
+
187
+
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
198
+ begin
199
+ if response.body
200
+ body = adapter.deserialize(response.body)
201
+ end
202
+ rescue StandardError => e
203
+ if is_success
204
+ raise HyperResource::ResponseError.new(
205
+ "Error when deserializing response body",
206
+ :response => response,
207
+ :cause => e
208
+ )
209
+ end
210
+ end
211
+
212
+ new_rsrc = resource.new_from(:link => self,
213
+ :body => body,
214
+ :response => response)
215
+
216
+ if status / 100 == 2
217
+ return new_rsrc
218
+ elsif status / 100 == 3
219
+ raise NotImplementedError,
220
+ "HyperResource has not implemented redirection."
221
+ elsif status / 100 == 4
222
+ raise HyperResource::ClientError.new(status.to_s,
223
+ :response => response,
224
+ :body => body)
225
+ elsif status / 100 == 5
226
+ raise HyperResource::ServerError.new(status.to_s,
227
+ :response => response,
228
+ :body => body)
229
+ else ## 1xx? really?
230
+ raise HyperResource::ResponseError.new("Unknown status #{status}",
231
+ :response => response,
232
+ :body => body)
233
+
234
+ end
235
+ end
236
+
237
+ end
238
+ end
239
+ end
240
+
@@ -0,0 +1,79 @@
1
+ module HyperResource::Modules
2
+ module InternalAttributes
3
+
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+
7
+ base._hr_class_attributes.each do |attr|
8
+ base._hr_class_attribute attr
9
+ end
10
+
11
+ (base._hr_attributes - base._hr_class_attributes).each do |attr|
12
+ base.send(:attr_accessor, attr)
13
+ end
14
+
15
+ ## Fallback attributes fall back from instance to class.
16
+ (base._hr_attributes & base._hr_class_attributes).each do |attr|
17
+ base._hr_fallback_attribute attr
18
+ end
19
+ end
20
+
21
+ module ClassMethods
22
+ # @private
23
+ def _hr_class_attributes
24
+ [ :root ]
25
+ end
26
+
27
+ # @private
28
+ def _hr_attributes
29
+ [ :root,
30
+ :href,
31
+ :request,
32
+ :response,
33
+ :body,
34
+ :attributes,
35
+ :links,
36
+ :objects,
37
+ :loaded
38
+ ]
39
+ end
40
+
41
+ ## Inheritable class attribute, kinda like in Rails.
42
+ # @private
43
+ def _hr_class_attribute(*names)
44
+ names.map(&:to_sym).each do |name|
45
+ instance_eval <<-EOT
46
+ def #{name}=(val)
47
+ @#{name} = val
48
+ end
49
+ def #{name}
50
+ return @#{name} if defined?(@#{name})
51
+ return superclass.#{name} if superclass.respond_to?(:#{name})
52
+ nil
53
+ end
54
+ EOT
55
+ end
56
+ end
57
+
58
+ ## Instance attributes which fall back to class attributes.
59
+ # @private
60
+ def _hr_fallback_attribute(*names)
61
+ names.map(&:to_sym).each do |name|
62
+ class_eval <<-EOT
63
+ def #{name}=(val)
64
+ @#{name} = val
65
+ end
66
+ def #{name}
67
+ return @#{name} if defined?(@#{name})
68
+ return self.class.#{name} if self.class.respond_to?(:#{name})
69
+ nil
70
+ end
71
+ EOT
72
+ end
73
+ end
74
+
75
+ end # ClassMethods
76
+
77
+ end
78
+ end
79
+