mongo_trails 10.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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