2015-12-09 10:58:27 -05:00
|
|
|
module PaperTrail
|
2016-03-13 16:02:43 -04:00
|
|
|
# "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`.
|
2015-12-09 10:58:27 -05:00
|
|
|
module AttributesSerialization
|
2016-04-09 01:08:34 -04:00
|
|
|
# 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.
|
2015-12-09 10:58:27 -05:00
|
|
|
class NoOpAttribute
|
|
|
|
def type_cast_for_database(value)
|
|
|
|
value
|
|
|
|
end
|
|
|
|
|
|
|
|
def type_cast_from_database(data)
|
|
|
|
data
|
|
|
|
end
|
|
|
|
end
|
|
|
|
NO_OP_ATTRIBUTE = NoOpAttribute.new
|
|
|
|
|
2016-04-09 01:08:34 -04:00
|
|
|
# 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.
|
2015-12-09 10:58:27 -05:00
|
|
|
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
|
|
|
|
|
2016-03-13 16:02:43 -04:00
|
|
|
# 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.
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
class AbstractSerializer
|
|
|
|
def initialize(klass)
|
|
|
|
@klass = klass
|
2015-12-09 10:58:27 -05:00
|
|
|
end
|
|
|
|
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
private
|
|
|
|
|
|
|
|
def apply_serialization(method, attr, val)
|
|
|
|
@klass.type_for_attribute(attr).send(method, val)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if ::ActiveRecord::VERSION::MAJOR >= 5
|
2016-04-09 01:08:34 -04:00
|
|
|
# This implementation uses AR 5's `serialize` and `deserialize`.
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
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
|
2016-04-09 01:08:34 -04:00
|
|
|
# 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`.
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
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
|
|
|
|
|
2016-04-09 01:08:34 -04:00
|
|
|
# Backport Rails 4.2 and later's `type_for_attribute` so we can build
|
2016-03-13 16:02:43 -04:00
|
|
|
# on a common interface.
|
2016-03-05 17:07:32 -05:00
|
|
|
if ::ActiveRecord::VERSION::STRING < "4.2"
|
2015-12-09 10:58:27 -05:00
|
|
|
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)
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
alter_attributes_for_paper_trail!(:serialize, attributes)
|
2015-12-09 10:58:27 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def unserialize_attributes_for_paper_trail!(attributes)
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
alter_attributes_for_paper_trail!(:deserialize, attributes)
|
2015-12-09 10:58:27 -05:00
|
|
|
end
|
|
|
|
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
def alter_attributes_for_paper_trail!(serialization_method, attributes)
|
2015-12-09 10:58:27 -05:00
|
|
|
# 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?
|
|
|
|
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
serializer = CastedAttributeSerializer.new(self)
|
2015-12-09 10:58:27 -05:00
|
|
|
attributes.each do |key, value|
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
attributes[key] = serializer.send(serialization_method, key, value)
|
2015-12-09 10:58:27 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Used for Version#object_changes attribute.
|
|
|
|
def serialize_attribute_changes_for_paper_trail!(changes)
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
alter_attribute_changes_for_paper_trail!(:serialize, changes)
|
2015-12-09 10:58:27 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def unserialize_attribute_changes_for_paper_trail!(changes)
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
alter_attribute_changes_for_paper_trail!(:deserialize, changes)
|
2015-12-09 10:58:27 -05:00
|
|
|
end
|
|
|
|
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
def alter_attribute_changes_for_paper_trail!(serialization_method, changes)
|
2015-12-09 10:58:27 -05:00
|
|
|
# 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?
|
|
|
|
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
serializer = CastedAttributeSerializer.new(self)
|
2015-12-09 10:58:27 -05:00
|
|
|
changes.clone.each do |key, change|
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
changes[key] = Array(change).map do |value|
|
|
|
|
serializer.send(serialization_method, key, value)
|
|
|
|
end
|
2015-12-09 10:58:27 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|