activemodel 4.2.0 → 6.1.0

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 (71) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +49 -37
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +16 -22
  5. data/lib/active_model/attribute/user_provided_default.rb +51 -0
  6. data/lib/active_model/attribute.rb +248 -0
  7. data/lib/active_model/attribute_assignment.rb +55 -0
  8. data/lib/active_model/attribute_methods.rb +150 -73
  9. data/lib/active_model/attribute_mutation_tracker.rb +181 -0
  10. data/lib/active_model/attribute_set/builder.rb +191 -0
  11. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  12. data/lib/active_model/attribute_set.rb +106 -0
  13. data/lib/active_model/attributes.rb +132 -0
  14. data/lib/active_model/callbacks.rb +31 -25
  15. data/lib/active_model/conversion.rb +20 -9
  16. data/lib/active_model/dirty.rb +142 -116
  17. data/lib/active_model/error.rb +207 -0
  18. data/lib/active_model/errors.rb +436 -202
  19. data/lib/active_model/forbidden_attributes_protection.rb +6 -3
  20. data/lib/active_model/gem_version.rb +5 -3
  21. data/lib/active_model/lint.rb +47 -42
  22. data/lib/active_model/locale/en.yml +2 -1
  23. data/lib/active_model/model.rb +7 -7
  24. data/lib/active_model/naming.rb +36 -18
  25. data/lib/active_model/nested_error.rb +22 -0
  26. data/lib/active_model/railtie.rb +8 -0
  27. data/lib/active_model/secure_password.rb +61 -67
  28. data/lib/active_model/serialization.rb +48 -17
  29. data/lib/active_model/serializers/json.rb +22 -13
  30. data/lib/active_model/translation.rb +5 -4
  31. data/lib/active_model/type/big_integer.rb +14 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +46 -0
  34. data/lib/active_model/type/date.rb +52 -0
  35. data/lib/active_model/type/date_time.rb +46 -0
  36. data/lib/active_model/type/decimal.rb +69 -0
  37. data/lib/active_model/type/float.rb +35 -0
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +49 -0
  39. data/lib/active_model/type/helpers/mutable.rb +20 -0
  40. data/lib/active_model/type/helpers/numeric.rb +48 -0
  41. data/lib/active_model/type/helpers/time_value.rb +90 -0
  42. data/lib/active_model/type/helpers/timezone.rb +19 -0
  43. data/lib/active_model/type/helpers.rb +7 -0
  44. data/lib/active_model/type/immutable_string.rb +35 -0
  45. data/lib/active_model/type/integer.rb +67 -0
  46. data/lib/active_model/type/registry.rb +70 -0
  47. data/lib/active_model/type/string.rb +35 -0
  48. data/lib/active_model/type/time.rb +46 -0
  49. data/lib/active_model/type/value.rb +133 -0
  50. data/lib/active_model/type.rb +53 -0
  51. data/lib/active_model/validations/absence.rb +6 -4
  52. data/lib/active_model/validations/acceptance.rb +72 -14
  53. data/lib/active_model/validations/callbacks.rb +23 -19
  54. data/lib/active_model/validations/clusivity.rb +18 -12
  55. data/lib/active_model/validations/confirmation.rb +27 -14
  56. data/lib/active_model/validations/exclusion.rb +7 -4
  57. data/lib/active_model/validations/format.rb +27 -27
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +8 -7
  60. data/lib/active_model/validations/length.rb +35 -32
  61. data/lib/active_model/validations/numericality.rb +72 -34
  62. data/lib/active_model/validations/presence.rb +3 -3
  63. data/lib/active_model/validations/validates.rb +17 -15
  64. data/lib/active_model/validations/with.rb +6 -12
  65. data/lib/active_model/validations.rb +58 -23
  66. data/lib/active_model/validator.rb +23 -17
  67. data/lib/active_model/version.rb +4 -2
  68. data/lib/active_model.rb +18 -11
  69. metadata +44 -25
  70. data/lib/active_model/serializers/xml.rb +0 -238
  71. data/lib/active_model/test_case.rb +0 -4
