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 = falsevalidates_each(attr_names, configuration) do |record, attr_name, value|unless value.blank?if found_non_blank_alreadyrecord.errors.add(:base, configuration[:message])elsefound_non_blank_already = trueendendendif !found_non_blank_already# currently failing to compile#record.errors.add(:base, configuration[:message])endend
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 = configurationsend(validation_method(options[:on] || :save)) do |record|found_non_blank_already = falseattrs.each do |attr|value = record.send(attr)unless value.blank?if found_non_blank_alreadyrecord.errors.add(:base, configuration[:message])elsefound_non_blank_already = trueendendendif !found_non_blank_alreadyrecord.errors.add(:base, configuration[:message])endendend
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 cant 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" dopromo = Promo.new(:discount_percentage => 25, :discount_amount => 5)promo.should_not be_validpromo.errors.should have(1).error_on(:base)promo = Promo.new(:discount_percentage => 25, :discount_amount => nil)promo.should be_validpromo.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!