activemodel 6.0.5 → 6.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -257
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/lib/active_model/attribute.rb +15 -14
  6. data/lib/active_model/attribute_assignment.rb +3 -4
  7. data/lib/active_model/attribute_methods.rb +74 -38
  8. data/lib/active_model/attribute_mutation_tracker.rb +8 -5
  9. data/lib/active_model/attribute_set/builder.rb +80 -13
  10. data/lib/active_model/attribute_set.rb +18 -16
  11. data/lib/active_model/attributes.rb +20 -24
  12. data/lib/active_model/dirty.rb +12 -4
  13. data/lib/active_model/error.rb +207 -0
  14. data/lib/active_model/errors.rb +316 -208
  15. data/lib/active_model/gem_version.rb +3 -3
  16. data/lib/active_model/lint.rb +1 -1
  17. data/lib/active_model/naming.rb +2 -2
  18. data/lib/active_model/nested_error.rb +22 -0
  19. data/lib/active_model/railtie.rb +1 -1
  20. data/lib/active_model/secure_password.rb +14 -14
  21. data/lib/active_model/serialization.rb +9 -6
  22. data/lib/active_model/serializers/json.rb +7 -0
  23. data/lib/active_model/type/date_time.rb +2 -2
  24. data/lib/active_model/type/float.rb +2 -0
  25. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +11 -7
  26. data/lib/active_model/type/helpers/numeric.rb +8 -3
  27. data/lib/active_model/type/helpers/time_value.rb +27 -17
  28. data/lib/active_model/type/helpers/timezone.rb +1 -1
  29. data/lib/active_model/type/immutable_string.rb +14 -10
  30. data/lib/active_model/type/integer.rb +11 -2
  31. data/lib/active_model/type/registry.rb +5 -0
  32. data/lib/active_model/type/string.rb +12 -2
  33. data/lib/active_model/type/value.rb +9 -1
  34. data/lib/active_model/validations/absence.rb +1 -1
  35. data/lib/active_model/validations/acceptance.rb +1 -1
  36. data/lib/active_model/validations/clusivity.rb +5 -1
  37. data/lib/active_model/validations/confirmation.rb +2 -2
  38. data/lib/active_model/validations/exclusion.rb +1 -1
  39. data/lib/active_model/validations/format.rb +2 -2
  40. data/lib/active_model/validations/inclusion.rb +1 -1
  41. data/lib/active_model/validations/length.rb +2 -2
  42. data/lib/active_model/validations/numericality.rb +48 -41
  43. data/lib/active_model/validations/presence.rb +1 -1
  44. data/lib/active_model/validations/validates.rb +6 -4
  45. data/lib/active_model/validations.rb +2 -2
  46. data/lib/active_model/validator.rb +7 -2
  47. data/lib/active_model.rb +2 -1
  48. metadata +15 -14
@@ -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