activemodel 5.2.2.1 → 6.0.2

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -51
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -2
  5. data/lib/active_model.rb +1 -1
  6. data/lib/active_model/attribute.rb +3 -4
  7. data/lib/active_model/attribute/user_provided_default.rb +1 -2
  8. data/lib/active_model/attribute_assignment.rb +1 -1
  9. data/lib/active_model/attribute_methods.rb +54 -15
  10. data/lib/active_model/attribute_mutation_tracker.rb +88 -34
  11. data/lib/active_model/attribute_set.rb +2 -10
  12. data/lib/active_model/attribute_set/builder.rb +1 -3
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
  14. data/lib/active_model/attributes.rb +60 -33
  15. data/lib/active_model/callbacks.rb +10 -7
  16. data/lib/active_model/conversion.rb +1 -1
  17. data/lib/active_model/dirty.rb +36 -99
  18. data/lib/active_model/errors.rb +104 -20
  19. data/lib/active_model/gem_version.rb +3 -3
  20. data/lib/active_model/naming.rb +19 -3
  21. data/lib/active_model/railtie.rb +6 -0
  22. data/lib/active_model/secure_password.rb +47 -48
  23. data/lib/active_model/serializers/json.rb +10 -9
  24. data/lib/active_model/type/binary.rb +1 -1
  25. data/lib/active_model/type/boolean.rb +10 -1
  26. data/lib/active_model/type/date.rb +2 -5
  27. data/lib/active_model/type/date_time.rb +4 -7
  28. data/lib/active_model/type/float.rb +0 -2
  29. data/lib/active_model/type/helpers.rb +1 -0
  30. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +4 -0
  31. data/lib/active_model/type/helpers/numeric.rb +9 -2
  32. data/lib/active_model/type/helpers/time_value.rb +16 -15
  33. data/lib/active_model/type/helpers/timezone.rb +19 -0
  34. data/lib/active_model/type/integer.rb +7 -19
  35. data/lib/active_model/type/registry.rb +2 -10
  36. data/lib/active_model/type/string.rb +2 -2
  37. data/lib/active_model/type/time.rb +2 -1
  38. data/lib/active_model/validations.rb +0 -2
  39. data/lib/active_model/validations/acceptance.rb +33 -25
  40. data/lib/active_model/validations/clusivity.rb +1 -1
  41. data/lib/active_model/validations/confirmation.rb +2 -2
  42. data/lib/active_model/validations/inclusion.rb +1 -1
  43. data/lib/active_model/validations/length.rb +1 -1
  44. data/lib/active_model/validations/numericality.rb +20 -11
  45. data/lib/active_model/validations/validates.rb +2 -2
  46. data/lib/active_model/validator.rb +1 -1
  47. metadata +13 -9
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/hash/indifferent_access"
4
+ require "active_support/core_ext/object/duplicable"
4
5
 
5
6
  module ActiveModel
6
7
  class AttributeMutationTracker # :nodoc:
7
8
  OPTION_NOT_GIVEN = Object.new
8
9
 
9
- def initialize(attributes)
10
+ def initialize(attributes, forced_changes = Set.new)
10
11
  @attributes = attributes
11
- @forced_changes = Set.new
12
+ @forced_changes = forced_changes
12
13
  end
13
14
 
14
15
  def changed_attribute_names
@@ -18,24 +19,22 @@ module ActiveModel
18
19
  def changed_values
19
20
  attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
20
21
  if changed?(attr_name)
21
- result[attr_name] = attributes[attr_name].original_value
22
+ result[attr_name] = original_value(attr_name)
22
23
  end
23
24
  end
24
25
  end
25
26
 
26
27
  def changes
27
28
  attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
28
- change = change_to_attribute(attr_name)
29
- if change
29
+ if change = change_to_attribute(attr_name)
30
30
  result.merge!(attr_name => change)
31
31
  end
32
32
  end
33
33
  end
34
34
 
35
35
  def change_to_attribute(attr_name)
36
- attr_name = attr_name.to_s
37
36
  if changed?(attr_name)
