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:
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 ValidatesExistence
def validates_existence_of(*attr_names)
# validator logic goes here
end
end
end
And this module was exposed to ActiveRecord::Base
by doing
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:
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.
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.
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.
# 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 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
.
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.
3 Comments
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.
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