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:
Errors
wraps aHash
and provides a specialized interface.- If you define
each
, you can include all theEnumerable
methods. []
and[]=
are just normal methods that define index operations.- Raising
NotImplementedError
is a common pattern for declaring an expected interface.
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.
More articles in this series
- Reading Rails - HTTP DELETEs With a Link
- Reading Rails - Time Travel
- Reading Rails - TimeWithZone
- Reading Rails - How Does MessageEncryptor Work?
- Reading Rails - How Does MessageVerifier Work?
- Reading Rails - How Do Batched Queries Work?
- Reading Rails - The Adapter Pattern
- Reading Rails - Errors and Validators
- Reading Rails - How Validations Are Configured
- Reading Rails - Concern
- Reading Rails - More Migrations
- Reading Rails - Migrations
- Reading Rails - Attribute Methods
- Reading Rails - Change Tracking
- Reading Rails - Handling Exceptions