module PaperTrail::Reifier
Given a version record and some options, builds a new model object. @api private
Public Class Methods
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
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
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
@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
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 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 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
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