api_resource 0.6.18 → 0.6.19

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,7 +4,7 @@ require 'active_support/core_ext'
4
4
  require 'active_support/string_inquirer'
5
5
 
6
6
  module ApiResource
7
-
7
+
8
8
  class Base
9
9
 
10
10
  # TODO: There's way too much in this class as it stands, some glaring problems:
@@ -30,35 +30,84 @@ module ApiResource
30
30
  # => 6) Implement an IdentityMap
31
31
  # => 7) Write documentation
32
32
  # => 8) Write Examples
33
-
34
- class_attribute :site, :proxy, :user, :password, :auth_type, :format,
33
+
34
+ class_attribute :site, :proxy, :user, :password, :auth_type, :format,
35
35
  :timeout, :open_timeout, :ssl_options, :token, :ttl
36
-
37
36
 
38
37
  class_attribute :include_root_in_json
39
38
  self.include_root_in_json = true
40
-
39
+
41
40
  class_attribute :include_nil_attributes_on_create
42
41
  self.include_nil_attributes_on_create = false
43
-
42
+
44
43
  class_attribute :include_all_attributes_on_update
45
44
  self.include_nil_attributes_on_create = false
46
45
 
47
46
  class_attribute :format
48
47
  self.format = ApiResource::Formats::JsonFormat
49
-
50
- class_attribute :primary_key
51
- self.primary_key = "id"
52
48
 
53
- delegate :logger, :to => ApiResource
54
-
49
+ class_attribute :resource_definition_mutex
50
+ self.resource_definition_mutex = Mutex.new
51
+
52
+ delegate :logger, to: ApiResource
53
+
55
54
  class << self
56
-
57
- # writers - accessors with defaults were not working
58
- attr_writer :element_name, :collection_name
59
55
 
60
- delegate :logger, :to => ApiResource
61
-
56
+ # @!attribute [w] collection_name
57
+ # @return [String]
58
+ attr_writer :collection_name
59
+
60
+ # @!attribute [w] element_name
61
+ # @return [String]
62
+ attr_writer :element_name
63
+
64
+ delegate :logger,
65
+ to: ApiResource
66
+
67
+ #
68
+ # Accessor for the connection
69
+ #
70
+ # @param refresh = false [Boolean] Whether to reconnect
71
+ #
72
+ # @return [Connection]
73
+ def connection(refresh = false)
74
+ if refresh || @connection.nil?
75
+ @connection = Connection.new(self.site, self.format, self.headers)
76
+ end
77
+ @connection.timeout = self.timeout
78
+ @connection
79
+ end
80
+
81
+ #
82
+ # Handles the setting of format to a MimeType
83
+ #
84
+ # @param mime_type_or_format [Symbol, MimeType]
85
+ #
86
+ # @return [MimeType] The new MimeType
87
+ def format_with_mimetype_or_format_set=(mime_type_or_format)
88
+ if mime_type_or_format.is_a?(Symbol)
89
+ format = ApiResource::Formats[mime_type_or_format]
90
+ else
91
+ format = mime_type_or_format
92
+ end
93
+ self.format_without_mimetype_or_format_set = format
94
+ if self.site
95
+ self.connection.format = format
96
+ end
97
+ format
98
+ end
99
+ alias_method_chain :format=, :mimetype_or_format_set
100
+
101
+ #
102
+ # Reader for headers
103
+ #
104
+ # @return [Hash] Headers for requests
105
+ def headers
106
+ {}.tap do |ret|
107
+ ret['Lifebooker-Token'] = self.token if self.token.present?
108
+ end
109
+ end
110
+
62
111
  def inherited(klass)
63
112
  # Call the methods of the superclass to make sure inheritable accessors and the like have been inherited
64
113
  super
@@ -73,97 +122,71 @@ module ApiResource
73
122
  true
74
123
  end
75
124
 
