activemodel 4.2.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
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