activemodel 5.2.7.1 → 6.1.4.6

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 +65 -111
  3. data/MIT-LICENSE +1 -1
  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 +4 -4
  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 +54 -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 +16 -16
  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/validations/absence.rb +2 -2
  46. data/lib/active_model/validations/acceptance.rb +34 -27
  47. data/lib/active_model/validations/callbacks.rb +15 -16
  48. data/lib/active_model/validations/clusivity.rb +6 -3
  49. data/lib/active_model/validations/confirmation.rb +4 -4
  50. data/lib/active_model/validations/exclusion.rb +1 -1
  51. data/lib/active_model/validations/format.rb +2 -3
  52. data/lib/active_model/validations/inclusion.rb +2 -2
  53. data/lib/active_model/validations/length.rb +3 -3
  54. data/lib/active_model/validations/numericality.rb +58 -44
  55. data/lib/active_model/validations/presence.rb +1 -1
  56. data/lib/active_model/validations/validates.rb +7 -6
  57. data/lib/active_model/validations.rb +6 -9
  58. data/lib/active_model/validator.rb +8 -3
  59. data/lib/active_model.rb +2 -1
  60. metadata +14 -9
@@ -4,11 +4,14 @@ require "active_support/core_ext/array/conversions"
4
4
  require "active_support/core_ext/string/inflections"
5
5
  require "active_support/core_ext/object/deep_dup"
6
6
  require "active_support/core_ext/string/filters"
7
+ require "active_model/error"
8
+ require "active_model/nested_error"
9
+ require "forwardable"
7
10
 
8
11
  module ActiveModel
9
12
  # == Active \Model \Errors
10
13
  #
11
- # Provides a modified +Hash+ that you can include in your object
14
+ # Provides error related functionalities you can include in your object
12
15
  # for handling error messages and interacting with Action View helpers.
13
16
  #
14
17
  # A minimal implementation could be:
@@ -59,10 +62,18 @@ module ActiveModel
59
62
  class Errors
60
63
  include Enumerable
61
64
 
62
- CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
63
- MESSAGE_OPTIONS = [:message]
65
+ extend Forwardable
66
+ def_delegators :@errors, :size, :clear, :blank?, :empty?, :uniq!, :any?
67
+ # TODO: forward all enumerable methods after `each` deprecation is removed.
68
+ def_delegators :@errors, :count
64
69
 
65
- attr_reader :messages, :details
70
+ LEGACY_ATTRIBUTES = [:messages, :details].freeze
71
+ private_constant :LEGACY_ATTRIBUTES
72
+
73
+ # The actual array of +Error+ objects
74
+ # This method is aliased to <tt>objects</tt>.
75
+ attr_reader :errors
76
+ alias :objects :errors
66
77
 
67
78
  # Pass in the instance of the object that is using the errors object.
68
79
  #
@@ -72,18 +83,17 @@ module ActiveModel
72
83
  # end
73
84
  # end
74
85
  def initialize(base)
75
- @base = base
76
- @messages = apply_default_array({})
77
- @details = apply_default_array({})
86
+ @base = base
87
+ @errors = []
78
88
  end
79
89
 
80
90
  def initialize_dup(other) # :nodoc:
81
- @messages = other.messages.dup
82
- @details = other.details.deep_dup
91
+ @errors = other.errors.deep_dup
83
92
  super
84
93
  end
85
94
 
86
95
  # Copies the errors from <tt>other</tt>.
96
+ # For copying errors but keep <tt>@base</tt> as is.
87
97
  #
88
98
  # other - The ActiveModel::Errors instance.
89
99
  #
@@ -91,11 +101,31 @@ module ActiveModel
91
101
  #
92
102
  # person.errors.copy!(other)
93
103
  def copy!(other) # :nodoc:
94
- @messages = other.messages.dup
95
- @details = other.details.dup
104
+ @errors = other.errors.deep_dup
105
+ @errors.each { |error|
106
+ error.instance_variable_set(:@base, @base)
107
+ }
108
+ end
109
+
110
+ # Imports one error
111
+ # Imported errors are wrapped as a NestedError,
112
+ # providing access to original error object.
113
+ # If attribute or type needs to be overridden, use +override_options+.
114
+ #
115
+ # override_options - Hash
116
+ # @option override_options [Symbol] :attribute Override the attribute the error belongs to
117
+ # @option override_options [Symbol] :type Override type of the error.
118
+ def import(error, override_options = {})
119
+ [:attribute, :type].each do |key|
120
+ if override_options.key?(key)
121
+ override_options[key] = override_options[key].to_sym
122
+ end
123
+ end
124
+ @errors.append(NestedError.new(@base, error, override_options))
96
125
  end
97
126
 
98
- # Merges the errors from <tt>other</tt>.
127
+ # Merges the errors from <tt>other</tt>,
128
+ # each <tt>Error</tt> wrapped as <tt>NestedError</tt>.
99
129
  #
100
130
  # other - The ActiveModel::Errors instance.
101
131
  #
@@ -103,18 +133,42 @@ module ActiveModel
103
133
  #
104
134
  # person.errors.merge!(other)
105
135
  def merge!(other)
106
- @messages.merge!(other.messages) { |_, ary1, ary2| ary1 + ary2 }
107
- @details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 }
136
+ other.errors.each { |error|
137
+ import(error)
138
+ }
108
139
  end
109
140
 
110
- # Clear the error messages.
141
+ # Removes all errors except the given keys. Returns a hash containing the removed errors.
111
142
  #
112
- # person.errors.full_messages # => ["name cannot be nil"]
113
- # person.errors.clear
114
- # person.errors.full_messages # => []
115
- def clear
116
- messages.clear
117
- details.clear
143
+ # person.errors.keys # => [:name, :age, :gender, :city]
144
+ # person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] }
145
+ # person.errors.keys # => [:age, :gender]
146
+ def slice!(*keys)
147
+ deprecation_removal_warning(:slice!)
148
+
149
+ keys = keys.map(&:to_sym)
150
+
151
+ results = messages.dup.slice!(*keys)
152
+
153
+ @errors.keep_if do |error|
154
+ keys.include?(error.attribute)
155
+ end
156
+
157
+ results
158
+ end
159
+
160
+ # Search for errors matching +attribute+, +type+ or +options+.
161
+ #
162
+ # Only supplied params will be matched.
163
+ #
164
+ # person.errors.where(:name) # => all name errors.
165
+ # person.errors.where(:name, :too_short) # => all name errors being too short
166
+ # person.errors.where(:name, :too_short, minimum: 2) # => all name errors being too short and minimum is 2
167
+ def where(attribute, type = nil, **options)
168
+ attribute, type, options = normalize_arguments(attribute, type, **options)
169
+ @errors.select { |error|
170
+ error.match?(attribute, type, **options)
171
+ }
118
172
  end
119
173
 
120
174
  # Returns +true+ if the error messages include an error for the given key
@@ -124,8 +178,9 @@ module ActiveModel
124
178
  # person.errors.include?(:name) # => true
125
179
  # person.errors.include?(:age) # => false
126
180
  def include?(attribute)
127
- attribute = attribute.to_sym
128
- messages.key?(attribute) && messages[attribute].present?
181
+ @errors.any? { |error|
182
+ error.match?(attribute.to_sym)
183
+ }
129
184
  end
130
185
  alias :has_key? :include?
131
186
  alias :key? :include?
@@ -135,10 +190,13 @@ module ActiveModel
135
190
  # person.errors[:name] # => ["cannot be nil"]
136
191
  # person.errors.delete(:name) # => ["cannot be nil"]
137
192
  # person.errors[:name] # => []
138
- def delete(key)
139
- attribute = key.to_sym
140
- details.delete(attribute)
141
- messages.delete(attribute)
193
+ def delete(attribute, type = nil, **options)
194
+ attribute, type, options = normalize_arguments(attribute, type, **options)
195
+ matches = where(attribute, type, **options)
196
+ matches.each do |error|
197
+ @errors.delete(error)
198
+ end
199
+ matches.map(&:message).presence
142
200
  end
143
201
 
144
202
  # When passed a symbol or a name of a method, returns an array of errors
@@ -147,48 +205,65 @@ module ActiveModel
147
205
  # person.errors[:name] # => ["cannot be nil"]
148
206
  # person.errors['name'] # => ["cannot be nil"]
