activemodel 5.2.6 → 6.1.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -109
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -4
  5. data/lib/active_model.rb +2 -1
  6. data/lib/active_model/attribute.rb +21 -21
  7. data/lib/active_model/attribute/user_provided_default.rb +1 -2
  8. data/lib/active_model/attribute_assignment.rb +4 -6
  9. data/lib/active_model/attribute_methods.rb +117 -40
  10. data/lib/active_model/attribute_mutation_tracker.rb +90 -33
  11. data/lib/active_model/attribute_set.rb +20 -28
  12. data/lib/active_model/attribute_set/builder.rb +81 -16
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
  14. data/lib/active_model/attributes.rb +65 -44
  15. data/lib/active_model/callbacks.rb +11 -9
  16. data/lib/active_model/conversion.rb +1 -1
  17. data/lib/active_model/dirty.rb +51 -101
  18. data/lib/active_model/error.rb +207 -0
  19. data/lib/active_model/errors.rb +347 -155
  20. data/lib/active_model/gem_version.rb +3 -3
  21. data/lib/active_model/lint.rb +1 -1
  22. data/lib/active_model/naming.rb +22 -7
  23. data/lib/active_model/nested_error.rb +22 -0
  24. data/lib/active_model/railtie.rb +6 -0
  25. data/lib/active_model/secure_password.rb +54 -55
  26. data/lib/active_model/serialization.rb +9 -7
  27. data/lib/active_model/serializers/json.rb +17 -9
  28. data/lib/active_model/translation.rb +1 -1
  29. data/lib/active_model/type/big_integer.rb +0 -1
  30. data/lib/active_model/type/binary.rb +1 -1
  31. data/lib/active_model/type/boolean.rb +0 -1
  32. data/lib/active_model/type/date.rb +0 -5
  33. data/lib/active_model/type/date_time.rb +3 -8
  34. data/lib/active_model/type/decimal.rb +0 -1
  35. data/lib/active_model/type/float.rb +2 -3
  36. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +14 -6
  37. data/lib/active_model/type/helpers/numeric.rb +17 -6
  38. data/lib/active_model/type/helpers/time_value.rb +37 -15
  39. data/lib/active_model/type/helpers/timezone.rb +1 -1
  40. data/lib/active_model/type/immutable_string.rb +14 -11
  41. data/lib/active_model/type/integer.rb +15 -18
  42. data/lib/active_model/type/registry.rb +16 -16
  43. data/lib/active_model/type/string.rb +12 -3
  44. data/lib/active_model/type/time.rb +1 -6
  45. data/lib/active_model/type/value.rb +9 -2
  46. data/lib/active_model/validations.rb +6 -9
  47. data/lib/active_model/validations/absence.rb +2 -2
  48. data/lib/active_model/validations/acceptance.rb +34 -27
  49. data/lib/active_model/validations/callbacks.rb +15 -16
  50. data/lib/active_model/validations/clusivity.rb +6 -3
  51. data/lib/active_model/validations/confirmation.rb +4 -4
  52. data/lib/active_model/validations/exclusion.rb +1 -1
  53. data/lib/active_model/validations/format.rb +2 -3
  54. data/lib/active_model/validations/inclusion.rb +2 -2
  55. data/lib/active_model/validations/length.rb +3 -3
  56. data/lib/active_model/validations/numericality.rb +58 -44
  57. data/lib/active_model/validations/presence.rb +1 -1
  58. data/lib/active_model/validations/validates.rb +7 -6
  59. data/lib/active_model/validator.rb +8 -3
  60. metadata +14 -9
@@ -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,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"].flatten
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