activemodel 5.2.8.1 → 6.1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +91 -97
  3. data/MIT-LICENSE +1 -2
  4. data/README.rdoc +6 -4
  5. data/lib/active_model/attribute/user_provided_default.rb +1 -2
  6. data/lib/active_model/attribute.rb +21 -21
  7. data/lib/active_model/attribute_assignment.rb +4 -6
  8. data/lib/active_model/attribute_methods.rb +117 -40
  9. data/lib/active_model/attribute_mutation_tracker.rb +90 -33
  10. data/lib/active_model/attribute_set/builder.rb +81 -16
  11. data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
  12. data/lib/active_model/attribute_set.rb +20 -28
  13. data/lib/active_model/attributes.rb +65 -44
  14. data/lib/active_model/callbacks.rb +11 -9
  15. data/lib/active_model/conversion.rb +1 -1
  16. data/lib/active_model/dirty.rb +51 -101
  17. data/lib/active_model/error.rb +207 -0
  18. data/lib/active_model/errors.rb +347 -155
  19. data/lib/active_model/gem_version.rb +3 -3
  20. data/lib/active_model/lint.rb +1 -1
  21. data/lib/active_model/naming.rb +22 -7
  22. data/lib/active_model/nested_error.rb +22 -0
  23. data/lib/active_model/railtie.rb +6 -0
  24. data/lib/active_model/secure_password.rb +55 -55
  25. data/lib/active_model/serialization.rb +9 -7
  26. data/lib/active_model/serializers/json.rb +17 -9
  27. data/lib/active_model/translation.rb +1 -1
  28. data/lib/active_model/type/big_integer.rb +0 -1
  29. data/lib/active_model/type/binary.rb +1 -1
  30. data/lib/active_model/type/boolean.rb +0 -1
  31. data/lib/active_model/type/date.rb +0 -5
  32. data/lib/active_model/type/date_time.rb +3 -8
  33. data/lib/active_model/type/decimal.rb +0 -1
  34. data/lib/active_model/type/float.rb +2 -3
  35. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +14 -6
  36. data/lib/active_model/type/helpers/numeric.rb +17 -6
  37. data/lib/active_model/type/helpers/time_value.rb +37 -15
  38. data/lib/active_model/type/helpers/timezone.rb +1 -1
  39. data/lib/active_model/type/immutable_string.rb +14 -11
  40. data/lib/active_model/type/integer.rb +15 -18
  41. data/lib/active_model/type/registry.rb +17 -19
  42. data/lib/active_model/type/string.rb +12 -3
  43. data/lib/active_model/type/time.rb +1 -6
  44. data/lib/active_model/type/value.rb +9 -2
  45. data/lib/active_model/type.rb +3 -2
  46. data/lib/active_model/validations/absence.rb +2 -2
  47. data/lib/active_model/validations/acceptance.rb +34 -27
  48. data/lib/active_model/validations/callbacks.rb +15 -16
  49. data/lib/active_model/validations/clusivity.rb +6 -3
  50. data/lib/active_model/validations/confirmation.rb +4 -4
  51. data/lib/active_model/validations/exclusion.rb +1 -1
  52. data/lib/active_model/validations/format.rb +2 -3
  53. data/lib/active_model/validations/inclusion.rb +2 -2
  54. data/lib/active_model/validations/length.rb +3 -3
  55. data/lib/active_model/validations/numericality.rb +58 -44
  56. data/lib/active_model/validations/presence.rb +1 -1
  57. data/lib/active_model/validations/validates.rb +7 -6
  58. data/lib/active_model/validations.rb +6 -9
  59. data/lib/active_model/validator.rb +8 -3
  60. data/lib/active_model.rb +2 -1
  61. metadata +13 -7
@@ -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?
@@ -37,48 +38,41 @@ module ActiveModel
37
38
  attributes.each_key.select { |name| self[name].initialized? }
38
39
  end
39
40
 
40
- if defined?(JRUBY_VERSION)
41
- # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
42
- # https://github.com/jruby/jruby/pull/2562
43
- def fetch_value(name, &block)
44
- self[name].value(&block)
45
- end
46
- else
47
- def fetch_value(name)
48
- self[name].value { |n| yield n if block_given? }
49
- end
41
+ def fetch_value(name, &block)
42
+ self[name].value(&block)
50
43
  end
51
44
 
52
45
  def write_from_database(name, value)
53
- attributes[name] = self[name].with_value_from_database(value)
46
+ @attributes[name] = self[name].with_value_from_database(value)
54
47
  end
55
48
 