76
- def resource_definition
77
- @resource_definition
78
- end
79
-
80
- # This makes a request to new_element_path
81
- def set_class_attributes_upon_load
82
- return true if self == ApiResource::Base
83
- begin
84
- @resource_definition = self.connection.get(
85
- self.new_element_path, self.headers
86
- )
87
- # Attributes go first
88
- if resource_definition["attributes"]
89
-
90
- define_attributes(
91
- *(resource_definition["attributes"]["public"] || [])
92
- )
93
- define_protected_attributes(
94
- *(resource_definition["attributes"]["protected"] || [])
95
- )
96
-
97
- end
98
- # Then scopes
99
- if resource_definition["scopes"]
100
- resource_definition["scopes"].each_pair do |scope_name, opts|
101
- self.scope(scope_name, opts)
102
- end
103
- end
104
- # Then associations
105
- if resource_definition["associations"]
106
- resource_definition["associations"].each_pair do |key, hash|
107
- hash.each_pair do |assoc_name, assoc_options|
108
- self.send(key, assoc_name, assoc_options)
109
- end
110
- end
111
- end
112
-
113
- # This is provided by ActiveModel::AttributeMethods, it should
114
- # define the basic methods but we need to override all the setters
115
- # so we do dirty tracking
116
- attrs = []
117
- if resource_definition["attributes"] && resource_definition["attributes"]["public"]
118
- attrs += resource_definition["attributes"]["public"].collect{|v|
119
- v.is_a?(Array) ? v.first : v
120
- }.flatten
121
- end
122
- if resource_definition["associations"]
123
- attrs += resource_definition["associations"].values.collect(&:keys).flatten
124
- end
125
-
126
- # Swallow up any loading errors because the site may be incorrect
127
- rescue Exception => e
128
- if ApiResource.raise_missing_definition_error
129
- raise e
125
+ #
126
+ # Explicit call to load the resource definition
127
+ #
128
+ # @return [Boolean] True if we loaded it, false if it was already
129
+ # loaded
130
+ def load_resource_definition
131
+ unless instance_variable_defined?(:@resource_definition)
132
+ # Lock the mutex to make sure only one thread does
133
+ # this at a time
134
+ self.resource_definition_mutex.synchronize do
135
+ # once we have the lock, check to make sure the resource
136
+ # definition wasn't fetched while we were sleeping
137
+ return true if instance_variable_defined?(:@resource_definition)
138
+ # the last time we checked
139
+ @resource_load_time = Time.now
140
+
141
+ # set to not nil so we don't get an infinite loop
142
+ @resource_definition = true
143
+ self.set_class_attributes_upon_load
144
+ return true
130
145
  end
131
- ApiResource.logger.warn(
132
- "#{self} accessing #{self.new_element_path}"
133
- )
134
- ApiResource.logger.warn(
135
- "#{self}: #{e.message[0..60].gsub(/[\n\r]/, '')} ...\n"
136
- )
137
- ApiResource.logger.debug(e.backtrace.pretty_inspect)
138
- return e.respond_to?(:request) ? e.request : nil
139
146
  end
147
+ # we didn't do anything
148
+ false
140
149
  end
141
-
142
- def reset_connection
143
- remove_instance_variable(:@connection) if @connection.present?
150
+
151
+ #
152
+ # Set the open timeout on the connection and connect
153
+ #
154
+ # @param timeout [Fixnum] Open timeout in number of seconds
155
+ #
156
+ # @return [Fixnum] The timeout
157
+ def open_timeout_with_connection_reset=(timeout)
158
+ @connection = nil
159
+ self.open_timeout_without_connection_reset = timeout
144
160
  end
161
+ alias_method_chain :open_timeout=, :connection_reset
145
162
 
146
- # load our resource definition to make sure we know what this class
147
- # responds to
148
- def respond_to?(*args)
149
- self.load_resource_definition
150
- super
163
+ #
164
+ # Prefix for the resource path
165
+ #
166
+ # @todo Are the options used?
167
+ #
168
+ # @param options = {} [Hash] Options
169
+ #
170
+ # @return [String] Collection prefix
171
+ def prefix(options = {})
172
+ default = (self.site ? self.site.path : '/')
173
+ default << '/' unless default[-1..-1] == '/'
174
+ self.prefix = default
175
+ prefix(options)
151
176
  end
