Skip to content

Commit

Permalink
Merge pull request #36125 from lulalala/doc-for-model-errors
Browse files Browse the repository at this point in the history
Document update for ActiveModel#errors
  • Loading branch information
rafaelfranca committed Apr 13, 2020
2 parents 3a255ab + bbf839d commit 6f6f4e4
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 123 deletions.
29 changes: 29 additions & 0 deletions activemodel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,34 @@

*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 error’s
extra parameters, found in the original `details` hash.

The change tries its best at maintaining backward compatibility, however
some edge cases won’t 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.

The list of deprecated methods and their planned future behavioral changes at the next major release are:

* `errors#slice!` will be removed.
* `errors#first` will return Error object instead.
* `errors#last` will return Error object instead.
* `errors#each` with the `key, value` two-arguments block will stop working, while the `error` single-argument block would return `Error` object.
* `errors#values` will be removed.
* `errors#keys` will be removed.
* `errors#to_xml` will be removed.
* `errors#to_h` will be removed, and can be replaced with `errors#to_hash`.
* Manipulating `errors` itself as a hash will have no effect (e.g. `errors[:foo] = 'bar'`).
* Manipulating the hash returned by `errors#messages` (e.g. `errors.messages[:foo] = 'bar'`) will have no effect.
* Manipulating the hash returned by `errors#details` (e.g. `errors.details[:foo].clear`) will have no effect.

*lulalala*

Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/activemodel/CHANGELOG.md) for previous changes.
15 changes: 14 additions & 1 deletion activemodel/lib/active_model/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,16 @@ def initialize_dup(other)
@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
Expand Down Expand Up @@ -159,6 +168,10 @@ def hash
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)]
Expand Down
89 changes: 63 additions & 26 deletions activemodel/lib/active_model/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
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:
Expand Down Expand Up @@ -68,7 +68,10 @@ class Errors
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

Expand Down Expand Up @@ -205,17 +208,37 @@ def [](attribute)
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
Expand Down Expand Up @@ -249,7 +272,7 @@ def each(&block)
# 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

Expand All @@ -258,7 +281,7 @@ def values
# 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
Expand Down Expand Up @@ -338,25 +361,25 @@ def group_by_attribute
@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
Expand Down Expand Up @@ -393,14 +416,14 @@ def add(attribute, type = :invalid, **options)
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 }
Expand All @@ -421,8 +444,8 @@ def added?(attribute, type = :invalid, options = {})
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 }
Expand All @@ -432,13 +455,13 @@ def added?(attribute, type = :invalid, options = {})
# 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

Expand Down Expand Up @@ -550,13 +573,27 @@ def add_from_legacy_details_hash(details)
}
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
Expand Down
11 changes: 0 additions & 11 deletions activemodel/lib/active_model/nested_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,6 @@
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
Expand Down

0 comments on commit 6f6f4e4

Please sign in to comment.