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