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:
lulalala 2018-03-15 16:13:18 +08:00
parent db0256cad7
commit ef68d3e35c
5 changed files with 321 additions and 16 deletions

View 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

View File

@ -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

View 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

View 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

View 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