Skip to content

Commit 6f6f4e4

Browse files
authored
Merge pull request rails#36125 from lulalala/doc-for-model-errors
Document update for ActiveModel#errors
2 parents 3a255ab + bbf839d commit 6f6f4e4

File tree

6 files changed

+167
-123
lines changed

6 files changed

+167
-123
lines changed

activemodel/CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,34 @@
2727

2828
*DHH*
2929

30+
* Encapsulate each validation error as an Error object.
31+
32+
The `ActiveModel`’s `errors` collection is now an array of these Error
33+
objects, instead of messages/details hash.
34+
35+
For each of these `Error` object, its `message` and `full_message` methods
36+
are for generating error messages. Its `details` method would return error’s
37+
extra parameters, found in the original `details` hash.
38+
39+
The change tries its best at maintaining backward compatibility, however
40+
some edge cases won’t be covered, mainly related to manipulating
41+
`errors.messages` and `errors.details` hashes directly. Moving forward,
42+
please convert those direct manipulations to use provided API methods instead.
43+
44+
The list of deprecated methods and their planned future behavioral changes at the next major release are:
45+
46+
* `errors#slice!` will be removed.
47+
* `errors#first` will return Error object instead.
48+
* `errors#last` will return Error object instead.
49+
* `errors#each` with the `key, value` two-arguments block will stop working, while the `error` single-argument block would return `Error` object.
50+
* `errors#values` will be removed.
51+
* `errors#keys` will be removed.
52+
* `errors#to_xml` will be removed.
53+
* `errors#to_h` will be removed, and can be replaced with `errors#to_hash`.
54+
* Manipulating `errors` itself as a hash will have no effect (e.g. `errors[:foo] = 'bar'`).
55+
* Manipulating the hash returned by `errors#messages` (e.g. `errors.messages[:foo] = 'bar'`) will have no effect.
56+
* Manipulating the hash returned by `errors#details` (e.g. `errors.details[:foo].clear`) will have no effect.
57+
58+
*lulalala*
3059

3160
Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/activemodel/CHANGELOG.md) for previous changes.

activemodel/lib/active_model/error.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,16 @@ def initialize_dup(other)
110110
@options = @options.deep_dup
111111
end
112112

113-
attr_reader :base, :attribute, :type, :raw_type, :options
113+
# The object which the error belongs to
114+
attr_reader :base
115+
# The attribute of +base+ which the error belongs to
116+
attr_reader :attribute
117+
# The type of error, defaults to `:invalid` unless specified
118+
attr_reader :type
119+
# The raw value provided as the second parameter when calling `errors#add`
120+
attr_reader :raw_type
121+
# The options provided when calling `errors#add`
122+
attr_reader :options
114123

115124
def message
116125
case raw_type
@@ -159,6 +168,10 @@ def hash
159168
attributes_for_hash.hash
160169
end
161170

171+
def inspect # :nodoc:
172+
"<##{self.class.name} attribute=#{@attribute}, type=#{@type}, options=#{@options.inspect}>"
173+
end
174+
162175
protected
163176
def attributes_for_hash
164177
[@base, @attribute, @raw_type, @options.except(*CALLBACKS_OPTIONS)]

activemodel/lib/active_model/errors.rb

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
module ActiveModel
1212
# == Active \Model \Errors
1313
#
14-
# Provides a modified +Hash+ that you can include in your object
14+
# Provides error related functionalities you can include in your object
1515
# for handling error messages and interacting with Action View helpers.
1616
#
1717
# A minimal implementation could be:
@@ -68,7 +68,10 @@ class Errors
6868
def_delegators :@errors, :count
6969

7070
LEGACY_ATTRIBUTES = [:messages, :details].freeze
71+
private_constant :LEGACY_ATTRIBUTES
7172

73+
# The actual array of +Error+ objects
74+
# This method is aliased to <tt>objects</tt>.
7275
attr_reader :errors
7376
alias :objects :errors
7477

