aptible-resource 0.1.0

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