activemodel 6.0.6.1 → 6.1.7.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -162
  3. data/MIT-LICENSE +1 -2
  4. data/README.rdoc +1 -1
  5. data/lib/active_model/attribute.rb +18 -17
  6. data/lib/active_model/attribute_assignment.rb +3 -4
  7. data/lib/active_model/attribute_methods.rb +74 -38
  8. data/lib/active_model/attribute_mutation_tracker.rb +8 -5
  9. data/lib/active_model/attribute_set/builder.rb +80 -13
  10. data/lib/active_model/attribute_set.rb +18 -16
  11. data/lib/active_model/attributes.rb +20 -24
  12. data/lib/active_model/callbacks.rb +1 -1
  13. data/lib/active_model/dirty.rb +17 -4
  14. data/lib/active_model/error.rb +207 -0
  15. data/lib/active_model/errors.rb +316 -208
  16. data/lib/active_model/gem_version.rb +3 -3
  17. data/lib/active_model/lint.rb +1 -1
  18. data/lib/active_model/naming.rb +2 -2
  19. data/lib/active_model/nested_error.rb +22 -0
  20. data/lib/active_model/railtie.rb +1 -1
  21. data/lib/active_model/secure_password.rb +15 -14
  22. data/lib/active_model/serialization.rb +9 -6
  23. data/lib/active_model/serializers/json.rb +7 -0
  24. data/lib/active_model/type/date_time.rb +2 -2
  25. data/lib/active_model/type/float.rb +2 -0
  26. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +11 -7
  27. data/lib/active_model/type/helpers/numeric.rb +8 -3
  28. data/lib/active_model/type/helpers/time_value.rb +27 -17
  29. data/lib/active_model/type/helpers/timezone.rb +1 -1
  30. data/lib/active_model/type/immutable_string.rb +14 -10
  31. data/lib/active_model/type/integer.rb +11 -2
  32. data/lib/active_model/type/registry.rb +12 -9
  33. data/lib/active_model/type/string.rb +12 -2
  34. data/lib/active_model/type/value.rb +9 -1
  35. data/lib/active_model/type.rb +3 -2
  36. data/lib/active_model/validations/absence.rb +1 -1
  37. data/lib/active_model/validations/acceptance.rb +1 -1
  38. data/lib/active_model/validations/callbacks.rb +15 -15
  39. data/lib/active_model/validations/clusivity.rb +5 -1
  40. data/lib/active_model/validations/confirmation.rb +2 -2
  41. data/lib/active_model/validations/exclusion.rb +1 -1
  42. data/lib/active_model/validations/format.rb +2 -2
  43. data/lib/active_model/validations/inclusion.rb +1 -1
  44. data/lib/active_model/validations/length.rb +2 -2
  45. data/lib/active_model/validations/numericality.rb +54 -41
  46. data/lib/active_model/validations/presence.rb +1 -1
  47. data/lib/active_model/validations/validates.rb +6 -4
  48. data/lib/active_model/validations.rb +6 -6
  49. data/lib/active_model/validator.rb +7 -1
  50. data/lib/active_model.rb +2 -1
  51. metadata +10 -8
@@ -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
 
@@ -249,7 +251,9 @@ 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
@@ -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.target}"
290
-
291
- if respond_to?(generate_method, true)
292
- send(generate_method, attr_name.to_s)
293
- else
294
- define_proxy_call true, generated_attribute_methods, method_name, matcher.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,12 +329,52 @@ 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
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
363
+
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
+
332
378
  def generated_attribute_methods
333
379
  @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
334
380
  end
@@ -352,18 +398,14 @@ module ActiveModel
352
398
 
353
399
  def attribute_method_matchers_matching(method_name)
354
400
  attribute_method_matchers_cache.compute_if_absent(method_name) do
355
- # Bump plain matcher to last place so that only methods that do not
356
- # match any other pattern match the actual attribute name.
357
- # This is currently only needed to support legacy usage.
358
- matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
359
- matchers.map { |matcher| matcher.match(method_name) }.compact
401
+ attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
360
402
  end
361
403
  end
362
404
 
363
405
  # Define a method `name` in `mod` that dispatches to `send`
364
406
  # using the given `extra` args. This falls back on `define_method`
365
407
  # and `send` if the given names cannot be compiled.
366
- def define_proxy_call(include_private, mod, name, target, *extra)
408
+ def define_proxy_call(include_private, code_generator, name, target, *extra)
367
409
  defn = if NAME_COMPILABLE_REGEXP.match?(name)
368
410
  "def #{name}(*args)"
369
411
  else
@@ -378,12 +420,11 @@ module ActiveModel
378
420
  "send(:'#{target}', #{extra})"
379
421
  end
380
422
 
381
- mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
382
- #{defn}
383
- #{body}
384
- end
385
- ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)
386
- RUBY
423
+ code_generator <<
424
+ defn <<
425
+ body <<
426
+ "end" <<
427
+ "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
387
428
  end
388
429
 
389
430
  class AttributeMethodMatcher #:nodoc:
@@ -407,10 +448,6 @@ module ActiveModel
407
448
  def method_name(attr_name)
408
449
  @method_name % attr_name
409
450
  end
410
-
411
- def plain?
412
- prefix.empty? && suffix.empty?
413
- end
414
451
  end
415
452
  end
416
453
 
@@ -499,10 +536,10 @@ module ActiveModel
499
536
  # to allocate an object on each call to the attribute method.
500
537
  # Making it frozen means that it doesn't get duped when used to
501
538
  # key the @attributes in read_attribute.
502
- def self.define_attribute_accessor_method(mod, attr_name, writer: false)
539
+ def self.define_attribute_accessor_method(owner, attr_name, writer: false)
503
540
  method_name = "#{attr_name}#{'=' if writer}"
504
541
  if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
505
- yield method_name, "'#{attr_name}'.freeze"
542
+ yield method_name, "'#{attr_name}'"
506
543
  else
507
544
  safe_name = attr_name.unpack1("h*")
508
545
  const_name = "ATTR_#{safe_name}"
@@ -510,8 +547,7 @@ module ActiveModel
510
547
  temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
511
548
  attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
512
549
  yield temp_method_name, attr_name_expr
513
- mod.alias_method method_name, temp_method_name
514
- mod.undef_method temp_method_name
550
+ owner.rename_method(temp_method_name, method_name)
515
551
  end
516
552
  end
517
553
  end
@@ -7,9 +7,8 @@ module ActiveModel
7
7
  class AttributeMutationTracker # :nodoc:
8
8
  OPTION_NOT_GIVEN = Object.new
9
9
 
10
- def initialize(attributes, forced_changes = Set.new)
10
+ def initialize(attributes)
11
11
  @attributes = attributes
12
- @forced_changes = forced_changes
13
12
  end
14
13
 
15
14
  def changed_attribute_names
@@ -62,11 +61,15 @@ module ActiveModel
62
61
  end
63
62
 
64
63
  def force_change(attr_name)
65
- forced_changes << attr_name
64
+ forced_changes[attr_name] = fetch_value(attr_name)
66
65
  end
67
66
 
68
67
  private
69
- attr_reader :attributes, :forced_changes
68
+ attr_reader :attributes
69
+
70
+ def forced_changes
71
+ @forced_changes ||= {}
72
+ end
70
73
 
71
74
  def attr_names
72
75
  attributes.keys
@@ -82,7 +85,7 @@ module ActiveModel
82
85
  end
83
86
 
84
87
  class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
85
- def initialize(attributes, forced_changes = {})
88
+ def initialize(attributes)
86
89
  super
87
90
  @finalized_changes = nil
88
91
  end
@@ -13,14 +13,86 @@ module ActiveModel
13
13
  end
14
14
 
15
15
  def build_from_database(values = {}, additional_types = {})
16
- attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
17
- AttributeSet.new(attributes)
16
+ LazyAttributeSet.new(values, types, additional_types, default_attributes)
18
17
  end
19
18
  end
20
19
  end
21
20
 
21
+ class LazyAttributeSet < AttributeSet # :nodoc:
22
+ def initialize(values, types, additional_types, default_attributes, attributes = {})
23
+ super(attributes)
24
+ @values = values
25
+ @types = types
26
+ @additional_types = additional_types
27
+ @default_attributes = default_attributes
28
+ @casted_values = {}
29
+ @materialized = false
30
+ end
31
+
32
+ def key?(name)
33
+ (values.key?(name) || types.key?(name) || @attributes.key?(name)) && self[name].initialized?
34
+ end
35
+
36
+ def keys
37
+ keys = values.keys | types.keys | @attributes.keys
38
+ keys.keep_if { |name| self[name].initialized? }
39
+ end
40
+
41
+ def fetch_value(name, &block)
42
+ if attr = @attributes[name]
43
+ return attr.value(&block)
44
+ end
45
+
46
+ @casted_values.fetch(name) do
47
+ value_present = true
48
+ value = values.fetch(name) { value_present = false }
49
+
50
+ if value_present
51
+ type = additional_types.fetch(name, types[name])
52
+ @casted_values[name] = type.deserialize(value)
53
+ else
54
+ attr = default_attribute(name, value_present, value)
55
+ attr.value(&block)
56
+ end
57
+ end
58
+ end
59
+
60
+ protected
61
+ def attributes
62
+ unless @materialized
63
+ values.each_key { |key| self[key] }
64
+ types.each_key { |key| self[key] }
65
+ @materialized = true
66
+ end
67
+ @attributes
68
+ end
69
+
70
+ private
71
+ attr_reader :values, :types, :additional_types, :default_attributes
72
+
73
+ def default_attribute(
74
+ name,
75
+ value_present = true,
76
+ value = values.fetch(name) { value_present = false }
77
+ )
78
+ type = additional_types.fetch(name, types[name])
79
+
80
+ if value_present
81
+ @attributes[name] = Attribute.from_database(name, value, type, @casted_values[name])
82
+ elsif types.key?(name)
83
+ if attr = default_attributes[name]
84
+ @attributes[name] = attr.dup
85
+ else
86
+ @attributes[name] = Attribute.uninitialized(name, type)
87
+ end
88
+ else
89
+ Attribute.null(name)
90
+ end
91
+ end
92
+ end
93
+
22
94
  class LazyAttributeHash # :nodoc:
23
- delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize
95
+ delegate :transform_values, :each_value, :fetch, :except, to: :materialize
24
96
 
25
97
  def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
26
98
  @types = types
@@ -40,9 +112,6 @@ module ActiveModel
40
112
  end
41
113
 
42
114
  def []=(key, value)
43
- if frozen?
44
- raise RuntimeError, "Can't modify frozen hash"
45
- end
46
115
  delegate_hash[key] = value
47
116
  end
48
117
 
@@ -57,14 +126,9 @@ module ActiveModel
57
126
  super
58
127
  end
59
128
 
60
- def select
129
+ def each_key(&block)
61
130
  keys = types.keys | values.keys | delegate_hash.keys
62
- keys.each_with_object({}) do |key, hash|
63
- attribute = self[key]
64
- if yield(key, attribute)
65
- hash[key] = attribute
66
- end
67
- end
131
+ keys.each(&block)
68
132
  end
69
133
 
70
134
  def ==(other)
@@ -81,6 +145,9 @@ module ActiveModel
81
145
 
82
146
  def marshal_load(values)
83
147
  if values.is_a?(Hash)
148
+ ActiveSupport::Deprecation.warn(<<~MSG)
149
+ Marshalling load from legacy attributes format is deprecated and will be removed in Rails 7.0.
150
+ MSG
84
151
  empty_hash = {}.freeze
85
152
  initialize(empty_hash, empty_hash, empty_hash, empty_hash, values)
86
153
  @materialized = true
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/enumerable"
3
4
  require "active_support/core_ext/object/deep_dup"
4
5
  require "active_model/attribute_set/builder"
5
6
  require "active_model/attribute_set/yaml_encoder"
@@ -13,11 +14,11 @@ module ActiveModel
13
14
  end
14
15
 
15
16
  def [](name)
16
- attributes[name] || Attribute.null(name)
17
+ @attributes[name] || default_attribute(name)
17
18
  end
18
19
 
19
20
  def []=(name, value)
20
- attributes[name] = value
21
+ @attributes[name] = value
21
22
  end
22
23
 
23
24
  def values_before_type_cast
@@ -25,9 +26,9 @@ module ActiveModel
25
26
  end
26
27
 
27
28
  def to_hash
28
- initialized_attributes.transform_values(&:value)
29
+ keys.index_with { |name| self[name].value }
29
30
  end
30
- alias_method :to_h, :to_hash
31
+ alias :to_h :to_hash
31
32
 
32
33
  def key?(name)
33
34
  attributes.key?(name) && self[name].initialized?
@@ -42,35 +43,36 @@ module ActiveModel
42
43
  end
43
44
 
44
45
  def write_from_database(name, value)
45
- attributes[name] = self[name].with_value_from_database(value)
46
+ @attributes[name] = self[name].with_value_from_database(value)
46
47
  end
47
48
 
48
49
  def write_from_user(name, value)
49
- attributes[name] = self[name].with_value_from_user(value)
50
+ raise FrozenError, "can't modify frozen attributes" if frozen?
51
+ @attributes[name] = self[name].with_value_from_user(value)
52
+ value
50
53
  end
51
54
 
52
55
  def write_cast_value(name, value)
53
- attributes[name] = self[name].with_cast_value(value)
56
+ @attributes[name] = self[name].with_cast_value(value)
57
+ value
54
58
  end
55
59
 
56
60
  def freeze
57
- @attributes.freeze
61
+ attributes.freeze
58
62
  super
59
63
  end
60
64
 
61
65
  def deep_dup
62
- self.class.allocate.tap do |copy|
63
- copy.instance_variable_set(:@attributes, attributes.deep_dup)
64
- end
66
+ AttributeSet.new(attributes.deep_dup)
65
67
  end
66
68
 
67
69
  def initialize_dup(_)
68
- @attributes = attributes.dup
70
+ @attributes = @attributes.dup
69
71
  super
70
72
  end
71
73
 
72
74
  def initialize_clone(_)
73
- @attributes = attributes.clone
75
+ @attributes = @attributes.clone
74
76
  super
75
77
  end
76
78
 
@@ -81,7 +83,7 @@ module ActiveModel
81
83
  end
82
84
 
83
85
  def accessed
84
- attributes.select { |_, attr| attr.has_been_read? }.keys
86
+ attributes.each_key.select { |name| self[name].has_been_read? }
85
87
  end
86
88
 
87
89
  def map(&block)
@@ -97,8 +99,8 @@ module ActiveModel
97
99
  attr_reader :attributes
98
100
 
99
101
  private
100
- def initialized_attributes
101
- attributes.select { |_, attr| attr.initialized? }
102
+ def default_attribute(name)
103
+ Attribute.null(name)
102
104
  end
103
105
  end
104
106
  end
@@ -42,16 +42,14 @@ module ActiveModel
42
42
  end
43
43
 
44
44
  private
45
- def define_method_attribute=(name)
45
+ def define_method_attribute=(name, owner:)
46
46
  ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
47
- generated_attribute_methods, name, writer: true,
47
+ owner, name, writer: true,
48
48
  ) do |temp_method_name, attr_name_expr|
49
- generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
50
- def #{temp_method_name}(value)
51
- name = #{attr_name_expr}
52
- write_attribute(name, value)
53
- end
54
- RUBY
49
+ owner <<
50
+ "def #{temp_method_name}(value)" <<
51
+ " _write_attribute(#{attr_name_expr}, value)" <<
52
+ "end"
55
53
  end
56
54
  end
57
55
 
@@ -79,10 +77,14 @@ module ActiveModel
79
77
  super
80
78
  end
81
79
 
80
+ def initialize_dup(other) # :nodoc:
81
+ @attributes = @attributes.deep_dup
82
+ super
83
+ end
84
+
82
85
  # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
83
86
  #
84
87
  # class Person
85
- # include ActiveModel::Model
86
88
  # include ActiveModel::Attributes
87
89
  #
88
90
  # attribute :name, :string
@@ -112,25 +114,19 @@ module ActiveModel
112
114
  @attributes.keys
113
115
  end
114
116
 
115
- private
116
- def write_attribute(attr_name, value)
117
- name = attr_name.to_s
118
- name = self.class.attribute_aliases[name] || name
117
+ def freeze
118
+ @attributes = @attributes.clone.freeze unless frozen?
119
+ super
120
+ end
119
121
 
120
- @attributes.write_from_user(name, value)
121
- value
122
+ private
123
+ def _write_attribute(attr_name, value)
124
+ @attributes.write_from_user(attr_name, value)
122
125
  end
126
+ alias :attribute= :_write_attribute
123
127
 
124
128
  def attribute(attr_name)
125
- name = attr_name.to_s
126
- name = self.class.attribute_aliases[name] || name
127
-
128
- @attributes.fetch_value(name)
129
- end
130
-
131
- # Dispatch target for <tt>*=</tt> attribute methods.
132
- def attribute=(attribute_name, value)
133
- write_attribute(attribute_name, value)
129
+ @attributes.fetch_value(attr_name)
134
130
  end
135
131
  end
136
132
  end
@@ -147,7 +147,7 @@ module ActiveModel
147
147
  conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
148
148
  v != false
149
149
  }
150
- options[:if] = Array(options[:if]) << conditional
150
+ options[:if] = Array(options[:if]) + [conditional]
151
151
  set_callback(:"#{callback}", :after, *args, options, &block)
152
152
  end
153
153
  end
@@ -83,7 +83,9 @@ module ActiveModel
83
83
  #
84
84
  # person.previous_changes # => {"name" => [nil, "Bill"]}
85
85
  # person.name_previously_changed? # => true
86
+ # person.name_previously_changed?(from: nil, to: "Bill") # => true
86
87
  # person.name_previous_change # => [nil, "Bill"]
88
+ # person.name_previously_was # => nil
87
89
  # person.reload!
88
90
  # person.previous_changes # => {}
89
91
  #
@@ -122,8 +124,9 @@ module ActiveModel
122
124
 
123
125
  included do
124
126
  attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
125
- attribute_method_suffix "_previously_changed?", "_previous_change"
127
+ attribute_method_suffix "_previously_changed?", "_previous_change", "_previously_was"
126
128
  attribute_method_affix prefix: "restore_", suffix: "!"
129
+ attribute_method_affix prefix: "clear_", suffix: "_change"
127
130
  end
128
131
 
129
132
  def initialize_dup(other) # :nodoc:
@@ -136,7 +139,12 @@ module ActiveModel
136
139
  @mutations_from_database = nil
137
140
  end
138
141
 
139
- # Clears dirty data and moves +changes+ to +previously_changed+ and
142
+ def as_json(options = {}) # :nodoc:
143
+ options[:except] = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
144
+ super(options)
145
+ end
146
+
147
+ # Clears dirty data and moves +changes+ to +previous_changes+ and
140
148
  # +mutations_from_database+ to +mutations_before_last_save+ respectively.
141
149
  def changes_applied
142
150
  unless defined?(@attributes)
@@ -176,8 +184,13 @@ module ActiveModel
176
184
  end
177
185
 
178
186
  # Dispatch target for <tt>*_previously_changed?</tt> attribute methods.
179
- def attribute_previously_changed?(attr_name) # :nodoc:
180
- mutations_before_last_save.changed?(attr_name.to_s)
187
+ def attribute_previously_changed?(attr_name, **options) # :nodoc:
188
+ mutations_before_last_save.changed?(attr_name.to_s, **options)
189
+ end
190
+
191
+ # Dispatch target for <tt>*_previously_was</tt> attribute methods.
192
+ def attribute_previously_was(attr_name) # :nodoc:
193
+ mutations_before_last_save.original_value(attr_name.to_s)
181
194
  end
182
195
 
183
196
  # Restore all previous data of the provided attributes.