mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add ActiveModel::Error and NestedError
Add initialize_dup to deep dup. Move proc eval and flexible message position out to Errors, because proc eval is needed for Errors#added? and Errors#delete
This commit is contained in:
parent
db0256cad7
commit
ef68d3e35c
5 changed files with 321 additions and 16 deletions
60
activemodel/lib/active_model/error.rb
Normal file
60
activemodel/lib/active_model/error.rb
Normal file
|
@ -0,0 +1,60 @@
|
|||
# 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, **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
|
||||
end
|
||||
end
|
|
@ -59,9 +59,6 @@ module ActiveModel
|
|||
class Errors
|
||||
include Enumerable
|
||||
|
||||
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
|
||||
MESSAGE_OPTIONS = [:message]
|
||||
|
||||
class << self
|
||||
attr_accessor :i18n_customize_full_message # :nodoc:
|
||||
end
|
||||
|
@ -532,19 +529,6 @@ module ActiveModel
|
|||
end
|
||||
|
||||
private
|
||||
def normalize_message(attribute, message, options)
|
||||
case message
|
||||
when Symbol
|
||||
generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
|
||||
else
|
||||
message
|
||||
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
|
||||
|
|
33
activemodel/lib/active_model/nested_error.rb
Normal file
33
activemodel/lib/active_model/nested_error.rb
Normal file
|
@ -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, :full_message, :message
|
||||
end
|
||||
end
|
174
activemodel/test/cases/error_test.rb
Normal file
174
activemodel/test/cases/error_test.rb
Normal file
|
@ -0,0 +1,174 @@
|
|||
# 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 :invalid, error.type
|
||||
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
|
||||
end
|
54
activemodel/test/cases/nested_error_test.rb
Normal file
54
activemodel/test/cases/nested_error_test.rb
Normal file
|
@ -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
|
Loading…
Reference in a new issue