hyperresource 0.1.3 → 0.1.9

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.
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