Document on ActiveModel::Errors changes

Mark private constants

Display alternative for deprecation removal warning

Annotate Error's attributes

More emphasis on adding an error instead of message

Rewrite scaffold template using new errors API

Set first and last with behavior change deprecation

Update more doc and example

Add inspect for easier debugging
This commit is contained in:
lulalala 2019-04-28 20:25:51 +08:00
parent 10a37f33b7
commit fcd1e41e82
6 changed files with 185 additions and 155 deletions

View File

@ -19,5 +19,20 @@
*DHH*
* Encapsulate each validation error as an Error object.
The `ActiveModel`s `errors` collection is now an array of these Error
objects, instead of messages/details hash.
For each of these `Error` object, its `message` and `full_message` methods
are for generating error messages. Its `details` method would return errors
extra parameters, found in the original `details` hash.
The change tries its best at maintaining backward compatibility, however
some edge cases wont be covered, mainly related to manipulating
`errors.messages` and `errors.details` hashes directly. Moving forward,
please convert those direct manipulations to use provided API methods instead.
*lulalala*
Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/activemodel/CHANGELOG.md) for previous changes.

View File

@ -110,7 +110,16 @@ module ActiveModel
@options = @options.deep_dup
end
attr_reader :base, :attribute, :type, :raw_type, :options
# The object which the error belongs to
attr_reader :base
# The attribute of +base+ which the error belongs to
attr_reader :attribute
# The type of error, defaults to `:invalid` unless specified
attr_reader :type
# The raw value provided as the second parameter when calling `errors#add`
attr_reader :raw_type
# The options provided when calling `errors#add`
attr_reader :options
def message
case raw_type
@ -159,6 +168,10 @@ module ActiveModel
attributes_for_hash.hash
end
def inspect # :nodoc:
"<##{self.class.name} attribute=#{@attribute}, type=#{@type}, options=#{@options.inspect}>"
end
protected
def attributes_for_hash
[@base, @attribute, @raw_type, @options.except(*CALLBACKS_OPTIONS)]

View File

