1
0
Fork 0
mirror of https://github.com/paper-trail-gem/paper_trail.git synced 2022-11-09 11:33:19 -05:00
paper-trail-gem--paper_trail/lib/paper_trail/attributes_serialization.rb

161 lines
5.5 KiB
Ruby

module PaperTrail
# "Serialization" here refers to the preparation of data for insertion into a
# database, particularly the `object` and `object_changes` columns in the
# `versions` table.
#
# Likewise, "deserialization" refers to any processing of data after they
# have been read from the database, for example preparing the result of
# `VersionConcern#changeset`.
module AttributesSerialization
# An attribute which needs no processing. It is part of our backport (shim)
# of rails 4.2's attribute API. See `type_for_attribute` below.
class NoOpAttribute
def type_cast_for_database(value)
value
end
def type_cast_from_database(data)
data
end
end
NO_OP_ATTRIBUTE = NoOpAttribute.new
# An attribute which requires manual (de)serialization to/from what we get
# from the database. It is part of our backport (shim) of rails 4.2's
# attribute API. See `type_for_attribute` below.
class SerializedAttribute
def initialize(coder)
@coder = coder.respond_to?(:dump) ? coder : PaperTrail.serializer
end
def type_cast_for_database(value)
@coder.dump(value)
end
def type_cast_from_database(data)
@coder.load(data)
end
end
# The `AbstractSerializer` (de)serializes model attribute values. For
# example, the string "1.99" serializes into the integer `1` when assigned
# to an attribute of type `ActiveRecord::Type::Integer`.
#
# This implementation depends on the `type_for_attribute` method, which was
# introduced in rails 4.2. In older versions of rails, we shim this method
# below.
#
# At runtime, the `AbstractSerializer` has only one child class, the
# `CastedAttributeSerializer`, whose implementation varies depending on the
# version of ActiveRecord.
class AbstractSerializer
def initialize(klass)
@klass = klass
end
private
def apply_serialization(method, attr, val)
@klass.type_for_attribute(attr).send(method, val)
end
end
if ::ActiveRecord::VERSION::MAJOR >= 5
# This implementation uses AR 5's `serialize` and `deserialize`.
class CastedAttributeSerializer < AbstractSerializer
def serialize(attr, val)
apply_serialization(:serialize, attr, val)
end
def deserialize(attr, val)
apply_serialization(:deserialize, attr, val)
end
end
else
# This implementation uses AR 4.2's `type_cast_for_database`. For
# versions of AR < 4.2 we provide an implementation of
# `type_cast_for_database` in our shim attribute type classes,
# `NoOpAttribute` and `SerializedAttribute`.
class CastedAttributeSerializer < AbstractSerializer
def serialize(attr, val)
val = defined_enums[attr][val] if defined_enums[attr]
apply_serialization(:type_cast_for_database, attr, val)
end
def deserialize(attr, val)
val = apply_serialization(:type_cast_from_database, attr, val)
if defined_enums[attr]
defined_enums[attr].key(val)
else
val
end
end
private
def defined_enums
@defined_enums ||= (@klass.respond_to?(:defined_enums) ? @klass.defined_enums : {})
end
end
end
# Backport Rails 4.2 and later's `type_for_attribute` so we can build
# on a common interface.
if ::ActiveRecord::VERSION::STRING < "4.2"
def type_for_attribute(attr_name)
serialized_attribute_types[attr_name.to_s] || NO_OP_ATTRIBUTE
end
def serialized_attribute_types
@attribute_types ||= Hash[serialized_attributes.map do |attr_name, coder|
[attr_name, SerializedAttribute.new(coder)]
end]
end
private :serialized_attribute_types
end
# Used for `Version#object` attribute.
def serialize_attributes_for_paper_trail!(attributes)
alter_attributes_for_paper_trail!(:serialize, attributes)
end
def unserialize_attributes_for_paper_trail!(attributes)
alter_attributes_for_paper_trail!(:deserialize, attributes)
end
def alter_attributes_for_paper_trail!(serialization_method, attributes)
# Don't serialize before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
return attributes if paper_trail_version_class.object_col_is_json?
serializer = CastedAttributeSerializer.new(self)
attributes.each do |key, value|
attributes[key] = serializer.send(serialization_method, key, value)
end
end
# Used for Version#object_changes attribute.
def serialize_attribute_changes_for_paper_trail!(changes)
alter_attribute_changes_for_paper_trail!(:serialize, changes)
end
def unserialize_attribute_changes_for_paper_trail!(changes)
alter_attribute_changes_for_paper_trail!(:deserialize, changes)
end
def alter_attribute_changes_for_paper_trail!(serialization_method, changes)
# Don't serialize before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
return changes if paper_trail_version_class.object_changes_col_is_json?
serializer = CastedAttributeSerializer.new(self)
changes.clone.each do |key, change|
# `change` is an Array with two elements, representing before and after.
changes[key] = Array(change).map do |value|
serializer.send(serialization_method, key, value)
end
end
end
end
end