hyperresource 0.2.4 → 0.9.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
+
@@ -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