149
207
  def [](attribute)
150
- messages[attribute.to_sym]
208
+ DeprecationHandlingMessageArray.new(messages_for(attribute), self, attribute)
151
209
  end
152
210
 
153
- # Iterates through each error key, value pair in the error messages hash.
211
+ # Iterates through each error object.
212
+ #
213
+ # person.errors.add(:name, :too_short, count: 2)
214
+ # person.errors.each do |error|
215
+ # # Will yield <#ActiveModel::Error attribute=name, type=too_short,
216
+ # options={:count=>3}>
217
+ # end
218
+ #
219
+ # To be backward compatible with past deprecated hash-like behavior,
220
+ # when block accepts two parameters instead of one, it
221
+ # iterates through each error key, value pair in the error messages hash.
154
222
  # Yields the attribute and the error for that attribute. If the attribute
155
223
  # has more than one error message, yields once for each error message.
156
224
  #
157
225
  # person.errors.add(:name, :blank, message: "can't be blank")
158
- # person.errors.each do |attribute, error|
226
+ # person.errors.each do |attribute, message|
159
227
  # # Will yield :name and "can't be blank"
160
228
  # end
161
229
  #
162
230
  # person.errors.add(:name, :not_specified, message: "must be specified")
163
- # person.errors.each do |attribute, error|
231
+ # person.errors.each do |attribute, message|
164
232
  # # Will yield :name and "can't be blank"
165
233
  # # then yield :name and "must be specified"
166
234
  # end
167
- def each
168
- messages.each_key do |attribute|
169
- messages[attribute].each { |error| yield attribute, error }
235
+ def each(&block)
236
+ if block.arity <= 1
237
+ @errors.each(&block)
238
+ else
239
+ ActiveSupport::Deprecation.warn(<<~MSG)
240
+ Enumerating ActiveModel::Errors as a hash has been deprecated.
241
+ In Rails 6.1, `errors` is an array of Error objects,
242
+ therefore it should be accessed by a block with a single block
243
+ parameter like this:
244
+
245
+ person.errors.each do |error|
246
+ attribute = error.attribute
247
+ message = error.message
248
+ end
249
+
250
+ You are passing a block expecting two parameters,
251
+ so the old hash behavior is simulated. As this is deprecated,
252
+ this will result in an ArgumentError in Rails 6.2.
253
+ MSG
254
+ @errors.
255
+ sort { |a, b| a.attribute <=> b.attribute }.
256
+ each { |error| yield error.attribute, error.message }
170
257
  end
171
258
  end
172
259
 
173
- # Returns the number of error messages.
174
- #
175
- # person.errors.add(:name, :blank, message: "can't be blank")
176
- # person.errors.size # => 1
177
- # person.errors.add(:name, :not_specified, message: "must be specified")
178
- # person.errors.size # => 2
179
- def size
180
- values.flatten.size
181
- end
182
- alias :count :size
183
-
184
260
  # Returns all message values.
185
261
  #
186
262
  # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
187
263
  # person.errors.values # => [["cannot be nil", "must be specified"]]
188
264
  def values
189
- messages.select do |key, value|
190
- !value.empty?
191
- end.values
265
+ deprecation_removal_warning(:values, "errors.map { |error| error.message }")
266
+ @errors.map(&:message).freeze
192
267
  end
193
268
 
194
269
  # Returns all message keys.
@@ -196,20 +271,19 @@ module ActiveModel
196
271
  # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
197
272
  # person.errors.keys # => [:name]
198
273
  def keys
199
- messages.select do |key, value|
200
- !value.empty?
201
- end.keys
274
+ deprecation_removal_warning(:keys, "errors.attribute_names")
275
+ keys = @errors.map(&:attribute)
276
+ keys.uniq!
277
+ keys.freeze
202
278
  end
203
279
 
204
- # Returns +true+ if no errors are found, +false+ otherwise.
205
- # If the error message is a string it can be empty.
280
+ # Returns all error attribute names
206
281
  #
207
- # person.errors.full_messages # => ["name cannot be nil"]
208
- # person.errors.empty? # => false
209
- def empty?
210
- size.zero?
282
+ # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
283
+ # person.errors.attribute_names # => [:name]
284
+ def attribute_names
285
+ @errors.map(&:attribute).uniq.freeze
211
286
  end
