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
|
Max: 13 # Goal: 6
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Max: 311
|
Max: 313
|
||||||
|
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 16 # Goal: 7
|
Max: 16 # Goal: 7
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
require "request_store"
|
require "request_store"
|
||||||
require "paper_trail/attributes_serialization"
|
|
||||||
require "paper_trail/cleaner"
|
require "paper_trail/cleaner"
|
||||||
require "paper_trail/config"
|
require "paper_trail/config"
|
||||||
require "paper_trail/has_paper_trail"
|
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 "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
|
module PaperTrail
|
||||||
# Extensions to `ActiveRecord::Base`. See `frameworks/active_record.rb`.
|
# 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
|
# Lazily include the instance methods so we don't clutter up
|
||||||
# any more ActiveRecord models than we have to.
|
# any more ActiveRecord models than we have to.
|
||||||
send :include, InstanceMethods
|
send :include, InstanceMethods
|
||||||
send :extend, AttributesSerialization
|
|
||||||
|
if ::ActiveRecord::VERSION::STRING < "4.2"
|
||||||
|
send :extend, AttributeSerializers::LegacyActiveRecordShim
|
||||||
|
end
|
||||||
|
|
||||||
class_attribute :version_association_name
|
class_attribute :version_association_name
|
||||||
self.version_association_name = options[:version] || :version
|
self.version_association_name = options[:version] || :version
|
||||||
|
@ -444,7 +449,9 @@ module PaperTrail
|
||||||
|
|
||||||
def changes_for_paper_trail
|
def changes_for_paper_trail
|
||||||
notable_changes = changes.delete_if { |k, _v| !notably_changed.include?(k) }
|
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
|
notable_changes.to_hash
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -577,7 +584,7 @@ module PaperTrail
|
||||||
# ommitting attributes to be skipped.
|
# ommitting attributes to be skipped.
|
||||||
def object_attrs_for_paper_trail
|
def object_attrs_for_paper_trail
|
||||||
attrs = attributes_before_change.except(*paper_trail_options[:skip])
|
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
|
attrs
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
require "paper_trail/attribute_serializers/object_attribute"
|
||||||
|
|
||||||
module PaperTrail
|
module PaperTrail
|
||||||
# Given a version record and some options, builds a new model object.
|
# Given a version record and some options, builds a new model object.
|
||||||
# @api private
|
# @api private
|
||||||
|
@ -164,10 +166,9 @@ module PaperTrail
|
||||||
# Set all the attributes in this version on the model.
|
# Set all the attributes in this version on the model.
|
||||||
def reify_attributes(model, version, attrs)
|
def reify_attributes(model, version, attrs)
|
||||||
enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
|
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|
|
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
|
# 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
|
# definition (in general) and thus will turn a (usually) string to 0 instead
|
||||||
# of the correct value
|
# of the correct value
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
require "active_support/concern"
|
require "active_support/concern"
|
||||||
|
require "paper_trail/attribute_serializers/object_changes_attribute"
|
||||||
|
|
||||||
module PaperTrail
|
module PaperTrail
|
||||||
# Originally, PaperTrail did not provide this module, and all of this
|
# Originally, PaperTrail did not provide this module, and all of this
|
||||||
|
@ -284,7 +285,7 @@ module PaperTrail
|
||||||
# First, deserialize the `object_changes` column.
|
# First, deserialize the `object_changes` column.
|
||||||
changes = HashWithIndifferentAccess.new(object_changes_deserialized)
|
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
|
# and appears to be responsible for custom attribute serializers. For an
|
||||||
# example of a custom attribute serializer, see
|
# example of a custom attribute serializer, see
|
||||||
# `Person::TimeZoneSerializer` in the test suite.
|
# `Person::TimeZoneSerializer` in the test suite.
|
||||||
|
@ -296,7 +297,9 @@ module PaperTrail
|
||||||
#
|
#
|
||||||
# Note: `item` returns nil if `event` is "destroy".
|
# Note: `item` returns nil if `event` is "destroy".
|
||||||
unless item.nil?
|
unless item.nil?
|
||||||
item.class.unserialize_attribute_changes_for_paper_trail!(changes)
|
AttributeSerializers::ObjectChangesAttribute.
|
||||||
|
new(item.class).
|
||||||
|
deserialize(changes)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Finally, return a Hash mapping each attribute name to
|
# Finally, return a Hash mapping each attribute name to
|
||||||
|
|
Loading…
Reference in New Issue