Merge pull request #32313 from lulalala/model_error_as_object

Model error as object
This commit is contained in:
Rafael França 2019-04-24 16:16:00 -04:00 committed by GitHub
commit d4d145a679
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 960 additions and 234 deletions

@ -20,11 +20,11 @@ def setup
super
@post = Post.new
@post.errors[:author_name] << "can't be empty"
@post.errors[:body] << "foo"
@post.errors[:category] << "must exist"
@post.errors[:published] << "must be accepted"
@post.errors[:updated_at] << "bar"
assert_deprecated { @post.errors[:author_name] << "can't be empty" }
assert_deprecated { @post.errors[:body] << "foo" }
assert_deprecated { @post.errors[:category] << "must exist" }
assert_deprecated { @post.errors[:published] << "must be accepted" }
assert_deprecated { @post.errors[:updated_at] << "bar" }
@post.author_name = ""
@post.body = "Back to the hill and over it again!"

@ -29,10 +29,20 @@ def secret
end
Continent = Struct.new("Continent", :continent_name, :countries)
Country = Struct.new("Country", :country_id, :country_name)
Firm = Struct.new("Firm", :time_zone)
Album = Struct.new("Album", :id, :title, :genre)
end
class Firm
include ActiveModel::Validations
extend ActiveModel::Naming
attr_accessor :time_zone
def initialize(time_zone = nil)
@time_zone = time_zone
end
end
module FakeZones
FakeZone = Struct.new(:name) do
def to_s; name; end
@ -1294,7 +1304,7 @@ def tz.===(zone); raise Exception; end
def test_time_zone_select_with_priority_zones_and_errors
@firm = Firm.new("D")
@firm.extend ActiveModel::Validations
@firm.errors[:time_zone] << "invalid"
assert_deprecated { @firm.errors[:time_zone] << "invalid" }
zones = [ ActiveSupport::TimeZone.new("A"), ActiveSupport::TimeZone.new("D") ]
html = time_zone_select("firm", "time_zone", zones)
assert_dom_equal "<div class=\"field_with_errors\">" \

@ -0,0 +1,81 @@
# frozen_string_literal: true
module ActiveModel
# == Active \Model \Error
#
# Represents one single error
class Error
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
MESSAGE_OPTIONS = [:message]
def initialize(base, attribute, type = :invalid, **options)
@base = base
@attribute = attribute
@raw_type = type
@type = type || :invalid
@options = options
end
def initialize_dup(other)
@attribute = @attribute.dup
@raw_type = @raw_type.dup
@type = @type.dup
@options = @options.deep_dup
end
attr_reader :base, :attribute, :type, :raw_type, :options
def message
case raw_type
when Symbol
base.errors.generate_message(attribute, raw_type, options.except(*CALLBACKS_OPTIONS))
else
raw_type
end
end
def detail
{ error: raw_type }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
end
def full_message
base.errors.full_message(attribute, message)
end
# See if error matches provided +attribute+, +type+ and +options+.
def match?(attribute, type = nil, **options)
if @attribute != attribute || (type && @type != type)
return false
end
options.each do |key, value|
if @options[key] != value
return false
end
end
true
end
def strict_match?(attribute, type, **options)
return false unless match?(attribute, type, **options)
full_message == Error.new(@base, attribute, type, **options).full_message
end
def ==(other)
other.is_a?(self.class) && attributes_for_hash == other.attributes_for_hash
end
alias eql? ==
def hash
attributes_for_hash.hash
end
protected
def attributes_for_hash
[@base, @attribute, @raw_type, @options]
end
end
end

