api_resource 0.2.1
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.
- data/.document +5 -0
- data/.rspec +3 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +152 -0
- data/Guardfile +22 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/api_resource.gemspec +154 -0
- data/lib/api_resource.rb +129 -0
- data/lib/api_resource/association_activation.rb +19 -0
- data/lib/api_resource/associations.rb +169 -0
- data/lib/api_resource/associations/association_proxy.rb +115 -0
- data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +16 -0
- data/lib/api_resource/associations/dynamic_resource_scope.rb +23 -0
- data/lib/api_resource/associations/has_many_remote_object_proxy.rb +16 -0
- data/lib/api_resource/associations/has_one_remote_object_proxy.rb +24 -0
- data/lib/api_resource/associations/multi_argument_resource_scope.rb +15 -0
- data/lib/api_resource/associations/multi_object_proxy.rb +73 -0
- data/lib/api_resource/associations/related_object_hash.rb +12 -0
- data/lib/api_resource/associations/relation_scope.rb +30 -0
- data/lib/api_resource/associations/resource_scope.rb +34 -0
- data/lib/api_resource/associations/scope.rb +107 -0
- data/lib/api_resource/associations/single_object_proxy.rb +81 -0
- data/lib/api_resource/attributes.rb +162 -0
- data/lib/api_resource/base.rb +587 -0
- data/lib/api_resource/callbacks.rb +49 -0
- data/lib/api_resource/connection.rb +171 -0
- data/lib/api_resource/core_extensions.rb +7 -0
- data/lib/api_resource/custom_methods.rb +119 -0
- data/lib/api_resource/exceptions.rb +87 -0
- data/lib/api_resource/formats.rb +14 -0
- data/lib/api_resource/formats/json_format.rb +25 -0
- data/lib/api_resource/formats/xml_format.rb +36 -0
- data/lib/api_resource/local.rb +12 -0
- data/lib/api_resource/log_subscriber.rb +15 -0
- data/lib/api_resource/mocks.rb +269 -0
- data/lib/api_resource/model_errors.rb +86 -0
- data/lib/api_resource/observing.rb +29 -0
- data/lib/api_resource/railtie.rb +22 -0
- data/lib/api_resource/scopes.rb +45 -0
- data/spec/lib/associations_spec.rb +656 -0
- data/spec/lib/attributes_spec.rb +121 -0
- data/spec/lib/base_spec.rb +504 -0
- data/spec/lib/callbacks_spec.rb +68 -0
- data/spec/lib/connection_spec.rb +76 -0
- data/spec/lib/local_spec.rb +20 -0
- data/spec/lib/mocks_spec.rb +28 -0
- data/spec/lib/model_errors_spec.rb +29 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/support/mocks/association_mocks.rb +46 -0
- data/spec/support/mocks/error_resource_mocks.rb +21 -0
- data/spec/support/mocks/test_resource_mocks.rb +43 -0
- data/spec/support/requests/association_requests.rb +14 -0
- data/spec/support/requests/error_resource_requests.rb +25 -0
- data/spec/support/requests/test_resource_requests.rb +31 -0
- data/spec/support/test_resource.rb +64 -0
- metadata +334 -0
@@ -0,0 +1,587 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
require 'active_support/string_inquirer'
|
5
|
+
|
6
|
+
module ApiResource
|
7
|
+
|
8
|
+
class Base
|
9
|
+
|
10
|
+
class_inheritable_accessor :site, :proxy, :user, :password, :auth_type, :format, :timeout, :ssl_options, :token
|
11
|
+
|
12
|
+
class_inheritable_accessor :include_root_in_json; self.include_root_in_json = true
|
13
|
+
class_inheritable_accessor :include_blank_attributes_on_create; self.include_blank_attributes_on_create = false
|
14
|
+
class_inheritable_accessor :include_all_attributes_on_update; self.include_blank_attributes_on_create = false
|
15
|
+
|
16
|
+
class_inheritable_accessor :primary_key; self.primary_key = "id"
|
17
|
+
|
18
|
+
attr_accessor :prefix_options
|
19
|
+
|
20
|
+
class << self
|
21
|
+
|
22
|
+
# writers - accessors with defaults were not working
|
23
|
+
attr_writer :element_name, :collection_name
|
24
|
+
|
25
|
+
def inherited(klass)
|
26
|
+
# Call the methods of the superclass to make sure inheritable accessors and the like have been inherited
|
27
|
+
super
|
28
|
+
# Now we need to define the inherited method on the klass that's doing the inheriting
|
29
|
+
# it calls super which will allow the chaining effect we need
|
30
|
+
klass.instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
31
|
+
def inherited(klass)
|
32
|
+
klass.send(:define_singleton_method, :collection_name, lambda {self.superclass.collection_name})
|
33
|
+
super(klass)
|
34
|
+
end
|
35
|
+
EOE
|
36
|
+
true
|
37
|
+
end
|
38
|
+
# This makes a request to new_element_path
|
39
|
+
def set_class_attributes_upon_load
|
40
|
+
return true if self == ApiResource::Base
|
41
|
+
begin
|
42
|
+
class_data = self.connection.get(self.new_element_path, self.headers)
|
43
|
+
# Attributes go first
|
44
|
+
if class_data["attributes"]
|
45
|
+
define_attributes *(class_data["attributes"]["public"] || [])
|
46
|
+
define_protected_attributes *(class_data["attributes"]["protected"] || [])
|
47
|
+
end
|
48
|
+
# Then scopes
|
49
|
+
if class_data["scopes"]
|
50
|
+
class_data["scopes"].each_pair do |scope_name, opts|
|
51
|
+
self.scope(scope_name, opts)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
# Then associations
|
55
|
+
if class_data["associations"]
|
56
|
+
class_data["associations"].each_pair do |key, hash|
|
57
|
+
hash.each_pair do |assoc_name, assoc_options|
|
58
|
+
self.send(key, assoc_name, assoc_options)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
# Swallow up any loading errors because the site may be incorrect
|
63
|
+
rescue Exception => e
|
64
|
+
if ApiResource.raise_missing_definition_error
|
65
|
+
raise e
|
66
|
+
end
|
67
|
+
ApiResource.logger.warn("#{self} accessing #{self.new_element_path}")
|
68
|
+
ApiResource.logger.warn("#{self}: #{e.message[0..60].gsub(/[\n\r]/, '')} ...\n")
|
69
|
+
ApiResource.logger.debug(e.backtrace.pretty_inspect)
|
70
|
+
return e.respond_to?(:request) ? e.request : nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def reload_class_attributes
|
75
|
+
# clear the public_attribute_names, protected_attribute_names
|
76
|
+
remove_instance_variable(:@class_data) if instance_variable_defined?(:@class_data)
|
77
|
+
self.clear_attributes
|
78
|
+
self.clear_associations
|
79
|
+
self.set_class_attributes_upon_load
|
80
|
+
end
|
81
|
+
|
82
|
+
def token=(new_token)
|
83
|
+
self.write_inheritable_attribute(:token, new_token)
|
84
|
+
self.descendants.each do |child|
|
85
|
+
child.send(:token=, new_token)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def site=(site)
|
90
|
+
# store so we can reload attributes if the site changed
|
91
|
+
old_site = self.site.to_s.clone
|
92
|
+
@connection = nil
|
93
|
+
|
94
|
+
if site.nil?
|
95
|
+
write_inheritable_attribute(:site, nil)
|
96
|
+
# no site, so we'll skip the reload
|
97
|
+
return site
|
98
|
+
else
|
99
|
+
write_inheritable_attribute(:site, create_site_uri_from(site))
|
100
|
+
end
|
101
|
+
|
102
|
+
# reset class attributes and try to reload them if the site changed
|
103
|
+
unless self.site.to_s == old_site
|
104
|
+
self.reload_class_attributes
|
105
|
+
end
|
106
|
+
|
107
|
+
return site
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
def format=(mime_type_or_format)
|
112
|
+
format = mime_type_or_format.is_a?(Symbol) ? ApiResource::Formats[mime_type_or_format] : mime_type_or_format
|
113
|
+
write_inheritable_attribute(:format, format)
|
114
|
+
self.connection.format = format if self.site
|
115
|
+
end
|
116
|
+
|
117
|
+
# Default format is json
|
118
|
+
def format
|
119
|
+
read_inheritable_attribute(:format) || ApiResource::Formats::JsonFormat
|
120
|
+
end
|
121
|
+
|
122
|
+
def timeout=(timeout)
|
123
|
+
@connection = nil
|
124
|
+
write_inheritable_attribute(:timeout, timeout)
|
125
|
+
end
|
126
|
+
|
127
|
+
def connection(refresh = false)
|
128
|
+
@connection = Connection.new(self.site, self.format) if refresh || @connection.nil?
|
129
|
+
@connection.timeout = self.timeout
|
130
|
+
@connection
|
131
|
+
end
|
132
|
+
|
133
|
+
def reset_connection
|
134
|
+
remove_instance_variable(:@connection) if @connection.present?
|
135
|
+
end
|
136
|
+
|
137
|
+
def headers
|
138
|
+
{}.tap do |ret|
|
139
|
+
ret['Lifebooker-Token'] = self.token if self.token.present?
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def prefix(options = {})
|
144
|
+
default = (self.site ? self.site.path : '/')
|
145
|
+
default << '/' unless default[-1..-1] == '/'
|
146
|
+
self.prefix = default
|
147
|
+
prefix(options)
|
148
|
+
end
|
149
|
+
|
150
|
+
def prefix_source
|
151
|
+
prefix
|
152
|
+
prefix_source
|
153
|
+
end
|
154
|
+
|
155
|
+
def prefix=(value = '/')
|
156
|
+
prefix_call = value.gsub(/:\w+/) { |key| "\#{URI.escape options[#{key}].to_s}"}
|
157
|
+
@prefix_parameters = nil
|
158
|
+
silence_warnings do
|
159
|
+
instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
160
|
+
def prefix_source() "#{value}" end
|
161
|
+
def prefix(options={}) "#{prefix_call}" end
|
162
|
+
EOE
|
163
|
+
end
|
164
|
+
rescue Exception => e
|
165
|
+
logger.error "Couldn't set prefix: #{e}\n #{code}" if logger
|
166
|
+
raise
|
167
|
+
end
|
168
|
+
|
169
|
+
# element_name with default
|
170
|
+
def element_name
|
171
|
+
@element_name ||= self.model_name.element
|
172
|
+
end
|
173
|
+
# collection_name with default
|
174
|
+
def collection_name
|
175
|
+
@collection_name ||= ActiveSupport::Inflector.pluralize(self.element_name)
|
176
|
+
end
|
177
|
+
|
178
|
+
# alias_method :set_prefix, :prefix=
|
179
|
+
# alias_method :set_element_name, :element_name=
|
180
|
+
# alias_method :set_collection_name, :collection_name=
|
181
|
+
|
182
|
+
def element_path(id, prefix_options = {}, query_options = nil)
|
183
|
+
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
184
|
+
"#{prefix(prefix_options)}#{collection_name}/#{URI.escape id.to_s}.#{format.extension}#{query_string(query_options)}"
|
185
|
+
end
|
186
|
+
|
187
|
+
def new_element_path(prefix_options = {})
|
188
|
+
"#{prefix(prefix_options)}#{collection_name}/new.#{format.extension}"
|
189
|
+
end
|
190
|
+
|
191
|
+
def collection_path(prefix_options = {}, query_options = nil)
|
192
|
+
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
193
|
+
"#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
|
194
|
+
end
|
195
|
+
|
196
|
+
def build(attributes = {})
|
197
|
+
self.new(attributes)
|
198
|
+
end
|
199
|
+
|
200
|
+
def create(attributes = {})
|
201
|
+
self.new(attributes).tap{ |resource| resource.save }
|
202
|
+
end
|
203
|
+
|
204
|
+
def find(*arguments)
|
205
|
+
scope = arguments.slice!(0)
|
206
|
+
options = arguments.slice!(0) || {}
|
207
|
+
|
208
|
+
case scope
|
209
|
+
when :all then find_every(options)
|
210
|
+
when :first then find_every(options).first
|
211
|
+
when :last then find_every(options).last
|
212
|
+
when :one then find_one(options)
|
213
|
+
else find_single(scope, options)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass
|
219
|
+
# in all the same arguments to this method as you can to
|
220
|
+
# <tt>find(:first)</tt>.
|
221
|
+
def first(*args)
|
222
|
+
find(:first, *args)
|
223
|
+
end
|
224
|
+
|
225
|
+
# A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass
|
226
|
+
# in all the same arguments to this method as you can to
|
227
|
+
# <tt>find(:last)</tt>.
|
228
|
+
def last(*args)
|
229
|
+
find(:last, *args)
|
230
|
+
end
|
231
|
+
|
232
|
+
# This is an alias for find(:all). You can pass in all the same
|
233
|
+
# arguments to this method as you can to <tt>find(:all)</tt>
|
234
|
+
def all(*args)
|
235
|
+
find(:all, *args)
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
# Deletes the resources with the ID in the +id+ parameter.
|
240
|
+
#
|
241
|
+
# ==== Options
|
242
|
+
# All options specify \prefix and query parameters.
|
243
|
+
#
|
244
|
+
# ==== Examples
|
245
|
+
# Event.delete(2) # sends DELETE /events/2
|
246
|
+
#
|
247
|
+
# Event.create(:name => 'Free Concert', :location => 'Community Center')
|
248
|
+
# my_event = Event.find(:first) # let's assume this is event with ID 7
|
249
|
+
# Event.delete(my_event.id) # sends DELETE /events/7
|
250
|
+
#
|
251
|
+
# # Let's assume a request to events/5/cancel.xml
|
252
|
+
# Event.delete(params[:id]) # sends DELETE /events/5
|
253
|
+
def delete(id, options = {})
|
254
|
+
connection.delete(element_path(id, options))
|
255
|
+
end
|
256
|
+
|
257
|
+
protected
|
258
|
+
def method_missing(meth, *args, &block)
|
259
|
+
# make one attempt to load remote attrs
|
260
|
+
unless self.instance_variable_defined?(:@class_data)
|
261
|
+
self.set_class_attributes_upon_load
|
262
|
+
self.instance_variable_set(:@class_data, true)
|
263
|
+
return self.send(meth, *args, &block)
|
264
|
+
end
|
265
|
+
super
|
266
|
+
end
|
267
|
+
|
268
|
+
private
|
269
|
+
# Find every resource
|
270
|
+
def find_every(options)
|
271
|
+
begin
|
272
|
+
case from = options[:from]
|
273
|
+
when Symbol
|
274
|
+
instantiate_collection(get(from, options[:params]))
|
275
|
+
when String
|
276
|
+
path = "#{from}#{query_string(options[:params])}"
|
277
|
+
instantiate_collection(connection.get(path, headers) || [])
|
278
|
+
else
|
279
|
+
prefix_options, query_options = split_options(options[:params])
|
280
|
+
path = collection_path(prefix_options, query_options)
|
281
|
+
instantiate_collection( (connection.get(path, headers) || []), prefix_options )
|
282
|
+
end
|
283
|
+
rescue ApiResource::ResourceNotFound
|
284
|
+
# Swallowing ResourceNotFound exceptions and return nil - as per
|
285
|
+
# ActiveRecord.
|
286
|
+
nil
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Find a single resource from a one-off URL
|
291
|
+
def find_one(options)
|
292
|
+
case from = options[:from]
|
293
|
+
when Symbol
|
294
|
+
instantiate_record(get(from, options[:params]))
|
295
|
+
when String
|
296
|
+
path = "#{from}#{query_string(options[:params])}"
|
297
|
+
instantiate_record(connection.get(path, headers))
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Find a single resource from the default URL
|
302
|
+
def find_single(scope, options)
|
303
|
+
prefix_options, query_options = split_options(options[:params])
|
304
|
+
path = element_path(scope, prefix_options, query_options)
|
305
|
+
instantiate_record(connection.get(path, headers), prefix_options)
|
306
|
+
end
|
307
|
+
|
308
|
+
def instantiate_collection(collection, prefix_options = {})
|
309
|
+
collection.collect! { |record| instantiate_record(record, prefix_options) }
|
310
|
+
end
|
311
|
+
|
312
|
+
def instantiate_record(record, prefix_options = {})
|
313
|
+
new(record).tap do |resource|
|
314
|
+
resource.prefix_options = prefix_options
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
|
319
|
+
# Accepts a URI and creates the site URI from that.
|
320
|
+
def create_site_uri_from(site)
|
321
|
+
site.is_a?(URI) ? site.dup : uri_parser.parse(site)
|
322
|
+
end
|
323
|
+
|
324
|
+
# Accepts a URI and creates the proxy URI from that.
|
325
|
+
def create_proxy_uri_from(proxy)
|
326
|
+
proxy.is_a?(URI) ? proxy.dup : uri_parser.parse(proxy)
|
327
|
+
end
|
328
|
+
|
329
|
+
# contains a set of the current prefix parameters.
|
330
|
+
def prefix_parameters
|
331
|
+
@prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
|
332
|
+
end
|
333
|
+
|
334
|
+
# Builds the query string for the request.
|
335
|
+
def query_string(options)
|
336
|
+
"?#{options.to_query}" unless options.nil? || options.empty?
|
337
|
+
end
|
338
|
+
|
339
|
+
# split an option hash into two hashes, one containing the prefix options,
|
340
|
+
# and the other containing the leftovers.
|
341
|
+
def split_options(options = {})
|
342
|
+
prefix_options, query_options = {}, {}
|
343
|
+
(options || {}).each do |key, value|
|
344
|
+
next if key.blank?
|
345
|
+
(prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
|
346
|
+
end
|
347
|
+
|
348
|
+
[ prefix_options, query_options ]
|
349
|
+
end
|
350
|
+
|
351
|
+
def uri_parser
|
352
|
+
@uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
356
|
+
|
357
|
+
def initialize(attributes = {})
|
358
|
+
@prefix_options = {}
|
359
|
+
# if we initialize this class, load the attributes
|
360
|
+
unless self.class.instance_variable_defined?(:@class_data)
|
361
|
+
self.class.set_class_attributes_upon_load
|
362
|
+
self.class.instance_variable_set(:@class_data, true)
|
363
|
+
end
|
364
|
+
# Now we can make a call to setup the inheriting klass with its attributes
|
365
|
+
load(attributes)
|
366
|
+
end
|
367
|
+
|
368
|
+
def new?
|
369
|
+
id.blank?
|
370
|
+
end
|
371
|
+
alias :new_record? :new?
|
372
|
+
|
373
|
+
def persisted?
|
374
|
+
!new?
|
375
|
+
end
|
376
|
+
|
377
|
+
def id
|
378
|
+
self.attributes[self.class.primary_key]
|
379
|
+
end
|
380
|
+
|
381
|
+
# Bypass dirty tracking for this field
|
382
|
+
def id=(id)
|
383
|
+
attributes[self.class.primary_key] = id
|
384
|
+
end
|
385
|
+
|
386
|
+
def ==(other)
|
387
|
+
other.equal?(self) || (other.instance_of?(self.class) && other.id == self.id && other.prefix_options == self.prefix_options)
|
388
|
+
end
|
389
|
+
|
390
|
+
def eql?(other)
|
391
|
+
self == other
|
392
|
+
end
|
393
|
+
|
394
|
+
def hash
|
395
|
+
id.hash
|
396
|
+
end
|
397
|
+
|
398
|
+
def dup
|
399
|
+
self.class.new.tap do |resource|
|
400
|
+
resource.attributes = self.attributes
|
401
|
+
resource.prefix_options = @prefix_options
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def update_attributes(attrs)
|
406
|
+
self.attributes = attrs
|
407
|
+
self.save
|
408
|
+
end
|
409
|
+
|
410
|
+
def save(*args)
|
411
|
+
new? ? create(*args) : update(*args)
|
412
|
+
end
|
413
|
+
|
414
|
+
def save!(*args)
|
415
|
+
save(*args) || raise(ApiResource::ResourceInvalid.new(self))
|
416
|
+
end
|
417
|
+
|
418
|
+
def destroy
|
419
|
+
connection.delete(element_path(self.id), self.class.headers)
|
420
|
+
end
|
421
|
+
|
422
|
+
def encode(options = {})
|
423
|
+
self.send("to_#{self.class.format.extension}", options)
|
424
|
+
end
|
425
|
+
|
426
|
+
def reload
|
427
|
+
self.load(self.class.find(to_param, :params => @prefix_options).attributes)
|
428
|
+
end
|
429
|
+
|
430
|
+
def to_param
|
431
|
+
# Stolen from active_record.
|
432
|
+
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
|
433
|
+
id && id.to_s # Be sure to stringify the id for routes
|
434
|
+
end
|
435
|
+
|
436
|
+
def load(attributes)
|
437
|
+
return if attributes.nil?
|
438
|
+
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
|
439
|
+
@prefix_options, attributes = split_options(attributes)
|
440
|
+
|
441
|
+
attributes.symbolize_keys.each do |key, value|
|
442
|
+
# If this attribute doesn't exist define it as a protected attribute
|
443
|
+
self.class.define_protected_attributes(key) unless self.respond_to?(key)
|
444
|
+
self.attributes[key] =
|
445
|
+
case value
|
446
|
+
when Array
|
447
|
+
if self.has_many?(key)
|
448
|
+
MultiObjectProxy.new(self.has_many_class_name(key), value)
|
449
|
+
elsif self.association?(key)
|
450
|
+
raise ArgumentError, "Expected a hash value or nil, got: #{value.inspect}"
|
451
|
+
else
|
452
|
+
value.dup rescue value
|
453
|
+
end
|
454
|
+
when Hash
|
455
|
+
if self.has_many?(key)
|
456
|
+
MultiObjectProxy.new(self.has_many_class_name(key), value)
|
457
|
+
elsif self.association?(key)
|
458
|
+
SingleObjectProxy.new(self.association_class_name(key), value)
|
459
|
+
else
|
460
|
+
value.dup rescue value
|
461
|
+
end
|
462
|
+
when NilClass
|
463
|
+
# If it's nil and an association then create a blank object
|
464
|
+
if self.has_many?(key)
|
465
|
+
return MultiObjectProxy.new(self.has_many_class_name(key), [])
|
466
|
+
elsif self.association?(key)
|
467
|
+
SingleObjectProxy.new(self.association_class_name(key), value)
|
468
|
+
end
|
469
|
+
else
|
470
|
+
raise ArgumentError, "expected an array or a hash for the association #{key}, got: #{value.inspect}" if self.association?(key)
|
471
|
+
value.dup rescue value
|
472
|
+
end
|
473
|
+
end
|
474
|
+
return self
|
475
|
+
end
|
476
|
+
|
477
|
+
# Override to_s and inspect so they only show attributes
|
478
|
+
# and not associations, this prevents force loading of associations
|
479
|
+
# when we call to_s or inspect on a descendent of base but allows it if we
|
480
|
+
# try to evaluate an association directly
|
481
|
+
def to_s
|
482
|
+
return "#<#{self.class}:#{(self.object_id * 2).to_s(16)} @attributes=#{self.attributes.inject({}){|accum,(k,v)| self.association?(k) ? accum : accum.merge(k => v)}}"
|
483
|
+
end
|
484
|
+
|
485
|
+
alias_method :inspect, :to_s
|
486
|
+
|
487
|
+
# Methods for serialization as json or xml, relying on the serializable_hash method
|
488
|
+
def to_xml(options = {})
|
489
|
+
self.serializable_hash(options).to_xml(:root => self.class.element_name)
|
490
|
+
end
|
491
|
+
|
492
|
+
def to_json(options = {})
|
493
|
+
self.class.include_root_in_json ? {self.class.element_name => self.serializable_hash(options)}.to_json : self.serializable_hash(options).to_json
|
494
|
+
end
|
495
|
+
|
496
|
+
def serializable_hash(options = {})
|
497
|
+
options[:include_associations] = options[:include_associations] ? options[:include_associations].symbolize_array : []
|
498
|
+
options[:include_extras] = options[:include_extras] ? options[:include_extras].symbolize_array : []
|
499
|
+
options[:except] ||= []
|
500
|
+
ret = self.attributes.inject({}) do |accum, (key,val)|
|
501
|
+
# If this is an association and it's in include_associations then include it
|
502
|
+
if options[:include_extras].include?(key.to_sym)
|
503
|
+
accum.merge(key => val)
|
504
|
+
elsif options[:except].include?(key.to_sym)
|
505
|
+
accum
|
506
|
+
else
|
507
|
+
!self.attribute?(key) || self.protected_attribute?(key) ? accum : accum.merge(key => val)
|
508
|
+
end
|
509
|
+
end
|
510
|
+
options[:include_associations].each do |assoc|
|
511
|
+
ret[assoc] = self.send(assoc).serializable_hash({:include_id => true}) if self.association?(assoc)
|
512
|
+
end
|
513
|
+
# include id - this is for nested updates
|
514
|
+
ret[:id] = self.id if options[:include_id] && !self.new?
|
515
|
+
ret
|
516
|
+
end
|
517
|
+
|
518
|
+
protected
|
519
|
+
def connection(refresh = false)
|
520
|
+
self.class.connection(refresh)
|
521
|
+
end
|
522
|
+
|
523
|
+
def load_attributes_from_response(response)
|
524
|
+
load(response)
|
525
|
+
end
|
526
|
+
|
527
|
+
def element_path(id, prefix_options = {}, query_options = nil)
|
528
|
+
self.class.element_path(id, prefix_options, query_options)
|
529
|
+
end
|
530
|
+
|
531
|
+
def new_element_path(prefix_options = {})
|
532
|
+
self.class.new_element_path(prefix_options)
|
533
|
+
end
|
534
|
+
|
535
|
+
def collection_path(prefix_options = {},query_options = nil)
|
536
|
+
self.class.collection_path(prefix_options, query_options)
|
537
|
+
end
|
538
|
+
|
539
|
+
def create(*args)
|
540
|
+
opts = args.extract_options!
|
541
|
+
# When we create we should not include any blank attributes unless they are associations
|
542
|
+
except = self.class.include_blank_attributes_on_create ? {} : self.attributes.select{|k,v| v.blank?}
|
543
|
+
opts[:except] = opts[:except] ? opts[:except].concat(except.keys).uniq.symbolize_array : except.keys.symbolize_array
|
544
|
+
opts[:include_associations] = opts[:include_associations] ? opts[:include_associations].concat(args) : []
|
545
|
+
opts[:include_extras] ||= []
|
546
|
+
body = RestClient::Payload.has_file?(self.attributes) ? self.serializable_hash(opts) : encode(opts)
|
547
|
+
connection.post(collection_path, body, self.class.headers).tap do |response|
|
548
|
+
load_attributes_from_response(response)
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
def update(*args)
|
553
|
+
opts = args.extract_options!
|
554
|
+
# When we create we should not include any blank attributes
|
555
|
+
except = self.class.attribute_names - self.changed.symbolize_array
|
556
|
+
changed_associations = self.changed.symbolize_array.select{|item| self.association?(item)}
|
557
|
+
opts[:except] = opts[:except] ? opts[:except].concat(except).uniq.symbolize_array : except.symbolize_array
|
558
|
+
opts[:include_associations] = opts[:include_associations] ? opts[:include_associations].concat(args).concat(changed_associations).uniq : changed_associations.concat(args)
|
559
|
+
opts[:include_extras] ||= []
|
560
|
+
opts[:except] = [:id] if self.class.include_all_attributes_on_update
|
561
|
+
body = RestClient::Payload.has_file?(self.attributes) ? self.serializable_hash(opts) : encode(opts)
|
562
|
+
# We can just ignore the response
|
563
|
+
connection.put(element_path(self.id, prefix_options), body, self.class.headers).tap do |response|
|
564
|
+
load_attributes_from_response(response)
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
private
|
569
|
+
|
570
|
+
def split_options(options = {})
|
571
|
+
self.class.__send__(:split_options, options)
|
572
|
+
end
|
573
|
+
|
574
|
+
end
|
575
|
+
|
576
|
+
class Base
|
577
|
+
extend ActiveModel::Naming
|
578
|
+
# Order is important here
|
579
|
+
# It should be Validations, Dirty Tracking, Callbacks so the include order is the opposite
|
580
|
+
include AssociationActivation
|
581
|
+
self.activate_associations
|
582
|
+
|
583
|
+
include Scopes, Callbacks, Attributes, ModelErrors
|
584
|
+
|
585
|
+
end
|
586
|
+
|
587
|
+
end
|