56
49
  def write_from_user(name, value)
57
- 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
58
53
  end
59
54
 
60
55
  def write_cast_value(name, value)
61
- attributes[name] = self[name].with_cast_value(value)
56
+ @attributes[name] = self[name].with_cast_value(value)
57
+ value
62
58
  end
63
59
 
64
60
  def freeze
65
- @attributes.freeze
61
+ attributes.freeze
66
62
  super
67
63
  end
68
64
 
69
65
  def deep_dup
70
- self.class.allocate.tap do |copy|
71
- copy.instance_variable_set(:@attributes, attributes.deep_dup)
72
- end
66
+ AttributeSet.new(attributes.deep_dup)
73
67
  end
74
68
 
75
69
  def initialize_dup(_)
76
- @attributes = attributes.dup
70
+ @attributes = @attributes.dup
77
71
  super
78
72
  end
79
73
 
80
74
  def initialize_clone(_)
81
- @attributes = attributes.clone
75
+ @attributes = @attributes.clone
82
76
  super
83
77
  end
84
78
 
@@ -89,7 +83,7 @@ module ActiveModel
89
83
  end
90
84
 
91
85
  def accessed
92
- attributes.select { |_, attr| attr.has_been_read? }.keys
86
+ attributes.each_key.select { |name| self[name].has_been_read? }
93
87
  end
94
88
 
95
89
  def map(&block)
@@ -102,13 +96,11 @@ module ActiveModel
102
96
  end
103
97
 
104
98
  protected
105
-
106
99
  attr_reader :attributes
107
100
 
108
101
  private
109
-
110
- def initialized_attributes
111
- attributes.select { |_, attr| attr.initialized? }
102
+ def default_attribute(name)
103
+ Attribute.null(name)
112
104
  end
113
105
  end
114
106
  end
@@ -26,20 +26,31 @@ module ActiveModel
26
26
  define_attribute_method(name)
27
27
  end
28
28
 
29
- private
30
-
31
- def define_method_attribute=(name)
32
- safe_name = name.unpack("h*".freeze).first
33
- ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name
29
+ # Returns an array of attribute names as strings
30
+ #
31
+ # class Person
32
+ # include ActiveModel::Attributes
33
+ #
34
+ # attribute :name, :string
35
+ # attribute :age, :integer
36
+ # end
37
+ #
38
+ # Person.attribute_names
39
+ # # => ["name", "age"]
40
+ def attribute_names
41
+ attribute_types.keys
42
+ end
34
43
 
35
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
36
- def __temp__#{safe_name}=(value)
37
- name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name}
38
- write_attribute(name, value)
39
- end
40
- alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
41
- undef_method :__temp__#{safe_name}=
42
- STR
44
+ private
45
+ def define_method_attribute=(name, owner:)
46
+ ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
47
+ owner, name, writer: true,
48
+ ) do |temp_method_name, attr_name_expr|
49
+ owner <<
50
+ "def #{temp_method_name}(value)" <<
51
+ " _write_attribute(#{attr_name_expr}, value)" <<
52
+ "end"
53
+ end
43
54
  end
44
55
 
45
56
  NO_DEFAULT_PROVIDED = Object.new # :nodoc:
@@ -66,46 +77,56 @@ module ActiveModel
66
77
  super
67
78
  end
68
79
 
80
+ def initialize_dup(other) # :nodoc:
81
+ @attributes = @attributes.deep_dup
82
+ super
83
+ end
84
+
85
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
86
+ #
87
+ # class Person
88
+ # include ActiveModel::Attributes
89
+ #
90
+ # attribute :name, :string
91
+ # attribute :age, :integer
92
+ # end
93
+ #
94
+ # person = Person.new(name: 'Francesco', age: 22)
95
+ # person.attributes
96
+ # # => {"name"=>"Francesco", "age"=>22}
69
97
  def attributes
70
98
  @attributes.to_hash
71
99
  end
72
100
 
73
- private
101
+ # Returns an array of attribute names as strings
102
+ #
103
+ # class Person
104
+ # include ActiveModel::Attributes
105
+ #
106
+ # attribute :name, :string
107
+ # attribute :age, :integer
108
+ # end
109
+ #
110
+ # person = Person.new
111
+ # person.attribute_names
112
+ # # => ["name", "age"]
113
+ def attribute_names
114
+ @attributes.keys
115
+ end
74
116
 
75
- def write_attribute(attr_name, value)
76
- name = if self.class.attribute_alias?(attr_name)
77
- self.class.attribute_alias(attr_name).to_s
78
- else
79
- attr_name.to_s
80
- end
117
+ def freeze
118
+ @attributes = @attributes.clone.freeze unless frozen?
119
+ super
120
+ end
81
121
 
