aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib/active_model/errors.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel/lib/active_model/errors.rb')
-rw-r--r--activemodel/lib/active_model/errors.rb357
1 files changed, 240 insertions, 117 deletions
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 3a692a3e64..a9af426fb1 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -4,6 +4,10 @@ require "active_support/core_ext/array/conversions"
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/object/deep_dup"
require "active_support/core_ext/string/filters"
+require "active_support/deprecation"
+require "active_model/error"
+require "active_model/nested_error"
+require "forwardable"
module ActiveModel
# == Active \Model \Errors
@@ -59,15 +63,20 @@ module ActiveModel
class Errors
include Enumerable
- CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
- MESSAGE_OPTIONS = [:message]
+ extend Forwardable
+ def_delegators :@errors, :size, :clear, :blank?, :empty?, :uniq!
+ # TODO: forward all enumerable methods after `each` deprecation is removed.
+ def_delegators :@errors, :count
+
+ LEGACY_ATTRIBUTES = [:messages, :details].freeze
class << self
attr_accessor :i18n_customize_full_message # :nodoc:
end
self.i18n_customize_full_message = false
- attr_reader :messages, :details
+ attr_reader :errors
+ alias :objects :errors
# Pass in the instance of the object that is using the errors object.
#
@@ -77,18 +86,17 @@ module ActiveModel
# end
# end
def initialize(base)
- @base = base
- @messages = apply_default_array({})
- @details = apply_default_array({})
+ @base = base
+ @errors = []
end
def initialize_dup(other) # :nodoc:
- @messages = other.messages.dup
- @details = other.details.deep_dup
+ @errors = other.errors.deep_dup
super
end
# Copies the errors from <tt>other</tt>.
+ # For copying errors but keep <tt>@base</tt> as is.
#
# other - The ActiveModel::Errors instance.
#
@@ -96,11 +104,31 @@ module ActiveModel
#
# person.errors.copy!(other)
def copy!(other) # :nodoc:
- @messages = other.messages.dup
- @details = other.details.dup
+ @errors = other.errors.deep_dup
+ @errors.each { |error|
+ error.instance_variable_set("@base", @base)
+ }
+ end
+
+ # Imports one error
+ # Imported errors are wrapped as a NestedError,
+ # providing access to original error object.
+ # If attribute or type needs to be overriden, use `override_options`.
+ #
+ # override_options - Hash
+ # @option override_options [Symbol] :attribute Override the attribute the error belongs to
+ # @option override_options [Symbol] :type Override type of the error.
+ def import(error, override_options = {})
+ [:attribute, :type].each do |key|
+ if override_options.key?(key)
+ override_options[key] = override_options[key].to_sym
+ end
+ end
+ @errors.append(NestedError.new(@base, error, override_options))
end
- # Merges the errors from <tt>other</tt>.
+ # Merges the errors from <tt>other</tt>,
+ # each <tt>Error</tt> wrapped as <tt>NestedError</tt>.
#
# other - The ActiveModel::Errors instance.
#
@@ -108,8 +136,9 @@ module ActiveModel
#
# person.errors.merge!(other)
def merge!(other)
- @messages.merge!(other.messages) { |_, ary1, ary2| ary1 + ary2 }
- @details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 }
+ other.errors.each { |error|
+ import(error)
+ }
end
# Removes all errors except the given keys. Returns a hash containing the removed errors.
@@ -118,19 +147,31 @@ module ActiveModel
# person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] }
# person.errors.keys # => [:age, :gender]
def slice!(*keys)
+ deprecation_removal_warning(:slice!)
+
keys = keys.map(&:to_sym)
- @details.slice!(*keys)
- @messages.slice!(*keys)
+
+ results = messages.dup.slice!(*keys)
+
+ @errors.keep_if do |error|
+ keys.include?(error.attribute)
+ end
+
+ results
end
- # Clear the error messages.
+ # Search for errors matching +attribute+, +type+ or +options+.
+ #
+ # Only supplied params will be matched.
#
- # person.errors.full_messages # => ["name cannot be nil"]
- # person.errors.clear
- # person.errors.full_messages # => []
- def clear
- messages.clear
- details.clear
+ # person.errors.where(:name) # => all name errors.
+ # person.errors.where(:name, :too_short) # => all name errors being too short
+ # person.errors.where(:name, :too_short, minimum: 2) # => all name errors being too short and minimum is 2
+ def where(attribute, type = nil, **options)
+ attribute, type, options = normalize_arguments(attribute, type, options)
+ @errors.select { |error|
+ error.match?(attribute, type, options)
+ }
end
# Returns +true+ if the error messages include an error for the given key
@@ -140,8 +181,9 @@ module ActiveModel
# person.errors.include?(:name) # => true
# person.errors.include?(:age) # => false
def include?(attribute)
- attribute = attribute.to_sym
- messages.key?(attribute) && messages[attribute].present?
+ @errors.any? { |error|
+ error.match?(attribute.to_sym)
+ }
end
alias :has_key? :include?
alias :key? :include?
@@ -151,10 +193,13 @@ module ActiveModel
# person.errors[:name] # => ["cannot be nil"]
# person.errors.delete(:name) # => ["cannot be nil"]
# person.errors[:name] # => []
- def delete(key)
- attribute = key.to_sym
- details.delete(attribute)
- messages.delete(attribute)
+ def delete(attribute, type = nil, **options)
+ attribute, type, options = normalize_arguments(attribute, type, options)
+ matches = where(attribute, type, options)
+ matches.each do |error|
+ @errors.delete(error)
+ end
+ matches.map(&:message)
end
# When passed a symbol or a name of a method, returns an array of errors
@@ -163,7 +208,7 @@ module ActiveModel
# person.errors[:name] # => ["cannot be nil"]
# person.errors['name'] # => ["cannot be nil"]
def [](attribute)
- messages[attribute.to_sym]
+ DeprecationHandlingMessageArray.new(messages_for(attribute), self, attribute)
end
# Iterates through each error key, value pair in the error messages hash.
@@ -180,31 +225,37 @@ module ActiveModel
# # Will yield :name and "can't be blank"
# # then yield :name and "must be specified"
# end
- def each
- messages.each_key do |attribute|
- messages[attribute].each { |error| yield attribute, error }
- end
- end
+ def each(&block)
+ if block.arity == 1
+ @errors.each(&block)
+ else
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Enumerating ActiveModel::Errors as a hash has been deprecated.
+ In Rails 6, `errors` is an array of Error objects,
+ therefore it should be accessed by a block with a single block
+ parameter like this:
+
+ person.errors.each do |error|
+ error.full_message
+ end
- # Returns the number of error messages.
- #
- # person.errors.add(:name, :blank, message: "can't be blank")
- # person.errors.size # => 1
- # person.errors.add(:name, :not_specified, message: "must be specified")
- # person.errors.size # => 2
- def size
- values.flatten.size
+ You are passing a block expecting 2 parameters,
+ so the old hash behavior is simulated. As this is deprecated,
+ this will result in an ArgumentError in Rails 6.1.
+ MSG
+ @errors.
+ sort { |a, b| a.attribute <=> b.attribute }.
+ each { |error| yield error.attribute, error.message }
+ end
end
- alias :count :size
# Returns all message values.
#
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
# person.errors.values # => [["cannot be nil", "must be specified"]]
def values
- messages.select do |key, value|
- !value.empty?
- end.values
+ deprecation_removal_warning(:values)
+ @errors.map(&:message).freeze
end
# Returns all message keys.
@@ -212,21 +263,12 @@ module ActiveModel
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
# person.errors.keys # => [:name]
def keys
- messages.select do |key, value|
- !value.empty?
- end.keys
+ deprecation_removal_warning(:keys)
+ keys = @errors.map(&:attribute)
+ keys.uniq!
+ keys.freeze
end
- # Returns +true+ if no errors are found, +false+ otherwise.
- # If the error message is a string it can be empty.
- #
- # person.errors.full_messages # => ["name cannot be nil"]
- # person.errors.empty? # => false
- def empty?
- size.zero?
- end
- alias :blank? :empty?
-
# Returns an xml formatted representation of the Errors hash.
#
# person.errors.add(:name, :blank, message: "can't be blank")
@@ -239,6 +281,7 @@ module ActiveModel
# # <error>name must be specified</error>
# # </errors>
def to_xml(options = {})
+ deprecation_removal_warning(:to_xml)
to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
end
@@ -258,13 +301,28 @@ module ActiveModel
# person.errors.to_hash # => {:name=>["cannot be nil"]}
# person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
def to_hash(full_messages = false)
- if full_messages
- messages.each_with_object({}) do |(attribute, array), messages|
- messages[attribute] = array.map { |message| full_message(attribute, message) }
- end
- else
- without_default_proc(messages)
+ hash = {}
+ message_method = full_messages ? :full_message : :message
+ group_by_attribute.each do |attribute, errors|
+ hash[attribute] = errors.map(&message_method)
end
+ hash
+ end
+
+ def messages
+ DeprecationHandlingMessageHash.new(self)
+ end
+
+ def details
+ hash = {}
+ group_by_attribute.each do |attribute, errors|
+ hash[attribute] = errors.map(&:detail)
+ end
+ DeprecationHandlingDetailsHash.new(hash)
+ end
+
+ def group_by_attribute
+ @errors.group_by(&:attribute)
end
# Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
@@ -308,17 +366,20 @@ module ActiveModel
# # => {:base=>["either name or email must be present"]}
# person.errors.details
# # => {:base=>[{error: :name_or_email_blank}]}
- def add(attribute, message = :invalid, options = {})
- message = message.call if message.respond_to?(:call)
- detail = normalize_detail(message, options)
- message = normalize_message(attribute, message, options)
+ def add(attribute, type = :invalid, **options)
+ error = Error.new(
+ @base,
+ *normalize_arguments(attribute, type, options)
+ )
+
if exception = options[:strict]
exception = ActiveModel::StrictValidationFailed if exception == true
- raise exception, full_message(attribute, message)
+ raise exception, error.full_message
end
- details[attribute.to_sym] << detail
- messages[attribute.to_sym] << message
+ @errors.append(error)
+
+ error
end
# Returns +true+ if an error on the attribute with the given message is
@@ -337,13 +398,15 @@ module ActiveModel
# person.errors.added? :name, :too_long, count: 24 # => false
# person.errors.added? :name, :too_long # => false
# person.errors.added? :name, "is too long" # => false
- def added?(attribute, message = :invalid, options = {})
- message = message.call if message.respond_to?(:call)
+ def added?(attribute, type = :invalid, options = {})
+ attribute, type, options = normalize_arguments(attribute, type, options)
- if message.is_a? Symbol
- details[attribute.to_sym].include? normalize_detail(message, options)
+ if type.is_a? Symbol
+ @errors.any? { |error|
+ error.strict_match?(attribute, type, options)
+ }
else
- self[attribute].include? message
+ messages_for(attribute).include?(type)
end
end
@@ -359,12 +422,12 @@ module ActiveModel
# person.errors.of_kind? :name, :not_too_long # => false
# person.errors.of_kind? :name, "is too long" # => false
def of_kind?(attribute, message = :invalid)
- message = message.call if message.respond_to?(:call)
+ attribute, message = normalize_arguments(attribute, message)
if message.is_a? Symbol
- details[attribute.to_sym].map { |e| e[:error] }.include? message
+ !where(attribute, message).empty?
else
- self[attribute].include? message
+ messages_for(attribute).include?(message)
end
end
@@ -379,7 +442,7 @@ module ActiveModel
# person.errors.full_messages
# # => ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]
def full_messages
- map { |attribute, message| full_message(attribute, message) }
+ @errors.map(&:full_message)
end
alias :to_a :full_messages
@@ -394,21 +457,16 @@ module ActiveModel
# person.errors.full_messages_for(:name)
# # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
def full_messages_for(attribute)
- attribute = attribute.to_sym
- messages[attribute].map { |message| full_message(attribute, message) }
+ where(attribute).map(&:full_message).freeze
+ end
+
+ def messages_for(attribute)
+ where(attribute).map(&:message)
end
# Returns a full message for a given attribute.
#
# person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
- #
- # The `"%{attribute} %{message}"` error format can be overridden with either
- #
- # * <tt>activemodel.errors.models.person/contacts/addresses.attributes.street.format</tt>
- # * <tt>activemodel.errors.models.person/contacts/addresses.format</tt>
- # * <tt>activemodel.errors.models.person.attributes.name.format</tt>
- # * <tt>activemodel.errors.models.person.format</tt>
- # * <tt>errors.format</tt>
def full_message(attribute, message)
return message if attribute == :base
attribute = attribute.to_s
@@ -514,46 +572,111 @@ module ActiveModel
I18n.translate(key, options)
end
- def marshal_dump # :nodoc:
- [@base, without_default_proc(@messages), without_default_proc(@details)]
- end
-
def marshal_load(array) # :nodoc:
- @base, @messages, @details = array
- apply_default_array(@messages)
- apply_default_array(@details)
+ # Rails 5
+ @errors = []
+ @base = array[0]
+ add_from_legacy_details_hash(array[2])
end
def init_with(coder) # :nodoc:
- coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) }
- @details ||= {}
- apply_default_array(@messages)
- apply_default_array(@details)
+ data = coder.map
+
+ data.each { |k, v|
+ next if LEGACY_ATTRIBUTES.include?(k.to_sym)
+ instance_variable_set(:"@#{k}", v)
+ }
+
+ @errors ||= []
+
+ # Legacy support Rails 5.x details hash
+ add_from_legacy_details_hash(data["details"]) if data.key?("details")
end
- private
- def normalize_message(attribute, message, options)
- case message
- when Symbol
- generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
- else
- message
+ private
+
+ def normalize_arguments(attribute, type, **options)
+ # Evaluate proc first
+ if type.respond_to?(:call)
+ type = type.call(@base, options)
+ end
+
+ [attribute.to_sym, type, options]
+ end
+
+ def add_from_legacy_details_hash(details)
+ details.each { |attribute, errors|
+ errors.each { |error|
+ type = error.delete(:error)
+ add(attribute, type, error)
+ }
+ }
+ end
+
+ def deprecation_removal_warning(method_name)
+ ActiveSupport::Deprecation.warn("ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.1")
+ 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
+ end
+
+ class DeprecationHandlingMessageHash < SimpleDelegator
+ def initialize(errors)
+ @errors = errors
+ super(prepare_content)
end
- def normalize_detail(message, options)
- { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
+ def []=(attribute, value)
+ ActiveSupport::Deprecation.warn("Calling `[]=` to an ActiveModel::Errors is deprecated. Please call `ActiveModel::Errors#add` instead.")
+
+ @errors.delete(attribute)
+ Array(value).each do |message|
+ @errors.add(attribute, message)
+ end
+
+ __setobj__ prepare_content
end
- def without_default_proc(hash)
- hash.dup.tap do |new_h|
- new_h.default_proc = nil
+ private
+
+ def prepare_content
+ content = @errors.to_hash
+ content.each do |attribute, value|
+ content[attribute] = DeprecationHandlingMessageArray.new(value, @errors, attribute)
+ end
+ content.default_proc = proc do |hash, attribute|
+ hash = hash.dup
+ hash[attribute] = DeprecationHandlingMessageArray.new([], @errors, attribute)
+ __setobj__ hash.freeze
+ hash[attribute]
+ end
+ content.freeze
end
+ end
+
+ class DeprecationHandlingMessageArray < SimpleDelegator
+ def initialize(content, errors, attribute)
+ @errors = errors
+ @attribute = attribute
+ super(content.freeze)
end
- def apply_default_array(hash)
- hash.default_proc = proc { |h, key| h[key] = [] }
- hash
+ def <<(message)
+ ActiveSupport::Deprecation.warn("Calling `<<` to an ActiveModel::Errors message array in order to add an error is deprecated. Please call `ActiveModel::Errors#add` instead.")
+
+ @errors.add(@attribute, message)
+ __setobj__ @errors.messages_for(@attribute)
+ self
+ end
+ end
+
+ class DeprecationHandlingDetailsHash < SimpleDelegator
+ def initialize(details)
+ details.default = []
+ details.freeze
+ super(details)
end
end