Reading Rails - Errors and Validators

Last time, we saw what happens in Rails when you declare validations, now let's see what happens when valid? is called.

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

Errors

If you try to save a record, Rails will validate it by calling valid?. This kicks off all the validation callbacks that have been configured on the model. Let's take a look:

def valid?(context = nil)
  #...
  errors.clear
  run_validations!
  #...
end

First, you will notice that any errors are cleared out, then it does what it says, and validations are run. Where does the errors object come from though, and what is it? errors is a method defined in ActiveModel::Validations which creates a special object to track validation errors:

def errors
  @errors ||= Errors.new(self)
end

The Errors class is defined in errors.rb. It is initialized with a reference to the record being validated, and stores a hash of messages where the keys are model attributes, and the values are an array of validation issues for that attribute:

def initialize(base)
  @base     = base
  @messages = {}
end

Reading through its source, you will see that like an array or hash, Errors includes Enumerable, meaning that you can call methods like each, map, and any? on it. To support Enumerable, a class must implement the method each, and yield once for ever item in its collection.

include Enumerable

#...
def each
  messages.each_key do |attribute|
    self[attribute].each { |error| yield attribute, error }
  end
end

Errors implements each by iterating over the attributes in messages, and yielding the attribute along with each error message.

You may have noticed that while Errors is not a Hash, when calling self[attribute], it behaves like one. This is achieved by defining the [] and []= methods which in turn call get and set:

def [](attribute)
  get(attribute.to_sym) || set(attribute.to_sym, [])
end

def []=(attribute, error)
  self[attribute] << error
end

def get(key)
  messages[key]
end

def set(key, value)
  messages[key] = value
end

[] first tries to access a value in the messages hash, but if none exists, it sets the value to an empty array. This allows []= to always append and error since it knows the value will always be an array. Error wraps a hash, and provides specialized semantics that make sense for tracking validation errors. This pattern can work well when it behaves the way you would expect, but notice the subtle difference between how []= and set will work. This is one reason why we read source.

The pattern of wrapping a hash continues with methods like clear, keys, and values. If you deal directly with the Errors object though, read it carefully, some methods such as size and count may sound the same, but have subtle differences.

Validators

Now that we know a little more about Errors, let's look at run_validations!:

def run_validations!
  run_callbacks :validate
  errors.empty?
end

This runs a callback queue named :validate. In the previous validations article, we saw that there are three possible types of callbacks: validators such as PresenceValidator, method names such as :must_have_valid_twitter_handle, and blocks. The method names and blocks are straightforward. Each of these are just expected to call Errors#add with any errors. The validator objects are a bit more interesting.

When plain Ruby objects are encountered in a Rails callback queue, the method named by the queue is called with a reference to the object the queue is on. So any objects in the :validate queue are called with validate(record).

ActiveModel::Validator provides a base class for implementing this pattern in validator.rb. By itself, Validator does not define much, but it does document the validate interface that should be implemented:

# Override this method in subclasses with validation logic, adding errors
# to the records +errors+ array where necessary.
def validate(record)
  raise NotImplementedError, "Subclasses must implement a validate(record) method."
end

Although it may seem pointless to implement a method that always throws an exception, this is a useful pattern in Ruby. By defining this method, Rails documents the method's expected behavior. By raising an exception instead of leaving the method blank, Rails communicates that this method is mandatory.

Since Validator itself obviously does not validate records, let's look at EachValidator, which inherits from Validator. EachValidator implements validate by iterating over an array of attributes, checking each one individually:

def validate(record)
  attributes.each do |attribute|
    value = record.read_attribute_for_validation(attribute)
    next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
    validate_each(record, attribute, value)
  end
end

Here we start to see why Rails uses validator objects internally. This implementation of validate handles fetching each attribute, and then checks two common validation options, :allow_nil and :allow_blank. Assuming those two checks pass, then validate_each is called. Let's see how that works:

# Override this method in subclasses with the validation logic, adding
# errors to the records +errors+ array where necessary.
def validate_each(record, attribute, value)
  raise NotImplementedError, "Subclasses must implement a validate_each(record, attribute, value) method"
end

Surprise! The EachValidator is another abstract class that implements validate, but asks you to implement validate_each. Subclasses can now focus on whether a single attribute is valid, and not worry about common scenarios like blank values. Let's look at a class that actually implements this, PresenceValidator:

class PresenceValidator < EachValidator
  def validate_each(record, attr_name, value)
    record.errors.add(attr_name, :blank, options) if value.blank?
  end
end

That's the entire class. It checks if the value passed in is missing. If so it records an error in the record's Errors object. Since Validator and EachValidator took care of most of the grunt work, PresenceValidator can focus on its one responsibility, validating the presence of an attribute.

Once all the validation callbacks has been called, run_validations! will return errors.empty?, which will be true if none of the validators or callbacks added a message to errors.

Recap

We saw that when valid? is called, each callback gets called. Objects in the callback queue must implement validate, or if it inherits from EachValidator, then validate_each. These validators then add messages to a record's errors.

We also came across a few other interesting points:

If you want to read more about how errors and validators work, investigate Errors#add and take some time to figure out how NumericalityValidator works.

Enjoyed this article?

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