Building Rails 3 custom validators

Actually this post is not so much about just building custom validators for Rails 3 but more like a in-depth introduction to how validations work in the old and new versions of rails.
And in addition to that I’m going to implement Rails 3 compatibility for the validates_existence gem, which we maintain and use extensively in our projects.

The “old” stuff

First I’m going to give you a quick overview of the gems current functionality, plus basic explanation of how it’s implemented.

The gem itself contains a single validation – validates_existence_of, which validates belongs_to relations by making a query to the database and checking if the record actually exists.
It supports regular relations (i.e where you know the class of the related class) and polymorphic relations, validations on both the association name and the association foreign key.

Quick example of the validation in action:

class Document < ActiveRecord::Base
  belongs_to :entity, :polymorphic => true

  validates_existence_of :entity
  validates_existence_of :entity_id
  validates_existence_of :entity, :allow_nil => true
end

And the validator itself consists of one file (I’m not going to paste the whole contents as it is pretty long, you can see it here if you want to).

module Perfectline
  module ValidatesExistence

    def validates_existence_of(*attr_names)
      # validator logic goes here
    end

  end
end

And this module was exposed to ActiveRecord::Base by doing

ActiveRecord::Base.extend Perfectline::ValidatesExistence

The Rails 3 Way

In Rails 2.x, validations are kept inside ActiveRecord::Validations and this module is then included inside ActiveRecord::Base.

One of the most common issues with this is that You are unable to use only the validations part in your regular ruby class, or anything not inheriting from ActiveRecord::Base at all for that matter. This is due to the nature of how the needed modules for validations are mixed into the base module (Errors, Validations etc)

Rails 3 introduces the concept of ActiveModel, containing features previously coupled tightly to ActiveRecord as decoupled and reusable modules. For example modules like Serialization, Errors, Dirty, Callbacks, Validations etc all provide specific chunks of functionality, wrapped into separate small units which can then easily be included and used in your code.

You still define your models as descendants of ActiveRecord::Base though, because including every single module manually would be cumbersome. ActiveRecord wraps a lot of the ActiveModel modules with its own Rails specific glue code and adding some sugar on top of everything, making your life a whole lot easier.

This applies to validations as well – they are usable inside your model class just as you have used to.

How do validations work?

In a nutshell – ActiveRecord comes with ActiveRecord::Validations module, which takes care of triggering validations when ActiveRecord::Base.save is called. It also includes the ActiveModel::Validations module – this stores the validations and errors attached to attributes and handles the :if and :on options.

Both modules also load the actual validators – ActiveModel::Validations loads the generic bundled validators, while ActiveRecord::Validations loads database specific validators like validates_uniqueness_of and validates_associated.

Example of differences in the old and new validation methods:

# The old way
validates_presence_of :name, :on => :create
validates_presence_of :friends, :allow_nil => true
validates_length_of :name, :maximum => 200, :minimum => 100, :if => :foo?

# The new way
validates :name, :presence => {:if => :foo?}, :length => {:maximum => 200, :minimum => 100}
validates :friends, :presence => {:on => :create}

Every validation extends the base class of ActiveModel::Validator (which handles validating the whole record) or ActiveModel::EachValidator (which handles validating the attribute on the record). The latter module also takes care of :allow_nil and :allow_blank options for you, so the validations are not even invoked when the value doesn’t match the given conditions.

I’m going to give a quick example of how these two base classes differ.

# define my validator
class MyValidator < ActiveModel::Validator
  # implement the method where the validation logic must reside
  def validate(record)
    # do my validations on the record and add errors if necessary
    record.errors[:base] << "This is some custom error message"
    record.errors[:first_name] << "This is some complex validation"
  end
end

class Person
  # include my validator and validate the record
  include ActiveModel::MyValidator
  validates_with MyValidator
end

# This method can be used to validate the whole record
# This method below, on the other hand, lets you validate one attribute

class TitleValidator < ActiveModel::EachValidator
  # implement the method called during validation
  def validate_each(record, attribute, value)
    record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value)
  end
end

class Person
  include ActiveModel::Validations
  # validate the :title attribute with PresenceValidator and TitleValidator
  validates :title, :presence => true, :title => true
end

You might have noticed that in the second example, the TitleValidator is “magically” invoked upon the :title => true option in the validates :title statement.