152
-
153
- def load_resource_definition
154
- unless instance_variable_defined?(:@resource_definition)
155
- # the last time we checked
156
- @resource_load_time = Time.now
157
-
158
- # set to not nil so we don't get an infinite loop
159
- @resource_definition = true
160
- self.set_class_attributes_upon_load
161
- return true
162
- end
163
- # we didn't do anything
164
- false
177
+
178
+ #
179
+ # @todo Not sure what this does
180
+ def prefix_source
181
+ prefix
182
+ prefix_source
165
183
  end
166
184
 
185
+ #
186
+ # Clear the old resource definition and reload it from the
187
+ # server
188
+ #
189
+ # @return [Boolean] True if it loaded
167
190
  def reload_resource_definition
168
191
  # clear the public_attribute_names, protected_attribute_names
169
192
  if instance_variable_defined?(:@resource_definition)
@@ -175,22 +198,71 @@ module ApiResource
175
198
  end
176
199
  # backwards compatibility
177
200
  alias_method :reload_class_attributes, :reload_resource_definition
178
-
179
- def token_with_new_token_set=(new_token)
180
- self.token_without_new_token_set = new_token
181
- self.connection(true)
182
- self.descendants.each do |child|
183
- child.send(:token=, new_token)
201
+
202
+ #
203
+ # Reset our connection instance so that we will reconnect the
204
+ # next time we need it
205
+ #
206
+ # @return [Boolean] true
207
+ def reset_connection
208
+ remove_instance_variable(:@connection) if @connection.present?
209
+ true
210
+ end
211
+
212
+ #
213
+ # Reader for the resource_definition
214
+ #
215
+ # @return [Hash, nil] Our stored resource definition
216
+ def resource_definition
217
+ @resource_definition
218
+ end
219
+
220
+ #
221
+ # Load our resource definition to make sure we know what this class
222
+ # responds to
223
+ #
224
+ # @return [Boolean] Whether or not it responss
225
+ def respond_to?(*args)
226
+ self.load_resource_definition
227
+ super
228
+ end
229
+
230
+ #
231
+ # This makes a request to new_element_path and sets up the correct
232
+ # attribute, scope and association methods for this class
233
+ #
234
+ # @return [Boolean] true
235
+ def set_class_attributes_upon_load
236
+ # this only happens in subclasses
237
+ return true if self == ApiResource::Base
238
+ begin
239
+ @resource_definition = self.connection.get(
240
+ self.new_element_path, self.headers
241
+ )
242
+ # set up methods derived from our class definition
243
+ self.define_all_attributes
244
+ self.define_all_scopes
245
+ self.define_all_associations
246
+
247
+ # Swallow up any loading errors because the site may be incorrect
248
+ rescue Exception => e
249
+ self.handle_resource_definition_error(e)
184
250
  end
251
+ true
185
252
  end
186
-
187
- alias_method_chain :token=, :new_token_set
188
253
 
254
+ #
255
+ # Handles the setting of site while reloading the resource
256
+ # definition to ensure we have the latest definition
257
+ #
258
+ # @param site [String] URL of the site
259
+ #
260
+ # @return [String] The newly set site
189
261
  def site_with_connection_reset=(site)
190
262
  # store so we can reload attributes if the site changed
191
263
  old_site = self.site.to_s.clone
192
264
  @connection = nil
193
-
265
+
194
266
  if site.nil?
195
267
  self.site_without_connection_reset = nil
196
268
  # no site, so we'll skip the reload
@@ -198,73 +270,56 @@ module ApiResource
198
270
  else
199
271
  self.site_without_connection_reset = create_site_uri_from(site)
200
272
  end
201
-
273
+
202
274
  # reset class attributes and try to reload them if the site changed
203
275
  unless self.site.to_s == old_site
204
276
  self.reload_resource_definition
205
277
  end
206
-
278
+
207
279
  return site
208
280
  end
209
-
210
281
  alias_method_chain :site=, :connection_reset
211
-
212
-
213
- def format_with_mimetype_or_format_set=(mime_type_or_format)
214
- format = mime_type_or_format.is_a?(Symbol) ? ApiResource::Formats[mime_type_or_format] : mime_type_or_format
215
- self.format_without_mimetype_or_format_set = format
216
- self.connection.format = format if self.site
217
- end
218
-
219
- alias_method_chain :format=, :mimetype_or_format_set
220
-
282
+
283
+ #
284
+ # Set the timeout on the connection and connect
285
+ #
286
+ # @param timeout [Fixnum] Timeout in number of seconds
287
+ #
288
+ # @return [Fixnum] The timeout
221
289
  def timeout_with_connection_reset=(timeout)
