api_resource 0.6.18 → 0.6.19

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