Validating Mutually-Exclusive Attributes 5

Posted by Bob Showalter Sun, 16 Sep 2007 19:47:00 GMT

Let’s say you have a model that holds a location, with a “state” and a “country”. Suppose that you want the user to enter either a state or a country, but not both.

Your first instinct might be to do something like this:

validates_presence_of :state, :if => lambda {|row| row.country.blank?}
validates_presence_of :country, :if => lambda {|row| row.state.blank?}

That’s not quite right however. You won’t get an error if both a state and a country are entered. Furthermore, if both attributes are entered, which one should be considered “wrong”?

Here’s where the generic validate method can help. Rather than validating a specific attribute, validate works on the whole row. If you need to add error messages you use the ActiveRecord::Errors#add() and #add_to_base() methods.

The validation for our state/country example looks like this:

validate do |row|
  if row.state.blank? ^ row.country.blank?
    row.errors.add_to_base 'Either a state or a country is required (but not both)'
  end
end

Note how validate takes a block. The row being saved is passed to the block. The caret (^) is Ruby’s “exclusive-or” operator. It returns true only if exactly one of its operands is true, which fits our requirements perfectly.

The add_to_base method adds an error message to the list of errors for the row, but the message is not specific to any single attribute. In this case, the error relates to two attributes, so I chose to use add_to_base.