212
- alias :blank? :empty?
213
287
 
214
288
  # Returns an xml formatted representation of the Errors hash.
215
289
  #
@@ -223,6 +297,7 @@ module ActiveModel
223
297
  # # <error>name must be specified</error>
224
298
  # # </errors>
225
299
  def to_xml(options = {})
300
+ deprecation_removal_warning(:to_xml)
226
301
  to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
227
302
  end
228
303
 
@@ -242,34 +317,68 @@ module ActiveModel
242
317
  # person.errors.to_hash # => {:name=>["cannot be nil"]}
243
318
  # person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
244
319
  def to_hash(full_messages = false)
245
- if full_messages
246
- messages.each_with_object({}) do |(attribute, array), messages|
247
- messages[attribute] = array.map { |message| full_message(attribute, message) }
248
- end
249
- else
250
- without_default_proc(messages)
320
+ message_method = full_messages ? :full_message : :message
321
+ group_by_attribute.transform_values do |errors|
322
+ errors.map(&message_method)
323
+ end
324
+ end
325
+
326
+ def to_h
327
+ ActiveSupport::Deprecation.warn(<<~EOM)
328
+ ActiveModel::Errors#to_h is deprecated and will be removed in Rails 6.2.
329
+ Please use `ActiveModel::Errors.to_hash` instead. The values in the hash
330
+ returned by `ActiveModel::Errors.to_hash` is an array of error messages.
331
+ EOM
332
+
333
+ to_hash.transform_values { |values| values.last }
334
+ end
335
+
336
+ # Returns a Hash of attributes with an array of their error messages.
337
+ #
338
+ # Updating this hash would still update errors state for backward
339
+ # compatibility, but this behavior is deprecated.
340
+ def messages
341
+ DeprecationHandlingMessageHash.new(self)
342
+ end
343
+
344
+ # Returns a Hash of attributes with an array of their error details.
345
+ #
346
+ # Updating this hash would still update errors state for backward
347
+ # compatibility, but this behavior is deprecated.
348
+ def details
349
+ hash = group_by_attribute.transform_values do |errors|
350
+ errors.map(&:details)
251
351
  end
352
+ DeprecationHandlingDetailsHash.new(hash)
353
+ end
354
+
355
+ # Returns a Hash of attributes with an array of their Error objects.
356
+ #
357
+ # person.errors.group_by_attribute
358
+ # # => {:name=>[<#ActiveModel::Error>, <#ActiveModel::Error>]}
359
+ def group_by_attribute
360
+ @errors.group_by(&:attribute)
252
361
  end
253
362
 
254
- # Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
363
+ # Adds a new error of +type+ on +attribute+.
255
364
  # More than one error can be added to the same +attribute+.
256
- # If no +message+ is supplied, <tt>:invalid</tt> is assumed.
365
+ # If no +type+ is supplied, <tt>:invalid</tt> is assumed.
257
366
  #
258
367
  # person.errors.add(:name)
259
- # # => ["is invalid"]
368
+ # # Adds <#ActiveModel::Error attribute=name, type=invalid>
260
369
  # person.errors.add(:name, :not_implemented, message: "must be implemented")
261
- # # => ["is invalid", "must be implemented"]
370
+ # # Adds <#ActiveModel::Error attribute=name, type=not_implemented,
371
+ # options={:message=>"must be implemented"}>
262
372
  #
263
373
  # person.errors.messages
264
374
  # # => {:name=>["is invalid", "must be implemented"]}
265
375
  #
266
- # person.errors.details
267
- # # => {:name=>[{error: :not_implemented}, {error: :invalid}]}
376
+ # If +type+ is a string, it will be used as error message.
268
377
  #
269
- # If +message+ is a symbol, it will be translated using the appropriate
378
+ # If +type+ is a symbol, it will be translated using the appropriate
270
379
  # scope (see +generate_message+).
271
380
  #
272
- # If +message+ is a proc, it will be called, allowing for things like
381
+ # If +type+ is a proc, it will be called, allowing for things like
273
382
  # <tt>Time.now</tt> to be used within an error.
274
383
  #
