hyperresource 0.1.3 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 36d09dbfdb629e25916386e38aec9638a5c92c55
4
- data.tar.gz: 0432b16c0fe0f5891f4c4efc559ce833f9ee148c
3
+ metadata.gz: 4befe203aa8c4fc93c4ba6d09c03bcc9d76a2183
4
+ data.tar.gz: 1deed89d174bd9ac761763a731d69c06b70eb318
5
5
  SHA512:
6
- metadata.gz: 2c3c9eb9d046bab3122d84be8e40905ccdfe84da104da78458dd44d853f64261410198ad33d6e695ee3c6c2aa8acf20244c94f2a239467354a908e93b5be6891
7
- data.tar.gz: bc7f063ff01e95bd16cf246584c650b8456637ab0ff6d58bc5034834912382bddbeaec04f9a46fc4574dd5bf5b85ea8b55a69af198fe81d98c34d9e163e08c17
6
+ metadata.gz: 4122f5fb687c63252bb05724cc8477c4ae64f3f72a2340af09e21d070066165591292ad6756c16c27381eb6aff43c6ea294ae88d482f5e8520fc9c4adb4adfef
7
+ data.tar.gz: e5170d246d1642ce97b5cdb312d47d1a3af99c462ca16116a146af39d1dd54e95e91313226d79853a4d115cf102d1fa55183d8e2a567cf48ae2f66575ae3fa44
@@ -8,31 +8,57 @@ require 'hyper_resource/exceptions'
8
8
 
9
9
  require 'hyper_resource/modules/utils'
10
10
  require 'hyper_resource/modules/http'
11
- require 'hyper_resource/modules/bless'
11
+
12
+ require 'hyper_resource/adapter'
13
+ require 'hyper_resource/adapter/hal_json'
12
14
 
13
15
  require 'pp'
14
16
 
17
+ ## TODO:
18
+ ## incoming_filter, outgoing_filter
19
+ ## as_json, to_json (in adapter?)
20
+ ## save, update, create, delete
21
+
15
22
  class HyperResource
16
23
  include HyperResource::Modules::Utils
17
24
  include HyperResource::Modules::HTTP
18
- include HyperResource::Modules::Bless
19
25
 
20
26
  private
21
27
 
22
28
  def self._hr_class_attributes
23
- %w( root auth headers namespace ).map(&:to_sym)
29
+ [ :root, ## e.g. 'https://example.com/api/v1'
30
+ :auth, ## e.g. {:basic => ['username', 'password']}
31
+ :headers, ## e.g. {'Accept' => 'application/vnd.example+json'}
32
+ :namespace, ## e.g. 'ExampleAPI', or the class ExampleAPI itself
33
+ :adapter ## subclass of HR::Adapter
34
+ ]
24
35
  end
25
36
 
26
37
  def self._hr_attributes
27
- %w( root href auth headers namespace
28
- request response response_body
29
- attributes links objects loaded).map(&:to_sym)
38
+ [ :root,
39
+ :href,
40
+ :auth,
41
+ :headers,
42
+ :namespace,
43
+ :adapter,
44
+
45
+ :request,
46
+ :response,
47
+ :response_object,
48
+
49
+ :attributes,
50
+ :links,
51
+ :objects,
52
+
53
+ :loaded
54
+ ]
30
55
  end
31
56
 
32
57
  public
33
58
 
34
- _hr_class_attributes.each {|attr| class_attribute attr}
35
- _hr_attributes.each {|attr| attr_accessor attr}
59
+ _hr_class_attributes.each {|attr| class_attribute attr}
60
+ (_hr_attributes & _hr_class_attributes).each {|attr| fallback_attribute attr}
61
+ (_hr_attributes - _hr_class_attributes).each {|attr| attr_accessor attr}
36
62
 
37
63
  # :nodoc:
38
64
  DEFAULT_HEADERS = {
@@ -53,10 +79,7 @@ public
53
79
  ## [headers] Headers to send along with requests for this resource (as
54
80
  ## well as its eventual child resources, if any).
55
81
  def initialize(opts={})
56
- if opts.is_a?(HyperResource)
57
- self.class._hr_attributes.each {|attr| self.send("#{attr}=".to_sym, opts.send(attr))}
58
- return
59
- end
82
+ return init_from_resource(opts) if opts.kind_of?(HyperResource)
60
83
 
61
84
  self.root = opts[:root] || self.class.root
62
85
  self.href = opts[:href] || ''
@@ -65,54 +88,141 @@ public
65
88
  self.headers = DEFAULT_HEADERS.merge(self.class.headers || {}).
66
89
  merge(opts[:headers] || {})
67
90
 
68
- self.attributes = Attributes.new(self)
69
- self.links = Links.new(self)
70
- self.objects = Objects.new(self)
91
+ ## There's a little acrobatics in getting Attributes, Links, and Objects
92
+ ## into the correct subclass.
93
+ if self.class != HyperResource
94
+ if self.class::Attributes == HyperResource::Attributes
95
+ Object.module_eval(
96
+ "class #{self.class}::Attributes < HyperResource::Attributes; end"
97
+ )
98
+ end
99
+ if self.class::Links == HyperResource::Links
100
+ Object.module_eval(
101
+ "class #{self.class}::Links < HyperResource::Links; end"
102
+ )
103
+ end
104
+ if self.class::Objects == HyperResource::Objects
105
+ Object.module_eval(
106
+ "class #{self.class}::Objects < HyperResource::Objects; end"
107
+ )
108
+ end
109
+ end
110
+
111
+ self.attributes = self.class::Attributes.new(self)
112
+ self.links = self.class::Links.new(self)
113
+ self.objects = self.class::Objects.new(self)
114
+
71
115
  self.loaded = false
116
+
117
+ self.adapter = opts[:adapter] || self.class.adapter ||
118
+ HyperResource::Adapter::HAL_JSON
72
119
  end
73
120
 
74
- ## Returns a new HyperResource based on the given HyperResource object.
75
- def new_from_resource(rsrc); self.class.new_from_resource(rsrc) end
76
- def self.new_from_resource(rsrc)
77
- new_rsrc = self.new
78
- _hr_attributes.each do |attr|
79
- new_rsrc.send("#{attr}=".to_sym, rsrc.send(attr))
121
+ private
122
+
123
+ def init_from_resource(resource)
124
+ (self.class._hr_attributes - [:attributes, :links, :objects]).each do |attr|
125
+ self.send("#{attr}=".to_sym, resource.send(attr))
80
126
  end
81
- new_rsrc
127
+ self.adapter.apply(self.response_object, self)
82
128
  end
83
129
 
84
- ## Returns a new HyperResource based on the given HAL document.
85
- def new_from_hal(obj)
86
- rsrc = self.class.new(:root => self.root,
87
- :auth => self.auth,
88
- :headers => self.headers,
89
- :namespace => self.namespace,
90
- :href => obj['_links']['self']['href'])
91
- rsrc.response_body = Response[obj]
92
- rsrc.init_from_response_body!
93
- rsrc
130
+ public
131
+
132
+ def changed?(*args)
133
+ attributes.changed?(*args)
94
134
  end
95
135
 
96
136
  ## Returns a new HyperResource based on the given link href.
97
- def new_from_link(href)
98
- rsrc = self.class.new(:root => self.root,
99
- :auth => self.auth,
100
- :headers => self.headers,
101
- :namespace => self.namespace,
102
- :href => href)
137
+ def _new_from_link(href)
138
+ self.class.new(:root => self.root,
139
+ :auth => self.auth,
140
+ :headers => self.headers,
141
+ :namespace => self.namespace,
142
+ :href => href)
143
+ end
144
+
145
+ def to_response_class
146
+ response_class = self.get_response_class
147
+ return self if self.class == response_class
148
+ response_class.new(self)
149
+ end
150
+
151
+ def incoming_filter(attr_hash)
152
+ attr_hash
153
+ end
154
+
155
+ def outgoing_filter(attr_hash)
156
+ attr_hash
157
+ end
158
+
159
+ def get_response_class
160
+ self.namespace ||= self.class.to_s unless self.class.to_s=='HyperResource'
161
+ self.class.get_response_class(self.response, self.namespace)
162
+ end
163
+
164
+ ## Returns the class into which the given response should be cast.
165
+ ## If the object is not loaded yet, or if +opts[:namespace]+ is
166
+ ## not set, returns +self+.
167
+ ##
168
+ ## Otherwise, +get_response_class+ uses +get_response_data_type+ to
169
+ ## determine subclass name, glues it to the given namespace, and
170
+ ## creates the class if it's not there yet. E.g., given a namespace of
171
+ ## +FooAPI+ and a response content-type of
172
+ ## "application/vnd.foocorp.fooapi.v1+json;type=User", this should
173
+ ## return +FooAPI::User+ (even if +FooAPI::User+ hadn't existed yet).
174
+
175
+ def self.get_response_class(response, namespace)
176
+ if self.to_s == 'HyperResource'
177
+ return self unless namespace
178
+ end
179
+
180
+ namespace ||= self.to_s
181
+
182
+ type_name = self.get_response_data_type(response)
183
+ return self unless type_name
184
+
185
+ class_name = "#{namespace}::#{type_name}"
186
+ class_name.gsub!(/[^_0-9A-Za-z:]/, '') ## sanitize class_name
187
+
188
+ ## Return data type class if it exists
189
+ klass = eval(class_name) rescue :sorry_dude
190
+ return klass if klass.is_a?(Class)
191
+
192
+ ## Data type class didn't exist -- create namespace (if necessary),
193
+ ## then the data type class
194
+ if namespace != ''
195
+ nsc = eval(namespace) rescue :bzzzzzt
196
+ unless nsc.is_a?(Class)
197
+ Object.module_eval "class #{namespace} < #{self}; end"
198
+ end
199
+ end
200
+ Object.module_eval "class #{class_name} < #{namespace}; end"
201
+ eval(class_name)
202
+ end
203
+
204
+
205
+ def get_response_data_type
206
+ self.class.get_response_data_type(self.response)
103
207
  end
104
208
 
105
- ## Populates +attributes+, +links+, and +objects+ from the contents of
106
- ## +response+. Sets +loaded = true+.
107
- def init_from_response_body!
108
- return unless self.response_body
109
- self.objects. init_from_hal(self.response_body);
110
- self.links. init_from_hal(self.response_body);
111
- self.attributes.init_from_hal(self.response_body);
112
- self.loaded = true
113
- self
209
+ ## Inspects the given response, and returns a string describing this
210
+ ## resource's data type.
211
+ ##
212
+ ## By default, this method looks for a +type=...+ modifier in the
213
+ ## response's +Content-type+ and returns that value, capitalized.
214
+ ##
215
+ ## Override this method in a subclass to alter HyperResource's behavior.
216
+
217
+ def self.get_response_data_type(response)
218
+ return nil unless response
219
+ return nil unless content_type = response['content-type']
220
+ return nil unless m=content_type.match(/;\s* type=(?<type> [0-9A-Za-z:]+)/x)
221
+ m[:type][0].upcase + m[:type][1..-1]
114
222
  end
115
223
 
224
+
225
+
116
226
  ## Returns the first object in the first collection of objects embedded
117
227
  ## in this resource. Equivalent to +self.objects.first+.
118
228
  def first; self.objects.first end
@@ -122,27 +232,20 @@ public
122
232
  def [](i); self.objects.ith(i) end
123
233
 
124
234
  ## method_missing will load this resource if not yet loaded, then
125
- ## attempt to delegate to +attributes+, then +objects+,
126
- ## then +links+. When it finds a match, it will define a method class-wide
127
- ## if self.class != HyperResource, instance-wide otherwise.
235
+ ## attempt to delegate to +attributes+, then +objects+, then +links+.
128
236
  def method_missing(method, *args)
129
237
  self.get unless self.loaded
130
238
 
131
- [:attributes, :objects, :links].each do |field|
132
- if self.send(field).respond_to?(method)
133
- if self.class == HyperResource
134
- define_singleton_method(method) do |*args|
135
- self.send(field).send(method, *args)
136
- end
137
- else
138
- self.class.send(:define_method, method) do |*args|
139
- self.send(field).send(method, *args)
140
- end
141
- end
142
- return self.send(field).send(method, *args)
143
- end
239
+ method = method.to_s
240
+ if method[-1] == '='
241
+ return attributes[method[0..-2]] = args.first if attributes[method[0..-2]]
242
+ else
243
+ return attributes[method] if attributes && attributes[method]
244
+ return objects[method] if objects && objects[method]
245
+ return links[method] if links && links[method]
144
246
  end
145
- super
247
+
248
+ raise NoMethodError, "undefined method `#{method}' for #{self.inspect}"
146
249
  end
147
250
 
148
251
 
@@ -152,4 +255,7 @@ public
152
255
  "@namespace=#{self.namespace.inspect} ...>"
153
256
  end
154
257
 
258
+ ## +response_body+ is deprecated in favor of +response_object+.
259
+ def response_body; response_object end
260
+
155
261
  end
@@ -0,0 +1,26 @@
1
+ class HyperResource
2
+ class Adapter
3
+ class << self
4
+
5
+ ## Serialize the object into a string.
6
+ def serialize(object)
7
+ raise NotImplementedError, "This is an abstract method -- subclasses "+
8
+ "of HyperResource::Adapter must implement it."
9
+ end
10
+
11
+ ## Deserialize a string into an object.
12
+ def deserialize(string)
13
+ raise NotImplementedError, "This is an abstract method -- subclasses "+
14
+ "of HyperResource::Adapter must implement it."
15
+ end
16
+
17
+ ## Apply a response object (generally parsed JSON or XML) to the given
18
+ ## HyperResource object. Returns the updated resource.
19
+ def apply(object, resource, opts={})
20
+ raise NotImplementedError, "This is an abstract method -- subclasses "+
21
+ "of HyperResource::Adapter must implement it."
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,97 @@
1
+ require 'json'
2
+
3
+ class HyperResource
4
+ class Adapter
5
+ class HAL_JSON < Adapter
6
+ class << self
7
+
8
+ def serialize(object)
9
+ JSON.dump(object)
10
+ end
11
+
12
+ def deserialize(string)
13
+ JSON.parse(string)
14
+ end
15
+
16
+ def apply(response, resource, opts={})
17
+ if !response.kind_of?(Hash)
18
+ raise ArgumentError, "'response' argument must be a Hash"
19
+ end
20
+ if !resource.kind_of?(HyperResource)
21
+ raise ArgumentError, "'resource' argument must be a HyperResource"
22
+ end
23
+
24
+ apply_objects(response, resource)
25
+ apply_links(response, resource)
26
+ apply_attributes(response, resource)
27
+ resource.loaded = true
28
+ resource.href = response['_links']['self']['href'] rescue nil
29
+ resource
30
+ end
31
+
32
+
33
+ private
34
+
35
+ def apply_objects(resp, rsrc)
36
+ return unless resp['_embedded']
37
+ rc = rsrc.class
38
+ rsrc.objects = rc::Objects.new(rsrc)
39
+ objs = rsrc.objects
40
+
41
+ resp['_embedded'].each do |name, collection|
42
+ if collection.is_a? Hash
43
+ r = rc.new
44
+ r.response_object = collection
45
+ objs[name] = apply(collection, r)
46
+ else
47
+ objs[name] = collection.map do |obj|
48
+ r = rc.new
49
+ r.response_object = obj
50
+ apply(obj, r)
51
+ end
52
+ end
53
+ end
54
+
55
+ objs.create_methods!
56
+ end
57
+
58
+
59
+ def apply_links(resp, rsrc)
60
+ return unless resp['_links']
61
+ rsrc.links = rsrc.get_response_class::Links.new(rsrc)
62
+ links = rsrc.links
63
+
64
+ resp['_links'].each do |rel, link_spec|
65
+ if link_spec.is_a? Array
66
+ links[rel] = link_spec.map do |link|
67
+ new_link_from_spec(rsrc, link)
68
+ end
69
+ else
70
+ links[rel] = new_link_from_spec(rsrc, link_spec)
71
+ end
72
+ end
73
+
74
+ links.create_methods!
75
+ end
76
+
77
+ def new_link_from_spec(resource, link_spec) # :nodoc:
78
+ resource.class::Link.new(resource, link_spec)
79
+ end
80
+
81
+
82
+ def apply_attributes(resp, rsrc)
83
+ rsrc.attributes = rsrc.get_response_class::Attributes.new(rsrc)
84
+ attrs = rsrc.attributes
85
+
86
+ (resp.keys - ['_links', '_embedded']).map(&:to_s).each do |attr|
87
+ attrs[attr] = resp[attr]
88
+ end
89
+
90
+ attrs.create_methods!
91
+ end
92
+
93
+ end
94
+ end
95
+ end
96
+ end
97
+
@@ -1,27 +1,85 @@
1
- class HyperResource::Attributes < Hash
2
- attr_accessor :parent_resource
1
+ class HyperResource
2
+ class Attributes < Hash
3
3
 
4
- def initialize(resource=nil)
5
- self.parent_resource = resource || HyperResource.new
6
- end
4
+ attr_accessor :_resource # :nodoc:
7
5
 
8
- ## Initialize attributes from a HAL response.
9
- def init_from_hal(hal_resp)
10
- (hal_resp.keys - ['_links', '_embedded']).map(&:to_s).each do |attr|
11
- self[attr] = hal_resp[attr]
6
+ def initialize(resource=nil) # :nodoc:
7
+ self._resource = resource || HyperResource.new
8
+ end
12
9
 
13
- unless self.respond_to?(attr.to_sym)
14
- define_singleton_method(attr.to_sym) { self[attr] }
15
- define_singleton_method("#{attr}=".to_sym) {|v| self[attr]=v}
16
- end
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 create_methods!(opts={})
14
+ return if self.class.to_s == 'HyperResource::Attributes' ||
15
+ self._resource.class.to_s == 'HyperResource'
16
+
17
+ self.keys.each do |attr|
18
+ attr_sym = attr.to_sym
19
+ attr_eq_sym = "#{attr}=".to_sym
20
+
21
+ self.class.send(:define_method, attr_sym) do
22
+ self[attr]
23
+ end
24
+ self.class.send(:define_method, attr_eq_sym) do |val|
25
+ self[attr] = val
26
+ end
17
27
 
18
- unless self.parent_resource.respond_to?(attr.to_sym)
19
- self.parent_resource.define_singleton_method(attr.to_sym) {
20
- self.attributes[attr]}
21
- self.parent_resource.define_singleton_method("#{attr}=".to_sym) do |v|
22
- self.attributes[attr] = v
28
+ ## Don't stomp on _resource's methods
29
+ unless _resource.respond_to?(attr_sym)
30
+ _resource.class.send(:define_method, attr_sym) do
31
+ attributes.send(attr_sym)
32
+ end
33
+ end
34
+ unless _resource.respond_to?(attr_eq_sym)
35
+ _resource.class.send(:define_method, attr_eq_sym) do |val|
36
+ attributes.send(attr_eq_sym, val)
37
+ end
23
38
  end
24
39
  end
40
+
41
+ ## This is a good time to mark this object as not-changed
42
+ _hr_clear_changed
43
+ end
44
+
45
+ ## Returns +true+ if the given attribute has been changed since creation
46
+ ## time, +false+ otherwise.
47
+ ## If no attribute is given, return whether any attributes have been
48
+ ## changed.
49
+ def changed?(attr=nil)
50
+ @_hr_changed ||= Hash.new(false)
51
+ return @_hr_changed[attr.to_sym] if attr
52
+ return @_hr_changed.keys.count > 0
53
+ end
54
+
55
+ def []=(attr, value) # :nodoc:
56
+ _hr_mark_changed(attr)
57
+ super(attr.to_s, value)
58
+ end
59
+
60
+ def [](key) # :nodoc:
61
+ return super(key.to_s) if self.has_key?(key.to_s)
62
+ return super(key.to_sym) if self.has_key?(key.to_sym)
63
+ nil
64
+ end
65
+
66
+ def method_missing(method, *args) # :nodoc:
67
+ return self[method] if self[method]
68
+ raise NoMethodError, "undefined method `#{method}' for #{self.inspect}"
69
+ end
70
+
71
+ private
72
+
73
+ def _hr_mark_changed(attr, is_changed=true) # :nodoc:
74
+ attr = attr.to_sym
75
+ @_hr_changed ||= Hash.new(false)
76
+ @_hr_changed[attr] = is_changed
77
+ end
78
+
79
+ def _hr_clear_changed
80
+ @_hr_changed = nil
25
81
  end
82
+
26
83
  end
27
84
  end
85
+
@@ -1,9 +1,12 @@
1
1
  class HyperResource::Exception < Exception
2
2
  end
3
3
 
4
- class HyperResource::ClientError < HyperResource::Exception
4
+ class HyperResource::ResponseError < HyperResource::Exception
5
5
  end
6
6
 
7
- class HyperResource::ServerError < HyperResource::Exception
7
+ class HyperResource::ClientError < HyperResource::ResponseError
8
+ end
9
+
10
+ class HyperResource::ServerError < HyperResource::ResponseError
8
11
  end
9
12
 
@@ -2,6 +2,7 @@ require 'uri_template'
2
2
 
3
3
  class HyperResource::Link
4
4
  attr_accessor :base_href,
5
+ :name,
5
6
  :templated,
6
7
  :params,
7
8
  :parent_resource
@@ -11,6 +12,7 @@ class HyperResource::Link
11
12
  def initialize(resource=nil, link_spec={})
12
13
  self.parent_resource = resource || HyperResource.new
13
14
  self.base_href = link_spec['href']
15
+ self.name = link_spec['name']
14
16
  self.templated = !!link_spec['templated']
15
17
  self.params = link_spec['params'] || {}
16
18
  end
@@ -29,13 +31,14 @@ class HyperResource::Link
29
31
  def where(params)
30
32
  self.class.new(self.parent_resource,
31
33
  'href' => self.base_href,
34
+ 'name' => self.name,
32
35
  'templated' => self.templated,
33
36
  'params' => self.params.merge(params))
34
37
  end
35
38
 
36
39
  ## Returns a HyperResource representing this link
37
40
  def resource
38
- parent_resource.new_from_link(self.href)
41
+ parent_resource._new_from_link(self.href)
39
42
  end
40
43
 
41
44
  ## Returns a HyperResource representing this link, and fetches it.
@@ -1,35 +1,53 @@
1
1
  class HyperResource
2
2
  class Links < Hash
3
- attr_accessor :resource
3
+ attr_accessor :_resource
4
4
 
5
5
  def initialize(resource=nil)
6
- self.resource = resource || HyperResource.new
6
+ self._resource = resource || HyperResource.new
7
7
  end
8
8
 
9
- # Initialize links from a HAL response.
10
- def init_from_hal(hal_resp)
11
- return unless hal_resp['_links']
12
- hal_resp['_links'].each do |rel, link_spec|
13
- self[rel] = new_link_from_spec(link_spec)
14
- create_methods_for_link_rel(rel) unless self.respond_to?(rel.to_sym)
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 create_methods!(opts={})
13
+ return if self.class.to_s == 'HyperResource::Links' ||
14
+ self._resource.class.to_s == 'HyperResource'
15
+
16
+ self.keys.each do |attr|
17
+ attr_sym = attr.to_sym
18
+
19
+ self.class.send(:define_method, attr_sym) do |*args|
20
+ if args.count > 0
21
+ self[attr].where(*args)
22
+ else
23
+ self[attr]
24
+ end
25
+ end
26
+
27
+ ## Don't stomp on _resource's methods
28
+ unless _resource.respond_to?(attr_sym)
29
+ _resource.class.send(:define_method, attr_sym) do |*args|
30
+ links.send(attr_sym, *args)
31
+ end
32
+ end
15
33
  end
16
34
  end
17
35
 
18
- protected
36
+ def []=(attr, value) # :nodoc:
37
+ super(attr.to_s, value)
38
+ end
19
39
 
20
- def new_link_from_spec(link_spec) # :nodoc:
21
- HyperResource::Link.new(resource, link_spec)
40
+ def [](key) # :nodoc:
41
+ return super(key.to_s) if self.has_key?(key.to_s)
42
+ return super(key.to_sym) if self.has_key?(key.to_sym)
43
+ nil
22
44
  end
23
45
 
24
- def create_methods_for_link_rel(rel) # :nodoc:
25
- link = self[rel]
26
- define_singleton_method(rel.to_sym) do |*args|
27
- return link if args.empty?
28
- link.where(*args)
29
- end
46
+ def method_missing(method, *args) # :nodoc:
47
+ return self[method] if self[method]
48
+ raise NoMethodError, "undefined method `#{method}' for #{self.inspect}"
30
49
  end
31
50
 
32
51
  end
33
52
  end
34
53
 
35
-
@@ -30,11 +30,13 @@ module HyperResource::Modules::HTTP
30
30
  private
31
31
 
32
32
  def finish_up
33
+ self.loaded = true
34
+ self.response_object = self.adapter.deserialize(self.response.body)
35
+ self.adapter.apply(self.response_object, self)
36
+
33
37
  status = self.response.status
34
38
  if status / 100 == 2
35
- self.response_body = JSON.parse(self.response.body)
36
- self.init_from_response_body!
37
- self.blessed
39
+ return self.to_response_class
38
40
  elsif status / 100 == 3
39
41
  ## TODO redirect logic?
40
42
  elsif status / 100 == 4
@@ -42,7 +44,7 @@ private
42
44
  elsif status / 100 == 5
43
45
  raise HyperResource::ServerError, status.to_s
44
46
  else ## 1xx? really?
45
- raise HyperResource::Exception, "Got status #{status}, wtf?"
47
+ raise HyperResource::ResponseError, "Got status #{status}, wtf?"
46
48
  end
47
49
  end
48
50
 
@@ -23,6 +23,22 @@ module HyperResource::Modules
23
23
  end
24
24
  end
25
25
 
26
+ ## Instance attributes which fall back to class attributes.
27
+ def fallback_attribute(*names)
28
+ names.map(&:to_sym).each do |name|
29
+ class_eval <<-EOT
30
+ def #{name}=(val)
31
+ @#{name} = val
32
+ end
33
+ def #{name}
34
+ return @#{name} if defined?(@#{name})
35
+ return self.class.#{name} if self.class.respond_to?(:#{name})
36
+ nil
37
+ end
38
+ EOT
39
+ end
40
+ end
41
+
26
42
  end # module ClassMethods
27
43
 
28
44
  end
@@ -1,33 +1,59 @@
1
- class HyperResource::Objects < Hash
2
- attr_accessor :parent_resource
3
- def initialize(parent_resource=nil)
4
- self.parent_resource = parent_resource || HyperResource.new
5
- end
6
- def init_from_hal(hal_resp)
7
- return unless hal_resp['_embedded']
8
- hal_resp['_embedded'].each do |name, collection|
9
- self[name] = collection.map do |obj|
10
- self.parent_resource.new_from_hal(obj)
11
- end
12
- unless self.respond_to?(name.to_sym)
13
- define_singleton_method(name.to_sym) { self[name] }
14
- end
15
- unless self.parent_resource.respond_to?(name.to_sym)
16
- self.parent_resource.define_singleton_method(name.to_sym) do
17
- self.objects[name]
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 create_methods!(opts={})
13
+ return if self.class.to_s == 'HyperResource::Objects' ||
14
+ self._resource.class.to_s == 'HyperResource'
15
+
16
+ self.keys.each do |attr|
17
+ attr_sym = attr.to_sym
18
+
19
+ self.class.send(:define_method, attr_sym) do
20
+ self[attr]
21
+ end
22
+
23
+ ## Don't stomp on _resource's methods
24
+ unless _resource.respond_to?(attr_sym)
25
+ _resource.class.send(:define_method, attr_sym) do
26
+ objects.send(attr_sym)
27
+ end
18
28
  end
19
29
  end
20
30
  end
21
- end
22
31
 
23
- ## Returns the first item in the first collection in +self+.
24
- alias_method :first_orig, :first
25
- def first
26
- self.first_orig[1][0] rescue caller
27
- end
32
+ ## Returns the first item in the first collection in +self+.
33
+ alias_method :first_orig, :first
34
+ def first
35
+ self.first_orig[1][0] rescue caller
36
+ end
37
+
38
+ ## Returns the ith item in the first collection in +self+.
39
+ def ith(i)
40
+ self.first_orig[1][i] rescue caller
41
+ end
42
+
43
+ def []=(attr, value) # :nodoc:
44
+ super(attr.to_s, value)
45
+ end
46
+
47
+ def [](key) # :nodoc:
48
+ return super(key.to_s) if self.has_key?(key.to_s)
49
+ return super(key.to_sym) if self.has_key?(key.to_sym)
50
+ nil
51
+ end
52
+
53
+ def method_missing(method, *args) # :nodoc:
54
+ return self[method] if self[method]
55
+ raise NoMethodError, "undefined method `#{method}' for #{self.inspect}"
56
+ end
28
57
 
29
- ## Returns the ith item in the first collection in +self+.
30
- def ith(i)
31
- self.first_orig[1][i] rescue caller
32
58
  end
33
59
  end
@@ -1,4 +1,4 @@
1
1
  class HyperResource
2
- VERSION = '0.1.3'
3
- VERSION_DATE = '2013-04-22'
2
+ VERSION = '0.1.9'
3
+ VERSION_DATE = '2013-09-27'
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperresource
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pete Gamache
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-04-22 00:00:00.000000000 Z
11
+ date: 2013-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uri_template
@@ -90,18 +90,19 @@ executables: []
90
90
  extensions: []
91
91
  extra_rdoc_files: []
92
92
  files:
93
- - lib/hyperresource.rb
94
- - lib/hyper_resource.rb
93
+ - lib/hyper_resource/adapter/hal_json.rb
94
+ - lib/hyper_resource/adapter.rb
95
+ - lib/hyper_resource/attributes.rb
96
+ - lib/hyper_resource/exceptions.rb
95
97
  - lib/hyper_resource/link.rb
96
98
  - lib/hyper_resource/links.rb
97
- - lib/hyper_resource/exceptions.rb
98
- - lib/hyper_resource/version.rb
99
- - lib/hyper_resource/objects.rb
100
- - lib/hyper_resource/attributes.rb
101
- - lib/hyper_resource/modules/utils.rb
102
99
  - lib/hyper_resource/modules/http.rb
103
- - lib/hyper_resource/modules/bless.rb
100
+ - lib/hyper_resource/modules/utils.rb
101
+ - lib/hyper_resource/objects.rb
104
102
  - lib/hyper_resource/response.rb
103
+ - lib/hyper_resource/version.rb
104
+ - lib/hyper_resource.rb
105
+ - lib/hyperresource.rb
105
106
  homepage: https://github.com/gamache/hyperresource
106
107
  licenses:
107
108
  - MIT
@@ -127,3 +128,4 @@ signing_key:
127
128
  specification_version: 4
128
129
  summary: Extensible hypermedia client for Ruby
129
130
  test_files: []
131
+ has_rdoc: true
@@ -1,57 +0,0 @@
1
- module HyperResource::Modules::Bless
2
-
3
- ## Returns this resource as an instance of +self.resource_class+.
4
- ## The returned object will share structure with the source object;
5
- ## beware.
6
- def blessed
7
- return self unless self.namespace
8
- self.resource_class.new(self)
9
- end
10
-
11
- ## Returns the class into which this resource should be cast.
12
- ## If the object is not loaded yet, or if +self.namespace+ is
13
- ## not set, returns +self.class+.
14
- ##
15
- ## Otherwise, +resource_class+ looks at the returned content-type, and
16
- ## attempts to match a 'type=...' modifier. Given a namespace of
17
- ## +FooAPI+ and a response content-type of
18
- ## "application/vnd.foocorp.fooapi.v1+json;type=User", this should
19
- ## return +FooAPI::User+ (even if +FooAPI::User+ hadn't existed yet).
20
- def resource_class
21
- return self.class unless self.namespace
22
- return self.class unless type_name = self.data_type_name
23
- class_name = "#{self.namespace}::#{type_name}".
24
- gsub(/[^_0-9A-Za-z:]/, '')
25
-
26
- ## Return data type class if it exists
27
- klass = eval(class_name) rescue :sorry_dude
28
- return klass if klass.is_a?(Class)
29
-
30
- ## Data type class didn't exist -- create namespace (if necessary),
31
- ## then the data type class
32
- if self.namespace != ''
33
- nsc = eval(self.namespace) rescue :bzzzzzt
34
- unless nsc.is_a?(Class)
35
- Object.module_eval "class #{self.namespace} < #{self.class}; end"
36
- end
37
- end
38
- Object.module_eval "class #{class_name} < #{self.namespace}; end"
39
- eval(class_name)
40
- end
41
-
42
- ## Inspects the response, and returns a string describing this
43
- ## resource's data type.
44
- ##
45
- ## By default, this method looks for a +type=...+ modifier in the
46
- ## response's +Content-type+. Override this method in a
47
- ## HyperResource subclass in order to implement different data type
48
- ## detection.
49
- def data_type_name
50
- return nil unless self.response
51
- return nil unless content_type = self.response['content-type']
52
- return nil unless m=content_type.match(/;\s* type=(?<type> [0-9A-Za-z:]+)/x)
53
- m[:type][0].upcase + m[:type][1..-1]
54
- end
55
-
56
-
57
- end