Ruby on Rails has amazing docs on validations. In this blog post I want to cover custom validations but I will also give an intro to the standard Active Record validations that Rails gives us in its infinite kindness.
Read about:
- ActiveRecord validations
- Custom Validations:
Active Record validations
There's already so much Active Record is doing for you under the hood. Take for instance this method that checks whether an instance we are trying to create has all the attributes needed:
class Person < ApplicationRecord
validates :name, presence: true
end
Person.create(name: "Frida Kahlo").valid? # => true
Person.create(name: nil).valid? # => false
You can go fancy and require that name
is unique, of particular length, contains (or does not) certain characters, etc. They even have an email validation that uses regex. You can also specify the message that will come if any of the validations is violated. I love the docs on validations so instead of dwelling more on them, I'll just move onto writing custom validators.
Custom validations: methods
Now, what would happen if what you want to validate is not covered by Rails safety umbrella? Say you want all your users to have at least 3 letters "o" in their username.
1. Validate and custom method
When you want to implement custom validations, you'll use validate
instead of validates
and add the method name that you declare below it. Here's a user class with validations and pseudocoded method — note that I made the custom validation a private method:
class User < ApplicationRecord
validates_presence_of :username
validate :has_three_os
private
def has_three_os
# take the username attribute
# check if it contains three o's
# if not, show an error message: "your username needs to contain three o's"
end
end
2. Write the method
So how are we going to tap into user's username if we are not passing in an argument? Well, when validate
calls on the has_three_os
method, it passes a user object to be created so we can just tap into that by calling self
.
def has_three_o
number = self.username.scan(/o/).length
if number < 3
self.errors.add(:username, "needs to have three o's and #{self.username} has only #{number}!")
end
end
Now if we try to create a user with the username of "Frida Kahlo", we'll get:
ActiveRecord::RecordInvalid (Validation failed: Username needs to have three o's and Frida Kahlo has only 1!)
Sorry, Frida!
Custom validations: classes
Now, imagine that we also have a Monkey model and we want the monkey's name to also have 3 o's. We'd need to copy-paste the same method and that's not very DRY. How about we place it someplace else? What you may find exciting to learn is that you can build your own Rails validator!
1. New folder, new file, new class
For the organizational purposes, create a new folder in your app
folder: app\validators
. Inside, create a new file, the name of which should describe what this code does. I names mine HasThreeOsValidator.rb
. Now, inside write a class that inherits from ActiveModel::EachValidator
:
class HasThreeOsValidator < ActiveModel::EachValidator
end
2. Use the validation in the model
Now, I will use this validator in the User model:
class User < ApplicationRecord
validates :username, has_three_os: true
end
As you see, you use it just like any other standard Rails validator: first call the validates
method, then specifying the attribute that needs to be validated and then calling the validator with the desired value.
3. Inspect validate_each
method
Now, let's get back to our validator. In Rails validators have access to validate_each
method that accepts three arguments: record
, attribute
and value
. Let's put a byebug
inside the method to see what these arguments are:
class HasThreeOsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
byebug
end
end
Now, in our terminal, let's run rails c
and in this console, let's create a new User instance: User.create!(username: "Frida", password: "Kahlo2020")
:
Now, let's call each argument:
record
# => #<User id: nil, username: "Frida", password: "Kahlo2020", created_at: nil, updated_at: nil>
record tells us about the instance in which the validator was used. This can be helpful to e.g. check if the username is the same as password.
attribute
# => :username
attribute indicates on which attribute this validator is called.
value
# => "Frida"
value tells us the user input for this attribute.
4. Write out your method
Now that we know what each parameter is doing, we can finally write our method. Here we will use the value
parameter to check the username:
class HasThreeOsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
number = value.scan(/o/).length
end
end
What happens if the number is less than 3? Such a situation should result in an error. For that, let's use ActiveRecord Error method add
. This method accepts three arguments:
-
attribute
, which specifies what the scope of the error should be — it can be either the attribute itself, or:base
, which would just add an error to the whole record; -
message
, is the message that will be shown for violating this validation; if the message is non-existent, it will just default to "is invalid"; message can either be a symbol or a hash — in majority of cases it's going to be a symbol with:blank
key to have the message display nicely (see below) -
options
, which I won't cover here.
Let's add our error and see what happens:
class HasThreeOsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
number = value.scan(/o/).length
if number < 3
record.errors.add(attribute, :blank, message: "needs to contain 3 o's")
end
end
end
And this is the error we will get now:
ActiveRecord::RecordInvalid (Validation failed: Username needs to contain 3 o's)
Now our validator can be use across different classes.
Cover picture by Luna Sea. Licensed under CC-BY.