@ -4,6 +4,10 @@
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 @@ class << self
# 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 @@ def initialize_dup(other) # :nodoc:
#
# 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
# Merges the errors from <tt>other</tt>.
# 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>,
# each <tt>Error</tt> wrapped as <tt>NestedError</tt>.
#
# other - The ActiveModel::Errors instance.
#
@ -108,8 +136,9 @@ def copy!(other) # :nodoc:
#
# 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 @@ def merge!(other)
# 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+.
#
# person.errors.full_messages # => ["name cannot be nil"]
# person.errors.clear
# person.errors.full_messages # => []
def clear
messages.clear
details.clear
# Only supplied params will be matched.
#
# 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 @@ def clear
# 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 @@ def include?(attribute)
# 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 @@ def delete(key)
# 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 @@ def [](attribute)
# # 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 }
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
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
# 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
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 @@ def values
# 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 @@ def empty?
# # <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 @@ def as_json(options = nil)
# 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 @@ def to_hash(full_messages = false)
# # => {: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 @@ def add(attribute, message = :invalid, options = {})
# 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 @@ def added?(attribute, message = :invalid, options = {})
# 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 @@ def of_kind?(attribute, message = :invalid)
# 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 @@ def full_messages
# 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 @@ def generate_message(attribute, type = :invalid, options = {})
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
end
def normalize_detail(message, options)
{ error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
end
def without_default_proc(hash)
hash.dup.tap do |new_h|
new_h.default_proc = nil
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 apply_default_array(hash)
hash.default_proc = proc { |h, key| h[key] = [] }
hash
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
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 <<(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

@ -101,7 +101,7 @@ def test_model_naming
# locale. If no error is present, the method should return an empty array.
def test_errors_aref
assert_respond_to model, :errors
assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array"
assert_equal [], model.errors[:hello], "errors#[] should return an empty Array"
end
private

@ -0,0 +1,33 @@
# frozen_string_literal: true
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
@inner_error = inner_error
@attribute = override_options.fetch(:attribute) { inner_error.attribute }
@type = override_options.fetch(:type) { inner_error.type }
@raw_type = inner_error.raw_type
@options = inner_error.options
end
attr_reader :inner_error
extend Forwardable
def_delegators :@inner_error, :message
end
end

@ -0,0 +1,200 @@
# frozen_string_literal: true
require "cases/helper"
require "active_model/error"
class ErrorTest < ActiveModel::TestCase
class Person
extend ActiveModel::Naming
def initialize
@errors = ActiveModel::Errors.new(self)
end
attr_accessor :name, :age
attr_reader :errors
def read_attribute_for_validation(attr)
send(attr)
end
def self.human_attribute_name(attr, options = {})
attr
end
def self.lookup_ancestors
[self]
end
end
def test_initialize
base = Person.new
error = ActiveModel::Error.new(base, :name, :too_long, foo: :bar)
assert_equal base, error.base
assert_equal :name, error.attribute
assert_equal :too_long, error.type
assert_equal({ foo: :bar }, error.options)
end
test "initialize without type" do
error = ActiveModel::Error.new(Person.new, :name)
assert_equal :invalid, error.type
assert_equal({}, error.options)
end
test "initialize without type but with options" do
options = { message: "bar" }
error = ActiveModel::Error.new(Person.new, :name, options)
assert_equal(options, error.options)
end
# match?
test "match? handles mixed condition" do
subject = ActiveModel::Error.new(Person.new, :mineral, :not_enough, count: 2)
assert_not subject.match?(:mineral, :too_coarse)
assert subject.match?(:mineral, :not_enough)
assert subject.match?(:mineral, :not_enough, count: 2)
assert_not subject.match?(:mineral, :not_enough, count: 1)
end
test "match? handles attribute match" do
subject = ActiveModel::Error.new(Person.new, :mineral, :not_enough, count: 2)
assert_not subject.match?(:foo)
assert subject.match?(:mineral)
end
test "match? handles error type match" do
subject = ActiveModel::Error.new(Person.new, :mineral, :not_enough, count: 2)
assert_not subject.match?(:mineral, :too_coarse)
assert subject.match?(:mineral, :not_enough)
end
test "match? handles extra options match" do
subject = ActiveModel::Error.new(Person.new, :mineral, :not_enough, count: 2)
assert_not subject.match?(:mineral, :not_enough, count: 1)
assert subject.match?(:mineral, :not_enough, count: 2)
end
# message
test "message with type as a symbol" do
error = ActiveModel::Error.new(Person.new, :name, :blank)
assert_equal "can't be blank", error.message
end
test "message with custom interpolation" do
subject = ActiveModel::Error.new(Person.new, :name, :inclusion, message: "custom message %{value}", value: "name")
assert_equal "custom message name", subject.message
end
test "message returns plural interpolation" do
subject = ActiveModel::Error.new(Person.new, :name, :too_long, count: 10)
assert_equal "is too long (maximum is 10 characters)", subject.message
end
test "message returns singular interpolation" do
subject = ActiveModel::Error.new(Person.new, :name, :too_long, count: 1)
assert_equal "is too long (maximum is 1 character)", subject.message
end
test "message returns count interpolation" do
subject = ActiveModel::Error.new(Person.new, :name, :too_long, message: "custom message %{count}", count: 10)
assert_equal "custom message 10", subject.message
end
test "message handles lambda in messages and option values, and i18n interpolation" do
subject = ActiveModel::Error.new(Person.new, :name, :invalid,
foo: "foo",
bar: "bar",
baz: Proc.new { "baz" },
message: Proc.new { |model, options|
"%{attribute} %{foo} #{options[:bar]} %{baz}"
}
)
assert_equal "name foo bar baz", subject.message
end
test "generate_message works without i18n_scope" do
person = Person.new
error = ActiveModel::Error.new(person, :name, :blank)
assert_not_respond_to Person, :i18n_scope
assert_nothing_raised {
error.message
}
end
test "message with type as custom message" do
error = ActiveModel::Error.new(Person.new, :name, message: "cannot be blank")
assert_equal "cannot be blank", error.message
end
test "message with options[:message] as custom message" do
error = ActiveModel::Error.new(Person.new, :name, :blank, message: "cannot be blank")
assert_equal "cannot be blank", error.message
end
test "message renders lazily using current locale" do
error = nil
I18n.backend.store_translations(:pl, errors: { messages: { invalid: "jest nieprawidłowe" } })
I18n.with_locale(:en) { error = ActiveModel::Error.new(Person.new, :name, :invalid) }
I18n.with_locale(:pl) {
assert_equal "jest nieprawidłowe", error.message
}
end
test "message uses current locale" do
I18n.backend.store_translations(:en, errors: { messages: { inadequate: "Inadequate %{attribute} found!" } })
error = ActiveModel::Error.new(Person.new, :name, :inadequate)
assert_equal "Inadequate name found!", error.message
end
# full_message
test "full_message returns the given message when attribute is :base" do
error = ActiveModel::Error.new(Person.new, :base, message: "press the button")
assert_equal "press the button", error.full_message
end
test "full_message returns the given message with the attribute name included" do
error = ActiveModel::Error.new(Person.new, :name, :blank)
assert_equal "name can't be blank", error.full_message
end
test "full_message uses default format" do
error = ActiveModel::Error.new(Person.new, :name, message: "can't be blank")
# Use a locale without errors.format
I18n.with_locale(:unknown) {
assert_equal "name can't be blank", error.full_message
}
end
test "equality by base attribute, type and options" do
person = Person.new
e1 = ActiveModel::Error.new(person, :name, foo: :bar)
e2 = ActiveModel::Error.new(person, :name, foo: :bar)
e2.instance_variable_set(:@_humanized_attribute, "Name")
assert_equal(e1, e2)
end
test "inequality" do
person = Person.new
error = ActiveModel::Error.new(person, :name, foo: :bar)
assert error != ActiveModel::Error.new(person, :name, foo: :baz)
assert error != ActiveModel::Error.new(person, :name)
assert error != ActiveModel::Error.new(person, :title, foo: :bar)
assert error != ActiveModel::Error.new(Person.new, :name, foo: :bar)
end
test "comparing against different class would not raise error" do
person = Person.new
error = ActiveModel::Error.new(person, :name, foo: :bar)
assert error != person
end
end

@ -10,7 +10,7 @@ def initialize
@errors = ActiveModel::Errors.new(self)
end
attr_accessor :name, :age
attr_accessor :name, :age, :gender, :city
attr_reader :errors
def validate!
@ -31,48 +31,47 @@ def self.lookup_ancestors
end
def test_delete
errors = ActiveModel::Errors.new(self)
errors[:foo] << "omg"
errors.delete("foo")
assert_empty errors[:foo]
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, :blank)
errors.delete("name")
assert_empty errors[:name]
end
def test_include?
errors = ActiveModel::Errors.new(self)
errors[:foo] << "omg"
errors = ActiveModel::Errors.new(Person.new)
assert_deprecated { errors[:foo] << "omg" }
assert_includes errors, :foo, "errors should include :foo"
assert_includes errors, "foo", "errors should include 'foo' as :foo"
end
def test_dup
errors = ActiveModel::Errors.new(self)
errors[:foo] << "bar"
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name)
errors_dup = errors.dup
errors_dup[:bar] << "omg"
assert_not_same errors_dup.messages, errors.messages
assert_not_same errors_dup.errors, errors.errors
end
def test_has_key?
errors = ActiveModel::Errors.new(self)
errors[:foo] << "omg"
errors = ActiveModel::Errors.new(Person.new)
errors.add(:foo, "omg")
assert_equal true, errors.has_key?(:foo), "errors should have key :foo"
assert_equal true, errors.has_key?("foo"), "errors should have key 'foo' as :foo"
end
def test_has_no_key
errors = ActiveModel::Errors.new(self)
errors = ActiveModel::Errors.new(Person.new)
assert_equal false, errors.has_key?(:name), "errors should not have key :name"
end
def test_key?
errors = ActiveModel::Errors.new(self)
errors[:foo] << "omg"
errors = ActiveModel::Errors.new(Person.new)
errors.add(:foo, "omg")
assert_equal true, errors.key?(:foo), "errors should have key :foo"
assert_equal true, errors.key?("foo"), "errors should have key 'foo' as :foo"
end
def test_no_key
errors = ActiveModel::Errors.new(self)
errors = ActiveModel::Errors.new(Person.new)
assert_equal false, errors.key?(:name), "errors should not have key :name"
end
@ -86,42 +85,58 @@ def test_no_key
end
test "error access is indifferent" do
errors = ActiveModel::Errors.new(self)
errors[:foo] << "omg"
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, "omg")
assert_equal ["omg"], errors["foo"]
assert_equal ["omg"], errors["name"]
end
test "values returns an array of messages" do
errors = ActiveModel::Errors.new(self)
errors.messages[:foo] = "omg"
errors.messages[:baz] = "zomg"
errors = ActiveModel::Errors.new(Person.new)
assert_deprecated { errors.messages[:foo] = "omg" }
assert_deprecated { errors.messages[:baz] = "zomg" }
assert_equal ["omg", "zomg"], errors.values
assert_deprecated do
assert_equal ["omg", "zomg"], errors.values
end
end
test "[]= overrides values" do
errors = ActiveModel::Errors.new(self)
assert_deprecated { errors.messages[:foo] = "omg" }
assert_deprecated { errors.messages[:foo] = "zomg" }
assert_equal ["zomg"], errors[:foo]
end
test "values returns an empty array after try to get a message only" do
errors = ActiveModel::Errors.new(self)
errors = ActiveModel::Errors.new(Person.new)
errors.messages[:foo]
errors.messages[:baz]
assert_equal [], errors.values
assert_deprecated do
assert_equal [], errors.values
end
end
test "keys returns the error keys" do
errors = ActiveModel::Errors.new(self)
errors.messages[:foo] << "omg"
errors.messages[:baz] << "zomg"
errors = ActiveModel::Errors.new(Person.new)
assert_deprecated { errors.messages[:foo] << "omg" }
assert_deprecated { errors.messages[:baz] << "zomg" }
assert_equal [:foo, :baz], errors.keys
assert_deprecated do
assert_equal [:foo, :baz], errors.keys
end
end
test "keys returns an empty array after try to get a message only" do
errors = ActiveModel::Errors.new(self)
errors = ActiveModel::Errors.new(Person.new)
errors.messages[:foo]
errors.messages[:baz]
assert_equal [], errors.keys
assert_deprecated do
assert_equal [], errors.keys
end
end
test "detecting whether there are errors with empty?, blank?, include?" do
@ -146,32 +161,108 @@ def test_no_key
assert_equal ["cannot be nil"], person.errors[:name]
end
test "add an error message on a specific attribute" do
test "add an error message on a specific attribute (deprecated)" do
person = Person.new
person.errors.add(:name, "cannot be blank")
assert_equal ["cannot be blank"], person.errors[:name]
end
test "add an error message on a specific attribute with a defined type" do
test "add an error message on a specific attribute with a defined type (deprecated)" do
person = Person.new
person.errors.add(:name, :blank, message: "cannot be blank")
assert_equal ["cannot be blank"], person.errors[:name]
end
test "add an error with a symbol" do
test "add an error with a symbol (deprecated)" do
person = Person.new
person.errors.add(:name, :blank)
message = person.errors.generate_message(:name, :blank)
assert_equal [message], person.errors[:name]
end
test "add an error with a proc" do
test "add an error with a proc (deprecated)" do
person = Person.new
message = Proc.new { "cannot be blank" }
person.errors.add(:name, message)
assert_equal ["cannot be blank"], person.errors[:name]
end
test "add creates an error object and returns it" do
person = Person.new
error = person.errors.add(:name, :blank)
assert_equal :name, error.attribute
assert_equal :blank, error.type
assert_equal error, person.errors.objects.first
end
test "add, with type as symbol" do
person = Person.new
person.errors.add(:name, :blank)
assert_equal :blank, person.errors.objects.first.type
assert_equal ["can't be blank"], person.errors[:name]
end
test "add, with type as String" do
msg = "custom msg"
person = Person.new
person.errors.add(:name, msg)
assert_equal [msg], person.errors[:name]
end
test "add, with type as nil" do
person = Person.new
person.errors.add(:name)
assert_equal :invalid, person.errors.objects.first.type
assert_equal ["is invalid"], person.errors[:name]
end
test "add, with type as Proc, which evaluates to String" do
msg = "custom msg"
type = Proc.new { msg }
person = Person.new
person.errors.add(:name, type)
assert_equal [msg], person.errors[:name]
end
test "add, type being Proc, which evaluates to Symbol" do
type = Proc.new { :blank }
person = Person.new
person.errors.add(:name, type)
assert_equal :blank, person.errors.objects.first.type
assert_equal ["can't be blank"], person.errors[:name]
end
test "initialize options[:message] as Proc, which evaluates to String" do
msg = "custom msg"
type = Proc.new { msg }
person = Person.new
person.errors.add(:name, :blank, message: type)
assert_equal :blank, person.errors.objects.first.type
assert_equal [msg], person.errors[:name]
end
test "add, with options[:message] as Proc, which evaluates to String, where type is nil" do
msg = "custom msg"
type = Proc.new { msg }
person = Person.new
person.errors.add(:name, message: type)
assert_equal :invalid, person.errors.objects.first.type
assert_equal [msg], person.errors[:name]
end
test "added? detects indifferent if a specific error was added to the object" do
person = Person.new
person.errors.add(:name, "cannot be blank")
@ -437,6 +528,32 @@ def test_no_key
assert_equal({ name: [{ error: :invalid }] }, person.errors.details)
end
test "details retains original type as error" do
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, "cannot be nil")
errors.add("foo", "bar")
errors.add(:baz, nil)
errors.add(:age, :invalid, count: 3, message: "%{count} is too low")
assert_equal(
{
name: [{ error: "cannot be nil" }],
foo: [{ error: "bar" }],
baz: [{ error: nil }],
age: [{ error: :invalid, count: 3 }]
},
errors.details
)
end
test "group_by_attribute" do
person = Person.new
error = person.errors.add(:name, :invalid, message: "is bad")
hash = person.errors.group_by_attribute
assert_equal({ name: [error] }, hash)
end
test "dup duplicates details" do
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, :invalid)
@ -449,7 +566,7 @@ def test_no_key
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, :invalid)
errors.delete(:name)
assert_empty errors.details[:name]
assert_not errors.added?(:name)
end
test "delete returns the deleted messages" do
@ -467,7 +584,7 @@ def test_no_key
assert_empty person.errors.details
end
test "copy errors" do
test "copy errors (deprecated)" do
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, :invalid)
person = Person.new
@ -477,7 +594,25 @@ def test_no_key
assert_equal [:name], person.errors.details.keys
end
test "merge errors" do
test "details returns empty array when accessed with non-existent attribute" do
errors = ActiveModel::Errors.new(Person.new)
assert_equal [], errors.details[:foo]
end
test "copy errors" do
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, :invalid)
person = Person.new
person.errors.copy!(errors)
assert person.errors.added?(:name, :invalid)
person.errors.each do |error|
assert_same person, error.base
end
end
test "merge errors (deprecated)" do
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, :invalid)
@ -489,6 +624,18 @@ def test_no_key
assert_equal({ name: [{ error: :blank }, { error: :invalid }] }, person.errors.details)
end
test "merge errors" do
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, :invalid)
person = Person.new
person.errors.add(:name, :blank)
person.errors.merge!(errors)
assert(person.errors.added?(:name, :invalid))
assert(person.errors.added?(:name, :blank))
end
test "slice! removes all errors except the given keys" do
person = Person.new
person.errors.add(:name, "cannot be nil")
@ -496,9 +643,9 @@ def test_no_key
person.errors.add(:gender, "cannot be nil")
person.errors.add(:city, "cannot be nil")
person.errors.slice!(:age, "gender")
assert_deprecated { person.errors.slice!(:age, "gender") }
assert_equal [:age, :gender], person.errors.keys
assert_equal [:age, :gender], assert_deprecated { person.errors.keys }
end
test "slice! returns the deleted errors" do
@ -508,7 +655,7 @@ def test_no_key
person.errors.add(:gender, "cannot be nil")
person.errors.add(:city, "cannot be nil")
removed_errors = person.errors.slice!(:age, "gender")
removed_errors = assert_deprecated { person.errors.slice!(:age, "gender") }
assert_equal({ name: ["cannot be nil"], city: ["cannot be nil"] }, removed_errors)
end
@ -518,10 +665,23 @@ def test_no_key
errors.add(:name, :invalid)
serialized = Marshal.load(Marshal.dump(errors))
assert_equal Person, serialized.instance_variable_get(:@base).class
assert_equal errors.messages, serialized.messages
assert_equal errors.details, serialized.details
end
test "errors are compatible with marshal dumped from Rails 5.x" do
# Derived from
# errors = ActiveModel::Errors.new(Person.new)
# errors.add(:name, :invalid)
dump = "\x04\bU:\x18ActiveModel::Errors[\bo:\x17ErrorsTest::Person\x06:\f@errorsU;\x00[\b@\a{\x00{\x00{\x06:\tname[\x06I\"\x0Fis invalid\x06:\x06ET{\x06;\b[\x06{\x06:\nerror:\finvalid"
serialized = Marshal.load(dump)
assert_equal Person, serialized.instance_variable_get(:@base).class
assert_equal({ name: ["is invalid"] }, serialized.messages)
assert_equal({ name: [{ error: :invalid }] }, serialized.details)
end
test "errors are backward compatible with the Rails 4.2 format" do
yaml = <<~CODE
--- !ruby/object:ActiveModel::Errors
@ -541,4 +701,54 @@ def test_no_key
assert_equal({}, errors.messages)
assert_equal({}, errors.details)
end
test "errors are compatible with YAML dumped from Rails 5.x" do
yaml = <<~CODE
--- !ruby/object:ActiveModel::Errors
base: &1 !ruby/object:ErrorsTest::Person
errors: !ruby/object:ActiveModel::Errors
base: *1
messages: {}
details: {}
messages:
:name:
- is invalid
details:
:name:
- :error: :invalid
CODE
errors = YAML.load(yaml)
assert_equal({ name: ["is invalid"] }, errors.messages)
assert_equal({ name: [{ error: :invalid }] }, errors.details)
errors.clear
assert_equal({}, errors.messages)
assert_equal({}, errors.details)
end
test "errors are compatible with YAML dumped from Rails 6.x" do
yaml = <<~CODE
--- !ruby/object:ActiveModel::Errors
base: &1 !ruby/object:ErrorsTest::Person
errors: !ruby/object:ActiveModel::Errors
base: *1
errors: []
errors:
- !ruby/object:ActiveModel::Error
base: *1
attribute: :name
type: :invalid
raw_type: :invalid
options: {}
CODE
errors = YAML.load(yaml)
assert_equal({ name: ["is invalid"] }, errors.messages)
assert_equal({ name: [{ error: :invalid }] }, errors.details)
errors.clear
assert_equal({}, errors.messages)
assert_equal({}, errors.details)
end
end

