hyperresource-aptible 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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8016963a4231079f134bfe019e5d8092f355321f
4
+ data.tar.gz: c51ccfdd3523321c47d9d9efb7a80c9039026407
5
+ SHA512:
6
+ metadata.gz: 91c88986ff52e6fe1f693559c887988de9dcfed25d8517b9f37d45f7e5b30609313647e7cdf9eaa97a833e6b9ccdfc215a8b13936eab0d86344461c535df6540
7
+ data.tar.gz: f0bcd175f570e738f7bb79452fb847d49adf67a02b6659bec16ed2e6fa9c6d2fa0fcaf7b31e42a8245be30a1930c3349919ede76e15d681b46017232bdf1bbb7
@@ -0,0 +1,302 @@
1
+ require 'hyper_resource/attributes'
2
+ require 'hyper_resource/exceptions'
3
+ require 'hyper_resource/link'
4
+ require 'hyper_resource/links'
5
+ require 'hyper_resource/objects'
6
+ require 'hyper_resource/response'
7
+ require 'hyper_resource/version'
8
+
9
+ require 'hyper_resource/adapter'
10
+ require 'hyper_resource/adapter/hal_json'
11
+
12
+ require 'hyper_resource/modules/http'
13
+ require 'hyper_resource/modules/internal_attributes'
14
+
15
+ require 'rubygems' if RUBY_VERSION[0..2] == '1.8'
16
+
17
+ require 'pp'
18
+
19
+ ## HyperResource is the main resource base class. Normally it will be used
20
+ ## through subclassing, though it may also be used directly.
21
+
22
+ class HyperResource
23
+
24
+ include HyperResource::Modules::HTTP
25
+ include HyperResource::Modules::InternalAttributes
26
+ include Enumerable
27
+
28
+ private
29
+
30
+ DEFAULT_HEADERS = { 'Accept' => 'application/json' }
31
+
32
+ public
33
+
34
+ ## Create a new HyperResource, given a hash of options. These options
35
+ ## include:
36
+ ##
37
+ ## [root] The root URL of the resource.
38
+ ##
39
+ ## [auth] Authentication information. Currently only +{basic:
40
+ ## ['key', 'secret']}+ is supported.
41
+ ##
42
+ ## [namespace] Class or class name, into which resources should be
43
+ ## instantiated.
44
+ ##
45
+ ## [headers] Headers to send along with requests for this resource (as
46
+ ## well as its eventual child resources, if any).
47
+ ##
48
+ ## [faraday_options] Configuration passed to +Faraday::Connection.initialize+,
49
+ ## such as +{request: {timeout: 30}}+.
50
+ ##
51
+ def initialize(opts={})
52
+ return init_from_resource(opts) if opts.kind_of?(HyperResource)
53
+
54
+ self.root = opts[:root] || self.class.root
55
+ self.href = opts[:href] || ''
56
+ self.auth = (self.class.auth || {}).merge(opts[:auth] || {})
57
+ self.namespace = opts[:namespace] || self.class.namespace
58
+ self.headers = DEFAULT_HEADERS.merge(self.class.headers || {}).
59
+ merge(opts[:headers] || {})
60
+ self.faraday_options = opts[:faraday_options] ||
61
+ self.class.faraday_options || {}
62
+
63
+ ## There's a little acrobatics in getting Attributes, Links, and Objects
64
+ ## into the correct subclass.
65
+ if self.class != HyperResource
66
+ if self.class::Attributes == HyperResource::Attributes
67
+ Object.module_eval(
68
+ "class #{self.class}::Attributes < HyperResource::Attributes; end"
69
+ )
70
+ end
71
+ if self.class::Links == HyperResource::Links
72
+ Object.module_eval(
73
+ "class #{self.class}::Links < HyperResource::Links; end"
74
+ )
75
+ end
76
+ if self.class::Objects == HyperResource::Objects
77
+ Object.module_eval(
78
+ "class #{self.class}::Objects < HyperResource::Objects; end"
79
+ )
80
+ end
81
+ end
82
+
83
+ self.attributes = self.class::Attributes.new(self)
84
+ self.links = self.class::Links.new(self)
85
+ self.objects = self.class::Objects.new(self)
86
+
87
+ self.loaded = false
88
+
89
+ self.adapter = opts[:adapter] || self.class.adapter ||
90
+ HyperResource::Adapter::HAL_JSON
91
+ end
92
+
93
+
94
+ ## Returns true if one or more of this object's attributes has been
95
+ ## reassigned.
96
+ def changed?(*args)
97
+ attributes.changed?(*args)
98
+ end
99
+
100
+
101
+ #### Filters
102
+
103
+ ## +incoming_body_filter+ filters a hash of attribute keys and values
104
+ ## on their way from a response body to a HyperResource. Override this
105
+ ## in a subclass of HyperResource to implement filters on incoming data.
106
+ def incoming_body_filter(attr_hash)
107
+ attr_hash
108
+ end
109
+
110
+ ## +outgoing_body_filter+ filters a hash of attribute keys and values
111
+ ## on their way from a HyperResource to a request body. Override this
112
+ ## in a subclass of HyperResource to implement filters on outgoing data.
113
+ def outgoing_body_filter(attr_hash)
114
+ attr_hash
115
+ end
116
+
117
+ ## +outgoing_uri_filter+ filters a hash of attribute keys and values
118
+ ## on their way from a HyperResource to a URL. Override this
119
+ ## in a subclass of HyperResource to implement filters on outgoing URI
120
+ ## parameters.
121
+ def outgoing_uri_filter(attr_hash)
122
+ attr_hash
123
+ end
124
+
125
+
126
+ #### Enumerable support
127
+
128
+ ## Returns the *i*th object in the first collection of objects embedded
129
+ ## in this resource. Returns nil on failure.
130
+ def [](i)
131
+ get unless loaded
132
+ self.objects.first[1][i] rescue nil
133
+ end
134
+
135
+ ## Iterates over the objects in the first collection of embedded objects
136
+ ## in this resource.
137
+ def each(&block)
138
+ get unless loaded
139
+ self.objects.first[1].each(&block) rescue nil
140
+ end
141
+
142
+ #### Magic
143
+
144
+ ## method_missing will load this resource if not yet loaded, then
145
+ ## attempt to delegate to +attributes+, then +objects+, then +links+.
146
+ ## Override with care.
147
+ def method_missing(method, *args)
148
+ self.get unless self.loaded
149
+
150
+ method = method.to_s
151
+ if method[-1,1] == '='
152
+ return attributes[method[0..-2]] = args.first if attributes[method[0..-2]]
153
+ else
154
+ return attributes[method] if attributes && attributes.has_key?(method)
155
+ return objects[method] if objects && objects[method]
156
+ if links && links[method]
157
+ if args.count > 0
158
+ return links[method].where(*args)
159
+ else
160
+ return links[method]
161
+ end
162
+ end
163
+ end
164
+
165
+ raise NoMethodError, "undefined method `#{method}' for #{self.inspect}"
166
+ end
167
+
168
+
169
+ def inspect # @private
170
+ "#<#{self.class}:0x#{"%x" % self.object_id} @root=#{self.root.inspect} "+
171
+ "@href=#{self.href.inspect} @loaded=#{self.loaded} "+
172
+ "@namespace=#{self.namespace.inspect} ...>"
173
+ end
174
+
175
+ ## +response_body+, +response_object+, and +deserialized_response+
176
+ ## are deprecated in favor of +body+. (Sorry. Naming things is hard.)
177
+ def response_body # @private
178
+ _hr_deprecate('HyperResource#response_body is deprecated. '+
179
+ 'Please use HyperResource#body instead.')
180
+ body
181
+ end
182
+ def response_object # @private
183
+ _hr_deprecate('HyperResource#response_object is deprecated. '+
184
+ 'Please use HyperResource#body instead.')
185
+ body
186
+ end
187
+ def deserialized_response # @private
188
+ _hr_deprecate('HyperResource#deserialized_response is deprecated. '+
189
+ 'Please use HyperResource#body instead.')
190
+ body
191
+ end
192
+
193
+
194
+
195
+ ## Return a new HyperResource based on this object and a given href.
196
+ def _hr_new_from_link(href) # @private
197
+ self.class.new(:root => self.root,
198
+ :auth => self.auth,
199
+ :headers => self.headers,
200
+ :namespace => self.namespace,
201
+ :faraday_options => self.faraday_options,
202
+ :href => href)
203
+ end
204
+
205
+
206
+ ## Returns the class into which the given response should be cast.
207
+ ## If the object is not loaded yet, or if +namespace+ is
208
+ ## not set, returns +self+.
209
+ ##
210
+ ## Otherwise, +response_class+ uses +get_data_type_from_response+ to
211
+ ## determine subclass name, glues it to the given namespace, and
212
+ ## creates the class if it's not there yet. E.g., given a namespace of
213
+ ## +FooAPI+ and a response content-type of
214
+ ## "application/vnd.foocorp.fooapi.v1+json;type=User", this should
215
+ ## return +FooAPI::User+ (even if +FooAPI::User+ hadn't existed yet).
216
+ def self.response_class(response, namespace)
217
+ if self.to_s == 'HyperResource'
218
+ return self unless namespace
219
+ end
220
+
221
+ namespace ||= self.to_s
222
+
223
+ type_name = self.get_data_type_from_response(response)
224
+ return self unless type_name
225
+
226
+ namespaced_class(type_name, namespace)
227
+ end
228
+
229
+ def self.namespaced_class(type_name, namespace)
230
+ class_name = "#{namespace}::#{type_name}"
231
+ class_name.gsub!(/[^_0-9A-Za-z:]/, '') ## sanitize class_name
232
+
233
+ ## Return data type class if it exists
234
+ klass = eval(class_name) rescue :sorry_dude
235
+ return klass if klass.is_a?(Class)
236
+
237
+ ## Data type class didn't exist -- create namespace (if necessary),
238
+ ## then the data type class
239
+ if namespace != ''
240
+ nsc = eval(namespace) rescue :bzzzzzt
241
+ unless nsc.is_a?(Class)
242
+ Object.module_eval "class #{namespace} < #{self}; end"
243
+ end
244
+ end
245
+ Object.module_eval "class #{class_name} < #{namespace}; end"
246
+ eval(class_name)
247
+ end
248
+
249
+ def _hr_response_class # @private
250
+ self.namespace ||= self.class.to_s unless self.class.to_s=='HyperResource'
251
+ self.class.response_class(self.response, self.namespace)
252
+ end
253
+
254
+
255
+ ## Inspects the given Faraday::Response, and returns a string describing
256
+ ## this resource's data type.
257
+ ##
258
+ ## By default, this method looks for a +type=...+ modifier in the
259
+ ## response's +Content-type+ and returns that value, capitalized.
260
+ ##
261
+ ## Override this method in a subclass to alter HyperResource's behavior.
262
+ def self.get_data_type_from_response(response)
263
+ return nil unless response
264
+ return nil unless content_type = response['content-type']
265
+ return nil unless m=content_type.match(/;\s* type=([0-9A-Za-z:]+)/x)
266
+ m[1][0,1].upcase + m[1][1..-1]
267
+ end
268
+
269
+ ## Uses +HyperResource.get_response_data_type+ to determine the proper
270
+ ## data type for this object. Override to change behavior (though you
271
+ ## probably just want to override the class method).
272
+ def get_data_type_from_response
273
+ self.class.get_data_type_from_response(self.response)
274
+ end
275
+
276
+ private
277
+
278
+ ## Return this object, "cast" into its proper response class.
279
+ def to_response_class
280
+ response_class = self._hr_response_class
281
+ return self if self.class == response_class
282
+ response_class.new(self)
283
+ end
284
+
285
+ ## Use the given resource's data to initialize this one.
286
+ def init_from_resource(resource)
287
+ (self.class._hr_attributes - [:attributes, :links, :objects]).each do |attr|
288
+ self.send("#{attr}=".to_sym, resource.send(attr))
289
+ end
290
+ self.adapter.apply(self.body, self)
291
+ end
292
+
293
+
294
+ ## Show a deprecation message.
295
+ def self._hr_deprecate(message) # @private
296
+ STDERR.puts "#{message} (called from #{caller[2]})"
297
+ end
298
+
299
+ def _hr_deprecate(*args) # @private
300
+ self.class._hr_deprecate(*args)
301
+ end
302
+ end
@@ -0,0 +1,31 @@
1
+ class HyperResource
2
+
3
+ ## HyperResource::Adapter is the interface/abstract base class for
4
+ ## adapters to different hypermedia formats (e.g., HAL+JSON). New
5
+ ## adapters must implement the public methods of this class.
6
+
7
+ class Adapter
8
+ class << self
9
+
10
+ ## Serialize the given object into a string.
11
+ def serialize(object)
12
+ raise NotImplementedError, "This is an abstract method -- subclasses "+
13
+ "of HyperResource::Adapter must implement it."
14
+ end
15
+
16
+ ## Deserialize a given string into an object (Hash).
17
+ def deserialize(string)
18
+ raise NotImplementedError, "This is an abstract method -- subclasses "+
19
+ "of HyperResource::Adapter must implement it."
20
+ end
21
+
22
+ ## Use a given deserialized response object (Hash) to update a given
23
+ ## resource (HyperResource), returning the updated resource.
24
+ def apply(response, resource, opts={})
25
+ raise NotImplementedError, "This is an abstract method -- subclasses "+
26
+ "of HyperResource::Adapter must implement it."
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,135 @@
1
+ require 'rubygems' if RUBY_VERSION[0..2] == '1.8'
2
+ require 'json'
3
+
4
+ class HyperResource
5
+ class Adapter
6
+
7
+ ## HyperResource::Adapter::HAL_JSON provides support for the HAL+JSON
8
+ ## hypermedia format by implementing the interface defined in
9
+ ## HyperResource::Adapter.
10
+
11
+ class HAL_JSON < Adapter
12
+ class << self
13
+
14
+ def serialize(object)
15
+ JSON.dump(object)
16
+ end
17
+
18
+ def deserialize(string)
19
+ JSON.parse(string)
20
+ end
21
+
22
+ def apply(response, resource, opts={})
23
+ if !response.kind_of?(Hash)
24
+ raise ArgumentError, "'response' argument must be a Hash"
25
+ end
26
+ if !resource.kind_of?(HyperResource)
27
+ raise ArgumentError, "'resource' argument must be a HyperResource"
28
+ end
29
+
30
+ apply_objects(response, resource)
31
+ apply_links(response, resource)
32
+ apply_attributes(response, resource)
33
+ resource.loaded = true
34
+ resource.href = response['_links']['self']['href'] rescue nil
35
+ resource
36
+ end
37
+
38
+
39
+ private
40
+
41
+ def apply_objects(resp, rsrc)
42
+ return unless resp['_embedded']
43
+ rc = rsrc.class
44
+ rsrc.objects = rc::Objects.new(rsrc)
45
+ objs = rsrc.objects
46
+
47
+ resp['_embedded'].each do |name, collection|
48
+ if collection.is_a? Hash
49
+ r = rc.new(:root => rsrc.root,
50
+ :headers => rsrc.headers,
51
+ :namespace => rsrc.namespace)
52
+ r.body = collection
53
+ objs[name] = apply(collection, r)
54
+ else
55
+ objs[name] = collection.map do |obj|
56
+ r = rc.new(:root => rsrc.root,
57
+ :headers => rsrc.headers,
58
+ :namespace => rsrc.namespace)
59
+ r.body = obj
60
+ r = classify(obj, r)
61
+ apply(obj, r)
62
+ end
63
+ end
64
+ end
65
+
66
+ # objs._hr_create_methods!
67
+ end
68
+
69
+ def classify(resp, rsrc)
70
+ return rsrc unless (type_name = get_data_type_from_object(resp)) &&
71
+ (namespace = rsrc.namespace)
72
+ klass = rsrc.class.namespaced_class(type_name, namespace)
73
+
74
+ if klass
75
+ rsrc = klass.new(:root => rsrc.root,
76
+ :headers => rsrc.headers,
77
+ :namespace => rsrc.namespace)
78
+ rsrc.body = resp
79
+ end
80
+ rsrc
81
+ end
82
+
83
+ def get_data_type_from_object(object)
84
+ return nil unless object && object['type']
85
+ object['type'][0].upcase + object['type'][1..-1]
86
+ end
87
+
88
+ def apply_links(resp, rsrc)
89
+ return unless resp['_links']
90
+ rsrc.links = rsrc._hr_response_class::Links.new(rsrc)
91
+ links = rsrc.links
92
+
93
+ resp['_links'].each do |rel, link_spec|
94
+ keys = [rel]
95
+ if m=rel.match(/.+:(.+)/)
96
+ keys << m[1]
97
+ end
98
+ keys.each do |key|
99
+ if link_spec.is_a? Array
100
+ links[key] = link_spec.map do |link|
101
+ new_link_from_spec(rsrc, link)
102
+ end
103
+ else
104
+ links[key] = new_link_from_spec(rsrc, link_spec)
105
+ end
106
+ end
107
+ end
108
+
109
+ # links._hr_create_methods!
110
+ end
111
+
112
+ def new_link_from_spec(resource, link_spec)
113
+ resource.class::Link.new(resource, link_spec)
114
+ end
115
+
116
+
117
+ def apply_attributes(resp, rsrc)
118
+ rsrc.attributes = rsrc._hr_response_class::Attributes.new(rsrc)
119
+
120
+ given_attrs = resp.reject{|k,v| %w(_links _embedded).include?(k)}
121
+ filtered_attrs = rsrc.incoming_body_filter(given_attrs)
122
+
123
+ filtered_attrs.keys.each do |attr|
124
+ rsrc.attributes[attr] = filtered_attrs[attr]
125
+ end
126
+
127
+ rsrc.attributes._hr_clear_changed
128
+ # rsrc.attributes._hr_create_methods!
129
+ end
130
+
131
+ end
132
+ end
133
+ end
134
+ end
135
+
@@ -0,0 +1,100 @@
1
+ class HyperResource
2
+ class Attributes < Hash
3
+
4
+ attr_accessor :_resource # @private
5
+
6
+ def initialize(resource=nil) # @private
7
+ self._resource = resource || HyperResource.new
8
+ end
9
+
10
+ ## Creates accessor methods in self.class and self._resource.class.
11
+ ## Protects against method creation into HyperResource::Attributes and
12
+ ## HyperResource classes. Just subclasses, please!
13
+ def _hr_create_methods!(opts={}) # @private
14
+ return if self.class.to_s == 'HyperResource::Attributes'
15
+ return if self._resource.class.to_s == 'HyperResource'
16
+ return if self.class.send(
17
+ :class_variable_defined?, :@@_hr_created_attributes_methods)
18
+
19
+ self.keys.each do |attr|
20
+ attr_sym = attr.to_sym
21
+ attr_eq_sym = "#{attr}=".to_sym
22
+
23
+ self.class.send(:define_method, attr_sym) do
24
+ self[attr]
25
+ end
26
+ self.class.send(:define_method, attr_eq_sym) do |val|
27
+ self[attr] = val
28
+ end
29
+
30
+ ## Don't stomp on _resource's methods
31
+ unless _resource.respond_to?(attr_sym)
32
+ _resource.class.send(:define_method, attr_sym) do
33
+ attributes.send(attr_sym)
34
+ end
35
+ end
36
+ unless _resource.respond_to?(attr_eq_sym)
37
+ _resource.class.send(:define_method, attr_eq_sym) do |val|
38
+ attributes.send(attr_eq_sym, val)
39
+ end
40
+ end
41
+ end
42
+
43
+ ## This is a good time to mark this object as not-changed
44
+ _hr_clear_changed
45
+
46
+ self.class.send(:class_variable_set, :@@_hr_created_attributes_methods, true)
47
+ end
48
+
49
+ ## Returns +true+ if the given attribute has been changed since creation
50
+ ## time, +false+ otherwise.
51
+ ## If no attribute is given, return whether any attributes have been
52
+ ## changed.
53
+ def changed?(attr=nil)
54
+ @_hr_changed ||= Hash.new(false)
55
+ return @_hr_changed[attr.to_sym] if attr
56
+ return @_hr_changed.keys.count > 0
57
+ end
58
+
59
+ ## Returns a hash of the attributes and values which have been changed
60
+ ## since creation time.
61
+ def changed_attributes
62
+ @_hr_changed.select{|k,v| v}.keys.inject({}) {|h,k| h[k]=self[k]; h}
63
+ end
64
+
65
+ def []=(attr, value) # @private
66
+ return self[attr] if self.has_key?(attr.to_s) && self[attr] == value
67
+ _hr_mark_changed(attr)
68
+ super(attr.to_s, value)
69
+ end
70
+
71
+ def [](key) # @private
72
+ return super(key.to_s) if self.has_key?(key.to_s)
73
+ return super(key.to_sym) if self.has_key?(key.to_sym)
74
+ nil
75
+ end
76
+
77
+ def method_missing(method, *args) # @private
78
+ method = method.to_s
79
+ if has_key?(method)
80
+ self[method]
81
+ elsif method[-1,1] == '='
82
+ self[method[0..-2]] = args.first
83
+ else
84
+ raise NoMethodError, "undefined method `#{method}' for #{self.inspect}"
85
+ end
86
+ end
87
+
88
+ def _hr_clear_changed # @private
89
+ @_hr_changed = nil
90
+ end
91
+
92
+ def _hr_mark_changed(attr, is_changed=true) # @private
93
+ attr = attr.to_sym
94
+ @_hr_changed ||= Hash.new(false)
95
+ @_hr_changed[attr] = is_changed
96
+ end
97
+
98
+ end
99
+ end
100
+
@@ -0,0 +1,40 @@
1
+ class HyperResource
2
+ class Exception < ::StandardError
3
+ ## The internal exception which led to this one, if any.
4
+ attr_accessor :cause
5
+
6
+ def initialize(message, attrs={}) # @private
7
+ self.cause = attrs[:cause]
8
+ super(message)
9
+ end
10
+ end
11
+
12
+ class ResponseError < Exception
13
+ ## The +Faraday::Response+ object which led to this exception.
14
+ attr_accessor :response
15
+
16
+ ## The deserialized response body which led to this exception.
17
+ ## May be blank, e.g. in case of deserialization errors.
18
+ attr_accessor :body
19
+
20
+ def initialize(message, attrs={}) # @private
21
+ self.response = attrs[:response]
22
+ self.body = attrs[:body]
23
+
24
+ ## Try to help out with the message
25
+ if self.body
26
+ if error = self.body['error']
27
+ message = "#{message} (#{error})"
28
+ end
29
+ elsif self.response
30
+ message = "#{message} (\"#{self.response.inspect}\")"
31
+ end
32
+
33
+ super(message, attrs)
34
+ end
35
+ end
36
+
37
+ class ClientError < ResponseError; end
38
+ class ServerError < ResponseError; end
39
+ end
40
+
@@ -0,0 +1,57 @@
1
+ require 'uri_template'
2
+
3
+ class HyperResource::Link
4
+ attr_accessor :base_href,
5
+ :name,
6
+ :templated,
7
+ :params,
8
+ :parent_resource
9
+
10
+ ## Returns true if this link is templated.
11
+ def templated?; templated end
12
+
13
+ def initialize(resource=nil, link_spec={})
14
+ self.parent_resource = resource || HyperResource.new
15
+ self.base_href = link_spec['href']
16
+ self.name = link_spec['name']
17
+ self.templated = !!link_spec['templated']
18
+ self.params = link_spec['params'] || {}
19
+ end
20
+
21
+ ## Returns this link's href, applying any URI template params.
22
+ def href
23
+ if self.templated?
24
+ filtered_params = self.parent_resource.outgoing_uri_filter(params)
25
+ URITemplate.new(self.base_href).expand(filtered_params)
26
+ else
27
+ self.base_href
28
+ end
29
+ end
30
+
31
+ ## Returns a new scope with the given params; that is, returns a copy of
32
+ ## itself with the given params applied.
33
+ def where(params)
34
+ params = Hash[ params.map{|(k,v)| [k.to_s, v]} ]
35
+ self.class.new(self.parent_resource,
36
+ 'href' => self.base_href,
37
+ 'name' => self.name,
38
+ 'templated' => self.templated,
39
+ 'params' => self.params.merge(params))
40
+ end
41
+
42
+ ## Returns a HyperResource representing this link
43
+ def resource
44
+ parent_resource._hr_new_from_link(self.href)
45
+ end
46
+
47
+ ## Returns a HyperResource representing this link, and fetches it.
48
+ def get
49
+ self.resource.get
50
+ end
51
+
52
+ ## If we were called with a method we don't know, load this resource
53
+ ## and pass the message along. This achieves implicit loading.
54
+ def method_missing(method, *args)
55
+ self.get.send(method, *args)
56
+ end
57
+ end
@@ -0,0 +1,63 @@
1
+ class HyperResource
2
+ class Links < Hash
3
+ attr_accessor :_resource
4
+
5
+ def initialize(resource=nil)
6
+ self._resource = resource || HyperResource.new
7
+ end
8
+
9
+ ## Creates accessor methods in self.class and self._resource.class.
10
+ ## Protects against method creation into HyperResource::Links and
11
+ ## HyperResource classes. Just subclasses, please!
12
+ def _hr_create_methods!(opts={}) # @private
13
+ return if self.class.to_s == 'HyperResource::Links'
14
+ return if self._resource.class.to_s == 'HyperResource'
15
+ return if self.class.send(
16
+ :class_variable_defined?, :@@_hr_created_links_methods)
17
+
18
+ self.keys.each do |attr|
19
+ attr_sym = attr.to_sym
20
+ self.class.send(:define_method, attr_sym) do |*args|
21
+ if args.count > 0
22
+ self[attr].where(*args)
23
+ else
24
+ self[attr]
25
+ end
26
+ end
27
+
28
+ ## Don't stomp on _resource's methods
29
+ unless _resource.respond_to?(attr_sym)
30
+ _resource.class.send(:define_method, attr_sym) do |*args|
31
+ links.send(attr_sym, *args)
32
+ end
33
+ end
34
+ end
35
+
36
+ self.class.send(:class_variable_set, :@@_hr_created_links_methods, true)
37
+ end
38
+
39
+ def []=(attr, value) # @private
40
+ super(attr.to_s, value)
41
+ end
42
+
43
+ def [](key) # @private
44
+ return super(key.to_s) if self.has_key?(key.to_s)
45
+ return super(key.to_sym) if self.has_key?(key.to_sym)
46
+ nil
47
+ end
48
+
49
+ def method_missing(method, *args) # @private
50
+ unless self[method]
51
+ raise NoMethodError, "undefined method `#{method}' for #{self.inspect}"
52
+ end
53
+
54
+ if args.count > 0
55
+ self[method].where(*args)
56
+ else
57
+ self[method]
58
+ end
59
+ end
60
+
61
+ end
62
+ end
63
+
@@ -0,0 +1,129 @@
1
+ require 'faraday'
2
+ require 'uri'
3
+ require 'json'
4
+
5
+ class HyperResource
6
+ module Modules
7
+ module HTTP
8
+
9
+ ## Loads and returns the resource pointed to by +href+. The returned
10
+ ## resource will be blessed into its "proper" class, if
11
+ ## +self.class.namespace != nil+.
12
+ def get
13
+ self.response = faraday_connection.get(self.href || '')
14
+ finish_up
15
+ end
16
+
17
+ ## By default, calls +post+ with the given arguments. Override to
18
+ ## change this behavior.
19
+ def create(*args)
20
+ post(*args)
21
+ end
22
+
23
+ ## POSTs the given attributes to this resource's href, and returns
24
+ ## the response resource.
25
+ def post(attrs)
26
+ self.response = faraday_connection.post do |req|
27
+ req.body = adapter.serialize(attrs)
28
+ end
29
+ finish_up
30
+ end
31
+
32
+ ## By default, calls +put+ with the given arguments. Override to
33
+ ## change this behavior.
34
+ def update(*args)
35
+ put(*args)
36
+ end
37
+
38
+ ## PUTs this resource's attributes to this resource's href, and returns
39
+ ## the response resource. If attributes are given, +put+ uses those
40
+ ## instead.
41
+ def put(attrs=nil)
42
+ attrs ||= self.attributes
43
+ self.response = faraday_connection.put do |req|
44
+ req.body = adapter.serialize(attrs)
45
+ end
46
+ finish_up
47
+ end
48
+
49
+ ## PATCHes this resource's changed attributes to this resource's href,
50
+ ## and returns the response resource. If attributes are given, +patch+
51
+ ## uses those instead.
52
+ def patch(attrs=nil)
53
+ attrs ||= self.attributes.changed_attributes
54
+ self.response = faraday_connection.patch do |req|
55
+ req.body = adapter.serialize(attrs)
56
+ end
57
+ finish_up
58
+ end
59
+
60
+ ## DELETEs this resource's href, and returns the response resource.
61
+ def delete
62
+ self.response = faraday_connection.delete
63
+ finish_up
64
+ end
65
+
66
+ ## Returns a raw Faraday connection to this resource's URL, with proper
67
+ ## headers (including auth). Threadsafe.
68
+ def faraday_connection(url=nil)
69
+ url ||= URI.join(self.root, self.href)
70
+ key = Digest::MD5.hexdigest({
71
+ 'faraday_connection' => {
72
+ 'url' => url,
73
+ 'headers' => self.headers,
74
+ 'ba' => self.auth[:basic]
75
+ }
76
+ }.to_json)
77
+ return Thread.current[key] if Thread.current[key]
78
+
79
+ fc = Faraday.new(self.faraday_options.merge(:url => url))
80
+ fc.headers.merge!('User-Agent' => "HyperResource #{HyperResource::VERSION}")
81
+ fc.headers.merge!(self.headers || {})
82
+ if ba=self.auth[:basic]
83
+ fc.basic_auth(*ba)
84
+ end
85
+ Thread.current[key] = fc
86
+ end
87
+
88
+ private
89
+
90
+ def finish_up
91
+ begin
92
+ self.body = self.adapter.deserialize(self.response.body) unless self.response.body.nil?
93
+ rescue StandardError => e
94
+ raise HyperResource::ResponseError.new(
95
+ "Error when deserializing response body",
96
+ :response => self.response,
97
+ :cause => e
98
+ )
99
+ end
100
+
101
+ self.adapter.apply(self.body, self)
102
+ self.loaded = true
103
+
104
+ status = self.response.status
105
+ if status / 100 == 2
106
+ return to_response_class
107
+ elsif status / 100 == 3
108
+ ## TODO redirect logic?
109
+ elsif status / 100 == 4
110
+ raise HyperResource::ClientError.new(status.to_s,
111
+ :response => self.response,
112
+ :body => self.body)
113
+ elsif status / 100 == 5
114
+ raise HyperResource::ServerError.new(status.to_s,
115
+ :response => self.response,
116
+ :body => self.body)
117
+
118
+ else ## 1xx? really?
119
+ raise HyperResource::ResponseError.new("Got status #{status}, wtf?",
120
+ :response => self.response,
121
+ :body => self.body)
122
+
123
+ end
124
+ end
125
+
126
+ end
127
+ end
128
+ end
129
+
@@ -0,0 +1,90 @@
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
+
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
+ ]
31
+ end
32
+
33
+ def _hr_attributes # @private
34
+ [ :root,
35
+ :href,
36
+ :auth,
37
+ :headers,
38
+ :namespace,
39
+ :adapter,
40
+ :faraday_options,
41
+
42
+ :request,
43
+ :response,
44
+ :body,
45
+
46
+ :attributes,
47
+ :links,
48
+ :objects,
49
+
50
+ :loaded
51
+ ]
52
+ end
53
+
54
+ ## Inheritable class attribute, kinda like in Rails.
55
+ def _hr_class_attribute(*names)
56
+ names.map(&:to_sym).each do |name|
57
+ instance_eval <<-EOT
58
+ def #{name}=(val)
59
+ @#{name} = val
60
+ end
61
+ def #{name}
62
+ return @#{name} if defined?(@#{name})
63
+ return superclass.#{name} if superclass.respond_to?(:#{name})
64
+ nil
65
+ end
66
+ EOT
67
+ end
68
+ end
69
+
70
+ ## Instance attributes which fall back to class attributes.
71
+ def _hr_fallback_attribute(*names)
72
+ names.map(&:to_sym).each do |name|
73
+ class_eval <<-EOT
74
+ def #{name}=(val)
75
+ @#{name} = val
76
+ end
77
+ def #{name}
78
+ return @#{name} if defined?(@#{name})
79
+ return self.class.#{name} if self.class.respond_to?(:#{name})
80
+ nil
81
+ end
82
+ EOT
83
+ end
84
+ end
85
+
86
+ end # ClassMethods
87
+
88
+ end
89
+ end
90
+
@@ -0,0 +1,60 @@
1
+ class HyperResource
2
+ class Objects < Hash
3
+ attr_accessor :_resource
4
+
5
+ def initialize(resource=nil)
6
+ self._resource = resource || HyperResource.new
7
+ end
8
+
9
+ ## Creates accessor methods in self.class and self._resource.class.
10
+ ## Protects against method creation into HyperResource::Objects and
11
+ ## HyperResource classes. Just subclasses, please!
12
+ def _hr_create_methods!(opts={}) # @private
13
+ return if self.class.to_s == 'HyperResource::Objects'
14
+ return if self._resource.class.to_s == 'HyperResource'
15
+ return if self.class.send(
16
+ :class_variable_defined?, :@@_hr_created_objects_methods)
17
+
18
+ self.keys.each do |attr|
19
+ attr_sym = attr.to_sym
20
+
21
+ self.class.send(:define_method, attr_sym) do
22
+ self[attr]
23
+ end
24
+
25
+ ## Don't stomp on _resource's methods
26
+ unless _resource.respond_to?(attr_sym)
27
+ _resource.class.send(:define_method, attr_sym) do
28
+ objects.send(attr_sym)
29
+ end
30
+ end
31
+ end
32
+
33
+ self.class.send(:class_variable_set, :@@_hr_created_objects_methods, true)
34
+ end
35
+
36
+ def []=(attr, value) # @private
37
+ super(attr.to_s, value)
38
+ end
39
+
40
+ ## When +key+ is a string, returns the array of objects under that name.
41
+ ## When +key+ is a number, returns +ith(key)+. Returns nil on lookup
42
+ ## failure.
43
+ def [](key)
44
+ case key
45
+ when String, Symbol
46
+ return super(key.to_s) if self.has_key?(key.to_s)
47
+ return super(key.to_sym) if self.has_key?(key.to_sym)
48
+ when Fixnum
49
+ return ith(key)
50
+ end
51
+ nil
52
+ end
53
+
54
+ def method_missing(method, *args) # @private
55
+ return self[method] if self[method]
56
+ raise NoMethodError, "undefined method `#{method}' for #{self.inspect}"
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,2 @@
1
+ class HyperResource::Response < Hash
2
+ end
@@ -0,0 +1,4 @@
1
+ class HyperResource
2
+ VERSION = '0.9.0'
3
+ VERSION_DATE = '2014-03-11'
4
+ end
@@ -0,0 +1,2 @@
1
+ require 'hyper_resource'
2
+
metadata ADDED
@@ -0,0 +1,172 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hyperresource-aptible
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Frank Macreery
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: uri_template
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.5.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.5.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.8.6
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.8.6
41
+ - !ruby/object:Gem::Dependency
42
+ name: json
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: 10.0.4
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: 10.0.4
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: 4.7.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: 4.7.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: mocha
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: 0.13.3
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: 0.13.3
111
+ - !ruby/object:Gem::Dependency
112
+ name: sinatra
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: 1.4.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: 1.4.0
125
+ description: |2
126
+ HyperResource is a hypermedia client library for Ruby. Its goals are to
127
+ interface directly with well-behaved hypermedia APIs, to allow the data
128
+ returned from these APIs to optionally be extended by client-side code,
129
+ and to present a modern replacement for ActiveResource.
130
+ email: frank@macreery.com
131
+ executables: []
132
+ extensions: []
133
+ extra_rdoc_files: []
134
+ files:
135
+ - lib/hyper_resource.rb
136
+ - lib/hyper_resource/adapter.rb
137
+ - lib/hyper_resource/adapter/hal_json.rb
138
+ - lib/hyper_resource/attributes.rb
139
+ - lib/hyper_resource/exceptions.rb
140
+ - lib/hyper_resource/link.rb
141
+ - lib/hyper_resource/links.rb
142
+ - lib/hyper_resource/modules/http.rb
143
+ - lib/hyper_resource/modules/internal_attributes.rb
144
+ - lib/hyper_resource/objects.rb
145
+ - lib/hyper_resource/response.rb
146
+ - lib/hyper_resource/version.rb
147
+ - lib/hyperresource.rb
148
+ homepage: https://github.com/fancyremarker/hyperresource
149
+ licenses:
150
+ - MIT
151
+ metadata: {}
152
+ post_install_message:
153
+ rdoc_options: []
154
+ require_paths:
155
+ - lib
156
+ required_ruby_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - '>='
159
+ - !ruby/object:Gem::Version
160
+ version: 1.8.7
161
+ required_rubygems_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ requirements: []
167
+ rubyforge_project:
168
+ rubygems_version: 2.2.1
169
+ signing_key:
170
+ specification_version: 4
171
+ summary: Extensible hypermedia client for Ruby
172
+ test_files: []