Reading Rails - How Validations Are Configured

Where do the validation methods like validates_presence_of come from? How are they configured? We are going to read some of ActiveModel's code and learn a few new things about Ruby along the way.

To follow along, open each library in your editor with qwandry, or just look it up on Github.

Validation Methods

Validations are provided by ActiveModel. Their core functionality is defined in validations.rb, open it up so that you can explore on your own once we are done. The ActiveModel::Validations module extends ActiveSupport::Concern, if you haven't yet, then read about how Concern is implemented.

module Validations
  extend ActiveSupport::Concern

  included do
    #...
    extend  HelperMethods
    include HelperMethods
    #...

The first curious bit is that HelperMethods are both included and extended on the class that includes Validations. What is in HelperMethods? If you search through the rest of validations.rb, you will find no further mention of them. This seems unlikely until you reach the very bottom of the file:

Dir[File.dirname(__FILE__) + "/validations/*.rb"].each { |file| require file }

This is a common, but possibly confusing file loading pattern that requires each of the files in the validations subdirectory. As we saw when investigating migrations, Dir[] uses a shell glob to search for files. __FILE__ is a special variable in ruby that returns the relative path to the current file.

# paths.rb contains:
# puts __FILE__

$ ruby paths.rb
paths.rb

$ cd ..
$ ruby reading_rails/paths.rb
reading_rails/paths.rb

In this case, __FILE__ returns the path to validations.rb. File.dirname strips the file name from the path and just returns the file's directory:

File.dirname("paths.rb") #=> "."
File.dirname("reading_rails/paths.rb") #=> "reading_rails"

Once Rails has the directory, it appends a shell glob that matches ruby files in the validations subdirectory. Each of the matches returned will then be required in turn. Alternatively, one could require each file:

# require_relative requires relative to the current file:
require_relative './validations/absence'
require_relative './validations/acceptance'
#...

If you need to control the load order, then this is preferable. Otherwise, the pattern at the bottom of validation.rb is useful because you can easily add new files without explicitly requiring them.

Take a look at the files in the subdirectory, most of them are defined along the lines of validations/presence.rb:

module ActiveModel
  module Validations

    class PresenceValidator < EachValidator
      #...
    end

    module HelperMethods
      def validates_presence_of(*attr_names)
        validates_with PresenceValidator, _merge_attributes(attr_names)
      end
      #...

Each of the validations defines a method on the HelperMethods module. These methods are then included into ActiveRecord::Base, and thus available to your models.

Let's return to where HelperMethods get included and explore another mystery:

module Validations
  extend ActiveSupport::Concern

  included do
    #...
    extend  HelperMethods
    include HelperMethods
    #...

Calling extend HelperMethods will make those methods available on your class. This supports the typical use of validations:

class Person < ActiveRecord::Base
  # Class level validation:
  validates_presence_of :name
end

What does include HelperMethods achieve though? Some git spelunking shows that instance validations were introduced in Rails 3. The associated test case illustrates that it is possible to declare instance validations:

# validations_test.rb
def test_validations_on_the_instance_level
  auto = Automobile.new

  assert          auto.invalid?
  assert_equal 2, auto.errors.size
  #...
end

# automobile.rb
class Automobile
  include ActiveModel::Validations

  validate :validations

  attr_accessor :make, :model

  def validations
    validates_presence_of :make
    validates_length_of   :model, :within => 2..10
  end
end

Although uncommon, this may be useful when validating a model with various stages or states. For instance could build a signup page that asks for different information at each stage:

class MultiStageSignupForm
  include ActiveModel::Validations

  validate :validations

  attr_accessor :stage, :name, :email, :company, :credit_card

  def initialize
    self.stage = 0
  end

  def validations
    case stage
    when 0 then validates_presence_of :name, :email
    when 1 then validates_presence_of :company
    when 2 then validates_presence_of :credit_card
    end
  end

  def next_stage
    self.stage += 1 if valid?
  end
end

form = MultiStageSignupForm.new
form.valid?               #=> false
form.name  = "Mortimer"
form.email = "mortimer@example.com"
form.valid?               #=> true
form.next_stage           #=> 1
form.valid?               #=> false

Later we can look into validation predicates, which solve a similar issue, but for now we have uncovered where the validation methods come from, and why they are made available as both class and instance methods.

Configuring Validations

Let's investigate how those validators get configured. We saw that the PresenceValidator defined validates_presence_of which calls validates_with:

def validates_presence_of(*attr_names)
  validates_with PresenceValidator, _merge_attributes(attr_names)
end

A reference to the PresenceValidator class is passed in as well as the result of _merge_attributes(attr_names). This helper normalizes validation options, which will eventually be used to configure a validator. It is defined along with validates_with in validations/with.rb.