222
290
  @connection = nil
223
291
  self.timeout_without_connection_reset = timeout
224
292
  end
225
-
226
293
  alias_method_chain :timeout=, :connection_reset
227
-
228
- def open_timeout_with_connection_reset=(timeout)
229
- @connection = nil
230
- self.open_timeout_without_connection_reset = timeout
231
- end
232
-
233
- alias_method_chain :open_timeout=, :connection_reset
234
-
235
- def connection(refresh = false)
236
- @connection = Connection.new(self.site, self.format, self.headers) if refresh || @connection.nil?
237
- @connection.timeout = self.timeout
238
- @connection
239
- end
240
-
241
- def headers
242
- {}.tap do |ret|
243
- ret['Lifebooker-Token'] = self.token if self.token.present?
294
+
295
+ #
296
+ # Handles the setting of tokens on descendants
297
+ #
298
+ # @param new_token [String] New token string
299
+ #
300
+ # @return [String] The token that was set
301
+ def token_with_new_token_set=(new_token)
302
+ self.token_without_new_token_set = new_token
303
+ self.connection(true)
304
+ self.descendants.each do |child|
305
+ child.send(:token=, new_token)
244
306
  end
307
+ new_token
245
308
  end
246
-
247
- def prefix(options = {})
248
- default = (self.site ? self.site.path : '/')
249
- default << '/' unless default[-1..-1] == '/'
250
- self.prefix = default
251
- prefix(options)
252
- end
253
-
254
- def prefix_source
255
- prefix
256
- prefix_source
257
- end
309
+ alias_method_chain :token=, :new_token_set
310
+
311
+
312
+
258
313
 
259
314
  def prefix=(value = '/')