82
- @attributes.write_from_user(name, value)
83
- value
122
+ private
123
+ def _write_attribute(attr_name, value)
124
+ @attributes.write_from_user(attr_name, value)
84
125
  end
126
+ alias :attribute= :_write_attribute
85
127
 
86
128
  def attribute(attr_name)
87
- name = if self.class.attribute_alias?(attr_name)
88
- self.class.attribute_alias(attr_name).to_s
89
- else
90
- attr_name.to_s
91
- end
92
- @attributes.fetch_value(name)
93
- end
94
-
95
- # Handle *= for method_missing.
96
- def attribute=(attribute_name, value)
97
- write_attribute(attribute_name, value)
98
- end
99
- end
100
-
101
- module AttributeMethods #:nodoc:
102
- AttrNames = Module.new {
103
- def self.set_name_cache(name, value)
104
- const_name = "ATTR_#{name}"
105
- unless const_defined? const_name
106
- const_set const_name, value.dup.freeze
107
- end
129
+ @attributes.fetch_value(attr_name)
108
130
  end
109
- }
110
131
  end
111
132
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/array/extract_options"
4
+ require "active_support/core_ext/hash/keys"
4
5
 
5
6
  module ActiveModel
6
7
  # == Active \Model \Callbacks
@@ -125,28 +126,29 @@ module ActiveModel
125
126
  end
126
127
 
127
128
  private
128
-
129
129
  def _define_before_model_callback(klass, callback)
130
- klass.define_singleton_method("before_#{callback}") do |*args, &block|
131
- set_callback(:"#{callback}", :before, *args, &block)
130
+ klass.define_singleton_method("before_#{callback}") do |*args, **options, &block|
131
+ options.assert_valid_keys(:if, :unless, :prepend)
132
+ set_callback(:"#{callback}", :before, *args, options, &block)
132
133
  end
133
134
  end
134
135
 
135
136
  def _define_around_model_callback(klass, callback)
136
- klass.define_singleton_method("around_#{callback}") do |*args, &block|
137
- set_callback(:"#{callback}", :around, *args, &block)
137
+ klass.define_singleton_method("around_#{callback}") do |*args, **options, &block|
138
+ options.assert_valid_keys(:if, :unless, :prepend)
139
+ set_callback(:"#{callback}", :around, *args, options, &block)
138
140
  end
139
141
  end
140
142
 
141
143
  def _define_after_model_callback(klass, callback)
142
- klass.define_singleton_method("after_#{callback}") do |*args, &block|
143
- options = args.extract_options!
144
+ klass.define_singleton_method("after_#{callback}") do |*args, **options, &block|
145
+ options.assert_valid_keys(:if, :unless, :prepend)
144
146
  options[:prepend] = true
145
147
  conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
146
148
  v != false
147
149
  }
148
- options[:if] = Array(options[:if]) << conditional
149
- set_callback(:"#{callback}", :after, *(args << options), &block)
150
+ options[:if] = Array(options[:if]) + [conditional]
151
+ set_callback(:"#{callback}", :after, *args, options, &block)
150
152
  end
151
153
  end
152
154
  end
@@ -103,7 +103,7 @@ module ActiveModel
103
103
  @_to_partial_path ||= begin
104
104
  element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
105
105
  collection = ActiveSupport::Inflector.tableize(name)
106
- "#{collection}/#{element}".freeze
106
+ "#{collection}/#{element}"
107
107
  end
108
108
  end
109
109
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/hash_with_indifferent_access"
4
- require "active_support/core_ext/object/duplicable"
5
3
  require "active_model/attribute_mutation_tracker"
6
4
 
7
5
  module ActiveModel
@@ -85,7 +83,9 @@ module ActiveModel
85
83
  #
86
84
  # person.previous_changes # => {"name" => [nil, "Bill"]}
87
85
  # person.name_previously_changed? # => true
86
+ # person.name_previously_changed?(from: nil, to: "Bill") # => true
88
87
  # person.name_previous_change # => [nil, "Bill"]
88
+ # person.name_previously_was # => nil
89
89
  # person.reload!
90
90
  # person.previous_changes # => {}
91
91
  #
@@ -122,13 +122,11 @@ module ActiveModel
122
122
  extend ActiveSupport::Concern
123
123
  include ActiveModel::AttributeMethods
124
124
 