@ -11,7 +11,7 @@ require "forwardable"
module ActiveModel
# == Active \Model \Errors
#
# Provides a modified +Hash+ that you can include in your object
# Provides error related functionalities you can include in your object
# for handling error messages and interacting with Action View helpers.
#
# A minimal implementation could be:
@ -68,7 +68,10 @@ module ActiveModel
def_delegators :@errors, :count
LEGACY_ATTRIBUTES = [:messages, :details].freeze
private_constant :LEGACY_ATTRIBUTES
# The actual array of +Error+ objects
# This method is aliased to <tt>objects</tt>.
attr_reader :errors
alias :objects :errors
@ -205,17 +208,37 @@ module ActiveModel
DeprecationHandlingMessageArray.new(messages_for(attribute), self, attribute)
end
# Iterates through each error key, value pair in the error messages hash.
def first
deprecation_index_access_warning(:first)
super
end
def last
deprecation_index_access_warning(:last)
super
end
# Iterates through each error object.
#
# person.errors.add(:name, :too_short, count: 2)
# person.errors.each do |error|
# # Will yield <#ActiveModel::Error attribute=name, type=too_short,
# options={:count=>3}>
# end
#
# To be backward compatible with past deprecated hash-like behavior,
# when block accepts two parameters instead of one, it
# iterates through each error key, value pair in the error messages hash.
# Yields the attribute and the error for that attribute. If the attribute
# has more than one error message, yields once for each error message.
#
# person.errors.add(:name, :blank, message: "can't be blank")
# person.errors.each do |attribute, error|
# person.errors.each do |attribute, message|
# # Will yield :name and "can't be blank"
# end
#
# person.errors.add(:name, :not_specified, message: "must be specified")
# person.errors.each do |attribute, error|
# person.errors.each do |attribute, message|
# # Will yield :name and "can't be blank"
# # then yield :name and "must be specified"
# end
@ -248,7 +271,7 @@ module ActiveModel
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
# person.errors.values # => [["cannot be nil", "must be specified"]]
def values
deprecation_removal_warning(:values)
deprecation_removal_warning(:values, "errors.map { |error| error.message }")
@errors.map(&:message).freeze
end
@ -257,7 +280,7 @@ module ActiveModel
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
# person.errors.keys # => [:name]
def keys
deprecation_removal_warning(:keys)
deprecation_removal_warning(:keys, "errors.map { |error| error.attribute }")
keys = @errors.map(&:attribute)
keys.uniq!
keys.freeze
@ -329,25 +352,25 @@ module ActiveModel
@errors.group_by(&:attribute)
end
# Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
# Adds a new error of +type+ on +attribute+.
# More than one error can be added to the same +attribute+.
# If no +message+ is supplied, <tt>:invalid</tt> is assumed.
# If no +type+ is supplied, <tt>:invalid</tt> is assumed.
#
# person.errors.add(:name)
# # => ["is invalid"]
# # Adds <#ActiveModel::Error attribute=name, type=invalid>
# person.errors.add(:name, :not_implemented, message: "must be implemented")
# # => ["is invalid", "must be implemented"]
# # Adds <#ActiveModel::Error attribute=name, type=not_implemented,
# options={:message=>"must be implemented"}>
#
# person.errors.messages
# # => {:name=>["is invalid", "must be implemented"]}
#
# person.errors.details
# # => {:name=>[{error: :not_implemented}, {error: :invalid}]}
# If +type+ is a string, it will be used as error message.
#
# If +message+ is a symbol, it will be translated using the appropriate
# If +type+ is a symbol, it will be translated using the appropriate
# scope (see +generate_message+).
#
# If +message+ is a proc, it will be called, allowing for things like
# If +type+ is a proc, it will be called, allowing for things like
# <tt>Time.now</tt> to be used within an error.
#
# If the <tt>:strict</tt> option is set to +true+, it will raise
@ -384,14 +407,14 @@ module ActiveModel
error
end
# Returns +true+ if an error on the attribute with the given message is
# present, or +false+ otherwise. +message+ is treated the same as for +add+.
# Returns +true+ if an error matches provided +attribute+ and +type+,
# or +false+ otherwise. +type+ is treated the same as for +add+.
#
# person.errors.add :name, :blank
# person.errors.added? :name, :blank # => true
# person.errors.added? :name, "can't be blank" # => true
#
# If the error message requires options, then it returns +true+ with
# If the error requires options, then it returns +true+ with
# the correct options, or +false+ with incorrect or missing options.
#
# person.errors.add :name, :too_long, { count: 25 }
@ -412,8 +435,8 @@ module ActiveModel
end
end
# Returns +true+ if an error on the attribute with the given message is
# present, or +false+ otherwise. +message+ is treated the same as for +add+.
# Returns +true+ if an error on the attribute with the given type is
# present, or +false+ otherwise. +type+ is treated the same as for +add+.
#
# person.errors.add :age
# person.errors.add :name, :too_long, { count: 25 }
@ -423,13 +446,13 @@ module ActiveModel
# person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true
# person.errors.of_kind? :name, :not_too_long # => false
# person.errors.of_kind? :name, "is too long" # => false
def of_kind?(attribute, message = :invalid)
attribute, message = normalize_arguments(attribute, message)
def of_kind?(attribute, type = :invalid)
attribute, type = normalize_arguments(attribute, type)
if message.is_a? Symbol
!where(attribute, message).empty?
if type.is_a? Symbol
!where(attribute, type).empty?
else
messages_for(attribute).include?(message)
messages_for(attribute).include?(type)
end
end
@ -541,13 +564,27 @@ module ActiveModel
}
end
def deprecation_removal_warning(method_name)
ActiveSupport::Deprecation.warn("ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.2")
def deprecation_removal_warning(method_name, alternative_message = nil)
message = +"ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.2."
if alternative_message
message << "\n\nTo achieve the same use:\n\n "
message << alternative_message
end
ActiveSupport::Deprecation.warn(message)
end
def deprecation_rename_warning(old_method_name, new_method_name)
ActiveSupport::Deprecation.warn("ActiveModel::Errors##{old_method_name} is deprecated. Please call ##{new_method_name} instead.")
end
def deprecation_index_access_warning(method_name, alternative_message)
message = +"ActiveModel::Errors##{method_name} is deprecated. In the next release it would return `Error` object instead."
if alternative_message
message << "\n\nTo achieve the same use:\n\n "
message << alternative_message
end
ActiveSupport::Deprecation.warn(message)
end
end
class DeprecationHandlingMessageHash < SimpleDelegator

View File

@ -4,17 +4,6 @@ require "active_model/error"
require "forwardable"
module ActiveModel
# Represents one single error
# @!attribute [r] base
# @return [ActiveModel::Base] the object which the error belongs to
# @!attribute [r] attribute
# @return [Symbol] attribute of the object which the error belongs to
# @!attribute [r] type
# @return [Symbol] error's type
# @!attribute [r] options
# @return [Hash] additional options
# @!attribute [r] inner_error
# @return [Error] inner error
class NestedError < Error
def initialize(base, inner_error, override_options = {})
@base = base

View File