@@ -1,5 +1,6 @@
1
- require 'thread_safe'
2
- require 'mutex_m'
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
3
4
 
4
5
  module ActiveModel
5
6
  # Raised when an attribute is not defined.
@@ -23,7 +24,7 @@ module ActiveModel
23
24
  # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to:
24
25
  #
25
26
  # * <tt>include ActiveModel::AttributeMethods</tt> in your class.
26
- # * Call each of its method you want to add, such as +attribute_method_suffix+
27
+ # * Call each of its methods you want to add, such as +attribute_method_suffix+
27
28
  # or +attribute_method_prefix+.
28
29
  # * Call +define_attribute_methods+ after the other methods are called.
29
30
  # * Define the various generic +_attribute+ methods that you have declared.
@@ -68,9 +69,8 @@ module ActiveModel
68
69
  CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
69
70
 
70
71
  included do
71
- class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false
72
- self.attribute_aliases = {}
73
- self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new]
72
+ class_attribute :attribute_aliases, instance_writer: false, default: {}
73
+ class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
74
74
  end
75
75
 
76
76
  module ClassMethods
@@ -207,10 +207,12 @@ module ActiveModel
207
207
  # person.nickname_short? # => true
208
208
  def alias_attribute(new_name, old_name)
209
209
  self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
210
- attribute_method_matchers.each do |matcher|
211
- matcher_new = matcher.method_name(new_name).to_s
212
- matcher_old = matcher.method_name(old_name).to_s
213
- define_proxy_call false, self, matcher_new, matcher_old
210
+ CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
211
+ attribute_method_matchers.each do |matcher|
212
+ matcher_new = matcher.method_name(new_name).to_s
213
+ matcher_old = matcher.method_name(old_name).to_s
214
+ define_proxy_call false, owner, matcher_new, matcher_old
215
+ end
214
216
  end
215
217
  end
216
218
 
@@ -225,9 +227,9 @@ module ActiveModel
225
227
  end
226
228
 
227
229
  # Declares the attributes that should be prefixed and suffixed by
228
- # ActiveModel::AttributeMethods.
230
+ # <tt>ActiveModel::AttributeMethods</tt>.
229
231
  #
230
- # To use, pass attribute names (as strings or symbols), be sure to declare
232
+ # To use, pass attribute names (as strings or symbols). Be sure to declare
231
233
  # +define_attribute_methods+ after you define any prefix, suffix or affix
232
234
  # methods, or they will not hook in.
233
235
  #
@@ -239,7 +241,7 @@ module ActiveModel
239
241
  #
240
242
  # # Call to define_attribute_methods must appear after the
241
243
  # # attribute_method_prefix, attribute_method_suffix or
242
- # # attribute_method_affix declares.
244
+ # # attribute_method_affix declarations.
243
245
  # define_attribute_methods :name, :age, :address
244
246
  #
245
247
  # private
@@ -249,13 +251,15 @@ module ActiveModel
249
251
  # end
250
252
  # end
251
253
  def define_attribute_methods(*attr_names)
252
- attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
254
+ CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
255
+ attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
256
+ end
253
257
  end
254
258
 
255
259
  # Declares an attribute that should be prefixed and suffixed by
256
- # ActiveModel::AttributeMethods.
260
+ # <tt>ActiveModel::AttributeMethods</tt>.
257
261
  #
258
- # To use, pass an attribute name (as string or symbol), be sure to declare
262
+ # To use, pass an attribute name (as string or symbol). Be sure to declare
259
263
  # +define_attribute_method+ after you define any prefix, suffix or affix
260
264
  # method, or they will not hook in.
261
265
  #
@@ -267,7 +271,7 @@ module ActiveModel
267
271
  #
268
272
  # # Call to define_attribute_method must appear after the
269
273
  # # attribute_method_prefix, attribute_method_suffix or
270
- # # attribute_method_affix declares.
274
+ # # attribute_method_affix declarations.
271
275
  # define_attribute_method :name
