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.
@@ -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
- self.response = faraday_connection.get(self.href || '')
15
- finish_up
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 || self.attributes
28
- self.response = faraday_connection.post do |req|
29
- req.body = adapter.serialize(attrs)
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
- finish_up
117
+ new_resource_from_response(response)
32
118
  end
33
119
 
34
- ## By default, calls +put+ with the given arguments. Override to
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
- self.response = faraday_connection.put do |req|
46
- req.body = adapter.serialize(attrs)
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
- finish_up
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
- self.response = faraday_connection.patch do |req|
57
- req.body = adapter.serialize(attrs)
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
- finish_up
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
- self.response = faraday_connection.delete
65
- finish_up
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
- url ||= URI.join(self.root, self.href)
72
- key = Digest::MD5.hexdigest({
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' => self.headers,
76
- 'ba' => self.auth[:basic]
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
- fc = Faraday.new(self.faraday_options.merge(:url => url))
82
- fc.headers.merge!('User-Agent' => "HyperResource #{HyperResource::VERSION}")
83
- fc.headers.merge!(self.headers || {})
84
- if ba=self.auth[:basic]
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
- def finish_up
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
- self.body = self.adapter.deserialize(self.response.body) unless self.response.body.nil?
199
+ if response.body
200
+ body = adapter.deserialize(response.body)
201
+ end
95
202
  rescue StandardError => e
96
- raise HyperResource::ResponseError.new(
97
- "Error when deserializing response body",
98
- :response => self.response,
99
- :cause => e
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
- self.adapter.apply(self.body, self)
104
- self.loaded = true
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 to_response_class
217
+ return new_rsrc
109
218
  elsif status / 100 == 3
110
- ## TODO redirect logic?
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 => self.response,
114
- :body => self.body)
223
+ :response => response,
224
+ :body => body)
115
225
  elsif status / 100 == 5
116
226
  raise HyperResource::ServerError.new(status.to_s,
117
- :response => self.response,
118
- :body => self.body)
119
-
227
+ :response => response,
228
+ :body => body)
120
229
  else ## 1xx? really?
121
- raise HyperResource::ResponseError.new("Got status #{status}, wtf?",
122
- :response => self.response,
123
- :body => self.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 # @private
24
- [ :root, ## e.g. 'https://example.com/api/v1'
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
- def _hr_attributes # @private
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