260
- prefix_call = value.gsub(/:\w+/) { |key|
315
+ prefix_call = value.gsub(/:\w+/) { |key|
261
316
  "\#{URI.escape options[#{key}].to_s}"
262
317
  }
263
318
  @prefix_parameters = nil
264
319
  silence_warnings do
265
320
  instance_eval <<-EOE, __FILE__, __LINE__ + 1
266
321
  def prefix_source() "#{value}" end
267
- def prefix(options={})
322
+ def prefix(options={})
268
323
  ret = "#{prefix_call}"
269
324
  ret =~ Regexp.new(Regexp.escape("//")) ? "/" : ret
270
325
  end
@@ -274,7 +329,7 @@ module ApiResource
274
329
  logger.error "Couldn't set prefix: #{e}\n #{code}" if logger
275
330
  raise
276
331
  end
277
-
332
+
278
333
  # element_name with default
279
334
  def element_name
280
335
  @element_name ||= self.model_name.element
@@ -283,11 +338,11 @@ module ApiResource
283
338
  def collection_name
284
339
  @collection_name ||= ActiveSupport::Inflector.pluralize(self.element_name)
285
340
  end
286
-
341
+
287
342
  # alias_method :set_prefix, :prefix=
288
343
  # alias_method :set_element_name, :element_name=
289
344
  # alias_method :set_collection_name, :collection_name=
290
-
345
+
291
346
  def element_path(id, prefix_options = {}, query_options = nil)
292
347
  prefix_options, query_options = split_options(prefix_options) if query_options.nil?
293
348
 
@@ -300,27 +355,27 @@ module ApiResource
300
355
  "#{prefix(prefix_options)}#{collection_name}/#{URI.escape id.to_s}.#{format.extension}#{query_string(query_options)}"
301
356
  end
302
357
  end
303
-
304
- # path to find
358
+
359
+ # path to find
305
360
  def new_element_path(prefix_options = {})
306
361
  File.join(
307
- self.prefix(prefix_options),
308
- self.collection_name,
362
+ self.prefix(prefix_options),
363
+ self.collection_name,
309
364
  "new.#{format.extension}"
310
365
  )
311
366
  end
312
-
367
+
313
368
  def collection_path(prefix_options = {}, query_options = nil)
314
369
  prefix_options, query_options = split_options(prefix_options) if query_options.nil?
315
370
 
316
371
  # Fall back on this rather than search without the id
317
372
  "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
318
373
  end
319
-
374
+
320
375
  def build(attributes = {})
321
376
  self.new(attributes)
322
377
  end
323
-
378
+
324
379
  def create(attributes = {})
325
380
  self.new(attributes).tap{ |resource| resource.save }
326
381
  end
@@ -334,7 +389,7 @@ module ApiResource
334
389
  # ==== Examples
335
390
  # Event.delete(2) # sends DELETE /events/2
336
391
  #
337
- # Event.create(:name => 'Free Concert', :location => 'Community Center')
392
+ # Event.create(name: 'Free Concert', location: 'Community Center')
338
393
  # my_event = Event.find(:first) # let's assume this is event with ID 7
339
394
  # Event.delete(my_event.id) # sends DELETE /events/7
340
395
  #
@@ -368,6 +423,32 @@ module ApiResource
368
423
 
369
424
  protected
370
425
 
426
+ #
427
+ # Handle any errors raised during the resource definition
428
+ # find
429
+ #
430
+ # @param e [Exception] Exception thrown
431
+ #
432
+ # @raise [Exception] Re-raised if
433
+ # ApiResource.raise_missing_definition_error is true
434
+ #
435
+ # @return [ApiResource::Request, nil] The Request associated with
436
+ # this error or nil if there is no request and the error came from
437
+ # something else
438
+ def handle_resource_definition_error(e)
439
+ if ApiResource.raise_missing_definition_error
440
+ raise e
441
+ end
442
+ ApiResource.logger.warn(
443
+ "#{self} accessing #{self.new_element_path}"
444
+ )
445
+ ApiResource.logger.warn(
446
+ "#{self}: #{e.message[0..60].gsub(/[\n\r]/, '')} ...\n"
447
+ )
448
+ ApiResource.logger.debug(e.backtrace.pretty_inspect)
449
+ return e.respond_to?(:request) ? e.request : nil
450
+ end
451
+
371
452
  def method_missing(meth, *args, &block)
372
453
  # make one attempt to load remote attrs
373
454
  if self.resource_definition_is_invalid?
@@ -380,7 +461,7 @@ module ApiResource
380
461
  super
381
462
  end
382
463
  end
383
-
464
+
384
465
  private
385
466
 
386
467
  # Accepts a URI and creates the site URI from that.
@@ -406,86 +487,86 @@ module ApiResource
406
487
  def uri_parser
407
488
  @uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
408
489
  end
409
-
490
+
410
491
  end
411
-
492
+
412
493
  def initialize(attributes = {})
413
494
  # call super's initialize to set up any variables that we need
414
495
  super(attributes)
415
496
  # if we initialize this class, load the attributes
416
497
  self.class.load_resource_definition
417
- # Now we can make a call to setup the inheriting
498
+ # Now we can make a call to setup the inheriting
418
499
  # klass with its attributes
419
500
  self.attributes = attributes
420
501
  end
421
-
502
+
422
503
  def new?
423
504
  id.blank?
424
505
  end
425
506
  alias :new_record? :new?
426
-
507
+
427
508
  def persisted?
428
509
  !new?
429
510
  end
430
-
511
+
431
512
  def id
432
513
  self.read_attribute(self.class.primary_key)
433
514
  end
434
-
515
+
435
516
  # Bypass dirty tracking for this field
436
517
  def id=(id)
437
518
  @attributes[self.class.primary_key] = id
438
519
  end
439
-
520
+
440
521
  def ==(other)
441
522
  other.equal?(self) || (other.instance_of?(self.class) && other.id == self.id)
442
523
  end
443
-
524
+
444
525
  def eql?(other)
445
526
  self == other
446
527
  end
447
-
528
+
448
529
  def hash
449
530
  id.hash
450
531
  end
451
-
532
+
452
533
  def dup
453
534
  self.class.instantiate_record(self.attributes)
454
535
  end
455
-
536
+
456
537
  def update_attributes(attrs)
457
538
  self.attributes = attrs
458
539
  self.save
459
540
  end
460
-
541
+
461
542
  def save(*args)
462
543
  new? ? create(*args) : update(*args)
463
544
  end
464
-
545
+
465
546
  def save!(*args)
466
547
  save(*args) || raise(ApiResource::ResourceInvalid.new(self))
467
548
  end
468
-
549
+
469
550
  def destroy
470
551
  connection.delete(element_path(self.id), self.class.headers)
471
552
  end
472
-
553
+
473
554
  def encode(options = {})
474
555
  self.send("to_#{self.class.format.extension}", options)
475
556
  end
476
-
557
+
477
558
  def reload
478
559
  # find the record from the remote service
479
560
  reloaded = self.class.find(self.id)
480
-
561
+
481
562
  # clear out the attributes cache
482
563
  @attributes_cache = HashWithIndifferentAccess.new
483
564
  # set up our attributes cache on our record
484
565
  @attributes = reloaded.instance_variable_get(:@attributes)
485
-
566
+
486
567
  reloaded
487
568
  end
488
-
569
+
489
570
  def to_param
490
571
  # Stolen from active_record.
491
572
  # We can't use alias_method here, because method 'id' optimizes itself on the fly.
@@ -505,92 +586,50 @@ module ApiResource
505
586
  return [] unless self.class.prefix_source =~ /\:/
506
587
  self.class.prefix_source.scan(/\:(\w+)/).collect{|match| match.first.to_sym}
507
588
  end
508
-
589
+
509
590
  # Override to_s and inspect so they only show attributes
510
591
  # and not associations, this prevents force loading of associations
511
- # when we call to_s or inspect on a descendent of base but allows it if we
592
+ # when we call to_s or inspect on a descendent of base but allows it if we
512
593
  # try to evaluate an association directly
513
594
  def to_s
514
595
  return "#<#{self.class}:#{(self.object_id * 2).to_s(16)} @attributes=#{self.attributes}"
515
596
  end
516
597
  alias_method :inspect, :to_s
517
-
598
+
518
599
  # Methods for serialization as json or xml, relying on the serializable_hash method
519
600
  def to_xml(options = {})
520
- self.serializable_hash(options).to_xml(:root => self.class.element_name)
601
+ self.serializable_hash(options).to_xml(root: self.class.element_name)
521
602
  end
522
-
603
+
523
604
  def to_json(options = {})
524
- self.class.include_root_in_json ? {self.class.element_name => self.serializable_hash(options)}.to_json : self.serializable_hash(options).to_json
605
+ # handle whether or not we include root in our JSON
606
+ if self.class.include_root_in_json
607
+ ret = {
608
+ self.class.element_name => self.serializable_hash(options)
609
+ }
610
+ else
611
+ ret = self.serializable_hash(options)
612
+ end
613
+ ret.to_json
525
614
  end
526
-
527
- # TODO: this method needs to change seriously to fit in with the
615
+
616
+ # TODO: (Updated 10/26/2013):
617
+ # Leaving this old message here though the behavior is now in Serializer.
618
+ # Any changes should be done there
619
+ #
620
+ # this method needs to change seriously to fit in with the
528
621
  # new typecasting scheme, it should call self.outgoing_attributes which
529
622
  # should return the converted versions after calling to_api, that should
530
623
  # be implemented in the attributes module though
531
624
  def serializable_hash(options = {})
532
-
533
- action = options[:action]
534
-
535
- include_nil_attributes = options[:include_nil_attributes]
536
-
537
- options[:include_associations] = options[:include_associations] ? options[:include_associations].symbolize_array : self.changes.keys.symbolize_array.select{|k| self.association?(k)}
538
-
539
- options[:include_extras] = options[:include_extras] ? options[:include_extras].symbolize_array : []
540
-
541
- options[:except] ||= []
542
-
543
- ret = self.attributes.inject({}) do |accum, (key,val)|
544
- # If this is an association and it's in include_associations then include it
545
- if options[:include_extras].include?(key.to_sym)
546
- accum.merge(key => val)
547
- elsif options[:except].include?(key.to_sym)
548
- accum
549
- # this attribute is already accounted for in the URL
550
- elsif self.prefix_attribute_names.include?(key.to_sym)
551
- accum
552
- elsif(!include_nil_attributes && val.nil? && self.changes[key].blank?)
553
- accum
554
- else
555
- !self.attribute?(key) || self.protected_attribute?(key) ? accum : accum.merge(key => val)
556
- end
557
- end
558
-
559
- # also add in the _id fields that are changed
560
- ret = self.association_names.inject(ret) do |accum, assoc_name|
561
-
562
- # get the id method for the association
563
- id_method = self.class.association_foreign_key_field(assoc_name)
564
-
565
- # only do this if they are not prefix_attribute_names
566
- # and we have changes
567
- if !self.prefix_attribute_names.include?(id_method.to_sym) &&
568
- self.changes[id_method].present?
569
-
570
- accum[id_method] = self.changes[id_method].last
571
- end
572
- accum
573
- end
574
-
575
- options[:include_associations].each do |assoc|
576
- if self.association?(assoc)
577
- ret[assoc] = self.send(assoc).serializable_hash({
578
- :include_id => true,
579
- :include_nil_attributes => include_nil_attributes,
580
- :action => action
581
- })
582
- end
583
- end
584
- # include id - this is for nested updates
585
- ret[:id] = self.id if options[:include_id] && !self.new?
586
- ret
625
+ return Serializer.new(self, options).to_hash
587
626
  end
588
-
627
+
589
628
  protected
590
629
  def connection(refresh = false)
591
630
  self.class.connection(refresh)
592
631
  end
593
-
632
+
594
633
  def load_attributes_from_response(response)
595
634
  if response.present?
596
635
  @attributes_cache = {}
@@ -601,27 +640,27 @@ module ApiResource
601
640
  response
602
641
  end
603
642
 
604
- def method_missing(meth, *args, &block)
605
- # make one attempt to load remote attrs
606
- if self.class.resource_definition_is_invalid?
607
- self.class.reload_resource_definition
608
- end
609
- # see if we respond to the method now
610
- if self.respond_to?(meth)
611
- return self.send(meth, *args, &block)
612
- else
613
- super
614
- end
615
- end
616
-
643
+ # def method_missing(meth, *args, &block)
644
+ # # make one attempt to load remote attrs
645
+ # if self.class.resource_definition_is_invalid?
646
+ # self.class.reload_resource_definition
647
+ # end
648
+ # # see if we respond to the method now
649
+ # if self.respond_to?(meth)
650
+ # return self.send(meth, *args, &block)
651
+ # else
652
+ # super
653
+ # end
654
+ # end
655
+
617
656
  def element_path(id, prefix_override_options = {}, query_options = nil)
618
657
  self.class.element_path(
619
- id,
620
- self.prefix_options.merge(prefix_override_options),
658
+ id,
659
+ self.prefix_options.merge(prefix_override_options),
621
660
  query_options
622
661
  )
623
662
  end
624
-
663
+
625
664
  # list of all attributes that are not nil
626
665
  def nil_attributes
627
666
  self.attributes.select{|k,v|
@@ -634,76 +673,108 @@ module ApiResource
634
673
  def new_element_path(prefix_options = {})
635
674
  self.class.new_element_path(prefix_options)
636
675
  end
637
-
676
+
638
677
  def collection_path(override_prefix_options = {},query_options = nil)
639
678
  self.class.collection_path(
640
- self.prefix_options.merge(override_prefix_options),
679
+ self.prefix_options.merge(override_prefix_options),
641
680
  query_options
642
681
  )
643
682
  end
644
-
683
+
684
+ #
685
+ # Create a new record
686
+ # @param *args [type] [description]
687
+ #
688
+ # @return [type] [description]
645
689
  def create(*args)
646
- body = setup_create_call(*args)
647
- connection.post(collection_path, body, self.class.headers).tap do |response|
690
+ path = self.collection_path
691
+ body = self.setup_create_call(*args)
692
+ headers = self.class.headers
693
+ # make the post call
694
+ connection.post(path, body, headers).tap do |response|
648
695
  load_attributes_from_response(response)
649
696
  end
650
697
  end
651
698
 
699
+ #
700
+ # Helper method to set up a call to create
701
+ #
702
+ # @param *args [type] [description]
703
+ #
704
+ # @return [type] [description]
652
705
  def setup_create_call(*args)
653
706
  opts = args.extract_options!
654
- # When we create we should not include any blank attributes unless they are associations
655
- except = self.class.include_nil_attributes_on_create ?
656
- {} : self.nil_attributes
657
- opts[:except] = opts[:except] ? opts[:except].concat(except.keys).uniq.symbolize_array : except.keys.symbolize_array
658
- opts[:include_nil_attributes] = self.class.include_nil_attributes_on_create
659
- opts[:include_associations] = opts[:include_associations] ? opts[:include_associations].concat(args) : []
660
- opts[:include_extras] ||= []
661
- opts[:action] = "create"
662
- # TODO: Remove this dependency for saving files
663
- body = RestClient::Payload.has_file?(self.attributes) ? self.serializable_hash(opts) : encode(opts)
664
- end
665
-
666
-
707
+
708
+ # handle nil attributes
709
+ opts[:include_nil_attributes] = self.include_nil_attributes_on_create
710
+
711
+ # more generic setup_save_call
712
+ self.setup_save_call(args, opts)
713
+ end
714
+
715
+
667
716
  def update(*args)
668
- body = setup_update_call(*args)
717
+ path = self.element_path(self.id)
718
+ body = self.setup_update_call(*args)
719
+ headers = self.class.headers
669
720
  # We can just ignore the response
670
- connection.put(element_path(self.id), body, self.class.headers).tap do |response|
721
+ connection.put(path, body, headers).tap do |response|
671
722
  load_attributes_from_response(response)
672
723
  end
673
724
  end
674
725
 
675
726
  def setup_update_call(*args)
676
- opts = args.extract_options!
727
+ options = args.extract_options!
728
+
677
729
  # When we create we should not include any blank attributes
678
- except = self.class.attribute_names - self.changed.symbolize_array
679
- changed_associations = self.changed.symbolize_array.select{|item| self.association?(item)}
680
- opts[:except] = opts[:except] ? opts[:except].concat(except).uniq.symbolize_array : except.symbolize_array
681
- opts[:include_nil_attributes] = self.include_all_attributes_on_update
682
- opts[:include_associations] = opts[:include_associations] ? opts[:include_associations].concat(args).concat(changed_associations).uniq : changed_associations.concat(args)
683
- opts[:include_extras] ||= []
684
- opts[:action] = "update"
685
- opts[:except] = [:id] if self.class.include_all_attributes_on_update
686
- # TODO: Remove this dependency for saving files
687
- body = RestClient::Payload.has_file?(self.attributes) ? self.serializable_hash(opts) : encode(opts)
688
- end
689
-
730
+ options[:include_nil_attributes] =
731
+ self.include_all_attributes_on_update
732
+
733
+ # exclude unchanged data
734
+ unless self.include_all_attributes_on_update
735
+ options[:except] ||= []
736
+ options[:except].concat(
737
+ self.attribute_names.select { |name| self.changes[name].blank? }
738
+ )
739
+ end
740
+
741
+ # more generic setup_save_call
742
+ self.setup_save_call(args, options)
743
+ end
744
+
745
+ def setup_save_call(additional_associations, options = {})
746
+ # We pass in associations as options and args for no good reason
747
+ options[:include_associations] ||= []
748
+ options[:include_associations].concat(additional_associations)
749
+
750
+ # get our data
751
+ data = self.serializable_hash(options)
752
+
753
+ # handle the root element
754
+ if self.include_root_in_json
755
+ data = { self.class.element_name.to_sym => data}
756
+ end
757
+
758
+ return data
759
+ end
760
+
690
761
  private
691
-
762
+
692
763
  def split_options(options = {})
693
764
  self.class.__send__(:split_options, options)
694
765
  end
695
-
766
+
696
767
  end
697
-
768
+
698
769
  class Base
699
770
  extend ActiveModel::Naming
700
771
  # Order is important here
701
772
  # It should be Validations, Dirty Tracking, Callbacks so the include order is the opposite
702
773
  include AssociationActivation
703
774
  self.activate_associations
704
-
775
+
705
776
  include Scopes, Callbacks, Observing, Attributes, ModelErrors, Conditions, Finders, Typecast
706
-
777
+
707
778
  end
708
-
779
+
709
780
  end