272
276
  #
273
277
  # private
@@ -281,21 +285,23 @@ module ActiveModel
281
285
  # person.name = 'Bob'
282
286
  # person.name # => "Bob"
283
287
  # person.name_short? # => true
284
- def define_attribute_method(attr_name)
285
- attribute_method_matchers.each do |matcher|
286
- method_name = matcher.method_name(attr_name)
287
-
288
- unless instance_method_already_implemented?(method_name)
289
- generate_method = "define_method_#{matcher.method_missing_target}"
290
-
291
- if respond_to?(generate_method, true)
292
- send(generate_method, attr_name)
293
- else
294
- define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s
288
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods)
289
+ CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
290
+ attribute_method_matchers.each do |matcher|
291
+ method_name = matcher.method_name(attr_name)
292
+
293
+ unless instance_method_already_implemented?(method_name)
294
+ generate_method = "define_method_#{matcher.target}"
295
+
296
+ if respond_to?(generate_method, true)
297
+ send(generate_method, attr_name.to_s, owner: owner)
298
+ else
299
+ define_proxy_call true, owner, method_name, matcher.target, attr_name.to_s
300
+ end
295
301
  end
296
302
  end
303
+ attribute_method_matchers_cache.clear
297
304
  end
298
- attribute_method_matchers_cache.clear
299
305
  end
300
306
 
301
307
  # Removes all the previously dynamically defined methods from the class.
@@ -323,50 +329,84 @@ module ActiveModel
323
329
  # person.name_short? # => NoMethodError
324
330
  def undefine_attribute_methods
325
331
  generated_attribute_methods.module_eval do
326
- instance_methods.each { |m| undef_method(m) }
332
+ undef_method(*instance_methods)
327
333
  end
328
334
  attribute_method_matchers_cache.clear
329
335
  end
330
336
 
331
- def generated_attribute_methods #:nodoc:
332
- @generated_attribute_methods ||= Module.new {
333
- extend Mutex_m
334
- }.tap { |mod| include mod }
335
- end
337
+ private
338
+ class CodeGenerator
339
+ class << self
340
+ def batch(owner, path, line)
341
+ if owner.is_a?(CodeGenerator)
342
+ yield owner
343
+ else
344
+ instance = new(owner, path, line)
345
+ result = yield instance
346
+ instance.execute
347
+ result
348
+ end
349
+ end
350
+ end
351
+
352
+ def initialize(owner, path, line)
353
+ @owner = owner
354
+ @path = path
355
+ @line = line
356
+ @sources = ["# frozen_string_literal: true\n"]
357
+ @renames = {}
358
+ end
359
+
360
+ def <<(source_line)
361
+ @sources << source_line
362
+ end
336
363
 
337
- protected
338
- def instance_method_already_implemented?(method_name) #:nodoc:
364
+ def rename_method(old_name, new_name)
365
+ @renames[old_name] = new_name
366
+ end
367
+
368
+ def execute
369
+ @owner.module_eval(@sources.join(";"), @path, @line - 1)
370
+ @renames.each do |old_name, new_name|
371
+ @owner.alias_method new_name, old_name
372
+ @owner.undef_method old_name
373
+ end
374
+ end
375
+ end
376
+ private_constant :CodeGenerator
377
+
378
+ def generated_attribute_methods
379
+ @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
380
+ end
381
+
382
+ def instance_method_already_implemented?(method_name)
339
383
  generated_attribute_methods.method_defined?(method_name)
340
384
  end
341
385
 
342
- private
343
386
  # The methods +method_missing+ and +respond_to?+ of this module are
344
387
  # invoked often in a typical rails, both of which invoke the method
345
- # +match_attribute_method?+. The latter method iterates through an
388
+ # +matched_attribute_method+. The latter method iterates through an
346
389
  # array doing regular expression matches, which results in a lot of
347
390
  # object creations. Most of the time it returns a +nil+ match. As the
348
391
  # match result is always the same given a +method_name+, this cache is