@ -0,0 +1,54 @@
# frozen_string_literal: true
require "cases/helper"
require "active_model/nested_error"
require "models/topic"
require "models/reply"
class ErrorTest < ActiveModel::TestCase
def test_initialize
topic = Topic.new
inner_error = ActiveModel::Error.new(topic, :title, :not_enough, count: 2)
reply = Reply.new
error = ActiveModel::NestedError.new(reply, inner_error)
assert_equal reply, error.base
assert_equal inner_error.attribute, error.attribute
assert_equal inner_error.type, error.type
assert_equal(inner_error.options, error.options)
end
test "initialize with overriding attribute and type" do
topic = Topic.new
inner_error = ActiveModel::Error.new(topic, :title, :not_enough, count: 2)
reply = Reply.new
error = ActiveModel::NestedError.new(reply, inner_error, attribute: :parent, type: :foo)
assert_equal reply, error.base
assert_equal :parent, error.attribute
assert_equal :foo, error.type
assert_equal(inner_error.options, error.options)
end
def test_message
topic = Topic.new(author_name: "Bruce")
inner_error = ActiveModel::Error.new(topic, :title, :not_enough, message: Proc.new { |model, options|
"not good enough for #{model.author_name}"
})
reply = Reply.new(author_name: "Mark")
error = ActiveModel::NestedError.new(reply, inner_error)
assert_equal "not good enough for Bruce", error.message
end
def test_full_message
topic = Topic.new(author_name: "Bruce")
inner_error = ActiveModel::Error.new(topic, :title, :not_enough, message: Proc.new { |model, options|
"not good enough for #{model.author_name}"
})
reply = Reply.new(author_name: "Mark")
error = ActiveModel::NestedError.new(reply, inner_error)
assert_equal "Title not good enough for Bruce", error.full_message
end
end

