hyperresource_zuhrig 0.1.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,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
+