349
392
  # used to alleviate the GC, which ultimately also speeds up the app
350
393
  # significantly (in our case our test suite finishes 10% faster with
351
394
  # this cache).
352
- def attribute_method_matchers_cache #:nodoc:
353
- @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(initial_capacity: 4)
395
+ def attribute_method_matchers_cache
396
+ @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
354
397
  end
355
398
 
356
- def attribute_method_matchers_matching(method_name) #:nodoc:
399
+ def attribute_method_matchers_matching(method_name)
357
400
  attribute_method_matchers_cache.compute_if_absent(method_name) do
358
- # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
359
- # will match every time.
360
- matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
361
- matchers.map { |method| method.match(method_name) }.compact
401
+ attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
362
402
  end
363
403
  end
364
404
 
365
405
  # Define a method `name` in `mod` that dispatches to `send`
366
- # using the given `extra` args. This fallbacks `define_method`
406
+ # using the given `extra` args. This falls back on `define_method`
367
407
  # and `send` if the given names cannot be compiled.
368
- def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc:
369
- defn = if name =~ NAME_COMPILABLE_REGEXP
408
+ def define_proxy_call(include_private, code_generator, name, target, *extra)
409
+ defn = if NAME_COMPILABLE_REGEXP.match?(name)
370
410
  "def #{name}(*args)"
371
411
  else
372
412
  "define_method(:'#{name}') do |*args|"
@@ -374,44 +414,40 @@ module ActiveModel
374
414
 
375
415
  extra = (extra.map!(&:inspect) << "*args").join(", ")
376
416
 
377
- target = if send =~ CALL_COMPILABLE_REGEXP
378
- "#{"self." unless include_private}#{send}(#{extra})"
417
+ body = if CALL_COMPILABLE_REGEXP.match?(target)
418
+ "#{"self." unless include_private}#{target}(#{extra})"
379
419
  else
380
- "send(:'#{send}', #{extra})"
420
+ "send(:'#{target}', #{extra})"
381
421
  end
382
422
 
383
- mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
384
- #{defn}
385
- #{target}
386
- end
387
- RUBY
423
+ code_generator <<
424
+ defn <<
425
+ body <<
426
+ "end" <<
427
+ "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
388
428
  end
389
429
 
390
430
  class AttributeMethodMatcher #:nodoc:
391
- attr_reader :prefix, :suffix, :method_missing_target
431
+ attr_reader :prefix, :suffix, :target
392
432
 
393
- AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
433
+ AttributeMethodMatch = Struct.new(:target, :attr_name)
394
434
 
395
435
  def initialize(options = {})
396
- @prefix, @suffix = options.fetch(:prefix, ''), options.fetch(:suffix, '')
436
+ @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
397
437
  @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
398
- @method_missing_target = "#{@prefix}attribute#{@suffix}"
438
+ @target = "#{@prefix}attribute#{@suffix}"
399
439
  @method_name = "#{prefix}%s#{suffix}"
400
440
  end
401
441
 
402
442
  def match(method_name)
403
443
  if @regex =~ method_name
404
- AttributeMethodMatch.new(method_missing_target, $1, method_name)
444
+ AttributeMethodMatch.new(target, $1)
405
445
  end
406
446
  end
407
447
 
408
448
  def method_name(attr_name)
409
449
  @method_name % attr_name
410
450
  end
411
-
412
- def plain?
413
- prefix.empty? && suffix.empty?
414
- end
415
451
  end
416
452
  end
417
453
 
@@ -419,7 +455,7 @@ module ActiveModel
419
455
  # returned by <tt>attributes</tt>, as though they were first-class
420
456
  # methods. So a +Person+ class with a +name+ attribute can for example use
421
457
  # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
422
- # the attributes hash -- except for multiple assigns with
458
+ # the attributes hash -- except for multiple assignments with
423
459
  # <tt>ActiveRecord::Base#attributes=</tt>.
424
460
  #
425
461
  # It's also possible to instantiate related objects, so a <tt>Client</tt>
