activemodel 6.0.3.2 → 6.1.0

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -182
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/active_model.rb +2 -1
  6. data/lib/active_model/attribute.rb +15 -14
  7. data/lib/active_model/attribute_assignment.rb +3 -4
  8. data/lib/active_model/attribute_methods.rb +74 -38
  9. data/lib/active_model/attribute_mutation_tracker.rb +8 -5
  10. data/lib/active_model/attribute_set.rb +18 -16
  11. data/lib/active_model/attribute_set/builder.rb +80 -13
  12. data/lib/active_model/attributes.rb +20 -24
  13. data/lib/active_model/dirty.rb +12 -4
  14. data/lib/active_model/error.rb +207 -0
  15. data/lib/active_model/errors.rb +316 -208
  16. data/lib/active_model/gem_version.rb +3 -3
  17. data/lib/active_model/lint.rb +1 -1
  18. data/lib/active_model/naming.rb +2 -2
  19. data/lib/active_model/nested_error.rb +22 -0
  20. data/lib/active_model/railtie.rb +1 -1
  21. data/lib/active_model/secure_password.rb +14 -14
  22. data/lib/active_model/serialization.rb +9 -6
  23. data/lib/active_model/serializers/json.rb +7 -0
  24. data/lib/active_model/type/date_time.rb +2 -2
  25. data/lib/active_model/type/float.rb +2 -0
  26. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +11 -7
  27. data/lib/active_model/type/helpers/numeric.rb +8 -3
  28. data/lib/active_model/type/helpers/time_value.rb +27 -17
  29. data/lib/active_model/type/helpers/timezone.rb +1 -1
  30. data/lib/active_model/type/immutable_string.rb +14 -10
  31. data/lib/active_model/type/integer.rb +11 -2
  32. data/lib/active_model/type/registry.rb +11 -4
  33. data/lib/active_model/type/string.rb +12 -2
  34. data/lib/active_model/type/value.rb +9 -1
  35. data/lib/active_model/validations.rb +6 -6
  36. data/lib/active_model/validations/absence.rb +1 -1
  37. data/lib/active_model/validations/acceptance.rb +1 -1
  38. data/lib/active_model/validations/clusivity.rb +5 -1
  39. data/lib/active_model/validations/confirmation.rb +2 -2
  40. data/lib/active_model/validations/exclusion.rb +1 -1
  41. data/lib/active_model/validations/format.rb +2 -2
  42. data/lib/active_model/validations/inclusion.rb +1 -1
  43. data/lib/active_model/validations/length.rb +2 -2
  44. data/lib/active_model/validations/numericality.rb +48 -41
  45. data/lib/active_model/validations/presence.rb +1 -1
  46. data/lib/active_model/validations/validates.rb +6 -4
  47. data/lib/active_model/validator.rb +7 -1
  48. metadata +13 -11
@@ -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,15 +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
- class << self
66
- attr_accessor :i18n_customize_full_message # :nodoc:
67
- end
68
- self.i18n_customize_full_message = false
70
+ LEGACY_ATTRIBUTES = [:messages, :details].freeze
71
+ private_constant :LEGACY_ATTRIBUTES
69
72
 
70
- attr_reader :messages, :details
73
+ # The actual array of +Error+ objects
74
+ # This method is aliased to <tt>objects</tt>.
75
+ attr_reader :errors
76
+ alias :objects :errors
71
77
 
72
78
  # Pass in the instance of the object that is using the errors object.
73
79
  #
@@ -77,18 +83,17 @@ module ActiveModel
77
83
  # end
78
84
  # end
79
85
  def initialize(base)
80
- @base = base
81
- @messages = apply_default_array({})
82
- @details = apply_default_array({})
86
+ @base = base
87
+ @errors = []
83
88
  end
84
89
 
85
90
  def initialize_dup(other) # :nodoc:
86
- @messages = other.messages.dup
87
- @details = other.details.deep_dup
91
+ @errors = other.errors.deep_dup
88
92
  super
89
93
  end
90
94
 
91
95
  # Copies the errors from <tt>other</tt>.
96
+ # For copying errors but keep <tt>@base</tt> as is.
92
97
  #
93
98
  # other - The ActiveModel::Errors instance.
94
99
  #
@@ -96,11 +101,31 @@ module ActiveModel
96
101
  #
97
102
  # person.errors.copy!(other)
98
103
  def copy!(other) # :nodoc:
99
- @messages = other.messages.dup
100
- @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))
101
125
  end