@ -165,7 +165,7 @@ Person.create(name: nil).valid? # => false
```
After Active Record has performed validations, any errors found can be accessed
through the `errors.messages` instance method, which returns a collection of errors.
through the `errors` instance method, which returns a collection of errors.
By definition, an object is valid if this collection is empty after running
validations.
@ -180,18 +180,18 @@ end
>> p = Person.new
# => #<Person id: nil, name: nil>
>> p.errors.messages
# => {}
>> p.errors.size
# => 0
>> p.valid?
# => false
>> p.errors.messages
# => {name:["can't be blank"]}
>> p.errors.objects.first.full_message
# => "Name can't be blank"
>> p = Person.create
# => #<Person id: nil, name: nil>
>> p.errors.messages
# => {name:["can't be blank"]}
>> p.errors.objects.first.full_message
# => "Name can't be blank"
>> p.save
# => false
@ -209,7 +209,7 @@ returning true if any errors were found in the object, and false otherwise.
### `errors[]`
To verify whether or not a particular attribute of an object is valid, you can
use `errors[:attribute]`. It returns an array of all the errors for
use `errors[:attribute]`. It returns an array of all the error messages for
`:attribute`. If there are no errors on the specified attribute, an empty array
is returned.
@ -231,32 +231,13 @@ end
We'll cover validation errors in greater depth in the [Working with Validation
Errors](#working-with-validation-errors) section.
### `errors.details`
To check which validations failed on an invalid attribute, you can use
`errors.details[:attribute]`. It returns an array of hashes with an `:error`
key to get the symbol of the validator:
```ruby
class Person < ApplicationRecord
validates :name, presence: true
end
>> person = Person.new
>> person.valid?
>> person.errors.details[:name] # => [{error: :blank}]
```
Using `details` with custom validators is covered in the [Working with
Validation Errors](#working-with-validation-errors) section.
Validation Helpers
------------------
Active Record offers many pre-defined validation helpers that you can use
directly inside your class definitions. These helpers provide common validation
rules. Every time a validation fails, an error message is added to the object's
`errors` collection, and this message is associated with the attribute being
rules. Every time a validation fails, an error is added to the object's
`errors` collection, and this is associated with the attribute being
validated.
Each helper accepts an arbitrary number of attribute names, so with a single
@ -747,7 +728,7 @@ end
The block receives the record, the attribute's name, and the attribute's value.
You can do anything you like to check for valid data within the block. If your
validation fails, you should add an error message to the model, therefore
validation fails, you should add an error to the model, therefore
making it invalid.
Common Validation Options
@ -1041,7 +1022,7 @@ own custom validators.
### Custom Methods
You can also create methods that verify the state of your models and add
messages to the `errors` collection when they are invalid. You must then
errors to the `errors` collection when they are invalid. You must then
register these methods by using the `validate`
([API](https://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate))
class method, passing in the symbols for the validation methods' names.
@ -1090,13 +1071,16 @@ end
Working with Validation Errors
------------------------------
In addition to the `valid?` and `invalid?` methods covered earlier, Rails provides a number of methods for working with the `errors` collection and inquiring about the validity of objects.
The `valid?` and `invalid?` methods only provide a summary status on validity. However you can dig deeper into each individual error by using various methods from the `errors` collection.
The following is a list of the most commonly used methods. Please refer to the `ActiveModel::Errors` documentation for a list of all the available methods.
### `errors`
Returns an instance of the class `ActiveModel::Errors` containing all errors. Each key is the attribute name and the value is an array of strings with all errors.
The gateway through which you can drill down into various details of each error.
This returns an instance of the class `ActiveModel::Errors` containing all errors,
each error is represented by an `ActiveModel::Error` object.
```ruby
class Person < ApplicationRecord
@ -1105,12 +1089,12 @@ end
person = Person.new
person.valid? # => false
person.errors.messages
# => {:name=>["can't be blank", "is too short (minimum is 3 characters)"]}
person.errors.full_messages
# => ["Name can't be blank", "Name is too short (minimum is 3 characters)"]
person = Person.new(name: "John Doe")
person.valid? # => true
person.errors.messages # => {}
person.errors.full_messages # => []
```
### `errors[]`
@ -1136,80 +1120,11 @@ person.errors[:name]
# => ["can't be blank", "is too short (minimum is 3 characters)"]
```
### `errors.add`
### `errors.where` and error object
The `add` method lets you add an error message related to a particular attribute. It takes as arguments the attribute and the error message.
Sometimes we may need more information about each error beside its message. Each error is encapsulated as an `ActiveModel::Error` object, and `where` method is the most common way of access.
The `errors.full_messages` method (or its equivalent, `errors.to_a`) returns the error messages in a user-friendly format, with the capitalized attribute name prepended to each message, as shown in the examples below.
```ruby
class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors.add(:name, "cannot contain the characters !@#%*()_-+=")
end
end
person = Person.create(name: "!@#")
person.errors[:name]
# => ["cannot contain the characters !@#%*()_-+="]
person.errors.full_messages
# => ["Name cannot contain the characters !@#%*()_-+="]
```
### `errors.details`
You can specify a validator type to the returned error details hash using the
`errors.add` method.
```ruby
class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors.add(:name, :invalid_characters)
end
end
person = Person.create(name: "!@#")
person.errors.details[:name]
# => [{error: :invalid_characters}]
```
To improve the error details to contain the unallowed characters set for instance,
you can pass additional keys to `errors.add`.
```ruby
class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors.add(:name, :invalid_characters, not_allowed: "!@#%*()_-+=")
end
end
person = Person.create(name: "!@#")
person.errors.details[:name]
# => [{error: :invalid_characters, not_allowed: "!@#%*()_-+="}]
```
All built in Rails validators populate the details hash with the corresponding
validator type.
### `errors[:base]`
You can add error messages that are related to the object's state as a whole, instead of being related to a specific attribute. You can use this method when you want to say that the object is invalid, no matter the values of its attributes. Since `errors[:base]` is an array, you can add a string to it and it will be used as an error message.
```ruby
class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors.add :base, "This person is invalid because ..."
end
end
```
### `errors.clear`
The `clear` method is used when you intentionally want to clear all the messages in the `errors` collection. Of course, calling `errors.clear` upon an invalid object won't actually make it valid: the `errors` collection will now be empty, but the next time you call `valid?` or any method that tries to save this object to the database, the validations will run again. If any of the validations fail, the `errors` collection will be filled again.
`where` returns an array of error objects, filtered by various degree of conditions.
```ruby
class Person < ApplicationRecord
@ -1218,21 +1133,82 @@ end
person = Person.new
person.valid? # => false
person.errors[:name]
# => ["can't be blank", "is too short (minimum is 3 characters)"]
>> person.errors.where(:name) # errors linked to :name attribute
>> person.errors.where(:name, :too_short) # further filtered to only :too_short type error
```
You can read various information from these error objects:
```ruby
>> error = person.errors.where(:name).last
>> error.attribute # => :name
>> error.type # => :too_short
>> error.options[:count] # => 3
```
You can also generate the error message:
>> error.message # => "is too short (minimum is 3 characters)"
>> error.full_message # => "Name is too short (minimum is 3 characters)"
The `full_message` method generates a more user-friendly message, with the capitalized attribute name prepended.
### `errors.add`
The `add` method creates the error object by taking the `attribute`, the error `type` and additional options hash. This is useful for writing your own validator.
```ruby
class Person < ApplicationRecord
validate do |person|
errors.add :name, :too_plain, message: "is not cool enough"
end
end
person = Person.create
person.errors.where(:name).first.type # => :too_plain
person.errors.where(:name).first.full_message # => "Name is not cool enough"
```
### `errors[:base]`
You can add errors that are related to the object's state as a whole, instead of being related to a specific attribute. You can add errors to `:base` when you want to say that the object is invalid, no matter the values of its attributes.
```ruby
class Person < ApplicationRecord
validate do |person|
errors.add :base, :invalid, message: "This person is invalid because ..."
end
end
person = Person.create
person.errors.where(:base).first.full_message # => "This person is invalid because ..."
```
### `errors.clear`
The `clear` method is used when you intentionally want to clear the `errors` collection. Of course, calling `errors.clear` upon an invalid object won't actually make it valid: the `errors` collection will now be empty, but the next time you call `valid?` or any method that tries to save this object to the database, the validations will run again. If any of the validations fail, the `errors` collection will be filled again.
```ruby
class Person < ApplicationRecord
validates :name, presence: true, length: { minimum: 3 }
end
person = Person.new
person.valid? # => false
person.errors.empty? # => false
person.errors.clear
person.errors.empty? # => true
person.save # => false
person.errors[:name]
# => ["can't be blank", "is too short (minimum is 3 characters)"]
person.errors.empty? # => false
```
### `errors.size`
The `size` method returns the total number of error messages for the object.
The `size` method returns the total number of errors for the object.
```ruby
class Person < ApplicationRecord
@ -1271,8 +1247,8 @@ Assuming we have a model that's been saved in an instance variable named
<h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>
<ul>
<% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% @article.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>

View File

@ -4,8 +4,8 @@
<h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
<ul>
<%% <%= singular_table_name %>.errors.full_messages.each do |message| %>
<li><%%= message %></li>
<%% <%= singular_table_name %>.errors.each do |error| %>
<li><%%= error.full_message %></li>
<%% end %>
</ul>
</div>