module PaperTrail::Reifier

Given a version record and some options, builds a new model object. @api private

Public Class Methods

reify(version, options) click to toggle source

See ‘VersionConcern#reify` for documentation. @api private

# File lib/paper_trail/reifier.rb, line 12
def reify(version, options)
  options = apply_defaults_to(options, version)
  attrs = version.object_deserialized
  model = init_model(attrs, options, version)
  reify_attributes(model, version, attrs)
  model.send "#{model.class.version_association_name}=", version
  model
end

Private Class Methods

apply_defaults_to(options, version) click to toggle source

Given a hash of ‘options` for `.reify`, return a new hash with default values applied. @api private

# File lib/paper_trail/reifier.rb, line 26
def apply_defaults_to(options, version)
  {
    version_at: version.created_at,
    mark_for_destruction: false,
    has_one: false,
    has_many: false,
    belongs_to: false,
    has_and_belongs_to_many: false,
    unversioned_attributes: :nil
  }.merge(options)
end
init_model(attrs, options, version) click to toggle source

Initialize a model object suitable for reifying ‘version` into. Does not perform reification, merely instantiates the appropriate model class and, if specified by `options`, sets unversioned attributes to `nil`.

Normally a polymorphic belongs_to relationship allows us to get the object we belong to by calling, in this case, ‘item`. However this returns nil if `item` has been destroyed, and we need to be able to retrieve destroyed objects.

In this situation we constantize the ‘item_type` to get hold of the class…except when the stored object’s attributes include a ‘type` key. If this is the case, the object we belong to is using single table inheritance (STI) and the `item_type` will be the base class, not the actual subclass. If `type` is present but empty, the class is the base class.

# File lib/paper_trail/reifier.rb, line 54
def init_model(attrs, options, version)
  klass = version_reification_class(version, attrs)

  # The `dup` option and destroyed version always returns a new object,
  # otherwise we should attempt to load item or to look for the item
  # outside of default scope(s).
  model = if options[:dup] == true || version.event == "destroy"
            klass.new
          else
            version.item || init_model_by_finding_item_id(klass, version) || klass.new
          end

  if options[:unversioned_attributes] == :nil && !model.new_record?
    init_unversioned_attrs(attrs, model)
  end

  model
end
init_model_by_finding_item_id(klass, version) click to toggle source

@api private

# File lib/paper_trail/reifier.rb, line 74
def init_model_by_finding_item_id(klass, version)
  klass.unscoped.where(klass.primary_key => version.item_id).first
end
init_unversioned_attrs(attrs, model) click to toggle source

Look for attributes that exist in ‘model` and not in this version. These attributes should be set to nil. Modifies `attrs`. @api private

# File lib/paper_trail/reifier.rb, line 81
def init_unversioned_attrs(attrs, model)
  (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
end
reify_attribute(k, v, model, version) click to toggle source

Reify onto ‘model` an attribute named `k` with value `v` from `version`.

‘ObjectAttribute#deserialize` will return the mapped enum value and in Rails < 5, the []= uses the integer type caster from the column definition (in general) and thus will turn a (usually) string to 0 instead of the correct value.

@api private

# File lib/paper_trail/reifier.rb, line 93
def reify_attribute(k, v, model, version)
  if model.has_attribute?(k)
    model[k.to_sym] = v
  elsif model.respond_to?("#{k}=")
    model.send("#{k}=", v)
  elsif version.logger
    version.logger.warn(
      "Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
    )
  end
end
reify_attributes(model, version, attrs) click to toggle source

Reify onto ‘model` all the attributes of `version`. @api private

# File lib/paper_trail/reifier.rb, line 107
def reify_attributes(model, version, attrs)
  AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
  attrs.each do |k, v|
    reify_attribute(k, v, model, version)
  end
end
version_reification_class(version, attrs) click to toggle source

Given a ‘version`, return the class to reify. This method supports Single Table Inheritance (STI) with custom inheritance columns and custom inheritance column values.

For example, imagine a ‘version` whose `item_type` is “Animal”. The `animals` table is an STI table (it has cats and dogs) and it has a custom inheritance column, `species`. If `attrs` is “Dog”, this method returns the constant `Dog`. If `attrs` is blank, this method returns the constant `Animal`.

The values contained in the inheritance columns may be non-camelized strings (e.g. ‘dog’ instead of ‘Dog’). To reify classes in this case we need to call the parents class ‘sti_class_for` method to retrieve the correct record class.

You can see these particular examples in action in ‘spec/models/animal_spec.rb` and `spec/models/plant_spec.rb`

# File lib/paper_trail/reifier.rb, line 131
def version_reification_class(version, attrs)
  clazz = version.item_type.constantize
  inheritance_column_name = clazz.inheritance_column
  inher_col_value = attrs[inheritance_column_name]
  return clazz if inher_col_value.blank?

  # Rails 6.1 adds a public method for clients to use to customize STI classes. If that
  # method is not available, fall back to using the private one
  if clazz.public_methods.include?(:sti_class_for)
    return clazz.sti_class_for(inher_col_value)
  end

  clazz.send(:find_sti_class, inher_col_value)
end