102
126
 
103
- # 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>.
104
129
  #
105
130
  # other - The ActiveModel::Errors instance.
106
131
  #
@@ -108,8 +133,9 @@ module ActiveModel
108
133
  #
109
134
  # person.errors.merge!(other)
110
135
  def merge!(other)
111
- @messages.merge!(other.messages) { |_, ary1, ary2| ary1 + ary2 }
112
- @details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 }
136
+ other.errors.each { |error|
137
+ import(error)
138
+ }
113
139
  end
114
140
 
115
141
  # Removes all errors except the given keys. Returns a hash containing the removed errors.
@@ -118,19 +144,31 @@ module ActiveModel
118
144
  # person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] }
119
145
  # person.errors.keys # => [:age, :gender]
120
146
  def slice!(*keys)
147
+ deprecation_removal_warning(:slice!)
148
+
121
149
  keys = keys.map(&:to_sym)
122
- @details.slice!(*keys)
123
- @messages.slice!(*keys)
150
+
151
+ results = messages.dup.slice!(*keys)
152
+
153
+ @errors.keep_if do |error|
154
+ keys.include?(error.attribute)
155
+ end
156
+
157
+ results
124
158
  end
125
159
 
126
- # Clear the error messages.
160
+ # Search for errors matching +attribute+, +type+ or +options+.
127
161
  #
128
- # person.errors.full_messages # => ["name cannot be nil"]
129
- # person.errors.clear
130
- # person.errors.full_messages # => []
131
- def clear
132
- messages.clear
133
- details.clear
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
+ }
134
172
  end
135
173
 
136
174
  # Returns +true+ if the error messages include an error for the given key
@@ -140,8 +178,9 @@ module ActiveModel
140
178
  # person.errors.include?(:name) # => true
141
179
  # person.errors.include?(:age) # => false
142
180
  def include?(attribute)
143
- attribute = attribute.to_sym
144
- messages.key?(attribute) && messages[attribute].present?
181
+ @errors.any? { |error|
182
+ error.match?(attribute.to_sym)
183
+ }
145
184
  end
146
185
  alias :has_key? :include?
147
186
  alias :key? :include?
@@ -151,10 +190,13 @@ module ActiveModel
151
190
  # person.errors[:name] # => ["cannot be nil"]
152
191
  # person.errors.delete(:name) # => ["cannot be nil"]
153
192
  # person.errors[:name] # => []
154
- def delete(key)
155
- attribute = key.to_sym
156
- details.delete(attribute)
157
- 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
158
200
  end
159
201
 
160
202
  # When passed a symbol or a name of a method, returns an array of errors
@@ -163,48 +205,65 @@ module ActiveModel
163
205
  # person.errors[:name] # => ["cannot be nil"]
164
206
  # person.errors['name'] # => ["cannot be nil"]
165
207
  def [](attribute)
166
- messages[attribute.to_sym]
208
+ DeprecationHandlingMessageArray.new(messages_for(attribute), self, attribute)
167
209
  end
168
210
 
169
- # 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.
170
222
  # Yields the attribute and the error for that attribute. If the attribute
171
223
  # has more than one error message, yields once for each error message.
172
224
  #
173
225
  # person.errors.add(:name, :blank, message: "can't be blank")
174
- # person.errors.each do |attribute, error|
226
+ # person.errors.each do |attribute, message|
175
227
  # # Will yield :name and "can't be blank"
176
228
  # end
177
229
  #
178
230
  # person.errors.add(:name, :not_specified, message: "must be specified")
179
- # person.errors.each do |attribute, error|
231
+ # person.errors.each do |attribute, message|
180
232
  # # Will yield :name and "can't be blank"
181
233
  # # then yield :name and "must be specified"
182
234
  # end
183
- def each
184
- messages.each_key do |attribute|
185
- messages[attribute].each { |error| yield attribute, error }
186
- end
187
- end
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
188
249
 
189
- # Returns the number of error messages.
190
- #
191
- # person.errors.add(:name, :blank, message: "can't be blank")
192
- # person.errors.size # => 1
193
- # person.errors.add(:name, :not_specified, message: "must be specified")
194
- # person.errors.size # => 2
195
- def size
196
- values.flatten.size
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 }
257
+ end
197
258
  end
198
- alias :count :size
199
259
 
200
260
  # Returns all message values.
201
261
  #
202
262
  # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
203
263
  # person.errors.values # => [["cannot be nil", "must be specified"]]