@@ -429,10 +465,11 @@ module ActiveModel
429
465
  if respond_to_without_attributes?(method, true)
430
466
  super
431
467
  else
432
- match = match_attribute_method?(method.to_s)
468
+ match = matched_attribute_method(method.to_s)
433
469
  match ? attribute_missing(match, *args, &block) : super
434
470
  end
435
471
  end
472
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
436
473
 
437
474
  # +attribute_missing+ is like +method_missing+, but for attributes. When
438
475
  # +method_missing+ is called we check to see if there is a matching
@@ -454,19 +491,18 @@ module ActiveModel
454
491
  # but found among all methods. Which means that the given method is private.
455
492
  false
456
493
  else
457
- !match_attribute_method?(method.to_s).nil?
494
+ !matched_attribute_method(method.to_s).nil?
458
495
  end
459
496
  end
460
497
 
461
- protected
462
- def attribute_method?(attr_name) #:nodoc:
498
+ private
499
+ def attribute_method?(attr_name)
463
500
  respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
464
501
  end
465
502
 
466
- private
467
503
  # Returns a struct representing the matching attribute method.
468
504
  # The struct's attributes are prefix, base and suffix.
469
- def match_attribute_method?(method_name)
505
+ def matched_attribute_method(method_name)
470
506
  matches = self.class.send(:attribute_method_matchers_matching, method_name)
471
507
  matches.detect { |match| attribute_method?(match.attr_name) }
472
508
  end
@@ -474,5 +510,46 @@ module ActiveModel
474
510
  def missing_attribute(attr_name, stack)
475
511
  raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
476
512
  end
513
+
514
+ def _read_attribute(attr)
515
+ __send__(attr)
516
+ end
517
+
518
+ module AttrNames # :nodoc:
519
+ DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
520
+
521
+ # We want to generate the methods via module_eval rather than
522
+ # define_method, because define_method is slower on dispatch.
523
+ # Evaluating many similar methods may use more memory as the instruction
524
+ # sequences are duplicated and cached (in MRI). define_method may
525
+ # be slower on dispatch, but if you're careful about the closure
526
+ # created, then define_method will consume much less memory.
527
+ #
528
+ # But sometimes the database might return columns with
529
+ # characters that are not allowed in normal method names (like
530
+ # 'my_column(omg)'. So to work around this we first define with
531
+ # the __temp__ identifier, and then use alias method to rename
532
+ # it to what we want.
533
+ #
534
+ # We are also defining a constant to hold the frozen string of
535
+ # the attribute name. Using a constant means that we do not have
536
+ # to allocate an object on each call to the attribute method.
537
+ # Making it frozen means that it doesn't get duped when used to
538
+ # key the @attributes in read_attribute.
539
+ def self.define_attribute_accessor_method(owner, attr_name, writer: false)
540
+ method_name = "#{attr_name}#{'=' if writer}"
541
+ if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
542
+ yield method_name, "'#{attr_name}'"
543
+ else
544
+ safe_name = attr_name.unpack1("h*")
545
+ const_name = "ATTR_#{safe_name}"
546
+ const_set(const_name, attr_name) unless const_defined?(const_name)
547
+ temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
548
+ attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
549
+ yield temp_method_name, attr_name_expr
550
+ owner.rename_method(temp_method_name, method_name)
551
+ end
552
+ end
553
+ end
477
554
  end
478
555
  end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/indifferent_access"