This is another really cool feature that ActiveModel::Validator introduces – all the options passed to validates method stripped of reserved keys (:if, :unless, :on, :allow_blank, :allow_nil) and the remaining keys are resolved to class names with a const_get("#{key.to_s.camelize}Validator") call.

In simple terms it’s a hash key to class name lookup, where everything before *Validator in you class name will be the name of your validation method as an option key.

# this is resolved to BarValidator class
validates :name, :bar => true

# this is resolved to FancyValidationWithPoniesValidator class
validates :name, :fancy_validation_with_ponies => true

# and this is what is called internally after resolving the class name
validates_with(FancyValidationWithPoniesValidator, options_and_attributes)

One more thing: if the value of a validation method key is a hash, it will be passed as options to the validator.

validates :foo, :bar => { :amount => 100, :if => :has_bar?, :on => :save, :allow_nil => true }

# this is what BarValidator is passed as options
options => { :amount => 100, :allow_nil => true }

What have we learned so far?

We have briefly covered how validations work in Rails 2. We have covered Rails 3 validators and their inner works.
We have explored the possibilities and conventions for building our own custom validators.

Now we just have to put that knowledge into practice, by implementing a custom validator – in this case refactoring th validates_existence gem to support Rails 3.

As the validates_existence_of validation “works” on a single field rather than the whole record, it’s clear that I’m better off extending ActiveModel::EachValidator and implementing my logic inside validate_each(record, attribute, value) method.

The validator currently handles the :allow_nil option manually for Rails 2 – this is no longer needed, as Rails 3 takes care of that by itself.
Other than that, there isn’t much difference in the actual validation code, except for some really minor changes in the association reflections API.

Convertify!?

To keep things consistent, i will use the same module namespace for both versions of the gem.
The difference is that the Perfectline::ValidatesExistence module for Rails 3 will have two submodules – InstanceMethods and ClassMethods.
This is required to be able to expose separate parts of the logic to separate modules in ActiveModel::Validations.

module Perfectline
  module ValidatesExistence

    module InstanceMethods
      class ExistenceValidator < ActiveModel::EachValidator
        # validation logic here
      end
    end

    module ClassMethods
      def validates_existence_of(*attr_names)
        validates_with ExistenceValidator, _merge_attributes(attr_names)
      end
    end

  end
end

Full source of the file can be seen here.

The gem structure obviously needs some changes to consolidate both versions of the validation for different versions of Rails.

So I will just create two separate subfolders with appropriate names, move the version specific code in there and refactor the validates_existence.rb to act as an autoload helper, including the relevant code according to Rails::VERSION::MAJOR.

if Rails::VERSION::MAJOR >= 3
  require "rails3/validator"

  # include InstanceMethods to expose the ExistenceValidator class to ActiveModel::Validations
  ActiveModel::Validations.__send__(:include, Perfectline::ValidatesExistence::InstanceMethods)

  # extend the ClassMethods to expose the validates_presence_of method as a class level method of ActiveModel::Validations
  ActiveModel::Validations.__send__(:extend, Perfectline::ValidatesExistence::ClassMethods)
else
  require "rails2/validator"
  ActiveRecord::Base.__send__(:extend, Perfectline::ValidatesExistence)
end

And that’s it! Now it’s just a matter of updating the documentation, bumping the gem version, building the gem and pushing it to http://rubygems.org.
The updated (and improved) version of validates_existence should be available at RubyGems in the coming days.

if you have any questions, suggestions, ideas or you just want to discuss the topic of validators and validations, leave a comment below.

Tanel Suurhans
Tanel is an experienced Software Engineer with strong background in variety of technologies. He is extremely passionate about creating high quality software and constantly explores new technologies.

3 Comments

  • Mattias Arro

    Thanks for this article. Is there any way to use in-model validation definitions like the good old def validate method in pre-rails3? Sometimes defining a new class and including a module is a bit overweight.

    • Tanel Suurhans

      Sure thing, just use the “validate :method_name” syntax, where :method_name points to an instance method in the same model. Note that it’s good practice to define these methods as private.

  • Алексей

    Шикарная статейка, наконец-то просветление наступает ро данной темя. from Russia with respect

Liked this post?

There’s more where that came from. Follow us on Facebook, Twitter or subscribe to our RSS feed to get all the latest posts immediately.