204
264
  def values
205
- messages.select do |key, value|
206
- !value.empty?
207
- end.values
265
+ deprecation_removal_warning(:values, "errors.map { |error| error.message }")
266
+ @errors.map(&:message).freeze
208
267
  end
209
268
 
210
269
  # Returns all message keys.
@@ -212,20 +271,19 @@ module ActiveModel
212
271
  # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
213
272
  # person.errors.keys # => [:name]
214
273
  def keys
215
- messages.select do |key, value|
216
- !value.empty?
217
- end.keys
274
+ deprecation_removal_warning(:keys, "errors.attribute_names")
275
+ keys = @errors.map(&:attribute)
276
+ keys.uniq!
277
+ keys.freeze
218
278
  end
219
279
 
220
- # Returns +true+ if no errors are found, +false+ otherwise.
221
- # If the error message is a string it can be empty.
280
+ # Returns all error attribute names
222
281
  #
223
- # person.errors.full_messages # => ["name cannot be nil"]
224
- # person.errors.empty? # => false
225
- def empty?
226
- 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
227
286
  end
228
- alias :blank? :empty?
229
287
 
230
288
  # Returns an xml formatted representation of the Errors hash.
231
289
  #
@@ -239,6 +297,7 @@ module ActiveModel
239
297
  # # <error>name must be specified</error>
240
298
  # # </errors>
241
299
  def to_xml(options = {})
300
+ deprecation_removal_warning(:to_xml)
242
301
  to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
243
302
  end
244
303
 
@@ -258,34 +317,68 @@ module ActiveModel
258
317
  # person.errors.to_hash # => {:name=>["cannot be nil"]}
259
318
  # person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
260
319
  def to_hash(full_messages = false)
261
- if full_messages
262
- messages.each_with_object({}) do |(attribute, array), messages|
263
- messages[attribute] = array.map { |message| full_message(attribute, message) }
264
- end
265
- else
266
- 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)
267
323
  end
268
324
  end
269
325
 
270
- # Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
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)
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)
361
+ end
362
+
363
+ # Adds a new error of +type+ on +attribute+.
271
364
  # More than one error can be added to the same +attribute+.
272
- # If no +message+ is supplied, <tt>:invalid</tt> is assumed.
365
+ # If no +type+ is supplied, <tt>:invalid</tt> is assumed.
273
366
  #
274
367
  # person.errors.add(:name)
275
- # # => ["is invalid"]
368
+ # # Adds <#ActiveModel::Error attribute=name, type=invalid>
276
369
  # person.errors.add(:name, :not_implemented, message: "must be implemented")
277
- # # => ["is invalid", "must be implemented"]
370
+ # # Adds <#ActiveModel::Error attribute=name, type=not_implemented,
371
+ # options={:message=>"must be implemented"}>
278
372
  #
279
373
  # person.errors.messages
280
374
  # # => {:name=>["is invalid", "must be implemented"]}
281
375
  #
282
- # person.errors.details
283
- # # => {:name=>[{error: :not_implemented}, {error: :invalid}]}
376
+ # If +type+ is a string, it will be used as error message.
284
377
  #
285
- # 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
286
379
  # scope (see +generate_message+).
287
380
  #
288
- # 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
289
382
  # <tt>Time.now</tt> to be used within an error.
290
383
  #
291
384
  # If the <tt>:strict</tt> option is set to +true+, it will raise
@@ -308,27 +401,28 @@ module ActiveModel
308
401
  # # => {:base=>["either name or email must be present"]}
309
402
  # person.errors.details
310
403
  # # => {:base=>[{error: :name_or_email_blank}]}
311
- def add(attribute, message = :invalid, options = {})
312
- message = message.call if message.respond_to?(:call)
313
- detail = normalize_detail(message, options)
314
- 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
+
315
408
  if exception = options[:strict]
316
409
  exception = ActiveModel::StrictValidationFailed if exception == true
317
- raise exception, full_message(attribute, message)
410
+ raise exception, error.full_message
318
411
  end
319
412
 
320
- details[attribute.to_sym] << detail
321
- messages[attribute.to_sym] << message
413
+ @errors.append(error)
414
+
415
+ error
322
416
  end
323
417
 
324
- # Returns +true+ if an error on the attribute with the given message is
325
- # 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+.
326
420
  #
327
421
  # person.errors.add :name, :blank
328
422
  # person.errors.added? :name, :blank # => true
329
423
  # person.errors.added? :name, "can't be blank" # => true
