mongo_trails 10.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +2 -0
  3. data/.gitignore +1 -0
  4. data/.travis.yml +13 -0
  5. data/Appraisals +7 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +62 -0
  8. data/LICENSE +20 -0
  9. data/README.md +36 -0
  10. data/Rakefile +13 -0
  11. data/gemfiles/rails_5.gemfile +9 -0
  12. data/gemfiles/rails_5.gemfile.lock +63 -0
  13. data/gemfiles/rails_6.gemfile +9 -0
  14. data/gemfiles/rails_6.gemfile.lock +63 -0
  15. data/lib/mongo_trails.rb +154 -0
  16. data/lib/mongo_trails/attribute_serializers/README.md +10 -0
  17. data/lib/mongo_trails/attribute_serializers/attribute_serializer_factory.rb +27 -0
  18. data/lib/mongo_trails/attribute_serializers/cast_attribute_serializer.rb +51 -0
  19. data/lib/mongo_trails/attribute_serializers/object_attribute.rb +41 -0
  20. data/lib/mongo_trails/attribute_serializers/object_changes_attribute.rb +44 -0
  21. data/lib/mongo_trails/cleaner.rb +60 -0
  22. data/lib/mongo_trails/compatibility.rb +51 -0
  23. data/lib/mongo_trails/config.rb +41 -0
  24. data/lib/mongo_trails/events/base.rb +323 -0
  25. data/lib/mongo_trails/events/create.rb +32 -0
  26. data/lib/mongo_trails/events/destroy.rb +42 -0
  27. data/lib/mongo_trails/events/update.rb +60 -0
  28. data/lib/mongo_trails/frameworks/cucumber.rb +33 -0
  29. data/lib/mongo_trails/frameworks/rails.rb +4 -0
  30. data/lib/mongo_trails/frameworks/rails/controller.rb +109 -0
  31. data/lib/mongo_trails/frameworks/rails/engine.rb +43 -0
  32. data/lib/mongo_trails/frameworks/rspec.rb +43 -0
  33. data/lib/mongo_trails/frameworks/rspec/helpers.rb +29 -0
  34. data/lib/mongo_trails/has_paper_trail.rb +86 -0
  35. data/lib/mongo_trails/model_config.rb +249 -0
  36. data/lib/mongo_trails/mongo_support/config.rb +9 -0
  37. data/lib/mongo_trails/mongo_support/version.rb +56 -0
  38. data/lib/mongo_trails/queries/versions/where_object.rb +65 -0
  39. data/lib/mongo_trails/queries/versions/where_object_changes.rb +75 -0
  40. data/lib/mongo_trails/record_history.rb +51 -0
  41. data/lib/mongo_trails/record_trail.rb +304 -0
  42. data/lib/mongo_trails/reifier.rb +130 -0
  43. data/lib/mongo_trails/request.rb +166 -0
  44. data/lib/mongo_trails/serializers/json.rb +46 -0
  45. data/lib/mongo_trails/serializers/yaml.rb +43 -0
  46. data/lib/mongo_trails/type_serializers/postgres_array_serializer.rb +48 -0
  47. data/lib/mongo_trails/version_concern.rb +336 -0
  48. data/lib/mongo_trails/version_number.rb +23 -0
  49. data/mongo_trails.gemspec +38 -0
  50. metadata +180 -0
