Module: ApiResource::Attributes

Extended by:
ActiveSupport::Concern
Includes:
ActiveModel::AttributeMethods, ActiveModel::Dirty
Included in:
Base
Defined in:
lib/api_resource/attributes.rb

Defined Under Namespace

Modules: ClassMethods

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(sym, *args, &block) ⇒ Object



543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
# File 'lib/api_resource/attributes.rb', line 543

def method_missing(sym, *args, &block)
  sym = sym.to_sym

  # Maybe the resource definition sucks...
  if self.class.resource_definition_is_invalid?
    self.class.reload_resource_definition
  end

  # If we don't respond by now...
  if self.respond_to?(sym)
    return self.send(sym, *args, &block)
  elsif @attributes.keys.symbolize_array.include?(sym)
    # Try returning the attributes from the attributes hash
    return @attributes[sym]
  end

  # Fall back to class method_missing
  super
end

Instance Method Details

#[](attr_name) ⇒ Object

Reads an attribute and raises MissingAttributeError

Parameters:

  • attr_name (Symbol)

    The attribute to read

Returns:

  • (Object)

    The value of the attribute



452
453
454
455
456
# File 'lib/api_resource/attributes.rb', line 452

def [](attr_name)
  read_attribute(attr_name) do |n|
    self.missing_attribute(attr_name, caller(0))
  end
end

#[]=(attr_name, value) ⇒ Object

Write the value to the given attribute

Parameters:

  • attr_name (Symbol)

    The attribute to write

  • value (Object)

    The value to be written

Returns:

  • (Object)

    Returns the value parameter



464
465
466
# File 'lib/api_resource/attributes.rb', line 464

def []=(attr_name, value)
  write_attribute(attr_name, value)
end

#attribute?(name) ⇒ Boolean

Wrapper for the class method attribute? that returns true if the name parameter is the name of any attribute

of an attribute

Parameters:

  • name (Symbol)

    The name to query

Returns:

  • (Boolean)

    True if the name param is the name



477
478
479
# File 'lib/api_resource/attributes.rb', line 477

def attribute?(name)
  self.class.attribute?(name)
end

#attributesHashWithIndifferentAccess

Returns a hash of attribute names as keys and typecasted values as hash values

Returns:

  • (HashWithIndifferentAccess)

    Map from attr name to value



411
412
413
414
415
416
417
# File 'lib/api_resource/attributes.rb', line 411

def attributes
  hash = HashWithIndifferentAccess.new

  self.class.attribute_names.each_with_object(hash) do |name, attrs|
    attrs[name] = read_attribute(name)
  end
end

#attributes=(attrs) ⇒ Hash

Handles mass assignment of attributes, including sanitizing them for mass assignment. Which by default does nothing but would if you were to use this in rails 4 or with strong_parameters

Parameters:

  • attrs (Hash)

    Hash of attributes to mass assign

Returns:

  • (Hash)

    The passed in attrs param (with keys symbolized)



427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/api_resource/attributes.rb', line 427

def attributes=(attrs)
  unless attrs.respond_to?(:symbolize_keys)
    raise ArgumentError, 'You must pass a hash when assigning attributes'
  end

  return if attrs.blank?

  attrs = attrs.symbolize_keys
  # First deal with sanitizing for mass assignment
  # this raises an error if attrs violates mass assignment rules
  attrs = self.sanitize_for_mass_assignment(attrs)

  attrs.each do |name, value|
    self._assign_attribute(name, value)
  end

  attrs
end

#clear_changes(*attrs) ⇒ Object



568
569
570
571
572
# File 'lib/api_resource/attributes.rb', line 568

def clear_changes(*attrs)
  @previously_changed = {}
  @changed_attributes = {}
  true
end

#initialize(*args) ⇒ Object

Override for initialize to set up attribute and attributes cache

ignored in this case

Parameters:

  • *args (Array)

    Arguments to initialize,

