Introduce config.object_changes_adapter

Allows users to write third-party adapters giving them complete
control over the contents of the `object_changes` column.
This commit is contained in:
Ashwin Hegde 2018-05-21 17:01:05 +05:30 committed by Jared Beck
parent 28aea8b271
commit 776189de57
9 changed files with 117 additions and 2 deletions

View File

@ -11,7 +11,11 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).
### Added
- None
- [#1093](https://github.com/paper-trail-gem/paper_trail/pull/1093) -
`PaperTrail.config.object_changes_adapter` - Allows specifying an adapter that will
determine how the changes for each version are stored in the object_changes column.
An example of this implementation using the hashdiff gem can be found here:
[paper_trail-hashdiff](https://github.com/hashwin/paper_trail-hashdiff)
### Fixed

View File

@ -54,6 +54,7 @@ has been destroyed.
- [6. Extensibility](#6-extensibility)
- [6.a. Custom Version Classes](#6a-custom-version-classes)
- [6.b. Custom Serializer](#6b-custom-serializer)
- [6.c. Custom Object Changes](#6c-custom-object-changes)
- [7. Testing](#7-testing)
- [7.a. Minitest](#7a-minitest)
- [7.b. RSpec](#7b-rspec)
@ -1378,6 +1379,23 @@ class ConvertVersionsObjectToJson < ActiveRecord::Migration
end
```
### 6.c. Custom Object Changes
By default, PaperTrail stores object changes in a before/after array of objects
containing keys of columns that have changed in that particular version. You can
override this behaviour by using the object_changes_adapter config option:
```ruby
PaperTrail.config.object_changes_adapter = MyObjectChangesAdapter.new
```
A valid adapter is a class that contains the following methods:
1. diff: Returns the changeset in the desired format given the changeset in the original format
2. load_changeset: Returns the changeset for a given version object
3. where_object_changes: Returns the records resulting from the given hash of attributes.
For an example of such an implementation, see [paper_trail-hashdiff](https://github.com/hashwin/paper_trail-hashdiff)
## 7. Testing
You may want to turn PaperTrail off to speed up your tests. See [Turning

View File

@ -26,7 +26,8 @@ module PaperTrail
STR
include Singleton
attr_accessor :serializer, :version_limit, :association_reify_error_behaviour
attr_accessor :serializer, :version_limit, :association_reify_error_behaviour,
:object_changes_adapter
def initialize
# Variables which affect all threads, whose access is synchronized.

View File

@ -23,6 +23,11 @@ module PaperTrail
# @api private
def execute
if PaperTrail.config.object_changes_adapter
return PaperTrail.config.object_changes_adapter.where_object_changes(
@version_model_class, @attributes
)
end
case @version_model_class.columns_hash["object_changes"].type
when :jsonb
jsonb

View File

@ -418,6 +418,10 @@ module PaperTrail
#
# @api private
def recordable_object_changes(changes)
if PaperTrail.config.object_changes_adapter
changes = PaperTrail.config.object_changes_adapter.diff(changes)
end
if @record.class.paper_trail.version_class.object_changes_col_is_json?
changes
else

View File

@ -265,6 +265,10 @@ module PaperTrail
# @api private
def load_changeset
if PaperTrail.config.object_changes_adapter
return PaperTrail.config.object_changes_adapter.load_changeset(self)
end
# First, deserialize the `object_changes` column.
changes = HashWithIndifferentAccess.new(object_changes_deserialized)

View File

@ -16,6 +16,28 @@ module PaperTrail
end
end
context "with object_changes_adapter" do
let(:adapter) { instance_spy("CustomObjectChangesAdapter") }
before do
PaperTrail.config.object_changes_adapter = adapter
allow(adapter).to(
receive(:diff).with(
hash_including("name" => [nil, "Dashboard"])
).and_return([["name", nil, "Dashboard"]])
)
end
after do
PaperTrail.config.object_changes_adapter = nil
end
it "creates a version with custom changes" do
expect(widget.versions.last.object_changes).to eq("---\n- - name\n - \n - Dashboard\n")
expect(adapter).to have_received(:diff)
end
end
context "serializer is JSON" do
before do
PaperTrail.serializer = PaperTrail::Serializers::JSON
@ -202,6 +224,25 @@ module PaperTrail
}.to raise_error(ArgumentError)
end
context "with object_changes_adapter configured" do
after do
PaperTrail.config.object_changes_adapter = nil
end
it "calls the adapter's where_object_changes method" do
adapter = instance_spy("CustomObjectChangesAdapter")
bicycle = Bicycle.create!(name: "abc")
allow(adapter).to(
receive(:where_object_changes).with(Version, name: "abc")
).and_return(bicycle.versions[0..1])
PaperTrail.config.object_changes_adapter = adapter
expect(
bicycle.versions.where_object_changes(name: "abc")
).to match_array(bicycle.versions[0..1])
expect(adapter).to have_received(:where_object_changes)
end
end
# Only test json and jsonb columns. where_object_changes no longer
# supports text columns.
if column_datatype_override

View File

@ -51,6 +51,28 @@ RSpec.describe(::PaperTrail, versioning: true) do
expect(changeset["updated_at"][0]).to be_nil
expect(changeset["updated_at"][1].to_i).to eq(@widget.updated_at.to_i)
end
context "custom object_changes_adapter" do
let(:adapter) { instance_spy("CustomObjectChangesAdapter") }
before do
PaperTrail.config.object_changes_adapter = adapter
allow(adapter).to(
receive(:load_changeset).with(@widget.versions.last).and_return(a: "b", c: "d")
)
end
after do
PaperTrail.config.object_changes_adapter = nil
end
it "calls the adapter's load_changeset method" do
changeset = @widget.versions.last.changeset
expect(changeset[:a]).to eq("b")
expect(changeset[:c]).to eq("d")
expect(adapter).to have_received(:load_changeset)
end
end
end
context "and then updated without any changes" do

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
# This custom serializer excludes nil values
class CustomObjectChangesAdapter
def diff(changes)
changes
end
def load_changeset(version)
version.changeset
end
def where_object_changes(klass, attributes)
klass.where(attributes)
end
end