275
384
  # If the <tt>:strict</tt> option is set to +true+, it will raise
@@ -292,42 +401,66 @@ module ActiveModel
292
401
  # # => {:base=>["either name or email must be present"]}
293
402
  # person.errors.details
294
403
  # # => {:base=>[{error: :name_or_email_blank}]}
295
- def add(attribute, message = :invalid, options = {})
296
- message = message.call if message.respond_to?(:call)
297
- detail = normalize_detail(message, options)
298
- message = normalize_message(attribute, message, options)
404
+ def add(attribute, type = :invalid, **options)
405
+ attribute, type, options = normalize_arguments(attribute, type, **options)
406
+ error = Error.new(@base, attribute, type, **options)
407
+
299
408
  if exception = options[:strict]
300
409
  exception = ActiveModel::StrictValidationFailed if exception == true
301
- raise exception, full_message(attribute, message)
410
+ raise exception, error.full_message
302
411
  end
303
412
 
304
- details[attribute.to_sym] << detail
305
- messages[attribute.to_sym] << message
413
+ @errors.append(error)
414
+
415
+ error
306
416
  end
307
417
 
308
- # Returns +true+ if an error on the attribute with the given message is
309
- # present, or +false+ otherwise. +message+ is treated the same as for +add+.
418
+ # Returns +true+ if an error matches provided +attribute+ and +type+,
419
+ # or +false+ otherwise. +type+ is treated the same as for +add+.
310
420
  #
311
421
  # person.errors.add :name, :blank
312
422
  # person.errors.added? :name, :blank # => true
313
423
  # person.errors.added? :name, "can't be blank" # => true
314
424
  #
315
- # If the error message requires an option, then it returns +true+ with
316
- # the correct option, or +false+ with an incorrect or missing option.
317
- #
318
- # person.errors.add :name, :too_long, { count: 25 }
319
- # person.errors.added? :name, :too_long, count: 25 # => true
320
- # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true
321
- # person.errors.added? :name, :too_long, count: 24 # => false
322
- # person.errors.added? :name, :too_long # => false
323
- # person.errors.added? :name, "is too long" # => false
324
- def added?(attribute, message = :invalid, options = {})
325
- message = message.call if message.respond_to?(:call)
425
+ # If the error requires options, then it returns +true+ with
426
+ # the correct options, or +false+ with incorrect or missing options.
427
+ #
428
+ # person.errors.add :name, :too_long, { count: 25 }
429
+ # person.errors.added? :name, :too_long, count: 25 # => true
430
+ # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true
431
+ # person.errors.added? :name, :too_long, count: 24 # => false
432
+ # person.errors.added? :name, :too_long # => false
433
+ # person.errors.added? :name, "is too long" # => false
434
+ def added?(attribute, type = :invalid, options = {})
435
+ attribute, type, options = normalize_arguments(attribute, type, **options)
436
+
437
+ if type.is_a? Symbol
438
+ @errors.any? { |error|
439
+ error.strict_match?(attribute, type, **options)
440
+ }
441
+ else
442
+ messages_for(attribute).include?(type)
443
+ end
444
+ end
326
445
 
327
- if message.is_a? Symbol
328
- details[attribute.to_sym].include? normalize_detail(message, options)
446
+ # Returns +true+ if an error on the attribute with the given type is
447
+ # present, or +false+ otherwise. +type+ is treated the same as for +add+.
448
+ #
449
+ # person.errors.add :age
450
+ # person.errors.add :name, :too_long, { count: 25 }
451
+ # person.errors.of_kind? :age # => true
452
+ # person.errors.of_kind? :name # => false
453
+ # person.errors.of_kind? :name, :too_long # => true
454
+ # person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true
455
+ # person.errors.of_kind? :name, :not_too_long # => false
456
+ # person.errors.of_kind? :name, "is too long" # => false
457
+ def of_kind?(attribute, type = :invalid)
458
+ attribute, type = normalize_arguments(attribute, type)
459
+
460
+ if type.is_a? Symbol
461
+ !where(attribute, type).empty?
329
462
  else
330
- self[attribute].include? message
463
+ messages_for(attribute).include?(type)
331
464
  end
332
465
  end
333
466
 
@@ -342,7 +475,7 @@ module ActiveModel
342
475
  # person.errors.full_messages