38
- [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
37
+ [original_value(attr_name), fetch_value(attr_name)]
39
38
  end
40
39
  end
41
40
 
@@ -44,81 +43,136 @@ module ActiveModel
44
43
  end
45
44
 
46
45
  def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
47
- attr_name = attr_name.to_s
48
- forced_changes.include?(attr_name) ||
49
- attributes[attr_name].changed? &&
50
- (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
51
- (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
46
+ attribute_changed?(attr_name) &&
47
+ (OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
48
+ (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
52
49
  end
53
50
 
54
51
  def changed_in_place?(attr_name)
55
- attributes[attr_name.to_s].changed_in_place?
52
+ attributes[attr_name].changed_in_place?
56
53
  end
57
54
 
58
55
  def forget_change(attr_name)
59
- attr_name = attr_name.to_s
60
56
  attributes[attr_name] = attributes[attr_name].forgetting_assignment
61
57
  forced_changes.delete(attr_name)
62
58
  end
63
59
 
64
60
  def original_value(attr_name)
65
- attributes[attr_name.to_s].original_value
61
+ attributes[attr_name].original_value
66
62
  end
67
63
 
68
64
  def force_change(attr_name)
69
- forced_changes << attr_name.to_s
65
+ forced_changes << attr_name
70
66
  end
71
67
 
72
- # TODO Change this to private once we've dropped Ruby 2.2 support.
73
- # Workaround for Ruby 2.2 "private attribute?" warning.
74
- protected
75
-
68
+ private
76
69
  attr_reader :attributes, :forced_changes
77
70
 
71
+ def attr_names
72
+ attributes.keys
73
+ end
74
+
75
+ def attribute_changed?(attr_name)
76
+ forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
77
+ end
78
+
79
+ def fetch_value(attr_name)
80
+ attributes.fetch_value(attr_name)
81
+ end
82
+ end
83
+
84
+ class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
85
+ def initialize(attributes, forced_changes = {})
86
+ super
87
+ @finalized_changes = nil
88
+ end
89
+
90
+ def changed_in_place?(attr_name)
91
+ false
92
+ end
93
+
94
+ def change_to_attribute(attr_name)
95
+ if finalized_changes&.include?(attr_name)
96
+ finalized_changes[attr_name].dup
97
+ else
98
+ super
99
+ end
100
+ end
101
+
102
+ def forget_change(attr_name)
103
+ forced_changes.delete(attr_name)
104
+ end
105
+
106
+ def original_value(attr_name)
107
+ if changed?(attr_name)
108
+ forced_changes[attr_name]
109
+ else
110
+ fetch_value(attr_name)
111
+ end
112
+ end
113
+
114
+ def force_change(attr_name)
115
+ forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
116
+ end
117
+
118
+ def finalize_changes
119
+ @finalized_changes = changes
120
+ end
121
+
78
122
  private
123
+ attr_reader :finalized_changes
79
124
 
80
125
  def attr_names
81
- attributes.keys
126
+ forced_changes.keys
127
+ end
128
+
129
+ def attribute_changed?(attr_name)
130
+ forced_changes.include?(attr_name)
131
+ end
132
+
133
+ def fetch_value(attr_name)
134
+ attributes.send(:_read_attribute, attr_name)
135
+ end
136
+
137
+ def clone_value(attr_name)
138
+ value = fetch_value(attr_name)
139
+ value.duplicable? ? value.clone : value
140
+ rescue TypeError, NoMethodError
141
+ value
82
142
  end
83
143
  end
84
144
 
85
145
  class NullMutationTracker # :nodoc:
86
146
  include Singleton
87
147
 
88
- def changed_attribute_names(*)
148
+ def changed_attribute_names
89
149
  []
90
150
  end
91
151
 
92
- def changed_values(*)
152
+ def changed_values
93
153
  {}
94
154
  end
95
155
 
96
- def changes(*)
156
+ def changes
97
157
  {}
98
158
  end
99
159
 
100
160
  def change_to_attribute(attr_name)
101
161
  end
102
162
 
103
- def any_changes?(*)
163
+ def any_changes?
104
164
  false
105
165
  end
106
166
 
107
- def changed?(*)
167
+ def changed?(attr_name, **)
108
168
  false
109
169
  end
110
170
 
111
- def changed_in_place?(*)
171
+ def changed_in_place?(attr_name)
112
172
  false
113
173
  end
114
174
 
115
- def forget_change(*)
116
- end
117
-
118
- def original_value(*)
119
- end
120
-
121
- def force_change(*)
175
+ def original_value(attr_name)
122
176
  end
123
177
  end
124
178
  end
@@ -37,16 +37,8 @@ module ActiveModel
37
37
  attributes.each_key.select { |name| self[name].initialized? }
38
38
  end
39
39
 
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
40
+ def fetch_value(name, &block)
41
+ self[name].value(&block)
50
42
  end
51
43
 
52
44
  def write_from_database(name, value)
@@ -90,9 +90,6 @@ module ActiveModel
90
90
  end
91
91
 
92
92
  protected
93
-
94
- attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
95
-
96
93
  def materialize
97
94
  unless @materialized
98
95
  values.each_key { |key| self[key] }
@@ -105,6 +102,7 @@ module ActiveModel
105
102
  end
106
103
 
107
104
  private
105
+ attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
108
106
 
109
107
  def assign_default_value(name)
110
108
  type = additional_types.fetch(name, types[name])
@@ -33,8 +33,7 @@ module ActiveModel
33
33
  end
34
34
  end
35
35
 
36
- protected
37
-
36
+ private
38
37
  attr_reader :default_types
39
38
  end
40
39
  end
@@ -26,20 +26,34 @@ module ActiveModel
26
26
  define_attribute_method(name)
27
27
  end
28
28
 
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
43
+
29
44
  private
30
45
 
31
46
  def define_method_attribute=(name)
32
- safe_name = name.unpack("h*".freeze).first
33
- ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name
34
-
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
47
+ ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
48
+ generated_attribute_methods, name, writer: true,
49
+ ) do |temp_method_name, attr_name_expr|
50
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
51
+ def #{temp_method_name}(value)
52
+ name = #{attr_name_expr}
53
+ write_attribute(name, value)
54
+ end
55
+ RUBY
56
+ end
43
57
  end
44
58
 
45
59
  NO_DEFAULT_PROVIDED = Object.new # :nodoc:
@@ -66,46 +80,59 @@ module ActiveModel
66
80
  super
67
81
  end
68
82
 
83
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
84
+ #
85
+ # class Person
86
+ # include ActiveModel::Model
87
+ # include ActiveModel::Attributes
88
+ #
89
+ # attribute :name, :string
90
+ # attribute :age, :integer
91
+ # end
92
+ #
93
+ # person = Person.new(name: 'Francesco', age: 22)
94
+ # person.attributes
95
+ # # => {"name"=>"Francesco", "age"=>22}
69
96
  def attributes
70
97
  @attributes.to_hash
71
98
  end
72
99
 
100
+ # Returns an array of attribute names as strings
101
+ #
102
+ # class Person
103
+ # include ActiveModel::Attributes
104
+ #
105
+ # attribute :name, :string
106
+ # attribute :age, :integer
107
+ # end
108
+ #
109
+ # person = Person.new
110
+ # person.attribute_names
111
+ # # => ["name", "age"]
112
+ def attribute_names
113
+ @attributes.keys
114
+ end
115
+
73
116
  private
74
117
 
75
118
  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
119
+ name = attr_name.to_s
120
+ name = self.class.attribute_aliases[name] || name
81
121
 
82
122
  @attributes.write_from_user(name, value)
83
123
  value
84
124
  end
85
125
 
86
126
  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
127
+ name = attr_name.to_s
128
+ name = self.class.attribute_aliases[name] || name
129
+
92
130
  @attributes.fetch_value(name)
93
131
  end
94
132
 
95
- # Handle *= for method_missing.
133
+ # Dispatch target for <tt>*=</tt> attribute methods.
96
134
  def attribute=(attribute_name, value)
97
135
  write_attribute(attribute_name, value)
98
136
  end
99
137
  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
108
- end
109
- }
110
- end
111
138
  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
@@ -127,26 +128,28 @@ module ActiveModel
127
128
  private
128
129
 
129
130
  def _define_before_model_callback(klass, callback)
130
- klass.define_singleton_method("before_#{callback}") do |*args, &block|
131
- set_callback(:"#{callback}", :before, *args, &block)
131
+ klass.define_singleton_method("before_#{callback}") do |*args, **options, &block|
132
+ options.assert_valid_keys(:if, :unless, :prepend)
133
+ set_callback(:"#{callback}", :before, *args, options, &block)
132
134
  end
133
135
  end
134
136
 
135
137
  def _define_around_model_callback(klass, callback)
136
- klass.define_singleton_method("around_#{callback}") do |*args, &block|
137
- set_callback(:"#{callback}", :around, *args, &block)
138
+ klass.define_singleton_method("around_#{callback}") do |*args, **options, &block|
139
+ options.assert_valid_keys(:if, :unless, :prepend)
140
+ set_callback(:"#{callback}", :around, *args, options, &block)
138
141
  end
139
142
  end
140
143
 
141
144
  def _define_after_model_callback(klass, callback)
142
- klass.define_singleton_method("after_#{callback}") do |*args, &block|
143
- options = args.extract_options!
145
+ klass.define_singleton_method("after_#{callback}") do |*args, **options, &block|
146
+ options.assert_valid_keys(:if, :unless, :prepend)
144
147
  options[:prepend] = true
145
148
  conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
146
149
  v != false
147
150
  }
148
151
  options[:if] = Array(options[:if]) << conditional
149
- set_callback(:"#{callback}", :after, *(args << options), &block)
152
+ set_callback(:"#{callback}", :after, *args, options, &block)
150
153
  end
151
154
  end
152
155
  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
@@ -122,9 +120,6 @@ module ActiveModel
122
120
  extend ActiveSupport::Concern
123
121
  include ActiveModel::AttributeMethods
124
122
 
125
- OPTION_NOT_GIVEN = Object.new # :nodoc:
126
- private_constant :OPTION_NOT_GIVEN
127
-
128
123
  included do
129
124
  attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
130
125
  attribute_method_suffix "_previously_changed?", "_previous_change"
@@ -145,21 +140,20 @@ module ActiveModel
145
140
  # +mutations_from_database+ to +mutations_before_last_save+ respectively.
146
141
  def changes_applied
147
142
  unless defined?(@attributes)
148
- @previously_changed = changes
143
+ mutations_from_database.finalize_changes
149
144
  end
150
145
  @mutations_before_last_save = mutations_from_database
151
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
152
146
  forget_attribute_assignments
153
147
  @mutations_from_database = nil
154
148
  end
155
149
 
156
- # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
150
+ # Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise.
157
151
  #
158
152
  # person.changed? # => false
159
153
  # person.name = 'bob'
160
154
  # person.changed? # => true
161
155
  def changed?
162
- changed_attributes.present?
156
+ mutations_from_database.any_changes?
163
157
  end
164
158
 
165
159
  # Returns an array with the name of the attributes with unsaved changes.
@@ -168,42 +162,37 @@ module ActiveModel
168
162
  # person.name = 'bob'
169
163
  # person.changed # => ["name"]
170
164
  def changed
171
- changed_attributes.keys
165
+ mutations_from_database.changed_attribute_names
172
166
  end
173
167
 
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])
168
+ # Dispatch target for <tt>*_changed?</tt> attribute methods.
169
+ def attribute_changed?(attr_name, **options) # :nodoc:
170
+ mutations_from_database.changed?(attr_name.to_s, options)
179
171
  end