def _merge_attributes(attr_names)
  options = attr_names.extract_options!.symbolize_keys
  attr_names.flatten!
  options[:attributes] = attr_names
  options
end

When learning about exception handling, we saw that extract_options! pops a hash off the end of an array. Next, the keys of those options are converted to symbols using ActiveSupport's symbolize_keys:

{"a" => 1, :b => 2}.symbolize_keys
#=> {:a=>1, :b=>2}

The remaining values in attr_names are flattened in place, and then stashed in the :attributes key of the options hash.

For example, let's look at what would result if we were validating the presence of a name and email field, unless a person were a guest:

# Declaring:
validates_presence_of :name, :email, :unless => :guest?

# Would call:
_merge_attributes([:name, :email, :unless => :guest?])
#=> {:unless=>:guest?, :attributes=>[:name, :email]}

Now let's investigate how validates_with uses the reference to PresenceValidator and options hash:

def validates_with(*args, &block)
  options = args.extract_options!
  options[:class] = self

  args.each do |klass|
    validator = klass.new(options, &block)

    if validator.respond_to?(:attributes) && !validator.attributes.empty?
      validator.attributes.each do |attribute|
        _validators[attribute.to_sym] << validator
      end
    else
      _validators[nil] << validator
    end

    validate(validator, options)
  end
end

Again, we see extract_options! used to get those options out. The remainder of args are one or more validation classes. Each of those validation classes is then instantiated: klass.new(options, &block). We'll look into the implementations of various validators later.

Validators can either be written to validate a whole record or specific attributes. Most of Rails' validators follow the latter pattern, for instance validates_presence_of validates that each specified attribute is defined. validates_with uses respond_to? to determine whether the validator operates on attributes. respond_to? is useful for determining if an object supports a given interface:

# StringIO behaves like a file backed IO object:
io = StringIO.new("IO Like Object")
io.respond_to? :read    #=> true
io.respond_to? :write   #=> true
io.respond_to? :rewind  #=> true
io.respond_to? :xyz     #=> false

This is a simple example of Ruby's duck typing. Instead of checking if the validator object is a certain class, Rails inspects the capabilities of an object, and acts according.

In our example above, the PresenceValidator responds to attributes, and will go down the first branch. A reference to the validator is added to the _validators hash for each of these attributes. Where does that hash come from? If you go back to the validations module, you will see it is created in the include block:

module Validations
  extend ActiveSupport::Concern

  included do
    #...
    class_attribute :_validators
    self._validators = Hash.new { |h,k| h[k] = [] }
  end

This defines and initializes _validators with a hash using a special block. Hash.new { |h,k| h[k] = [] } is an extremely handy pattern. If Hash is initialized with a block, any time a missing key is accessed, the block will be called instead. The block's arguments are the hash itself, and the missing key. The block { |h,k| h[k] = [] } sets that missing key to an empty array and returns it. This is useful when accumulating values:

# Accumulate into an array:
menu = Hash.new { |h,k| h[k] = [] }
menu[:desserts] << :cake
menu[:desserts] << :ice_cream
menu[:drinks]   << :espresso
menu #=> {:desserts=>[:cake, :ice_cream], :drinks=>[:espresso]}

The Validations module uses this to keep track of which validators should be called for each of your attributes. In our example, _validators would contain two references to our PresenceValidator under :name and :email.

Finally validate(validator, options) is called.

def validate(*args, &block)
  #...
  set_callback(:validate, *args, &block)
end

This is doesn't actually validate anything, instead it adds the validator to a callback queue using set_callback. Since validate just registers callbacks, it can also be used to register custom methods and blocks:

class Person < ActiveRecord::Base
  # Register a validation block callback:
  validate do |person|
    if first_name == last_name.reverse
      errors.add(:base, "Full name cannot be a palindrome")
    end
  end

  # Register a validation method callback:
  validate :must_have_valid_twitter_handle, :if => :handle?

  private
  def must_have_valid_twitter_handle
    if !twitter_api.exists?(handle)
      errors.add(:handle, "must exist")
    end
  end
end

Your validator objects, blocks, and method references are stored in the :validate callback queue, which may be a topic for another day.

Recap

Validation methods like validates_presence_of are defined on ActiveModel::Validations::HelperMethods by each Rails validator in the validations subfolder. When ActiveRecord includes ActiveModel::Validations, those files are required, and the HelperMethods declare included both as class and instance methods. When called, they register themselves on your model using validate_with which adds them to the validation callback chain.

We also saw a few other interesting things:

Now would be a good time to look at how validators like PresenceValidator are defined, or if you have a hardy constitution you could start investigating ActiveSupport::Callbacks.

Enjoyed this article?

Read 8 more articles in Reading Rails.
blog comments powered by Disqus
Monkey Small Crow Small