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.
Files changed (59) hide show
  1. data/.document +5 -0
  2. data/.rspec +3 -0
  3. data/Gemfile +29 -0
  4. data/Gemfile.lock +152 -0
  5. data/Guardfile +22 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.rdoc +19 -0
  8. data/Rakefile +49 -0
  9. data/VERSION +1 -0
  10. data/api_resource.gemspec +154 -0
  11. data/lib/api_resource.rb +129 -0
  12. data/lib/api_resource/association_activation.rb +19 -0
  13. data/lib/api_resource/associations.rb +169 -0
  14. data/lib/api_resource/associations/association_proxy.rb +115 -0
  15. data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +16 -0
  16. data/lib/api_resource/associations/dynamic_resource_scope.rb +23 -0
  17. data/lib/api_resource/associations/has_many_remote_object_proxy.rb +16 -0
  18. data/lib/api_resource/associations/has_one_remote_object_proxy.rb +24 -0
  19. data/lib/api_resource/associations/multi_argument_resource_scope.rb +15 -0
  20. data/lib/api_resource/associations/multi_object_proxy.rb +73 -0
  21. data/lib/api_resource/associations/related_object_hash.rb +12 -0
  22. data/lib/api_resource/associations/relation_scope.rb +30 -0
  23. data/lib/api_resource/associations/resource_scope.rb +34 -0
  24. data/lib/api_resource/associations/scope.rb +107 -0
  25. data/lib/api_resource/associations/single_object_proxy.rb +81 -0
  26. data/lib/api_resource/attributes.rb +162 -0
  27. data/lib/api_resource/base.rb +587 -0
  28. data/lib/api_resource/callbacks.rb +49 -0
  29. data/lib/api_resource/connection.rb +171 -0
  30. data/lib/api_resource/core_extensions.rb +7 -0
  31. data/lib/api_resource/custom_methods.rb +119 -0
  32. data/lib/api_resource/exceptions.rb +87 -0
  33. data/lib/api_resource/formats.rb +14 -0
  34. data/lib/api_resource/formats/json_format.rb +25 -0
  35. data/lib/api_resource/formats/xml_format.rb +36 -0
  36. data/lib/api_resource/local.rb +12 -0
  37. data/lib/api_resource/log_subscriber.rb +15 -0
  38. data/lib/api_resource/mocks.rb +269 -0
  39. data/lib/api_resource/model_errors.rb +86 -0
  40. data/lib/api_resource/observing.rb +29 -0
  41. data/lib/api_resource/railtie.rb +22 -0
  42. data/lib/api_resource/scopes.rb +45 -0
  43. data/spec/lib/associations_spec.rb +656 -0
  44. data/spec/lib/attributes_spec.rb +121 -0
  45. data/spec/lib/base_spec.rb +504 -0
  46. data/spec/lib/callbacks_spec.rb +68 -0
  47. data/spec/lib/connection_spec.rb +76 -0
  48. data/spec/lib/local_spec.rb +20 -0
  49. data/spec/lib/mocks_spec.rb +28 -0
  50. data/spec/lib/model_errors_spec.rb +29 -0
  51. data/spec/spec_helper.rb +36 -0
  52. data/spec/support/mocks/association_mocks.rb +46 -0
  53. data/spec/support/mocks/error_resource_mocks.rb +21 -0
  54. data/spec/support/mocks/test_resource_mocks.rb +43 -0
  55. data/spec/support/requests/association_requests.rb +14 -0
  56. data/spec/support/requests/error_resource_requests.rb +25 -0
  57. data/spec/support/requests/test_resource_requests.rb +31 -0
  58. data/spec/support/test_resource.rb +64 -0
  59. 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