aptible-resource 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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,59 @@
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
+ ## Delegate HTTP methods to the resource.
48
+ def get(*args); self.resource.get(*args) end
49
+ def post(*args); self.resource.post(*args) end
50
+ def patch(*args); self.resource.patch(*args) end
51
+ def put(*args); self.resource.put(*args) end
52
+ def delete(*args); self.resource.delete(*args) end
53
+
54
+ ## If we were called with a method we don't know, load this resource
55
+ ## and pass the message along. This achieves implicit loading.
56
+ def method_missing(method, *args)
57
+ self.get.send(method, *args)
58
+ end
59
+ 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,131 @@
1
+ require 'faraday'
2
+ require 'uri'
3
+ require 'json'
4
+ require 'digest/md5'
5
+
6
+ class HyperResource
7
+ module Modules
8
+ module HTTP
9
+
10
+ ## Loads and returns the resource pointed to by +href+. The returned
11
+ ## resource will be blessed into its "proper" class, if
12
+ ## +self.class.namespace != nil+.
13
+ def get
14
+ self.response = faraday_connection.get(self.href || '')
15
+ finish_up
16
+ end
17
+
18
+ ## By default, calls +post+ with the given arguments. Override to
19
+ ## change this behavior.
20
+ def create(*args)
21
+ post(*args)
22
+ end
23
+
24
+ ## POSTs the given attributes to this resource's href, and returns
25
+ ## the response resource.
26
+ def post(attrs=nil)
27
+ attrs || self.attributes
28
+ self.response = faraday_connection.post do |req|
29
+ req.body = adapter.serialize(attrs)
30
+ end
31
+ finish_up
32
+ end
33
+
34
+ ## By default, calls +put+ with the given arguments. Override to
35
+ ## change this behavior.
36
+ def update(*args)
37
+ put(*args)
38
+ end
39
+
40
+ ## PUTs this resource's attributes to this resource's href, and returns
41
+ ## the response resource. If attributes are given, +put+ uses those
42
+ ## instead.
43
+ def put(attrs=nil)
44
+ attrs ||= self.attributes
45
+ self.response = faraday_connection.put do |req|
46
+ req.body = adapter.serialize(attrs)
47
+ end
48
+ finish_up
49
+ end
50
+
51
+ ## PATCHes this resource's changed attributes to this resource's href,
52
+ ## and returns the response resource. If attributes are given, +patch+
53
+ ## uses those instead.
54
+ def patch(attrs=nil)
55
+ attrs ||= self.attributes.changed_attributes
56
+ self.response = faraday_connection.patch do |req|
57
+ req.body = adapter.serialize(attrs)
58
+ end
59
+ finish_up
60
+ end
61
+
62
+ ## DELETEs this resource's href, and returns the response resource.
63
+ def delete
64
+ self.response = faraday_connection.delete
65
+ finish_up
66
+ end
67
+
68
+ ## Returns a raw Faraday connection to this resource's URL, with proper
69
+ ## headers (including auth). Threadsafe.
70
+ def faraday_connection(url=nil)
71
+ url ||= URI.join(self.root, self.href)
72
+ key = Digest::MD5.hexdigest({
73
+ 'faraday_connection' => {
74
+ 'url' => url,
75
+ 'headers' => self.headers,
76
+ 'ba' => self.auth[:basic]
77
+ }
78
+ }.to_json)
79
+ return Thread.current[key] if Thread.current[key]
80
+
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]
85
+ fc.basic_auth(*ba)
86
+ end
87
+ Thread.current[key] = fc
88
+ end
89
+
90
+ private
91
+
92
+ def finish_up
93
+ begin
94
+ self.body = self.adapter.deserialize(self.response.body) unless self.response.body.nil?
95
+ rescue StandardError => e
96
+ raise HyperResource::ResponseError.new(
97
+ "Error when deserializing response body",
98
+ :response => self.response,
99
+ :cause => e
100
+ )
101
+ end
102
+
103
+ self.adapter.apply(self.body, self)
104
+ self.loaded = true
105
+
106
+ status = self.response.status
107
+ if status / 100 == 2
108
+ return to_response_class
109
+ elsif status / 100 == 3
110
+ ## TODO redirect logic?
111
+ elsif status / 100 == 4
112
+ raise HyperResource::ClientError.new(status.to_s,
113
+ :response => self.response,
114
+ :body => self.body)
115
+ elsif status / 100 == 5
116
+ raise HyperResource::ServerError.new(status.to_s,
117
+ :response => self.response,
118
+ :body => self.body)
119
+
120
+ else ## 1xx? really?
121
+ raise HyperResource::ResponseError.new("Got status #{status}, wtf?",
122
+ :response => self.response,
123
+ :body => self.body)
124
+
125
+ end
126
+ end
127
+
128
+ end
129
+ end
130
+ end
131
+