@ -169,8 +169,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
# [ case, validation_options, generate_message_options]
[ "given no options", {}, {}],
[ "given custom message", { message: "custom" }, { message: "custom" }],
[ "given if condition", { if: lambda { true } }, {}],
[ "given unless condition", { unless: lambda { false } }, {}],
[ "given if condition", { if: lambda { true } }, {}],
[ "given unless condition", { unless: lambda { false } }, {}],
[ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }]
]
@ -181,6 +181,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: "Title")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -191,6 +192,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :accepted, generate_message_options]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -201,6 +203,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :blank, generate_message_options]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -211,6 +214,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :too_short, generate_message_options.merge(count: 3)]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -222,6 +226,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :too_long, generate_message_options.merge(count: 5)]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -232,6 +237,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :wrong_length, generate_message_options.merge(count: 5)]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -243,6 +249,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :invalid, generate_message_options.merge(value: "72x")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -254,6 +261,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :inclusion, generate_message_options.merge(value: "z")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -265,6 +273,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :inclusion, generate_message_options.merge(value: "z")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -276,6 +285,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :exclusion, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -287,6 +297,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :exclusion, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -298,6 +309,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :not_a_number, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -309,6 +321,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :not_an_integer, generate_message_options.merge(value: "0.0")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -320,6 +333,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :odd, generate_message_options.merge(value: 0)]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end
@ -331,6 +345,7 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
call = [:title, :less_than, generate_message_options.merge(value: 1, count: 0)]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@person.errors.messages
end
end
end

