hyperresource-aptible 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []