activemodel 6.0.5.1 → 6.1.7.4

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +95 -160
  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 +9 -7
@@ -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.