@@ -205,17 +208,37 @@ def [](attribute)
205208
DeprecationHandlingMessageArray.new(messages_for(attribute), self, attribute)
206209
end
207210

208-
# Iterates through each error key, value pair in the error messages hash.
211+
def first
212+
deprecation_index_access_warning(:first)
213+
super
214+
end
215+
216+
def last
217+
deprecation_index_access_warning(:last)
218+
super
219+
end
220+
221+
# Iterates through each error object.
222+
#
223+
# person.errors.add(:name, :too_short, count: 2)
224+
# person.errors.each do |error|
225+
# # Will yield <#ActiveModel::Error attribute=name, type=too_short,
226+
# options={:count=>3}>
227+
# end
228+
#
229+
# To be backward compatible with past deprecated hash-like behavior,
230+
# when block accepts two parameters instead of one, it
231+
# iterates through each error key, value pair in the error messages hash.
209232
# Yields the attribute and the error for that attribute. If the attribute
210233
# has more than one error message, yields once for each error message.
211234
#
212235
# person.errors.add(:name, :blank, message: "can't be blank")
213-
# person.errors.each do |attribute, error|
236+
# person.errors.each do |attribute, message|
214237
# # Will yield :name and "can't be blank"
215238
# end
216239
#
217240
# person.errors.add(:name, :not_specified, message: "must be specified")
218-
# person.errors.each do |attribute, error|
241+
# person.errors.each do |attribute, message|
219242
# # Will yield :name and "can't be blank"
220243
# # then yield :name and "must be specified"
221244
# end
@@ -249,7 +272,7 @@ def each(&block)
249272
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
250273
# person.errors.values # => [["cannot be nil", "must be specified"]]
251274
def values
252-
deprecation_removal_warning(:values)
275+
deprecation_removal_warning(:values, "errors.map { |error| error.message }")
253276
@errors.map(&:message).freeze
254277
end
255278

@@ -258,7 +281,7 @@ def values
258281
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
259282
# person.errors.keys # => [:name]
260283
def keys
261-
deprecation_removal_warning(:keys)
284+
deprecation_removal_warning(:keys, "errors.map { |error| error.attribute }")
262285
keys = @errors.map(&:attribute)
263286
keys.uniq!
264287
keys.freeze
@@ -338,25 +361,25 @@ def group_by_attribute
338361
@errors.group_by(&:attribute)
339362
end
340363

341-
# Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
364+
# Adds a new error of +type+ on +attribute+.
342365
# More than one error can be added to the same +attribute+.
343-
# If no +message+ is supplied, <tt>:invalid</tt> is assumed.
366+
# If no +type+ is supplied, <tt>:invalid</tt> is assumed.
344367
#
345368
# person.errors.add(:name)
346-
# # => ["is invalid"]
369+
# # Adds <#ActiveModel::Error attribute=name, type=invalid>
347370
# person.errors.add(:name, :not_implemented, message: "must be implemented")
348-
# # => ["is invalid", "must be implemented"]
371+
# # Adds <#ActiveModel::Error attribute=name, type=not_implemented,
372+
# options={:message=>"must be implemented"}>
349373
#
350374
# person.errors.messages
351375
# # => {:name=>["is invalid", "must be implemented"]}
352376
#
353-
# person.errors.details
354-
# # => {:name=>[{error: :not_implemented}, {error: :invalid}]}
377+
# If +type+ is a string, it will be used as error message.
355378
#
356-
# If +message+ is a symbol, it will be translated using the appropriate
379+
# If +type+ is a symbol, it will be translated using the appropriate
357380
# scope (see +generate_message+).
358381
#
359-
# If +message+ is a proc, it will be called, allowing for things like
382+
# If +type+ is a proc, it will be called, allowing for things like
360383
# <tt>Time.now</tt> to be used within an error.
361384
#
362385
# If the <tt>:strict</tt> option is set to +true+, it will raise
@@ -393,14 +416,14 @@ def add(attribute, type = :invalid, **options)
393416
error
394417
end
395418