330
424
  #
331
- # If the error message requires options, then it returns +true+ with
425
+ # If the error requires options, then it returns +true+ with
332
426
  # the correct options, or +false+ with incorrect or missing options.
333
427
  #
334
428
  # person.errors.add :name, :too_long, { count: 25 }
@@ -337,18 +431,20 @@ module ActiveModel
337
431
  # person.errors.added? :name, :too_long, count: 24 # => false
338
432
  # person.errors.added? :name, :too_long # => false
339
433
  # person.errors.added? :name, "is too long" # => false
340
- def added?(attribute, message = :invalid, options = {})
341
- message = message.call if message.respond_to?(:call)
434
+ def added?(attribute, type = :invalid, options = {})
435
+ attribute, type, options = normalize_arguments(attribute, type, **options)
342
436
 
343
- if message.is_a? Symbol
344
- details[attribute.to_sym].include? normalize_detail(message, options)
437
+ if type.is_a? Symbol
438
+ @errors.any? { |error|
439
+ error.strict_match?(attribute, type, **options)
440
+ }
345
441
  else
346
- self[attribute].include? message
442
+ messages_for(attribute).include?(type)
347
443
  end
348
444
  end
349
445
 
350
- # Returns +true+ if an error on the attribute with the given message is
351
- # present, or +false+ otherwise. +message+ is treated the same as for +add+.
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+.
352
448
  #
353
449
  # person.errors.add :age
354
450
  # person.errors.add :name, :too_long, { count: 25 }
@@ -358,13 +454,13 @@ module ActiveModel
358
454
  # person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true
359
455
  # person.errors.of_kind? :name, :not_too_long # => false
360
456
  # person.errors.of_kind? :name, "is too long" # => false
361
- def of_kind?(attribute, message = :invalid)
362
- message = message.call if message.respond_to?(:call)
457
+ def of_kind?(attribute, type = :invalid)
458
+ attribute, type = normalize_arguments(attribute, type)
363
459
 
364
- if message.is_a? Symbol
365
- details[attribute.to_sym].map { |e| e[:error] }.include? message
460
+ if type.is_a? Symbol
461
+ !where(attribute, type).empty?
366
462
  else
367
- self[attribute].include? message
463
+ messages_for(attribute).include?(type)
368
464
  end
369
465
  end
370
466
 
@@ -379,7 +475,7 @@ module ActiveModel
379
475
  # person.errors.full_messages
380
476
  # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]
381
477
  def full_messages
382
- map { |attribute, message| full_message(attribute, message) }
478
+ @errors.map(&:full_message)
383
479
  end
384
480
  alias :to_a :full_messages
385
481
 
@@ -394,63 +490,28 @@ module ActiveModel
394
490
  # person.errors.full_messages_for(:name)
395
491
  # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
396
492
  def full_messages_for(attribute)
397
- attribute = attribute.to_sym
398
- messages[attribute].map { |message| full_message(attribute, message) }
493
+ where(attribute).map(&:full_message).freeze
399
494
  end
400
495
 
401
- # Returns a full message for a given attribute.
496
+ # Returns all the error messages for a given attribute in an array.
402
497
  #
403
- # person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
498
+ # class Person
499
+ # validates_presence_of :name, :email
500
+ # validates_length_of :name, in: 5..30
501
+ # end
404
502
  #
405
- # The `"%{attribute} %{message}"` error format can be overridden with either
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)
508
+ end
509
+
510
+ # Returns a full message for a given attribute.
406
511
  #
407
- # * <tt>activemodel.errors.models.person/contacts/addresses.attributes.street.format</tt>
408
- # * <tt>activemodel.errors.models.person/contacts/addresses.format</tt>
409
- # * <tt>activemodel.errors.models.person.attributes.name.format</tt>
410
- # * <tt>activemodel.errors.models.person.format</tt>
411
- # * <tt>errors.format</tt>
512
+ # person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
412
513
  def full_message(attribute, message)