343
476
  # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]
344
477
  def full_messages
345
- map { |attribute, message| full_message(attribute, message) }
478
+ @errors.map(&:full_message)
346
479
  end
347
480
  alias :to_a :full_messages
348
481
 
@@ -357,21 +490,28 @@ module ActiveModel
357
490
  # person.errors.full_messages_for(:name)
358
491
  # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
359
492
  def full_messages_for(attribute)
360
- attribute = attribute.to_sym
361
- messages[attribute].map { |message| full_message(attribute, message) }
493
+ where(attribute).map(&:full_message).freeze
494
+ end
495
+
496
+ # Returns all the error messages for a given attribute in an array.
497
+ #
498
+ # class Person
499
+ # validates_presence_of :name, :email
500
+ # validates_length_of :name, in: 5..30
501
+ # end
502
+ #
503
+ # person = Person.create()
504
+ # person.errors.messages_for(:name)
505
+ # # => ["is too short (minimum is 5 characters)", "can't be blank"]
506
+ def messages_for(attribute)
507
+ where(attribute).map(&:message)
362
508
  end
363
509
 
364
510
  # Returns a full message for a given attribute.
365
511
  #
366
512
  # person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
367
513
  def full_message(attribute, message)
368
- return message if attribute == :base
369
- attr_name = attribute.to_s.tr(".", "_").humanize
370
- attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
371
- I18n.t(:"errors.format",
372
- default: "%{attribute} %{message}",
373
- attribute: attr_name,
374
- message: message)
514
+ Error.full_message(attribute, message, @base)
375
515
  end
376
516
 
377
517
  # Translates an error message in its default scope
@@ -399,77 +539,129 @@ module ActiveModel
399
539
  # * <tt>errors.attributes.title.blank</tt>
400
540
  # * <tt>errors.messages.blank</tt>
401
541
  def generate_message(attribute, type = :invalid, options = {})
402
- type = options.delete(:message) if options[:message].is_a?(Symbol)
542
+ Error.generate_message(attribute, type, @base, options)
543
+ end
403
544
 
404
- if @base.class.respond_to?(:i18n_scope)
405
- i18n_scope = @base.class.i18n_scope.to_s
406
- defaults = @base.class.lookup_ancestors.flat_map do |klass|
407
- [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
408
- :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
545
+ def marshal_load(array) # :nodoc:
546
+ # Rails 5
547
+ @errors = []
548
+ @base = array[0]
549
+ add_from_legacy_details_hash(array[2])
550
+ end
551
+
552
+ def init_with(coder) # :nodoc:
553
+ data = coder.map
554
+
555
+ data.each { |k, v|
556
+ next if LEGACY_ATTRIBUTES.include?(k.to_sym)
557
+ instance_variable_set(:"@#{k}", v)
558
+ }
559
+
560
+ @errors ||= []
561
+
562
+ # Legacy support Rails 5.x details hash
563
+ add_from_legacy_details_hash(data["details"]) if data.key?("details")
564
+ end
565
+
566
+ private
567
+ def normalize_arguments(attribute, type, **options)
568
+ # Evaluate proc first
569
+ if type.respond_to?(:call)
570
+ type = type.call(@base, options)
409
571
  end
410
- defaults << :"#{i18n_scope}.errors.messages.#{type}"
411
- else
412
- defaults = []
572
+
573
+ [attribute.to_sym, type, options]
413
574
  end
414
575
 
415
- defaults << :"errors.attributes.#{attribute}.#{type}"
416
- defaults << :"errors.messages.#{type}"
576
+ def add_from_legacy_details_hash(details)
577
+ details.each { |attribute, errors|
578
+ errors.each { |error|
579
+ type = error.delete(:error)
580
+ add(attribute, type, **error)
581
+ }
582
+ }
583
+ end
417
584
 
418
- key = defaults.shift
419
- defaults = options.delete(:message) if options[:message]
420
- value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
585
+ def deprecation_removal_warning(method_name, alternative_message = nil)
586
+ message = +"ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.2."
587
+ if alternative_message
588
+ message << "\n\nTo achieve the same use:\n\n "
589
+ message << alternative_message
590
+ end
591
+ ActiveSupport::Deprecation.warn(message)
592
+ end
421
593
 
422
- options = {
423
- default: defaults,
424
- model: @base.model_name.human,
425
- attribute: @base.class.human_attribute_name(attribute),
426
- value: value,
427
- object: @base
428
- }.merge!(options)
594
+ def deprecation_rename_warning(old_method_name, new_method_name)
595
+ ActiveSupport::Deprecation.warn("ActiveModel::Errors##{old_method_name} is deprecated. Please call ##{new_method_name} instead.")
596
+ end
597
+ end
429
598
 
430
- I18n.translate(key, options)
599
+ class DeprecationHandlingMessageHash < SimpleDelegator
600
+ def initialize(errors)
601
+ @errors = errors
602
+ super(prepare_content)
431
603
  end
432
604
 
433
- def marshal_dump # :nodoc:
434
- [@base, without_default_proc(@messages), without_default_proc(@details)]
435
- end
605
+ def []=(attribute, value)
606
+ ActiveSupport::Deprecation.warn("Calling `[]=` to an ActiveModel::Errors is deprecated. Please call `ActiveModel::Errors#add` instead.")
436
607
 
437
- def marshal_load(array) # :nodoc:
438
- @base, @messages, @details = array
439
- apply_default_array(@messages)
440
- apply_default_array(@details)
608
+ @errors.delete(attribute)
609
+ Array(value).each do |message|
610
+ @errors.add(attribute, message)
611
+ end
612
+
613
+ __setobj__ prepare_content
441
614
  end
442
615
 
443
- def init_with(coder) # :nodoc:
444
- coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) }
445
- @details ||= {}
446
- apply_default_array(@messages)
447
- apply_default_array(@details)
616
+ def delete(attribute)
617
+ ActiveSupport::Deprecation.warn("Calling `delete` to an ActiveModel::Errors messages hash is deprecated. Please call `ActiveModel::Errors#delete` instead.")
618
+
619
+ @errors.delete(attribute)
448
620
  end
