Merge pull request #810 from airblade/break_up_attribute_serialization
Break AttributesSerialization into small modules
This commit is contained in:
commit
829e028cb2
|
@ -8,7 +8,7 @@ Metrics/CyclomaticComplexity:
|
|||
Max: 13 # Goal: 6
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Max: 311
|
||||
Max: 313
|
||||
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 16 # Goal: 7
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
require "request_store"
|
||||
require "paper_trail/attributes_serialization"
|
||||
require "paper_trail/cleaner"
|
||||
require "paper_trail/config"
|
||||
require "paper_trail/has_paper_trail"
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
Attribute Serializers
|
||||
=====================
|
||||
|
||||
"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`.
|
|
@ -0,0 +1,58 @@
|
|||
module PaperTrail
|
||||
# :nodoc:
|
||||
module AttributeSerializers
|
||||
# The `CastAttributeSerializer` (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
|
||||
# with `LegacyActiveRecordShim`.
|
||||
if ::ActiveRecord::VERSION::MAJOR >= 5
|
||||
# This implementation uses AR 5's `serialize` and `deserialize`.
|
||||
class CastAttributeSerializer
|
||||
def initialize(klass)
|
||||
@klass = klass
|
||||
end
|
||||
|
||||
def serialize(attr, val)
|
||||
@klass.type_for_attribute(attr).serialize(val)
|
||||
end
|
||||
|
||||
def deserialize(attr, val)
|
||||
@klass.type_for_attribute(attr).deserialize(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 CastAttributeSerializer
|
||||
def initialize(klass)
|
||||
@klass = klass
|
||||
end
|
||||
|
||||
def serialize(attr, val)
|
||||
val = defined_enums[attr][val] if defined_enums[attr]
|
||||
@klass.type_for_attribute(attr).type_cast_for_database(val)
|
||||
end
|
||||
|
||||
def deserialize(attr, val)
|
||||
val = @klass.type_for_attribute(attr).type_cast_from_database(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
|
||||
end
|
||||
end
|
|
@ -0,0 +1,48 @@
|
|||
module PaperTrail
|
||||
module AttributeSerializers
|
||||
# Included into model if AR version is < 4.2. Backport Rails 4.2 and later's
|
||||
# `type_for_attribute` so we can build on a common interface.
|
||||
module LegacyActiveRecordShim
|
||||
# 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
|
||||
|
||||
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
|
||||
end
|
||||
end
|
|
@ -0,0 +1,39 @@
|
|||
require "paper_trail/attribute_serializers/cast_attribute_serializer"
|
||||
|
||||
module PaperTrail
|
||||
module AttributeSerializers
|
||||
# Serialize or deserialize the `version.object` column.
|
||||
class ObjectAttribute
|
||||
def initialize(model_class)
|
||||
@model_class = model_class
|
||||
end
|
||||
|
||||
def serialize(attributes)
|
||||
alter(attributes, :serialize)
|
||||
end
|
||||
|
||||
def deserialize(attributes)
|
||||
alter(attributes, :deserialize)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Modifies `attributes` in place.
|
||||
# TODO: Return a new hash instead.
|
||||
def alter(attributes, serialization_method)
|
||||
# Don't serialize before values before inserting into columns of type
|
||||
# `JSON` on `PostgreSQL` databases.
|
||||
return attributes if object_col_is_json?
|
||||
|
||||
serializer = CastAttributeSerializer.new(@model_class)
|
||||
attributes.each do |key, value|
|
||||
attributes[key] = serializer.send(serialization_method, key, value)
|
||||
end
|
||||
end
|
||||
|
||||
def object_col_is_json?
|
||||
@model_class.paper_trail_version_class.object_col_is_json?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,42 @@
|
|||
require "paper_trail/attribute_serializers/cast_attribute_serializer"
|
||||
|
||||
module PaperTrail
|
||||
module AttributeSerializers
|
||||
# Serialize or deserialize the `version.object_changes` column.
|
||||
class ObjectChangesAttribute
|
||||
def initialize(item_class)
|
||||
@item_class = item_class
|
||||
end
|
||||
|
||||
def serialize(changes)
|
||||
alter(changes, :serialize)
|
||||
end
|
||||
|
||||
def deserialize(changes)
|
||||
alter(changes, :deserialize)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Modifies `changes` in place.
|
||||
# TODO: Return a new hash instead.
|
||||
def alter(changes, serialization_method)
|
||||
# Don't serialize before values before inserting into columns of type
|
||||
# `JSON` on `PostgreSQL` databases.
|
||||
return changes if object_changes_col_is_json?
|
||||
|
||||
serializer = CastAttributeSerializer.new(@item_class)
|
||||
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
|
||||
|
||||
def object_changes_col_is_json?
|
||||
@item_class.paper_trail_version_class.object_changes_col_is_json?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,161 +0,0 @@
|
|||
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
|
|
@ -1,5 +1,7 @@
|
|||
require "active_support/core_ext/object" # provides the `try` method
|
||||
require "paper_trail/attributes_serialization"
|
||||
require "paper_trail/attribute_serializers/legacy_active_record_shim"
|
||||
require "paper_trail/attribute_serializers/object_attribute"
|
||||
require "paper_trail/attribute_serializers/object_changes_attribute"
|
||||
|
||||
module PaperTrail
|
||||
# Extensions to `ActiveRecord::Base`. See `frameworks/active_record.rb`.
|
||||
|
@ -110,7 +112,10 @@ module PaperTrail
|
|||
# Lazily include the instance methods so we don't clutter up
|
||||
# any more ActiveRecord models than we have to.
|
||||
send :include, InstanceMethods
|
||||
send :extend, AttributesSerialization
|
||||
|
||||
if ::ActiveRecord::VERSION::STRING < "4.2"
|
||||
send :extend, AttributeSerializers::LegacyActiveRecordShim
|
||||
end
|
||||
|
||||
class_attribute :version_association_name
|
||||
self.version_association_name = options[:version] || :version
|
||||
|
@ -444,7 +449,9 @@ module PaperTrail
|
|||
|
||||
def changes_for_paper_trail
|
||||
notable_changes = changes.delete_if { |k, _v| !notably_changed.include?(k) }
|
||||
self.class.serialize_attribute_changes_for_paper_trail!(notable_changes)
|
||||
AttributeSerializers::ObjectChangesAttribute.
|
||||
new(self.class).
|
||||
serialize(notable_changes)
|
||||
notable_changes.to_hash
|
||||
end
|
||||
|
||||
|
@ -577,7 +584,7 @@ module PaperTrail
|
|||
# ommitting attributes to be skipped.
|
||||
def object_attrs_for_paper_trail
|
||||
attrs = attributes_before_change.except(*paper_trail_options[:skip])
|
||||
self.class.serialize_attributes_for_paper_trail!(attrs)
|
||||
AttributeSerializers::ObjectAttribute.new(self.class).serialize(attrs)
|
||||
attrs
|
||||
end
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require "paper_trail/attribute_serializers/object_attribute"
|
||||
|
||||
module PaperTrail
|
||||
# Given a version record and some options, builds a new model object.
|
||||
# @api private
|
||||
|
@ -164,10 +166,9 @@ module PaperTrail
|
|||
# Set all the attributes in this version on the model.
|
||||
def reify_attributes(model, version, attrs)
|
||||
enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
|
||||
model.class.unserialize_attributes_for_paper_trail! attrs
|
||||
|
||||
AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
|
||||
attrs.each do |k, v|
|
||||
# `unserialize_attributes_for_paper_trail!` will return the mapped enum value
|
||||
# `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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require "active_support/concern"
|
||||
require "paper_trail/attribute_serializers/object_changes_attribute"
|
||||
|
||||
module PaperTrail
|
||||
# Originally, PaperTrail did not provide this module, and all of this
|
||||
|
@ -284,7 +285,7 @@ module PaperTrail
|
|||
# First, deserialize the `object_changes` column.
|
||||
changes = HashWithIndifferentAccess.new(object_changes_deserialized)
|
||||
|
||||
# The next step is, perhaps unfortunately, called "un-serialization",
|
||||
# The next step is, perhaps unfortunately, called "de-serialization",
|
||||
# and appears to be responsible for custom attribute serializers. For an
|
||||
# example of a custom attribute serializer, see
|
||||
# `Person::TimeZoneSerializer` in the test suite.
|
||||
|
@ -296,7 +297,9 @@ module PaperTrail
|
|||
#
|
||||
# Note: `item` returns nil if `event` is "destroy".
|
||||
unless item.nil?
|
||||
item.class.unserialize_attribute_changes_for_paper_trail!(changes)
|
||||
AttributeSerializers::ObjectChangesAttribute.
|
||||
new(item.class).
|
||||
deserialize(changes)
|
||||
end
|
||||
|
||||
# Finally, return a Hash mapping each attribute name to
|
||||
|
|
Loading…
Reference in New Issue