Dup options in validates_with

Some validators, such as validators that inherit from `EachValidator`,
mutate the options they receive.  This can cause problems when passing
multiple validators and options to `validates_with`.  This can also be a
problem if a validator deletes standard options such as `:if` and `:on`,
because the validation callback would then not receive them.

This commit modifies `validates_with` to `dup` options before passing
them to validators, thus preventing these issues.

Fixes #44460.
Closes #44476.

Co-authored-by: Dieter Späth <dieter.spaeth@lanes-planes.com>
This commit is contained in:
Jonathan Hefner 2022-05-26 14:50:48 -05:00
parent 8a610a53a3
commit 5389c56292
2 changed files with 28 additions and 2 deletions

View File

@ -83,7 +83,7 @@ module ActiveModel
options[:class] = self
args.each do |klass|
validator = klass.new(options, &block)
validator = klass.new(options.dup, &block)
if validator.respond_to?(:attributes) && !validator.attributes.empty?
validator.attributes.each do |attribute|
@ -139,7 +139,7 @@ module ActiveModel
options[:class] = self.class
args.each do |klass|
validator = klass.new(options, &block)
validator = klass.new(options.dup, &block)
validator.validate(self)
end
end

View File

@ -29,6 +29,13 @@ class ValidatesWithTest < ActiveModel::TestCase
end
end
class ValidatorThatClearsOptions < ValidatorThatDoesNotAddErrors
def initialize(options)
super
options.clear
end
end
class ValidatorThatValidatesOptions < ActiveModel::Validator
def validate(record)
if options[:field] == :first_name
@ -89,6 +96,25 @@ class ValidatesWithTest < ActiveModel::TestCase
assert_includes topic.errors[:base], ERROR_MESSAGE
end
test "validates_with preserves standard options" do
Topic.validates_with(ValidatorThatClearsOptions, ValidatorThatAddsErrors, on: :specific_context)
topic = Topic.new
assert topic.invalid?(:specific_context), "validation should work"
assert topic.valid?, "Standard options should be preserved"
end
test "validates_with preserves validator options" do
Topic.validates_with(ValidatorThatClearsOptions, ValidatorThatValidatesOptions, field: :first_name)
topic = Topic.new
assert topic.invalid?, "Validator options should be preserved"
end
test "instance validates_with method preserves validator options" do
topic = Topic.new
topic.validates_with(ValidatorThatClearsOptions, ValidatorThatValidatesOptions, field: :first_name)
assert_includes topic.errors[:base], ERROR_MESSAGE, "Validator options should be preserved"
end
test "validates_with each validator" do
Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content])
topic = Topic.new title: "Title", content: "Content"