396-
# Returns +true+ if an error on the attribute with the given message is
397-
# present, or +false+ otherwise. +message+ is treated the same as for +add+.
419+
# Returns +true+ if an error matches provided +attribute+ and +type+,
420+
# or +false+ otherwise. +type+ is treated the same as for +add+.
398421
#
399422
# person.errors.add :name, :blank
400423
# person.errors.added? :name, :blank # => true
401424
# person.errors.added? :name, "can't be blank" # => true
402425
#
403-
# If the error message requires options, then it returns +true+ with
426+
# If the error requires options, then it returns +true+ with
404427
# the correct options, or +false+ with incorrect or missing options.
405428
#
406429
# person.errors.add :name, :too_long, { count: 25 }
@@ -421,8 +444,8 @@ def added?(attribute, type = :invalid, options = {})
421444
end
422445
end
423446

424-
# Returns +true+ if an error on the attribute with the given message is
425-
# present, or +false+ otherwise. +message+ is treated the same as for +add+.
447+
# Returns +true+ if an error on the attribute with the given type is
448+
# present, or +false+ otherwise. +type+ is treated the same as for +add+.
426449
#
427450
# person.errors.add :age
428451
# person.errors.add :name, :too_long, { count: 25 }
@@ -432,13 +455,13 @@ def added?(attribute, type = :invalid, options = {})
432455
# person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true
433456
# person.errors.of_kind? :name, :not_too_long # => false
434457
# person.errors.of_kind? :name, "is too long" # => false
435-
def of_kind?(attribute, message = :invalid)
436-
attribute, message = normalize_arguments(attribute, message)
458+
def of_kind?(attribute, type = :invalid)
459+
attribute, type = normalize_arguments(attribute, type)
437460

438-
if message.is_a? Symbol
439-
!where(attribute, message).empty?
461+
if type.is_a? Symbol
462+
!where(attribute, type).empty?
440463
else
441-
messages_for(attribute).include?(message)
464+
messages_for(attribute).include?(type)
442465
end
443466
end
444467

@@ -550,13 +573,27 @@ def add_from_legacy_details_hash(details)
550573
}
551574
end
552575

553-
def deprecation_removal_warning(method_name)
554-
ActiveSupport::Deprecation.warn("ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.2")
576+
def deprecation_removal_warning(method_name, alternative_message = nil)
577+
message = +"ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.2."
578+
if alternative_message
579+
message << "\n\nTo achieve the same use:\n\n "
580+
message << alternative_message
581+
end
582+
ActiveSupport::Deprecation.warn(message)
555583
end
556584

557585
def deprecation_rename_warning(old_method_name, new_method_name)
558586
ActiveSupport::Deprecation.warn("ActiveModel::Errors##{old_method_name} is deprecated. Please call ##{new_method_name} instead.")
559587
end
588+
589+
def deprecation_index_access_warning(method_name, alternative_message)
590+
message = +"ActiveModel::Errors##{method_name} is deprecated. In the next release it would return `Error` object instead."
591+
if alternative_message
592+
message << "\n\nTo achieve the same use:\n\n "
593+
message << alternative_message
594+
end
595+
ActiveSupport::Deprecation.warn(message)
596+
end
560597
end
561598

562599
class DeprecationHandlingMessageHash < SimpleDelegator

activemodel/lib/active_model/nested_error.rb

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,6 @@
44
require "forwardable"
55

66
module ActiveModel
7-
# Represents one single error
8-
# @!attribute [r] base
9-
# @return [ActiveModel::Base] the object which the error belongs to
10-
# @!attribute [r] attribute
11-
# @return [Symbol] attribute of the object which the error belongs to
12-
# @!attribute [r] type
13-
# @return [Symbol] error's type
14-
# @!attribute [r] options
15-
# @return [Hash] additional options
16-
# @!attribute [r] inner_error
17-
# @return [Error] inner error
187
class NestedError < Error
198
def initialize(base, inner_error, override_options = {})
209
@base = base

0 commit comments

Comments
 (0)