Search code examples
ruby-on-railsvalidationreadability

Clarifying a custom Rails 3.0 Validation with methods


I've created a custom validator in Rails 3.0 which validates whether a combination of columns is unique within a table. The entire code of the validation is:

class UniqueInProjectValidator < ActiveModel::EachValidator

  def validate_each(object, attribute, value)
    unless object.class.where("project_id = ? AND #{attribute} = ?", object.project_id, value).empty?
      if object.new_record?
        object.errors[attribute] << (options[:message] || "must be unique in each project")
      else
        orig_rec = object.class.find(object.id)
        if value != orig_rec.method(attribute).call || object.project_id != orig_rec.project_id
          object.errors[attribute] << (options[:message] || "must be unique in each project")
        end
      end
    end

end

Note that it is not easy to recognize what the if statements do, so I was hoping to be able to replace the unless conditional with a def attribute_and_project_exist? method and the second if statement with a def attribute_or_project_changed? method. However when creating those methods, the arguments from validates_each do not pass because of encapsulation.

Now the question: Is there a way to somehow cleanly allow those variables to be accessed by my two newly created methods as one can do with column names in a model, or am I stuck with the options of either passing each argument again or leaving the hard to read conditional statements?

Thanks in advance!


Solution

  • I suppose you could clean it up a bit with one variable, one lambda, and one "return as soon as possible":

    def validate_each(object, attribute, value)
      # If there is no duplication then bail out right away as
      # there is nothing to check. This reduces your nesting by
      # one level. Using a variable here helps to make your
      # intention clear.
      attribute_and_project_exists = object.class.where("project_id = ? AND #{attribute} = ?", object.project_id, value).empty?
      return unless attribute_and_project_exists
    
      # This lambda wraps up your second chunk of ugly if-ness and saves
      # you from computing the result unless you have to.
      attribute_or_project_changed = lambda do
        orig_rec = object.class.find(object.id)
        value != orig_rec.method(attribute).call || object.project_id != orig_rec.project_id
      end
    
      # Note that || short-circuits so the lambda will only be 
      # called if you have an existing record.
      if object.new_record? || attribute_or_project_changed.call
        object.errors[attribute] << (options[:message] || "must be unique in each project")
      end
    end
    

    I don't know how much better that is than your original but the logic and control flow is a lot clearer to me due to the nicer chunking.