Returns:

  • (Object)

    The object in question



271
272
273
274
275
276
277
# File 'lib/api_resource/attributes.rb', line 271

def initialize(*args)
  @attributes = HashWithIndifferentAccess[ self.class.attribute_names.zip([]) ]
  @attributes_cache = HashWithIndifferentAccess.new
  @previously_changed = HashWithIndifferentAccess.new
  @changed_attributes = HashWithIndifferentAccess.new
  super()
end

#make_changes_currentObject



563
564
565
566
# File 'lib/api_resource/attributes.rb', line 563

def make_changes_current
  @previously_changed = self.changes
  @changed_attributes.clear
end

#protected_attribute?(name) ⇒ Boolean

Wrapper for the class method protected_attribute?

Parameters:

  • name (Symbol)

    The name to query

Returns:

  • (Boolean)

    True if name is a protected attribute



487
488
489
# File 'lib/api_resource/attributes.rb', line 487

def protected_attribute?(name)
  self.class.protected_attribute?(name)
end

#public_attribute?(name) ⇒ Boolean

Wrapper for the class method public_attribute?

Parameters:

  • name (Symbol)

    The name to query

Returns:

  • (Boolean)

    True if name is a public attribute



497
498
499
# File 'lib/api_resource/attributes.rb', line 497

def public_attribute?(name)
  self.class.public_attribute?(name)
end

#read_attribute(attr_name) ⇒ Object

Reads an attribute typecasting if necessary and setting the cache so as to only typecast the one time. Takes a block which is called if the attribute is not found

is not found

Parameters:

  • attr_name (Symbol)

    The name of the attribute to read

Returns:

  • (Object)

    The value of the attribute or nil if it



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/api_resource/attributes.rb', line 289

def read_attribute(attr_name)
  attr_name = attr_name.to_sym

  @attributes_cache[attr_name] || @attributes_cache.fetch(attr_name) do
    data = @attributes.fetch(attr_name) do
      # This statement overrides id to return the primary key
      # if it is set to something other than :id
      if attr_name == :id && self.class.primary_key != attr_name
        return read_attribute(self.class.primary_key)
      end

      # For some reason hashes return false for key? if the value
      # at that key is nil.  It also executes this block for fetch
      # when it really shouldn't.  Detect that error here and give
      # back nil for data
      if @attributes.keys.include?(attr_name)
        nil
      else
        # In this case the attribute was truly not found, if we're
        # given a block execute that otherwise return nil
        return block_given? ? yield(attr_name) : nil
      end
    end
    # This sets and returns the typecasted value
    @attributes_cache[attr_name] = self.typecast_attribute_for_read(
      attr_name,
      data
    )
  end
end

#read_attribute_before_type_cast(attr_name) ⇒ Object

Reads the attribute directly out of the attributes hash without applying any typecasting if that attribute is not found

Parameters:

  • attr_name (Symbol)

    The name of the attribute to be read

Returns:

  • (Object)

    The untypecasted value of the attribute or nil



327
328
329
# File 'lib/api_resource/attributes.rb', line 327

def read_attribute_before_type_cast(attr_name)
  return @attributes[attr_name.to_sym]
end

#reset_changesObject



574
575
576
577
578
579
580
581
# File 'lib/api_resource/attributes.rb', line 574

def reset_changes
  self.class.attribute_names.each do |attr_name|
    attr_name = attr_name.to_sym

    reset_attribute!(attr_name)
  end
  true
end

#respond_to?(sym, include_private_methods = false) ⇒ Boolean

Override to respond_to? for finding attribute methods even if they are not defined

we should consider private methods

Parameters:

  • sym (Symbol)

    The method that we may respond to

  • include_private_methods (defaults to: false)

    = false [Boolean] Whether or not

Returns:

  • (Boolean)

    True if we respond to sym



532
533
534
535
536
537
538
539
540
541
# File 'lib/api_resource/attributes.rb', line 532

