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 = "[email protected]"
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:
- Files in subdirectories can be searched for and loaded automatically.
- Validation methods can be used both as class and instance methods.
symbolize_keys
returns a hash where the keys are all symbols.respond_to?
can be used to determine what methods an object supports.Hash.new { |h,k| h[k] = [] }
is useful for accumulating lists.
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
.
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