Multiple Field custom validations in Rails

I just took a spin around the bowls of ActiveRecord Validations trying to create a validator that would check if one, and only one, field of a set of fields is non blank on an ActiveRecord object. While I know I could have put the logic into an overridden `validation` method, I wanted to use the nice declarative syntax of `validate_either_or :attribute_a, :attribute_b` syntax with an eye to reusing it in the future.

I thought what I could do was mimic the existing validates_confirmation_of code and iterate through each attribute looking for one that is not nil. Set a variable, and then if I hit a second nil, add an error. After cycling through the attributes, if none had been found not nil, then also add an exception:

found_non_blank_already = false
validates_each(attr_names, configuration) do |record, attr_name, value|
unless value.blank?
if found_non_blank_already
record.errors.add(:base, configuration[:message])
else
found_non_blank_already = true
end
end
end

if !found_non_blank_already
# currently failing to compile
#record.errors.add(:base, configuration[:message])
end
end

However, what I discovered was that if I have an ActiveRecord object declare like this:

validate_either_or :discount_amount, :discount_percentage

and call it multiple times like this:

promo = Promo.new(:discount_percentage => nil, :discount_amount => 5)
promo.valid?
promo = Promo.new(:discount_percentage => 25, :discount_amount => nil)
promo.valid?

The second time the valid call fails validation because the line `found_non_blank_already = false` executed only once when the validation class is first loaded, and when you call `.valid?`, only the method `validates_each` chunk gets executed! So the second `promo.valid?` starts out thinking it has already hit a non null, and fails on the very first attribute! Clearly, the pattern used by most of the validate_* methods use is meant to validate each field passed in individually in isolation.

I then dug into what `validates_each` does and duplicated a lot of what it did, ending up with:

def self.validate_either_or(*attrs)
configuration = { :message => “one of #{attrs.to_sentence :connector => ‘or’} must be set”, :on => :save }
configuration.update(attrs.pop) if attrs.last.is_a?(Hash)

options = configuration
send(validation_method(options[:on] || :save)) do |record|
found_non_blank_already = false
attrs.each do |attr|
value = record.send(attr)
unless value.blank?

if found_non_blank_already
record.errors.add(:base, configuration[:message])
else
found_non_blank_already = true
end
end
end
if !found_non_blank_already
record.errors.add(:base, configuration[:message])
end
end
end

There still seems to be a lot of black magic going on, for instance, the line `send(validation_method(options[:on] || :save)) do |record|` works, but I can’t figure out how it works, or where the `validation_method` comes from. If you want to test the code, here is what my specification looks like:

it “should allow either a discount_amount OR a discount_percentage, but not both” do
promo = Promo.new(:discount_percentage => 25, :discount_amount => 5)
promo.should_not be_valid
promo.errors.should have(1).error_on(:base)

promo = Promo.new(:discount_percentage => 25, :discount_amount => nil)
promo.should be_valid
promo.errors.should have(0).error_on(:base)
end

Oh, and thanks to Jay Fields for his post on `to_sentence`, it made the error message look nice and pretty!

2 Responses to “Multiple Field custom validations in Rails”

  1. Guy Roberts Says:

    That worked for me, just what I was looking for.

    Thanks Eric, and thanks to Mr. Google too.

  2. How to ensure at least on of two fields are present (Ruby on Rails) at Guy Roberts Says:

    […] this post nearly did it, I had to modify Eric’s code slightly and here is what I came up […]

Leave a Reply