180
172
 
181
- # Handles <tt>*_was</tt> for +method_missing+.
182
- def attribute_was(attr) # :nodoc:
183
- attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
173
+ # Dispatch target for <tt>*_was</tt> attribute methods.
174
+ def attribute_was(attr_name) # :nodoc:
175
+ mutations_from_database.original_value(attr_name.to_s)
184
176
  end
185
177
 
186
- # Handles <tt>*_previously_changed?</tt> for +method_missing+.
187
- def attribute_previously_changed?(attr) #:nodoc:
188
- previous_changes_include?(attr)
178
+ # 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)
189
181
  end
190
182
 
191
183
  # Restore all previous data of the provided attributes.
192
- def restore_attributes(attributes = changed)
193
- attributes.each { |attr| restore_attribute! attr }
184
+ def restore_attributes(attr_names = changed)
185
+ attr_names.each { |attr_name| restore_attribute!(attr_name) }
194
186
  end
195
187
 
196
188
  # Clears all dirty data: current changes and previous changes.
197
189
  def clear_changes_information
198
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
199
190
  @mutations_before_last_save = nil
200
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
201
191
  forget_attribute_assignments
202
192
  @mutations_from_database = nil
203
193
  end
204
194
 
205
195
  def clear_attribute_changes(attr_names)