413
- return message if attribute == :base
414
- attribute = attribute.to_s
415
-
416
- if self.class.i18n_customize_full_message && @base.class.respond_to?(:i18n_scope)
417
- attribute = attribute.remove(/\[\d\]/)
418
- parts = attribute.split(".")
419
- attribute_name = parts.pop
420
- namespace = parts.join("/") unless parts.empty?
421
- attributes_scope = "#{@base.class.i18n_scope}.errors.models"
422
-
423
- if namespace
424
- defaults = @base.class.lookup_ancestors.map do |klass|
425
- [
426
- :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format",
427
- :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format",
428
- ]
429
- end
430
- else
431
- defaults = @base.class.lookup_ancestors.map do |klass|
432
- [
433
- :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format",
434
- :"#{attributes_scope}.#{klass.model_name.i18n_key}.format",
435
- ]
436
- end
437
- end
438
-
439
- defaults.flatten!
440
- else
441
- defaults = []
442
- end
443
-
444
- defaults << :"errors.format"
445
- defaults << "%{attribute} %{message}"
446
-
447
- attr_name = attribute.tr(".", "_").humanize
448
- attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
449
-
450
- I18n.t(defaults.shift,
451
- default: defaults,
452
- attribute: attr_name,
453
- message: message)
514
+ Error.full_message(attribute, message, @base)
454
515
  end
455
516
 
456
517
  # Translates an error message in its default scope
@@ -478,82 +539,129 @@ module ActiveModel
478
539
  # * <tt>errors.attributes.title.blank</tt>
479
540
  # * <tt>errors.messages.blank</tt>
480
541
  def generate_message(attribute, type = :invalid, options = {})
481
- type = options.delete(:message) if options[:message].is_a?(Symbol)
482
- value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
483
-
484
- options = {
485
- model: @base.model_name.human,
486
- attribute: @base.class.human_attribute_name(attribute),
487
- value: value,
488
- object: @base
489
- }.merge!(options)
490
-
491
- if @base.class.respond_to?(:i18n_scope)
492
- i18n_scope = @base.class.i18n_scope.to_s
493
- defaults = @base.class.lookup_ancestors.flat_map do |klass|
494
- [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
495
- :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
542
+ Error.generate_message(attribute, type, @base, options)
543
+ end
544
+
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)
496
571
  end
497
- defaults << :"#{i18n_scope}.errors.messages.#{type}"
498
572
 
499
- catch(:exception) do
500
- translation = I18n.translate(defaults.first, **options.merge(default: defaults.drop(1), throw: true))
501
- return translation unless translation.nil?
502
- end unless options[:message]
503
- else
504
- defaults = []
573
+ [attribute.to_sym, type, options]
505
574
  end
506
575
 
507
- defaults << :"errors.attributes.#{attribute}.#{type}"
508
- 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
509
584
 
510
- key = defaults.shift
511
- defaults = options.delete(:message) if options[:message]
512
- options[:default] = defaults
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
513
593
 
514
- I18n.translate(key, **options)
515
- end
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
516
598
 
517
- def marshal_dump # :nodoc:
518
- [@base, without_default_proc(@messages), without_default_proc(@details)]
599
+ class DeprecationHandlingMessageHash < SimpleDelegator
600
+ def initialize(errors)
601
+ @errors = errors
602
+ super(prepare_content)
519
603
  end
520
604
 
521
- def marshal_load(array) # :nodoc:
522
- @base, @messages, @details = array
523
- apply_default_array(@messages)
524
- apply_default_array(@details)
605
+ def []=(attribute, value)
606
+ ActiveSupport::Deprecation.warn("Calling `[]=` to an ActiveModel::Errors is deprecated. Please call `ActiveModel::Errors#add` instead.")
607
+
608
+ @errors.delete(attribute)
609
+ Array(value).each do |message|
610
+ @errors.add(attribute, message)
611
+ end
612
+
613
+ __setobj__ prepare_content
525
614
  end
526
615
 
527
- def init_with(coder) # :nodoc:
528
- coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) }
529
- @details ||= {}
530
- apply_default_array(@messages)
531
- 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)
532
620
  end
533
621
 
534
- private
535
- def normalize_message(attribute, message, options)
536
- case message
537
- when Symbol
538
- generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
539
- else
540
- 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
541
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)
542
643
  end
543
644
 
544
- def normalize_detail(message, options)
545
- { 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
546
651
  end
547
652
 
548
- def without_default_proc(hash)
549
- hash.dup.tap do |new_h|
550
- new_h.default_proc = nil
551
- 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)
552
657
  end
658
+ end
553
659
 
554
- def apply_default_array(hash)
555
- hash.default_proc = proc { |h, key| h[key] = [] }
556
- hash
660
+ class DeprecationHandlingDetailsHash < SimpleDelegator
661
+ def initialize(details)
662
+ details.default = []
663
+ details.freeze
664
+ super(details)
557
665
  end
558
666
  end
559
667