@ -14,13 +14,13 @@ def teardown
class ValidatorThatAddsErrors < ActiveModel::Validator
def validate(record)
record.errors[:base] << ERROR_MESSAGE
record.errors.add(:base, ERROR_MESSAGE)
end
end
class AnotherValidatorThatAddsErrors < ActiveModel::Validator
def validate(record)
record.errors[:base] << ANOTHER_ERROR_MESSAGE
record.errors.add(:base, ANOTHER_ERROR_MESSAGE)
end
end

@ -14,13 +14,13 @@ def teardown
class ValidatorThatAddsErrors < ActiveModel::Validator
def validate(record)
record.errors[:base] << ERROR_MESSAGE
record.errors.add(:base, message: ERROR_MESSAGE)
end
end
class OtherValidatorThatAddsErrors < ActiveModel::Validator
def validate(record)
record.errors[:base] << OTHER_ERROR_MESSAGE
record.errors.add(:base, message: OTHER_ERROR_MESSAGE)
end
end
@ -32,14 +32,14 @@ def validate(record)
class ValidatorThatValidatesOptions < ActiveModel::Validator
def validate(record)
if options[:field] == :first_name
record.errors[:base] << ERROR_MESSAGE
record.errors.add(:base, message: ERROR_MESSAGE)
end
end
end
class ValidatorPerEachAttribute < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << "Value is #{value}"
record.errors.add(attribute, message: "Value is #{value}")
end
end