@@ -0,0 +1,10 @@
1
+ Attribute Serializers
2
+ =====================
3
+
4
+ "Serialization" here refers to the preparation of data for insertion into a
5
+ database, particularly the `object` and `object_changes` columns in the
6
+ `versions` table.
7
+
8
+ Likewise, "deserialization" refers to any processing of data after they
9
+ have been read from the database, for example preparing the result of
10
+ `VersionConcern#changeset`.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mongo_trails/type_serializers/postgres_array_serializer"
4
+
5
+ module PaperTrail
6
+ module AttributeSerializers
7
+ # Values returned by some Active Record serializers are
8
+ # not suited for writing JSON to a text column. This factory
9
+ # replaces certain default Active Record serializers
10
+ # with custom PaperTrail ones.
11
+ module AttributeSerializerFactory
12
+ AR_PG_ARRAY_CLASS = "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array"
13
+
14
+ def self.for(klass, attr)
15
+ active_record_serializer = klass.type_for_attribute(attr)
16
+ if active_record_serializer.class.name == AR_PG_ARRAY_CLASS
17
+ TypeSerializers::PostgresArraySerializer.new(
18
+ active_record_serializer.subtype,
19
+ active_record_serializer.delimiter
20
+ )
21
+ else
22
+ active_record_serializer
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mongo_trails/attribute_serializers/attribute_serializer_factory"
4
+
5
+ module PaperTrail
6
+ # :nodoc:
7
+ module AttributeSerializers
8
+ # The `CastAttributeSerializer` (de)serializes model attribute values. For
9
+ # example, the string "1.99" serializes into the integer `1` when assigned
10
+ # to an attribute of type `ActiveRecord::Type::Integer`.
11
+ #
12
+ # This implementation depends on the `type_for_attribute` method, which was
13
+ # introduced in rails 4.2. As of PT 8, we no longer support rails < 4.2.
14
+ class CastAttributeSerializer
15
+ def initialize(klass)
16
+ @klass = klass
17
+ end
18
+
19
+ private
20
+
21
+ # Returns a hash mapping attributes to hashes that map strings to
22
+ # integers. Example:
23
+ #
24
+ # ```
25
+ # { "status" => { "draft"=>0, "published"=>1, "archived"=>2 } }
26
+ # ```
27
+ #
28
+ # ActiveRecord::Enum was added in AR 4.1
29
+ # http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums
30
+ def defined_enums
31
+ @defined_enums ||= (@klass.respond_to?(:defined_enums) ? @klass.defined_enums : {})
32
+ end
33
+ end
34
+
35
+ # Uses AR 5's `serialize` and `deserialize`.
36
+ class CastAttributeSerializer
37
+ def serialize(attr, val)
38
+ AttributeSerializerFactory.for(@klass, attr).serialize(val)
39
+ end
40
+
41
+ def deserialize(attr, val)
42
+ if defined_enums[attr] && val.is_a?(::String)
43
+ # Because PT 4 used to save the string version of enums to `object_changes`
44
+ val
45
+ else
46
+ AttributeSerializerFactory.for(@klass, attr).deserialize(val)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mongo_trails/attribute_serializers/cast_attribute_serializer"
4
+
5
+ module PaperTrail
6
+ module AttributeSerializers
7
+ # Serialize or deserialize the `version.object` column.
8
+ class ObjectAttribute
9
+ def initialize(model_class)
10
+ @model_class = model_class
11
+ end
12
+
13
+ def serialize(attributes)
14
+ alter(attributes, :serialize)
15
+ end
16
+
17
+ def deserialize(attributes)
18
+ alter(attributes, :deserialize)
19
+ end
20
+
21
+ private
22
+
23
+ # Modifies `attributes` in place.
24
+ # TODO: Return a new hash instead.
25
+ def alter(attributes, serialization_method)
26
+ # Don't serialize before values before inserting into columns of type
27
+ # `JSON` on `PostgreSQL` databases.
28
+ return attributes if object_col_is_json?
29
+
30
+ serializer = CastAttributeSerializer.new(@model_class)
31
+ attributes.each do |key, value|
32
+ attributes[key] = serializer.send(serialization_method, key, value)
33
+ end
34
+ end
35
+
36
+ def object_col_is_json?
37
+ @model_class.paper_trail.version_class.object_col_is_json?
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mongo_trails/attribute_serializers/cast_attribute_serializer"
4
+
5
+ module PaperTrail
6
+ module AttributeSerializers
7
+ # Serialize or deserialize the `version.object_changes` column.
8
+ class ObjectChangesAttribute
9
+ def initialize(item_class)
10
+ @item_class = item_class
11
+ end
12
+
13
+ def serialize(changes)
14
+ alter(changes, :serialize)
15
+ end
16
+
17
+ def deserialize(changes)
18
+ alter(changes, :deserialize)
19
+ end
20
+
21
+ private
22
+
23
+ # Modifies `changes` in place.
24
+ # TODO: Return a new hash instead.
25
+ def alter(changes, serialization_method)
26
+ # Don't serialize before values before inserting into columns of type
27
+ # `JSON` on `PostgreSQL` databases.
28
+ return changes if object_changes_col_is_json?
29
+
30
+ serializer = CastAttributeSerializer.new(@item_class)
31
+ changes.clone.each do |key, change|
32
+ # `change` is an Array with two elements, representing before and after.
33
+ changes[key] = Array(change).map do |value|
34
+ serializer.send(serialization_method, key, value)
35
+ end
36
+ end
37
+ end
38
+
39
+ def object_changes_col_is_json?
40
+ @item_class.paper_trail.version_class.object_changes_col_is_json?
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ # Utilities for deleting version records.
5
+ module Cleaner
6
+ # Destroys all but the most recent version(s) for items on a given date
7
+ # (or on all dates). Useful for deleting drafts.
8
+ #
9
+ # Options:
10
+ #
11
+ # - :keeping - An `integer` indicating the number of versions to be kept for
12
+ # each item per date. Defaults to `1`. The most recent matching versions
13
+ # are kept.
14
+ # - :date - Should either be a `Date` object specifying which date to
15
+ # destroy versions for or `:all`, which will specify that all dates
16
+ # should be cleaned. Defaults to `:all`.
17
+ # - :item_id - The `id` for the item to be cleaned on, or `nil`, which
18
+ # causes all items to be cleaned. Defaults to `nil`.
19
+ #
20
+ def clean_versions!(options = {})
21
+ options = { keeping: 1, date: :all }.merge(options)
22
+ gather_versions(options[:item_id], options[:date]).each do |_item_id, item_versions|
23
+ group_versions_by_date(item_versions).each do |_date, date_versions|
24
+ # Remove the number of versions we wish to keep from the collection
25
+ # of versions prior to destruction.
26
+ date_versions.pop(options[:keeping])
27
+ date_versions.map(&:destroy)
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # Returns a hash of versions grouped by the `item_id` attribute formatted
35
+ # like this: {:item_id => PaperTrail::Version}. If `item_id` or `date` is
36
+ # set, versions will be narrowed to those pointing at items with those ids
37
+ # that were created on specified date. Versions are returned in
38
+ # chronological order.
39
+ def gather_versions(item_id = nil, date = :all)
40
+ unless date == :all || date.respond_to?(:to_date)
41
+ raise ArgumentError, "Expected date to be a Timestamp or :all"
42
+ end
43
+ versions = item_id ? PaperTrail::Version.where(item_id: item_id) : PaperTrail::Version
44
+ versions = versions.order(PaperTrail::Version.timestamp_sort_order)
45
+ versions = versions.between(date.to_date, date.to_date + 1.day) unless date == :all
46
+
47
+ # If `versions` has not been converted to an ActiveRecord::Relation yet,
48
+ # do so now.
49
+ versions = PaperTrail::Version.all if versions == PaperTrail::Version
50
+ versions.group_by(&:item_id)
51
+ end
52
+
53
+ # Given an array of versions, returns a hash mapping dates to arrays of
54
+ # versions.
55
+ # @api private
56
+ def group_versions_by_date(versions)
57
+ versions.group_by { |v| v.created_at.to_date }
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ # Rails does not follow SemVer, makes breaking changes in minor versions.
5
+ # Breaking changes are expected, and are generally good for the rails
6
+ # ecosystem. However, they often require dozens of hours to fix, even with the
7
+ # [help of experts](https://github.com/paper-trail-gem/mongo_trails/pull/899).
8
+ #
9
+ # It is not safe to assume that a new version of rails will be compatible with
10
+ # PaperTrail. PT is only compatible with the versions of rails that it is
11
+ # tested against. See `.travis.yml`.
12
+ #
13
+ # However, as of
14
+ # [#1213](https://github.com/paper-trail-gem/mongo_trails/pull/1213) our
15
+ # gemspec allows installation with newer, incompatible rails versions. We hope
16
+ # this will make it easier for contributors to work on compatibility with
17
+ # newer rails versions. Most PT users should avoid incompatible rails
18
+ # versions.
19
+ module Compatibility
20
+ ACTIVERECORD_GTE = ">= 5.2" # enforced in gemspec
21
+ ACTIVERECORD_LT = "< 6.1" # not enforced in gemspec
22
+
23
+ E_INCOMPATIBLE_AR = <<-EOS
24
+ PaperTrail %s is not compatible with ActiveRecord %s. We allow PT
25
+ contributors to install incompatible versions of ActiveRecord, and this
26
+ warning can be silenced with an environment variable, but this is a bad
27
+ idea for normal use. Please install a compatible version of ActiveRecord
28
+ instead (%s). Please see the discussion in mongo_trails/compatibility.rb
29
+ for details.
30
+ EOS
31
+
32
+ # Normal users need a warning if they accidentally install an incompatible
33
+ # version of ActiveRecord. Contributors can silence this warning with an
34
+ # environment variable.
35
+ def self.check_activerecord(ar_version)
36
+ raise ::TypeError unless ar_version.instance_of?(::Gem::Version)
37
+ return if ::ENV["PT_SILENCE_AR_COMPAT_WARNING"].present?
38
+ req = ::Gem::Requirement.new([ACTIVERECORD_GTE, ACTIVERECORD_LT])
39
+ unless req.satisfied_by?(ar_version)
40
+ ::Kernel.warn(
41
+ format(
42
+ E_INCOMPATIBLE_AR,
43
+ ::PaperTrail.gem_version,
44
+ ar_version,
45
+ req
46
+ )
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "mongo_trails/serializers/yaml"
5
+
6
+ module PaperTrail
7
+ # Global configuration affecting all threads. Some thread-specific
8
+ # configuration can be found in `paper_trail.rb`, others in `controller.rb`.
9
+ class Config
10
+ include Singleton
11
+
12
+ attr_accessor(
13
+ :association_reify_error_behaviour,
14
+ :object_changes_adapter,
15
+ :serializer,
16
+ :version_limit,
17
+ :has_paper_trail_defaults,
18
+ :mongo_config,
19
+ :mongo_prefix
20
+ )
21
+
22
+ def initialize
23
+ # Variables which affect all threads, whose access is synchronized.
24
+ @mutex = Mutex.new
25
+ @enabled = true
26
+
27
+ # Variables which affect all threads, whose access is *not* synchronized.
28
+ @serializer = PaperTrail::Serializers::YAML
29
+ @has_paper_trail_defaults = {}
30
+ end
31
+
32
+ # Indicates whether PaperTrail is on or off. Default: true.
33
+ def enabled
34
+ @mutex.synchronize { !!@enabled }
35
+ end
36
+
37
+ def enabled=(enable)
38
+ @mutex.synchronize { @enabled = enable }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Events
5
+ # We refer to times in the lifecycle of a record as "events". There are
6
+ # three events:
7
+ #
8
+ # - create
9
+ # - `after_create` we call `RecordTrail#record_create`
10
+ # - update
11
+ # - `after_update` we call `RecordTrail#record_update`
12
+ # - `after_touch` we call `RecordTrail#record_update`
13
+ # - `RecordTrail#save_with_version` calls `RecordTrail#record_update`
14
+ # - `RecordTrail#update_columns` is also referred to as an update, though
15
+ # it uses `RecordTrail#record_update_columns` rather than
16
+ # `RecordTrail#record_update`
17
+ # - destroy
18
+ # - `before_destroy` or `after_destroy` we call `RecordTrail#record_destroy`
19
+ #
20
+ # The value inserted into the `event` column of the versions table can also
21
+ # be overridden by the user, with `paper_trail_event`.
22
+ #
23
+ # @api private
24
+ class Base
25
+ RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
26
+
27
+ # @api private
28
+ def initialize(record, in_after_callback)
29
+ @record = record
30
+ @in_after_callback = in_after_callback
31
+ end
32
+
33
+ # Determines whether it is appropriate to generate a new version
34
+ # instance. A timestamp-only update (e.g. only `updated_at` changed) is
35
+ # considered notable unless an ignored attribute was also changed.
36
+ #
37
+ # @api private
38
+ def changed_notably?
39
+ if ignored_attr_has_changed?
40
+ timestamps = @record.send(:timestamp_attributes_for_update_in_model).map(&:to_s)
41
+ (notably_changed - timestamps).any?
42
+ else
43
+ notably_changed.any?
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
50
+ # https://github.com/paper-trail-gem/mongo_trails/pull/899
51
+ #
52
+ # @api private
53
+ def attribute_changed_in_latest_version?(attr_name)
54
+ if @in_after_callback && RAILS_GTE_5_1
55
+ @record.saved_change_to_attribute?(attr_name.to_s)
56
+ else
57
+ @record.attribute_changed?(attr_name.to_s)
58
+ end
59
+ end
60
+
61
+ # @api private
62
+ def nonskipped_attributes_before_change(is_touch)
63
+ cache_changed_attributes do
64
+ record_attributes = @record.attributes.except(*@record.paper_trail_options[:skip])
65
+
66
+ record_attributes.each_key do |k|
67
+ if @record.class.column_names.include?(k)
68
+ record_attributes[k] = attribute_in_previous_version(k, is_touch)
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`.
75
+ # @api private
76
+ def cache_changed_attributes
77
+ if RAILS_GTE_5_1
78
+ # Everything works fine as it is
79
+ yield
80
+ else
81
+ # Any particular call to `changed_attributes` produces the huge memory allocation.
82
+ # Lets use the generic AR workaround for that.
83
+ @record.send(:cache_changed_attributes) { yield }
84
+ end
85
+ end
86
+
87
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
88
+ # https://github.com/paper-trail-gem/mongo_trails/pull/899
89
+ #
90
+ # Event can be any of the three (create, update, destroy).
91
+ #
92
+ # @api private
93
+ def attribute_in_previous_version(attr_name, is_touch)
94
+ if RAILS_GTE_5_1
95
+ if @in_after_callback && !is_touch
96
+ # For most events, we want the original value of the attribute, before
97
+ # the last save.
98
+ @record.attribute_before_last_save(attr_name.to_s)
99
+ else
100
+ # We are either performing a `record_destroy` or a
101
+ # `record_update(is_touch: true)`.
102
+ @record.attribute_in_database(attr_name.to_s)
103
+ end
104
+ else
105
+ @record.attribute_was(attr_name.to_s)
106
+ end
107
+ end
108
+
109
+ # @api private
110
+ def calculated_ignored_array
111
+ ignore = @record.paper_trail_options[:ignore].dup
112
+ # Remove Hash arguments and then evaluate whether the attributes (the
113
+ # keys of the hash) should also get pushed into the collection.
114
+ ignore.delete_if do |obj|
115
+ obj.is_a?(Hash) &&
116
+ obj.each { |attr, condition|
117
+ ignore << attr if condition.respond_to?(:call) && condition.call(@record)
118
+ }
119
+ end
120
+ end
121
+
122
+ # @api private
123
+ def changed_and_not_ignored
124
+ skip = @record.paper_trail_options[:skip]
125
+ (changed_in_latest_version - calculated_ignored_array) - skip
126
+ end
127
+
128
+ # @api private
129
+ def changed_in_latest_version
130
+ # Memoized to reduce memory usage
131
+ @changed_in_latest_version ||= changes_in_latest_version.keys
132
+ end
133
+
134
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
135
+ # https://github.com/paper-trail-gem/mongo_trails/pull/899
136
+ #
137
+ # @api private
138
+ def changes_in_latest_version
139
+ # Memoized to reduce memory usage
140
+ @changes_in_latest_version ||= begin
141
+ if @in_after_callback && RAILS_GTE_5_1
142
+ @record.saved_changes
143
+ else
144
+ @record.changes
145
+ end
146
+ end
147
+ end
148
+
149
+ # An attributed is "ignored" if it is listed in the `:ignore` option
150
+ # and/or the `:skip` option. Returns true if an ignored attribute has
151
+ # changed.
152
+ #
153
+ # @api private
154
+ def ignored_attr_has_changed?
155
+ ignored = calculated_ignored_array + @record.paper_trail_options[:skip]
156
+ ignored.any? && (changed_in_latest_version & ignored).any?
157
+ end
158
+
159
+ # PT 10 has a new optional column, `item_subtype`
160
+ #
161
+ # @api private
162
+ def merge_item_subtype_into(data)
163
+ if @record.class.paper_trail.version_class.fields.key?("item_subtype")
164
+ data.merge!(item_subtype: @record.class.name)
165
+ end
166
+ end
167
+
168
+ # Updates `data` from the model's `meta` option and from `controller_info`.
169
+ # Metadata is always recorded; that means all three events (create, update,
170
+ # destroy) and `update_columns`.
171
+ #
172
+ # @api private
173
+ def merge_metadata_into(data)
174
+ merge_metadata_from_model_into(data)
175
+ merge_metadata_from_controller_into(data)
176
+ end
177
+
178
+ # Updates `data` from `controller_info`.
179
+ #
180
+ # @api private
181
+ def merge_metadata_from_controller_into(data)
182
+ data.merge(PaperTrail.request.controller_info || {})
183
+ end
184
+
185
+ # Updates `data` from the model's `meta` option.
186
+ #
187
+ # @api private
188
+ def merge_metadata_from_model_into(data)
189
+ @record.paper_trail_options[:meta].each do |k, v|
190
+ data[k] = model_metadatum(v, data[:event])
191
+ end
192
+ end
193
+
194
+ # Given a `value` from the model's `meta` option, returns an object to be
195
+ # persisted. The `value` can be a simple scalar value, but it can also
196
+ # be a symbol that names a model method, or even a Proc.
197
+ #
198
+ # @api private
199
+ def model_metadatum(value, event)
200
+ if value.respond_to?(:call)
201
+ value.call(@record)
202
+ elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
203
+ # If it is an attribute that is changing in an existing object,
204
+ # be sure to grab the current version.
205
+ if event != "create" &&
206
+ @record.has_attribute?(value) &&
207
+ attribute_changed_in_latest_version?(value)
208
+ attribute_in_previous_version(value, false)
209
+ else
210
+ @record.send(value)
211
+ end
212
+ else
213
+ value
214
+ end
215
+ end
216
+
217
+ # @api private
218
+ def notable_changes
219
+ changes_in_latest_version.delete_if { |k, _v|
220
+ !notably_changed.include?(k)
221
+ }
222
+ end
223
+
224
+ # @api private
225
+ def notably_changed
226
+ # Memoized to reduce memory usage
227
+ @notably_changed ||= begin
228
+ only = @record.paper_trail_options[:only].dup
229
+ # Remove Hash arguments and then evaluate whether the attributes (the
230
+ # keys of the hash) should also get pushed into the collection.
231
+ only.delete_if do |obj|
232
+ obj.is_a?(Hash) &&
233
+ obj.each { |attr, condition|
234
+ only << attr if condition.respond_to?(:call) && condition.call(@record)
235
+ }
236
+ end
237
+ only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
238
+ end
239
+ end
240
+
241
+ # Returns hash of attributes (with appropriate attributes serialized),
242
+ # omitting attributes to be skipped.
243
+ #
244
+ # @api private
245
+ def object_attrs_for_paper_trail(is_touch)
246
+ attrs = nonskipped_attributes_before_change(is_touch)
247
+ AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs)
248
+ attrs
249
+ end
250
+
251
+ # @api private
252
+ def prepare_object_changes(changes)
253
+ changes = serialize_object_changes(changes)
254
+ recordable_object_changes(changes)
255
+ end
256
+
257
+ # Returns an object which can be assigned to the `object_changes`
258
+ # attribute of a nascent version record. If the `object_changes` column is
259
+ # a postgres `json` column, then a hash can be used in the assignment,
260
+ # otherwise the column is a `text` column, and we must perform the
261
+ # serialization here, using `PaperTrail.serializer`.
262
+ #
263
+ # @api private
264
+ # @param changes HashWithIndifferentAccess
265
+ def recordable_object_changes(changes)
266
+ if PaperTrail.config.object_changes_adapter&.respond_to?(:diff)
267
+ # We'd like to avoid the `to_hash` here, because it increases memory
268
+ # usage, but that would be a breaking change because
269
+ # `object_changes_adapter` expects a plain `Hash`, not a
270
+ # `HashWithIndifferentAccess`.
271
+ changes = PaperTrail.config.object_changes_adapter.diff(changes.to_hash)
272
+ end
273
+
274
+ if @record.class.paper_trail.version_class.object_changes_col_is_json?
275
+ changes
276
+ else
277
+ PaperTrail.serializer.dump(changes)
278
+ end
279
+ end
280
+
281
+ # Returns a boolean indicating whether to store serialized version diffs
282
+ # in the `object_changes` column of the version record.
283
+ #
284
+ # @api private
285
+ def record_object_changes?
286
+ @record.class.paper_trail.version_class.fields.keys.include?("object_changes")
287
+ end
288
+
289
+ # Returns a boolean indicating whether to store the original object during save.
290
+ #
291
+ # @api private
292
+ def record_object?
293
+ @record.class.paper_trail.version_class.fields.keys.include?("object")
294
+ end
295
+
296
+ # Returns an object which can be assigned to the `object` attribute of a
297
+ # nascent version record. If the `object` column is a postgres `json`
298
+ # column, then a hash can be used in the assignment, otherwise the column
299
+ # is a `text` column, and we must perform the serialization here, using
300
+ # `PaperTrail.serializer`.
301
+ #
302
+ # @api private
303
+ def recordable_object(is_touch)
304
+ if @record.class.paper_trail.version_class.object_col_is_json?
305
+ object_attrs_for_paper_trail(is_touch)
306
+ else
307
+ PaperTrail.serializer.dump(object_attrs_for_paper_trail(is_touch))
308
+ end
309
+ end
310
+
311
+ # @api private
312
+ def serialize_object_changes(changes)
313
+ AttributeSerializers::ObjectChangesAttribute.
314
+ new(@record.class).
315
+ serialize(changes)
316
+
317
+ # We'd like to convert this `HashWithIndifferentAccess` to a plain
318
+ # `Hash`, but we don't, to save memory.
319
+ changes
320
+ end
321
+ end
322
+ end
323
+ end