hyperresource_zuhrig 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.
- checksums.yaml +7 -0
- data/lib/hyper_resource.rb +241 -0
- data/lib/hyper_resource/adapter.rb +31 -0
- data/lib/hyper_resource/adapter/hal_json.rb +92 -0
- data/lib/hyper_resource/attributes.rb +74 -0
- data/lib/hyper_resource/configuration.rb +159 -0
- data/lib/hyper_resource/exceptions.rb +42 -0
- data/lib/hyper_resource/link.rb +100 -0
- data/lib/hyper_resource/links.rb +69 -0
- data/lib/hyper_resource/modules/config_attributes.rb +293 -0
- data/lib/hyper_resource/modules/data_type.rb +98 -0
- data/lib/hyper_resource/modules/deprecations.rb +56 -0
- data/lib/hyper_resource/modules/http.rb +240 -0
- data/lib/hyper_resource/modules/internal_attributes.rb +79 -0
- data/lib/hyper_resource/objects.rb +41 -0
- data/lib/hyper_resource/version.rb +5 -0
- data/lib/hyperresource.rb +2 -0
- metadata +177 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 04783c914f04b3bd5312d7a58a7f59d9b171fb1c
|
4
|
+
data.tar.gz: 0efdff138134a4e51ab8b75b01a7cdadf730bddb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c069c3a59156831c56987c5075d3bac00ecb0bb7e135d47dc2749f27fec1469226eb7b65346ce1561ed20c4f598535aeda7a8c15bd16137d41eee20098f804fc
|
7
|
+
data.tar.gz: b06dd416f2e835e7f53d921b2e644a90ea589ecec87aab44d4ef5dcd35f17b8ba9a1e0097f13f0b9a7cd58754067b62d69560d1a6d65141f27a8cca61bb16b36
|
@@ -0,0 +1,241 @@
|
|
1
|
+
require 'hyper_resource/attributes'
|
2
|
+
require 'hyper_resource/configuration'
|
3
|
+
require 'hyper_resource/exceptions'
|
4
|
+
require 'hyper_resource/link'
|
5
|
+
require 'hyper_resource/links'
|
6
|
+
require 'hyper_resource/objects'
|
7
|
+
require 'hyper_resource/version'
|
8
|
+
|
9
|
+
require 'hyper_resource/adapter'
|
10
|
+
require 'hyper_resource/adapter/hal_json'
|
11
|
+
|
12
|
+
require 'hyper_resource/modules/data_type'
|
13
|
+
require 'hyper_resource/modules/deprecations'
|
14
|
+
require 'hyper_resource/modules/http'
|
15
|
+
require 'hyper_resource/modules/config_attributes'
|
16
|
+
require 'hyper_resource/modules/internal_attributes'
|
17
|
+
|
18
|
+
require 'rubygems' if RUBY_VERSION[0..2] == '1.8'
|
19
|
+
|
20
|
+
require 'pp'
|
21
|
+
|
22
|
+
## HyperResource is the main resource base class. Normally it will be used
|
23
|
+
## through subclassing, though it may also be used directly.
|
24
|
+
|
25
|
+
class HyperResource
|
26
|
+
|
27
|
+
include HyperResource::Modules::ConfigAttributes
|
28
|
+
include HyperResource::Modules::DataType
|
29
|
+
include HyperResource::Modules::Deprecations
|
30
|
+
include HyperResource::Modules::InternalAttributes
|
31
|
+
include Enumerable
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
DEFAULT_HEADERS = { 'Accept' => 'application/json' }
|
36
|
+
|
37
|
+
public
|
38
|
+
|
39
|
+
## Create a new HyperResource, given a hash of options. These options
|
40
|
+
## include:
|
41
|
+
##
|
42
|
+
## [root] The root URL of the resource.
|
43
|
+
##
|
44
|
+
## [auth] Authentication information. Currently only +{basic:
|
45
|
+
## ['key', 'secret']}+ is supported.
|
46
|
+
##
|
47
|
+
## [namespace] Class or class name, into which resources should be
|
48
|
+
## instantiated.
|
49
|
+
##
|
50
|
+
## [headers] Headers to send along with requests for this resource (as
|
51
|
+
## well as its eventual child resources, if any).
|
52
|
+
##
|
53
|
+
## [faraday_options] Configuration passed to +Faraday::Connection.initialize+,
|
54
|
+
## such as +{request: {timeout: 30}}+.
|
55
|
+
##
|
56
|
+
def initialize(opts={})
|
57
|
+
self.root = opts[:root] if opts[:root]
|
58
|
+
self.href = opts[:href] if opts[:href]
|
59
|
+
|
60
|
+
self.hr_config = self.class.hr_config.clone
|
61
|
+
|
62
|
+
self.adapter = opts[:adapter] if opts[:adapter]
|
63
|
+
self.faraday_options = opts[:faraday_options] if opts[:faraday_options]
|
64
|
+
self.auth = opts[:auth] if opts[:auth]
|
65
|
+
|
66
|
+
self.headers = DEFAULT_HEADERS.
|
67
|
+
merge(self.class.headers || {}).
|
68
|
+
merge(opts[:headers] || {})
|
69
|
+
|
70
|
+
self.namespace = opts[:namespace] if opts[:namespace]
|
71
|
+
if !self.namespace && self.class != HyperResource
|
72
|
+
self.namespace = self.class.namespace || self.class.to_s
|
73
|
+
end
|
74
|
+
|
75
|
+
## There's a little acrobatics in getting Attributes, Links, and Objects
|
76
|
+
## into the correct subclass.
|
77
|
+
if self.class != HyperResource
|
78
|
+
if self.class::Attributes == HyperResource::Attributes
|
79
|
+
Object.module_eval(
|
80
|
+
"class #{self.class}::Attributes < HyperResource::Attributes; end"
|
81
|
+
)
|
82
|
+
end
|
83
|
+
if self.class::Links == HyperResource::Links
|
84
|
+
Object.module_eval(
|
85
|
+
"class #{self.class}::Links < HyperResource::Links; end"
|
86
|
+
)
|
87
|
+
end
|
88
|
+
if self.class::Objects == HyperResource::Objects
|
89
|
+
Object.module_eval(
|
90
|
+
"class #{self.class}::Objects < HyperResource::Objects; end"
|
91
|
+
)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
self.attributes = self.class::Attributes.new(self)
|
96
|
+
self.links = self.class::Links.new(self)
|
97
|
+
self.objects = self.class::Objects.new(self)
|
98
|
+
self.loaded = false
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
## Creates a new resource given args :link, :resource, :href, :response, :url,
|
104
|
+
## and :body. Either :link or (:resource and :href and :url) are required.
|
105
|
+
# @private
|
106
|
+
def self.new_from(args)
|
107
|
+
link = args[:link]
|
108
|
+
resource = args[:resource] || link.resource
|
109
|
+
href = args[:href] || link.href
|
110
|
+
url = args[:url] || URI.join(resource.root, href || '')
|
111
|
+
response = args[:response]
|
112
|
+
body = args[:body] || {}
|
113
|
+
|
114
|
+
old_rsrc = resource
|
115
|
+
new_class = old_rsrc.get_data_type_class(:resource => old_rsrc,
|
116
|
+
:link => link,
|
117
|
+
:url => url,
|
118
|
+
:response => response,
|
119
|
+
:body => body)
|
120
|
+
new_rsrc = new_class.new(:root => old_rsrc.root,
|
121
|
+
:href => href)
|
122
|
+
new_rsrc.hr_config = old_rsrc.hr_config.clone
|
123
|
+
new_rsrc.response = response
|
124
|
+
new_rsrc.body = body
|
125
|
+
new_rsrc.adapter.apply(body, new_rsrc)
|
126
|
+
new_rsrc.loaded = true
|
127
|
+
new_rsrc
|
128
|
+
end
|
129
|
+
|
130
|
+
# @private
|
131
|
+
def new_from(args)
|
132
|
+
self.class.new_from(args)
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
## Returns true if one or more of this object's attributes has been
|
137
|
+
## reassigned.
|
138
|
+
def changed?(*args)
|
139
|
+
attributes.changed?(*args)
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
#### Filters
|
144
|
+
|
145
|
+
## +incoming_body_filter+ filters a hash of attribute keys and values
|
146
|
+
## on their way from a response body to a HyperResource. Override this
|
147
|
+
## in a subclass of HyperResource to implement filters on incoming data.
|
148
|
+
def incoming_body_filter(attr_hash)
|
149
|
+
attr_hash
|
150
|
+
end
|
151
|
+
|
152
|
+
## +outgoing_body_filter+ filters a hash of attribute keys and values
|
153
|
+
## on their way from a HyperResource to a request body. Override this
|
154
|
+
## in a subclass of HyperResource to implement filters on outgoing data.
|
155
|
+
def outgoing_body_filter(attr_hash)
|
156
|
+
attr_hash
|
157
|
+
end
|
158
|
+
|
159
|
+
## +outgoing_uri_filter+ filters a hash of attribute keys and values
|
160
|
+
## on their way from a HyperResource to a URL. Override this
|
161
|
+
## in a subclass of HyperResource to implement filters on outgoing URI
|
162
|
+
## parameters.
|
163
|
+
def outgoing_uri_filter(attr_hash)
|
164
|
+
attr_hash
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
#### Enumerable support
|
169
|
+
|
170
|
+
## Returns the *i*th object in the first collection of objects embedded
|
171
|
+
## in this resource. Returns nil on failure.
|
172
|
+
def [](i)
|
173
|
+
get unless loaded
|
174
|
+
self.objects.first[1][i] rescue nil
|
175
|
+
end
|
176
|
+
|
177
|
+
## Iterates over the objects in the first collection of embedded objects
|
178
|
+
## in this resource.
|
179
|
+
def each(&block)
|
180
|
+
get unless loaded
|
181
|
+
self.objects.first[1].each(&block) rescue nil
|
182
|
+
end
|
183
|
+
|
184
|
+
#### Magic
|
185
|
+
|
186
|
+
## method_missing will load this resource if not yet loaded, then
|
187
|
+
## attempt to delegate to +attributes+, then +objects+, then +links+.
|
188
|
+
## Override with extreme care.
|
189
|
+
def method_missing(method, *args)
|
190
|
+
## If not loaded, load and retry.
|
191
|
+
unless loaded
|
192
|
+
return self.get.send(method, *args)
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
## Otherwise, try to match against attributes, then objects, then links.
|
197
|
+
method = method.to_s
|
198
|
+
if method[-1,1] == '='
|
199
|
+
return attributes[method[0..-2]] = args.first if attributes[method[0..-2]]
|
200
|
+
else
|
201
|
+
return attributes[method] if attributes && attributes.has_key?(method.to_s)
|
202
|
+
return objects[method] if objects && objects[method]
|
203
|
+
if links && links[method]
|
204
|
+
if args.count > 0
|
205
|
+
return links[method].where(*args)
|
206
|
+
else
|
207
|
+
return links[method]
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
raise NoMethodError, "undefined method `#{method}' for #{self.inspect}"
|
213
|
+
end
|
214
|
+
|
215
|
+
## respond_to? is patched to return +true+ if +method_missing+ would
|
216
|
+
## successfully delegate a method call to +attributes+, +links+, or
|
217
|
+
## +objects+.
|
218
|
+
def respond_to?(method, *args)
|
219
|
+
return true if self.links && self.links.respond_to?(method)
|
220
|
+
return true if self.attributes && self.attributes.respond_to?(method)
|
221
|
+
return true if self.objects && self.objects.respond_to?(method)
|
222
|
+
super
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
def inspect # @private
|
227
|
+
"#<#{self.class}:0x#{"%x" % self.object_id} @root=#{self.root.inspect} "+
|
228
|
+
"@href=#{self.href.inspect} @loaded=#{self.loaded} "+
|
229
|
+
"@namespace=#{self.namespace.inspect} ...>"
|
230
|
+
end
|
231
|
+
|
232
|
+
def self.user_agent # @private
|
233
|
+
"HyperResource #{HyperResource::VERSION}"
|
234
|
+
end
|
235
|
+
|
236
|
+
def user_agent # @private
|
237
|
+
self.class.user_agent
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
|
@@ -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,92 @@
|
|
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 (got #{response.inspect})"
|
25
|
+
end
|
26
|
+
if !resource.kind_of?(HyperResource)
|
27
|
+
raise ArgumentError, "'resource' argument must be a HyperResource (got #{resource.inspect})"
|
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
|
+
objs = rsrc.objects
|
44
|
+
|
45
|
+
resp['_embedded'].each do |name, collection|
|
46
|
+
if collection.is_a? Hash
|
47
|
+
objs[name] =
|
48
|
+
rsrc.new_from(:resource => rsrc,
|
49
|
+
:body => collection,
|
50
|
+
:href => collection['_links']['self']['href'] )
|
51
|
+
else
|
52
|
+
objs[name] = collection.map do |obj|
|
53
|
+
rsrc.new_from(:resource => rsrc,
|
54
|
+
:body => obj,
|
55
|
+
:href => obj['_links']['self']['href'] )
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def apply_links(resp, rsrc)
|
63
|
+
return unless resp['links']
|
64
|
+
links = rsrc.links
|
65
|
+
|
66
|
+
resp['links'].each do | link |
|
67
|
+
rel = link['rel']
|
68
|
+
links[rel] = new_link_from_spec(rsrc, link)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def new_link_from_spec(resource, link_spec)
|
73
|
+
resource.class::Link.new(resource, link_spec)
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def apply_attributes(resp, rsrc)
|
78
|
+
given_attrs = resp.reject{|k,v| %w(_links _embedded).include?(k)}
|
79
|
+
filtered_attrs = rsrc.incoming_body_filter(given_attrs)
|
80
|
+
|
81
|
+
filtered_attrs.keys.each do |attr|
|
82
|
+
rsrc.attributes[attr] = filtered_attrs[attr]
|
83
|
+
end
|
84
|
+
|
85
|
+
rsrc.attributes._hr_clear_changed
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
class HyperResource
|
2
|
+
class Attributes < Hash
|
3
|
+
|
4
|
+
attr_accessor :_resource # @private
|
5
|
+
|
6
|
+
def initialize(resource=nil)
|
7
|
+
self._resource = resource || HyperResource.new
|
8
|
+
end
|
9
|
+
|
10
|
+
## Returns +true+ if the given attribute has been changed since creation
|
11
|
+
## time, +false+ otherwise.
|
12
|
+
## If no attribute is given, return whether any attributes have been
|
13
|
+
## changed.
|
14
|
+
def changed?(attr=nil)
|
15
|
+
@_hr_changed ||= Hash.new(false)
|
16
|
+
return @_hr_changed[attr.to_sym] if attr
|
17
|
+
return @_hr_changed.keys.count > 0
|
18
|
+
end
|
19
|
+
|
20
|
+
## Returns a hash of the attributes and values which have been changed
|
21
|
+
## since creation time.
|
22
|
+
def changed_attributes
|
23
|
+
@_hr_changed.select{|k,v| v}.keys.inject({}) {|h,k| h[k]=self[k]; h}
|
24
|
+
end
|
25
|
+
|
26
|
+
# @private
|
27
|
+
def []=(attr, value)
|
28
|
+
return self[attr] if self.has_key?(attr.to_s) && self[attr] == value
|
29
|
+
_hr_mark_changed(attr)
|
30
|
+
super(attr.to_s, value)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @private
|
34
|
+
def [](key)
|
35
|
+
return super(key.to_s) if self.has_key?(key.to_s)
|
36
|
+
return super(key.to_sym) if self.has_key?(key.to_sym)
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# @private
|
41
|
+
def method_missing(method, *args)
|
42
|
+
method = method.to_s
|
43
|
+
if self.has_key?(method)
|
44
|
+
self[method]
|
45
|
+
elsif method[-1,1] == '='
|
46
|
+
self[method[0..-2]] = args.first
|
47
|
+
else
|
48
|
+
raise NoMethodError, "undefined method `#{method}' for #{self.inspect}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @private
|
53
|
+
def respond_to?(method, *args)
|
54
|
+
method = method.to_s
|
55
|
+
return true if self.has_key?(method)
|
56
|
+
return true if method[-1,1] == '=' && self.has_key?(method[0..-2])
|
57
|
+
super
|
58
|
+
end
|
59
|
+
|
60
|
+
# @private
|
61
|
+
def _hr_clear_changed # @private
|
62
|
+
@_hr_changed = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
# @private
|
66
|
+
def _hr_mark_changed(attr, is_changed=true)
|
67
|
+
attr = attr.to_sym
|
68
|
+
@_hr_changed ||= Hash.new(false)
|
69
|
+
@_hr_changed[attr] = is_changed
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|