@ -53,7 +53,7 @@ def test_single_error_per_attr_iteration
r = Reply.new
r.valid?
errors = r.errors.collect { |attr, messages| [attr.to_s, messages] }
errors = assert_deprecated { r.errors.collect { |attr, messages| [attr.to_s, messages] } }
assert_includes errors, ["title", "is Empty"]
assert_includes errors, ["content", "is Empty"]
@ -74,7 +74,7 @@ def test_multiple_errors_per_attr_iteration_with_full_error_composition
def test_errors_on_nested_attributes_expands_name
t = Topic.new
t.errors["replies.name"] << "can't be blank"
assert_deprecated { t.errors["replies.name"] << "can't be blank" }
assert_equal ["Replies name can't be blank"], t.errors.full_messages
end
@ -216,7 +216,7 @@ def test_errors_conversions
t = Topic.new
assert_predicate t, :invalid?
xml = t.errors.to_xml
xml = assert_deprecated { t.errors.to_xml }
assert_match %r{<errors>}, xml
assert_match %r{<error>Title can't be blank</error>}, xml
assert_match %r{<error>Content can't be blank</error>}, xml
@ -241,14 +241,14 @@ def test_validation_order
t = Topic.new title: ""
assert_predicate t, :invalid?
assert_equal :title, key = t.errors.keys[0]
assert_equal :title, key = assert_deprecated { t.errors.keys[0] }
assert_equal "can't be blank", t.errors[key][0]
assert_equal "is too short (minimum is 2 characters)", t.errors[key][1]
assert_equal :author_name, key = t.errors.keys[1]
assert_equal :author_name, key = assert_deprecated { t.errors.keys[1] }
assert_equal "can't be blank", t.errors[key][0]
assert_equal :author_email_address, key = t.errors.keys[2]
assert_equal :author_email_address, key = assert_deprecated { t.errors.keys[2] }
assert_equal "will never be valid", t.errors[key][0]
assert_equal :content, key = t.errors.keys[3]
assert_equal :content, key = assert_deprecated { t.errors.keys[3] }
assert_equal "is too short (minimum is 2 characters)", t.errors[key][0]
end

@ -5,7 +5,7 @@ class PersonWithValidator
class PresenceValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << "Local validator#{options[:custom]}" if value.blank?
record.errors.add(attribute, message: "Local validator#{options[:custom]}") if value.blank?
end
end

@ -11,24 +11,24 @@ class Reply < Topic
validate :check_wrong_update, on: :update
def check_empty_title
errors[:title] << "is Empty" unless title && title.size > 0
errors.add(:title, "is Empty") unless title && title.size > 0
end
def errors_on_empty_content
errors[:content] << "is Empty" unless content && content.size > 0
errors.add(:content, "is Empty") unless content && content.size > 0
end
def check_content_mismatch
if title && content && content == "Mismatch"
errors[:title] << "is Content Mismatch"
errors.add(:title, "is Content Mismatch")
end
end
def title_is_wrong_create
errors[:title] << "is Wrong Create" if title && title == "Wrong Create"
errors.add(:title, "is Wrong Create") if title && title == "Wrong Create"
end
def check_wrong_update
errors[:title] << "is Wrong Update" if title && title == "Wrong Update"
errors.add(:title, "is Wrong Update") if title && title == "Wrong Update"
end
end

@ -2,7 +2,8 @@
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << (options[:message] || "is not an email") unless
/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
unless /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
record.errors.add(attribute, message: options[:message] || "is not an email")
end
end
end

@ -330,21 +330,16 @@ def association_valid?(reflection, record, index = nil)
if reflection.options[:autosave]
indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors)
record.errors.each do |attribute, message|
record.errors.group_by_attribute.each { |attribute, errors|
attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
errors[attribute] << message
errors[attribute].uniq!
end
record.errors.details.each_key do |attribute|
reflection_attribute =
normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym
record.errors.details[attribute].each do |error|
errors.details[reflection_attribute] << error
errors.details[reflection_attribute].uniq!
end
end
errors.each { |error|
self.errors.import(
error,
attribute: attribute
)
}
}
else
errors.add(reflection.name)
end
@ -500,9 +495,7 @@ def save_belongs_to_association(reflection)
end
def _ensure_no_duplicate_errors
errors.messages.each_key do |attribute|
errors[attribute].uniq!
end
errors.uniq!
end
end
end

@ -749,10 +749,14 @@ def test_push_with_invalid_join_record
firm = companies(:first_firm)
lifo = Developer.new(name: "lifo")
assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
assert_raises(ActiveRecord::RecordInvalid) do
assert_deprecated { firm.developers << lifo }
end
lifo = Developer.create!(name: "lifo")
assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
assert_raises(ActiveRecord::RecordInvalid) do
assert_deprecated { firm.developers << lifo }
end
end
end
@ -1163,7 +1167,7 @@ def test_primary_key_option_on_source
def test_create_should_not_raise_exception_when_join_record_has_errors
repair_validations(Categorization) do
Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
Category.create(name: "Fishing", authors: [Author.first])
assert_deprecated { Category.create(name: "Fishing", authors: [Author.first]) }
end
end
@ -1176,7 +1180,7 @@ def test_create_bang_should_raise_exception_when_join_record_has_errors
repair_validations(Categorization) do
Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
assert_raises(ActiveRecord::RecordInvalid) do
Category.create!(name: "Fishing", authors: [Author.first])
assert_deprecated { Category.create!(name: "Fishing", authors: [Author.first]) }
end
end
end
@ -1186,7 +1190,7 @@ def test_save_bang_should_raise_exception_when_join_record_has_errors
Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
c = Category.new(name: "Fishing", authors: [Author.first])
assert_raises(ActiveRecord::RecordInvalid) do
c.save!
assert_deprecated { c.save! }
end
end
end
@ -1195,7 +1199,7 @@ def test_save_returns_falsy_when_join_record_has_errors
repair_validations(Categorization) do
Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
c = Category.new(name: "Fishing", authors: [Author.first])
assert_not c.save
assert_deprecated { assert_not c.save }
end
end

@ -183,7 +183,7 @@ def test_becomes_includes_errors
assert_not_predicate company, :valid?
original_errors = company.errors
client = company.becomes(Client)
assert_equal original_errors.keys, client.errors.keys
assert_equal assert_deprecated { original_errors.keys }, assert_deprecated { client.errors.keys }
end
def test_becomes_errors_base
@ -197,7 +197,7 @@ def self.name; "Admin::ChildUser"; end
admin.errors.add :token, :invalid
child = admin.becomes(child_class)
assert_equal [:token], child.errors.keys
assert_equal [:token], assert_deprecated { child.errors.keys }
assert_nothing_raised do
child.errors.add :foo, :invalid
end

@ -40,11 +40,11 @@ def replied_topic
COMMON_CASES = [
# [ case, validation_options, generate_message_options]
[ "given no options", {}, {}],
[ "given custom message", { message: "custom" }, { message: "custom" }],
[ "given if condition", { if: lambda { true } }, {}],
[ "given unless condition", { unless: lambda { false } }, {}],
[ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }],
[ "given on condition", { on: [:create, :update] }, {}]
[ "given custom message", { message: "custom" }, { message: "custom" }],
[ "given if condition", { if: lambda { true } }, {}],
[ "given unless condition", { unless: lambda { false } }, {}],
[ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }],
[ "given on condition", { on: [:create, :update] }, {}]
]
COMMON_CASES.each do |name, validation_options, generate_message_options|
@ -53,6 +53,7 @@ def replied_topic
@topic.title = unique_topic.title
assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(value: "unique!")]) do
@topic.valid?
@topic.errors.messages
end
end
end
@ -62,6 +63,7 @@ def replied_topic
Topic.validates_associated :replies, validation_options
assert_called_with(replied_topic.errors, :generate_message, [:replies, :invalid, generate_message_options.merge(value: replied_topic.replies)]) do
replied_topic.save
replied_topic.errors.messages
end
end
end