125
- OPTION_NOT_GIVEN = Object.new # :nodoc:
126
- private_constant :OPTION_NOT_GIVEN
127
-
128
125
  included do
129
126
  attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
130
- attribute_method_suffix "_previously_changed?", "_previous_change"
127
+ attribute_method_suffix "_previously_changed?", "_previous_change", "_previously_was"
131
128
  attribute_method_affix prefix: "restore_", suffix: "!"
129
+ attribute_method_affix prefix: "clear_", suffix: "_change"
132
130
  end
133
131
 
134
132
  def initialize_dup(other) # :nodoc:
@@ -141,25 +139,29 @@ module ActiveModel
141
139
  @mutations_from_database = nil
142
140
  end
143
141
 
144
- # 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
145
148
  # +mutations_from_database+ to +mutations_before_last_save+ respectively.
146
149
  def changes_applied
147
150
  unless defined?(@attributes)
148
- @previously_changed = changes
151
+ mutations_from_database.finalize_changes
149
152
  end
150
153
  @mutations_before_last_save = mutations_from_database
151
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
152
154
  forget_attribute_assignments
153
155
  @mutations_from_database = nil
154
156
  end
155
157
 
156
- # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
158
+ # Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise.
157
159
  #
158
160
  # person.changed? # => false
159
161
  # person.name = 'bob'
160
162
  # person.changed? # => true
161
163
  def changed?
162
- changed_attributes.present?
164
+ mutations_from_database.any_changes?
163
165
  end
164
166
 
165
167
  # Returns an array with the name of the attributes with unsaved changes.
@@ -168,42 +170,42 @@ module ActiveModel
168
170
  # person.name = 'bob'
169
171
  # person.changed # => ["name"]
170
172
  def changed
171
- changed_attributes.keys
173
+ mutations_from_database.changed_attribute_names
172
174
  end
173
175
 