449
621
 
450
- private
451
- def normalize_message(attribute, message, options)
452
- case message
453
- when Symbol
454
- generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
455
- else
456
- message
622
+ private
623
+ def prepare_content
624
+ content = @errors.to_hash
625
+ content.each do |attribute, value|
626
+ content[attribute] = DeprecationHandlingMessageArray.new(value, @errors, attribute)
627
+ end
628
+ content.default_proc = proc do |hash, attribute|
629
+ hash = hash.dup
630
+ hash[attribute] = DeprecationHandlingMessageArray.new([], @errors, attribute)
631
+ __setobj__ hash.freeze
632
+ hash[attribute]
633
+ end
634
+ content.freeze
457
635
  end
636
+ end
637
+
638
+ class DeprecationHandlingMessageArray < SimpleDelegator
639
+ def initialize(content, errors, attribute)
640
+ @errors = errors
641
+ @attribute = attribute
642
+ super(content.freeze)
458
643
  end
459
644
 
460
- def normalize_detail(message, options)
461
- { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
645
+ def <<(message)
646
+ ActiveSupport::Deprecation.warn("Calling `<<` to an ActiveModel::Errors message array in order to add an error is deprecated. Please call `ActiveModel::Errors#add` instead.")
647
+
648
+ @errors.add(@attribute, message)
649
+ __setobj__ @errors.messages_for(@attribute)
650
+ self
462
651
  end
463
652
 
464
- def without_default_proc(hash)
465
- hash.dup.tap do |new_h|
466
- new_h.default_proc = nil
467
- end
653
+ def clear
654
+ ActiveSupport::Deprecation.warn("Calling `clear` to an ActiveModel::Errors message array in order to delete all errors is deprecated. Please call `ActiveModel::Errors#delete` instead.")
655
+
656
+ @errors.delete(@attribute)
468
657
  end
658
+ end
469
659
 
470
- def apply_default_array(hash)
471
- hash.default_proc = proc { |h, key| h[key] = [] }
472
- hash
660
+ class DeprecationHandlingDetailsHash < SimpleDelegator
661
+ def initialize(details)
662
+ details.default = []
663
+ details.freeze
664
+ super(details)
473
665
  end
474
666
  end
475
667