def respond_to?(sym, include_private_methods = false)
  if sym =~ /\?$/
    return true if self.attribute?($`)
  elsif sym =~ /=$/
    return true if self.class.public_attribute_names.include?($`)
  else
    return true if self.attribute?(sym.to_sym)
  end
  super
end

#save_with_dirty_tracking(*args) ⇒ Boolean

Override for the save method to update our dirty tracking of attributes

embedded in this save provided it succeeds

Parameters:

  • *args (Array)

    Used to clear changes on any associations

Returns:

  • (Boolean)

    True if the save succeeded, false otherwise



509
510
511
512
513
514
515
516
517
518
519
520
521
# File 'lib/api_resource/attributes.rb', line 509

def save_with_dirty_tracking(*args)
  if save_without_dirty_tracking(*args)
    self.make_changes_current
    if args.first.is_a?(Hash) && args.first[:include_associations]
      args.first[:include_associations].each do |assoc|
        Array.wrap(self.send(assoc).internal_object).each(&:make_changes_current)
      end
    end
    return true
  else
    return false
  end
end

#typecast_attribute_for_read(attr_name, value) ⇒ Object

Returns the typecasted value of an attribute for being read (calls from_api on the typecaster). Raises TypecasterNotFound if no typecaster exists for this attribute

Parameters:

  • attr_name (Symbol)

    The name of the attribute

  • value (Object)

    The value to be typecasted

Returns:

  • (Object)

    The typecasted value



385
386
387
388
389
# File 'lib/api_resource/attributes.rb', line 385

def typecast_attribute_for_read(attr_name, value)
  self
    .find_typecaster(attr_name)
    .from_api(value)
end

#typecast_attribute_for_write(attr_name, value) ⇒ Object

Returns the typecasted value of the attribute for being written (calls to_api on the typecaster). Raises TypecasterNotFound if no typecaster exists for this attribute

Parameters:

  • attr_name (Symbol)

    The attribute in question

  • value (Object)

    The value to be typecasted

Returns:

  • (Object)

    The typecasted value



400
401
402
403
404
# File 'lib/api_resource/attributes.rb', line 400

def typecast_attribute_for_write(attr_name, value)
  self
    .find_typecaster(attr_name)
    .to_api(value)
end

#write_attribute(attr_name, value) ⇒ Object

Writes an attribute, first typecasting it to the proper type with typecast_attribute_for_write and then setting it in the attributes hash. Raises MissingAttributeError if no such attribute exists

Parameters:

  • attr_name (Symbol)

    The name of the attribute to set

  • value (Object)

    The value to write

Returns:

  • (Object)

    The value parameter is always returned



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/api_resource/attributes.rb', line 340

def write_attribute(attr_name, value)
  attr_name = attr_name.to_sym
  # Change a write attribute for id to the primary key
  attr_name = self.class.primary_key if attr_name == :id && self.class.primary_key
  # The value we expect here should be typecasted for going to
  # the api
  typed_value = self.typecast_attribute_for_write(attr_name, value)

  if attribute_changed?(attr_name)
    old = changed_attributes[attr_name]
    changed_attributes.delete(attr_name) if old == typed_value
  else
    old = clone_attribute_value(:read_attribute, attr_name)
    changed_attributes[attr_name] = old if old != typed_value
  end

  # Remove this attribute from the attributes cache
  @attributes_cache.delete(attr_name)
  # Raise an error if this is not an attribute
  if !self.attribute?(attr_name)
    raise ActiveModel::MissingAttributeError.new(
      "can't write unknown attribute #{attr_name}",
      caller(0)
    )
  end
  # Raise another error if this is a protected attribute
  # if self.protected_attribute?(attr_name)
  #   raise ApiResource::AttributeAccessError.new(
  #     "cannot write to protected attribute #{attr_name}",
  #     caller(0)
  #   )
  # end
  @attributes[attr_name] = typed_value
  value
end