174
- # Handles <tt>*_changed?</tt> for +method_missing+.
175
- def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
176
- !!changes_include?(attr) &&
177
- (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
178
- (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
176
+ # Dispatch target for <tt>*_changed?</tt> attribute methods.
177
+ def attribute_changed?(attr_name, **options) # :nodoc:
178
+ mutations_from_database.changed?(attr_name.to_s, **options)
179
179
  end
180
180
 
181
- # Handles <tt>*_was</tt> for +method_missing+.
182
- def attribute_was(attr) # :nodoc:
183
- attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
181
+ # Dispatch target for <tt>*_was</tt> attribute methods.
182
+ def attribute_was(attr_name) # :nodoc:
183
+ mutations_from_database.original_value(attr_name.to_s)
184
184
  end
185
185
 
186
- # Handles <tt>*_previously_changed?</tt> for +method_missing+.
187
- def attribute_previously_changed?(attr) #:nodoc:
188
- previous_changes_include?(attr)
186
+ # Dispatch target for <tt>*_previously_changed?</tt> attribute methods.
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)
189
194
  end
190
195
 
191
196
  # Restore all previous data of the provided attributes.
192
- def restore_attributes(attributes = changed)
193
- attributes.each { |attr| restore_attribute! attr }
197
+ def restore_attributes(attr_names = changed)
198
+ attr_names.each { |attr_name| restore_attribute!(attr_name) }
194
199
  end
195
200
 
196
201
  # Clears all dirty data: current changes and previous changes.
197
202
  def clear_changes_information
198
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
199
203
  @mutations_before_last_save = nil
200
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
201
204
  forget_attribute_assignments
202
205
  @mutations_from_database = nil
203
206
  end
204
207
 
205
208
  def clear_attribute_changes(attr_names)
206
- attributes_changed_by_setter.except!(*attr_names)
207
209
  attr_names.each do |attr_name|
208
210
  clear_attribute_change(attr_name)
209
211
  end
@@ -216,13 +218,7 @@ module ActiveModel
216
218
  # person.name = 'robert'
217
219
  # person.changed_attributes # => {"name" => "bob"}
218
220
  def changed_attributes
219
- # This should only be set by methods which will call changed_attributes
220
- # multiple times when it is known that the computed value cannot change.
221
- if defined?(@cached_changed_attributes)
222
- @cached_changed_attributes
223
- else
224
- attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
225
- end
221
+ mutations_from_database.changed_values
226
222
  end
227
223
 
228
224
  # Returns a hash of changed attributes indicating their original
@@ -232,9 +228,7 @@ module ActiveModel
232
228
  # person.name = 'bob'
233
229
  # person.changes # => { "name" => ["bill", "bob"] }
234
230
  def changes
235
- cache_changed_attributes do
236
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
237
- end
231
+ mutations_from_database.changes
238
232
  end
239
233
 
240
234
  # Returns a hash of attributes that were changed before the model was saved.
@@ -244,27 +238,23 @@ module ActiveModel
244
238
  # person.save
245
239
  # person.previous_changes # => {"name" => ["bob", "robert"]}
246
240
  def previous_changes
247
- @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
248
- @previously_changed.merge(mutations_before_last_save.changes)
241
+ mutations_before_last_save.changes
249
242
  end
250
243
 
251
244
  def attribute_changed_in_place?(attr_name) # :nodoc:
252
- mutations_from_database.changed_in_place?(attr_name)
245
+ mutations_from_database.changed_in_place?(attr_name.to_s)
253
246
  end
254
247
 
255
248
  private
256
249
  def clear_attribute_change(attr_name)
257
- mutations_from_database.forget_change(attr_name)
250
+ mutations_from_database.forget_change(attr_name.to_s)
258
251
  end
259
252
 
260
253
  def mutations_from_database
261
- unless defined?(@mutations_from_database)
262
- @mutations_from_database = nil
263
- end
264
254
  @mutations_from_database ||= if defined?(@attributes)
265
255
  ActiveModel::AttributeMutationTracker.new(@attributes)
266
256
  else
267
- NullMutationTracker.instance
257
+ ActiveModel::ForcedMutationTracker.new(self)
268
258
  end
269
259
  end
270
260
 
@@ -276,68 +266,28 @@ module ActiveModel
276
266
  @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
277
267
  end
278
268
 
279
- def cache_changed_attributes
280
- @cached_changed_attributes = changed_attributes
281
- yield
282
- ensure
283
- clear_changed_attributes_cache
284
- end
285
-
286
- def clear_changed_attributes_cache
287
- remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
288
- end
289
-
290
- # Returns +true+ if attr_name is changed, +false+ otherwise.
291
- def changes_include?(attr_name)
292
- attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
293
- end
294
- alias attribute_changed_by_setter? changes_include?
295
-
296
- # Returns +true+ if attr_name were changed before the model was saved,
297
- # +false+ otherwise.
298
- def previous_changes_include?(attr_name)
299
- previous_changes.include?(attr_name)
269
+ # Dispatch target for <tt>*_change</tt> attribute methods.
270
+ def attribute_change(attr_name)
271
+ mutations_from_database.change_to_attribute(attr_name.to_s)
300
272
  end
301
273
 
302
- # Handles <tt>*_change</tt> for +method_missing+.
303
- def attribute_change(attr)
304
- [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
274
+ # Dispatch target for <tt>*_previous_change</tt> attribute methods.
275
+ def attribute_previous_change(attr_name)
276
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
305
277
  end
306
278
 
307
- # Handles <tt>*_previous_change</tt> for +method_missing+.
308
- def attribute_previous_change(attr)
309
- previous_changes[attr] if attribute_previously_changed?(attr)
279
+ # Dispatch target for <tt>*_will_change!</tt> attribute methods.
280
+ def attribute_will_change!(attr_name)
281
+ mutations_from_database.force_change(attr_name.to_s)
310
282
  end
311
283
 
312
- # Handles <tt>*_will_change!</tt> for +method_missing+.
313
- def attribute_will_change!(attr)
314
- unless attribute_changed?(attr)
315
- begin
316
- value = _read_attribute(attr)
317
- value = value.duplicable? ? value.clone : value
318
- rescue TypeError, NoMethodError
319
- end
320
-
321
- set_attribute_was(attr, value)
284
+ # Dispatch target for <tt>restore_*!</tt> attribute methods.
285
+ def restore_attribute!(attr_name)
286
+ attr_name = attr_name.to_s
287
+ if attribute_changed?(attr_name)
288
+ __send__("#{attr_name}=", attribute_was(attr_name))
289
+ clear_attribute_change(attr_name)
322
290
  end
323
- mutations_from_database.force_change(attr)
324
- end
325
-
326
- # Handles <tt>restore_*!</tt> for +method_missing+.
327
- def restore_attribute!(attr)
328
- if attribute_changed?(attr)
329
- __send__("#{attr}=", changed_attributes[attr])
330
- clear_attribute_changes([attr])
331
- end
332
- end
333
-
334
- def attributes_changed_by_setter
335
- @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
336
- end
337
-
338
- # Force an attribute to have a particular "before" value
339
- def set_attribute_was(attr, old_value)
340
- attributes_changed_by_setter[attr] = old_value
341
291
  end
342
292
  end
343
293
  end