4
+ require "active_support/core_ext/object/duplicable"
5
+
6
+ module ActiveModel
7
+ class AttributeMutationTracker # :nodoc:
8
+ OPTION_NOT_GIVEN = Object.new
9
+
10
+ def initialize(attributes)
11
+ @attributes = attributes
12
+ end
13
+
14
+ def changed_attribute_names
15
+ attr_names.select { |attr_name| changed?(attr_name) }
16
+ end
17
+
18
+ def changed_values
19
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
20
+ if changed?(attr_name)
21
+ result[attr_name] = original_value(attr_name)
22
+ end
23
+ end
24
+ end
25
+
26
+ def changes
27
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
28
+ if change = change_to_attribute(attr_name)
29
+ result.merge!(attr_name => change)
30
+ end
31
+ end
32
+ end
33
+
34
+ def change_to_attribute(attr_name)
35
+ if changed?(attr_name)
36
+ [original_value(attr_name), fetch_value(attr_name)]
37
+ end
38
+ end
39
+
40
+ def any_changes?
41
+ attr_names.any? { |attr| changed?(attr) }
42
+ end
43
+
44
+ def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
45
+ attribute_changed?(attr_name) &&
46
+ (OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
47
+ (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
48
+ end
49
+
50
+ def changed_in_place?(attr_name)
51
+ attributes[attr_name].changed_in_place?
52
+ end
53
+
54
+ def forget_change(attr_name)
55
+ attributes[attr_name] = attributes[attr_name].forgetting_assignment
56
+ forced_changes.delete(attr_name)
57
+ end
58
+
59
+ def original_value(attr_name)
60
+ attributes[attr_name].original_value
61
+ end
62
+
63
+ def force_change(attr_name)
64
+ forced_changes[attr_name] = fetch_value(attr_name)
65
+ end
66
+
67
+ private
68
+ attr_reader :attributes
69
+
70
+ def forced_changes
71
+ @forced_changes ||= {}
72
+ end
73
+
74
+ def attr_names
75
+ attributes.keys
76
+ end
77
+
78
+ def attribute_changed?(attr_name)
79
+ forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
80
+ end
81
+
82
+ def fetch_value(attr_name)
83
+ attributes.fetch_value(attr_name)
84
+ end
85
+ end
86
+
87
+ class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
88
+ def initialize(attributes)
89
+ super
90
+ @finalized_changes = nil
91
+ end
92
+
93
+ def changed_in_place?(attr_name)
94
+ false
95
+ end
96
+
97
+ def change_to_attribute(attr_name)
98
+ if finalized_changes&.include?(attr_name)
99
+ finalized_changes[attr_name].dup
100
+ else
101
+ super
102
+ end
103
+ end
104
+
105
+ def forget_change(attr_name)
106
+ forced_changes.delete(attr_name)
107
+ end
108
+
109
+ def original_value(attr_name)
110
+ if changed?(attr_name)
111
+ forced_changes[attr_name]
112
+ else
113
+ fetch_value(attr_name)
114
+ end
115
+ end
116
+
117
+ def force_change(attr_name)
118
+ forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
119
+ end
120
+
121
+ def finalize_changes
122
+ @finalized_changes = changes
123
+ end
124
+
125
+ private
126
+ attr_reader :finalized_changes
127
+
128
+ def attr_names
129
+ forced_changes.keys
130
+ end
131
+
132
+ def attribute_changed?(attr_name)
133
+ forced_changes.include?(attr_name)
134
+ end
135
+
136
+ def fetch_value(attr_name)
137
+ attributes.send(:_read_attribute, attr_name)
138
+ end
139
+
140
+ def clone_value(attr_name)
141
+ value = fetch_value(attr_name)
142
+ value.duplicable? ? value.clone : value
143
+ rescue TypeError, NoMethodError
144
+ value
145
+ end
146
+ end
147
+
148
+ class NullMutationTracker # :nodoc:
149
+ include Singleton
150
+
151
+ def changed_attribute_names
152
+ []
153
+ end
154
+
155
+ def changed_values
156
+ {}
157
+ end
158
+
159
+ def changes
160
+ {}
161
+ end
162
+
163
+ def change_to_attribute(attr_name)
164
+ end
165
+
166
+ def any_changes?
167
+ false
168
+ end
169
+
170
+ def changed?(attr_name, **)
171
+ false
172
+ end
173
+
174
+ def changed_in_place?(attr_name)
175
+ false
176
+ end
177
+
178
+ def original_value(attr_name)
179
+ end
180
+ end
181
+ end