206
- attributes_changed_by_setter.except!(*attr_names)
207
196
  attr_names.each do |attr_name|
208
197
  clear_attribute_change(attr_name)
209
198
  end
@@ -216,13 +205,7 @@ module ActiveModel
216
205
  # person.name = 'robert'
217
206
  # person.changed_attributes # => {"name" => "bob"}
218
207
  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
208
+ mutations_from_database.changed_values
226
209
  end
227
210
 
228
211
  # Returns a hash of changed attributes indicating their original
@@ -232,9 +215,7 @@ module ActiveModel
232
215
  # person.name = 'bob'
233
216
  # person.changes # => { "name" => ["bill", "bob"] }
234
217
  def changes
235
- cache_changed_attributes do
236
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
237
- end
218
+ mutations_from_database.changes
238
219
  end
239
220
 
240
221
  # Returns a hash of attributes that were changed before the model was saved.
@@ -244,27 +225,23 @@ module ActiveModel
244
225
  # person.save
245
226
  # person.previous_changes # => {"name" => ["bob", "robert"]}
246
227
  def previous_changes
247
- @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
248
- @previously_changed.merge(mutations_before_last_save.changes)
228
+ mutations_before_last_save.changes
249
229
  end
250
230
 
251
231
  def attribute_changed_in_place?(attr_name) # :nodoc:
252
- mutations_from_database.changed_in_place?(attr_name)
232
+ mutations_from_database.changed_in_place?(attr_name.to_s)
253
233
  end
254
234
 
255
235
  private
256
236
  def clear_attribute_change(attr_name)
257
- mutations_from_database.forget_change(attr_name)
237
+ mutations_from_database.forget_change(attr_name.to_s)
258
238
  end
259
239
 
260
240
  def mutations_from_database
261
- unless defined?(@mutations_from_database)
262
- @mutations_from_database = nil
263
- end
264
241
  @mutations_from_database ||= if defined?(@attributes)
265
242
  ActiveModel::AttributeMutationTracker.new(@attributes)
266
243
  else
267
- NullMutationTracker.instance
244
+ ActiveModel::ForcedMutationTracker.new(self)
268
245
  end
269
246
  end
270
247
 
@@ -276,68 +253,28 @@ module ActiveModel
276
253
  @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
277
254
  end
278
255
 
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)
300
- end
301
-
302
- # Handles <tt>*_change</tt> for +method_missing+.
303
- def attribute_change(attr)
304
- [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
256
+ # Dispatch target for <tt>*_change</tt> attribute methods.
257
+ def attribute_change(attr_name)
258
+ mutations_from_database.change_to_attribute(attr_name.to_s)
305
259
  end
306
260
 
307
- # Handles <tt>*_previous_change</tt> for +method_missing+.
308
- def attribute_previous_change(attr)
309
- previous_changes[attr] if attribute_previously_changed?(attr)
261
+ # Dispatch target for <tt>*_previous_change</tt> attribute methods.
262
+ def attribute_previous_change(attr_name)
263
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
310
264
  end
311
265
 
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)
322
- end
323
- mutations_from_database.force_change(attr)
266
+ # Dispatch target for <tt>*_will_change!</tt> attribute methods.
267
+ def attribute_will_change!(attr_name)
268
+ mutations_from_database.force_change(attr_name.to_s)
324
269
  end
325
270
 
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])
271
+ # Dispatch target for <tt>restore_*!</tt> attribute methods.
272
+ def restore_attribute!(attr_name)
273
+ attr_name = attr_name.to_s
274
+ if attribute_changed?(attr_name)
275
+ __send__("#{attr_name}=", attribute_was(attr_name))
276
+ clear_attribute_change(attr_name)
331
277
  end
332
278
  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
- end
342
279
  end
343
280
  end