resource 0.1.0

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