@ -34,29 +34,29 @@ class WrongReply < Reply
validate :check_author_name_is_secret, on: :special_case
def check_empty_title
errors[:title] << "Empty" unless attribute_present?("title")
errors.add(:title, "Empty") unless attribute_present?("title")
end
def errors_on_empty_content
errors[:content] << "Empty" unless attribute_present?("content")
errors.add(:content, "Empty") unless attribute_present?("content")
end
def check_content_mismatch
if attribute_present?("title") && attribute_present?("content") && content == "Mismatch"
errors[:title] << "is Content Mismatch"
errors.add(:title, "is Content Mismatch")
end
end
def title_is_wrong_create
errors[:title] << "is Wrong Create" if attribute_present?("title") && title == "Wrong Create"
errors.add(:title, "is Wrong Create") if attribute_present?("title") && title == "Wrong Create"
end
def check_wrong_update
errors[:title] << "is Wrong Update" if attribute_present?("title") && title == "Wrong Update"
errors.add(:title, "is Wrong Update") if attribute_present?("title") && title == "Wrong Update"
end
def check_author_name_is_secret
errors[:author_name] << "Invalid" unless author_name == "secret"
errors.add(:author_name, "Invalid") unless author_name == "secret"
end
end

@ -664,7 +664,7 @@ This helper passes the record to a separate class for validation.
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if record.first_name == "Evil"
record.errors[:base] << "This person is evil"
record.errors.add :base, "This person is evil"
end
end
end
@ -692,7 +692,7 @@ validator class as `options`:
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if options[:fields].any?{|field| record.send(field) == "Evil" }
record.errors[:base] << "This person is evil"
record.errors.add :base, "This person is evil"
end
end
end
@ -723,7 +723,7 @@ class GoodnessValidator
def validate
if some_complex_condition_involving_ivars_and_private_methods?
@person.errors[:base] << "This person is evil"
@person.errors.add :base, "This person is evil"
end
end
@ -1004,7 +1004,7 @@ and performs the validation on it. The custom validator is called using the
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.name.starts_with? 'X'
record.errors[:name] << 'Need a name starting with X please!'
record.errors.add :name, "Need a name starting with X please!"
end
end
end
@ -1026,7 +1026,7 @@ instance.
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || "is not an email")
record.errors.add attribute, (options[:message] || "is not an email")
end
end
end
@ -1203,7 +1203,7 @@ You can add error messages that are related to the object's state as a whole, in
```ruby
class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors[:base] << "This person is invalid because ..."
errors.add :base, "This person is invalid because ..."
end
end
```