For a few days I worked on custom validators in Rails. First what are validators? When you want to check some data which comes to your application, you use validators. For example:
- if email has specific format,
- if number is odd,
- or if you simply want to check if name is required
for all of this we use validators.
Rails has many different validators already in side. Check the documentation. But sometimes you want to do more. In my case I need validator for black list of words for string field. I know I can use build-in validator. But I need much more customization:
- The list of excluded words was very long.
- I didn’t want to have this words in my Ruby file.
- I always want to add new wards without changing my application.
So I decide to do my own validator.
I look to documentation and I find this. I choosed ActiveModel::EachValidator
where I have access to whole record, specific attribute to check and the value of this attribute. This was all I needed. To do my custom validator I must only write one method: validate_each
. It look like this:
# app/validators/blacklist_validator.rb
# Validate list of words that can not be use in specifig field
class BlacklistValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors.add(attribute, :on_blacklist) if blacklist.include? value
end
private
def blacklist
File.readlines(Rails.root.join('config', 'blacklist.txt')).map(&:strip)
end
end
How this works?
I add error :on_blacklist
to record when value is located on black list. I put my black list – blacklist.txt
– in Rails config
directory. To use this validator we put something like this in our model:
validates :name, blacklist: true
Remember convention:
When your valdator class is named BlacklistValidator
then in your model you use blacklist: true
parameter.
This was perfect for me, but what about put some custom parameters to validator? Something like this:
validates :age, numericality: { greater_than: 18 }
This is not a problem. In custom validators you have available something like options
, where you have access to all custom parameters. When you call options
for our example you see:
=> { greater_than: 18 }
I use this for array validator, where I check size of array:
# app/validators/array_lenght_validator.rb
# Validate if array is not too short
class ArrayLenghtValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return unless options.key?(:minimum)
array_size = (value.try(:size) || 0)
minimum = options[:minimum]
return if array_size >= minimum
record.errors.add(attribute, :too_short_array, count: minimum)
end
end
What happened here?
I check if :minimum
parameter is set. I count number of elements in array. If array_size
is lower then minimum
size, I add error :too_short_array
to record.
I can call this validator like this:
validates :array, array_lenght: { minimum: 1 }
It is one more think to say. How put translation to our validators in locales
?
We use Rails Convention. This is one but not only way to do this (all convention is explain in guides.rubyonrails.org/i18n.html#error-message-scopes:
en:
activerecord:
errors:
models:
our_model_name:
attributes:
name:
on_blacklist: is on words blacklist
array:
too_short_array:
one: is too short (minimum is 1 item)
other: is too short (minimum is %{count} items)
In our_model_name:
key we put model name. For example when we have model User
, we have key user:
. Next we put names of our validated fields:
name:
field (form black list validation),array:
field (from array length validation).
Then we put names of our errors: on_blacklist:
and too_short_array:
. For on_blacklist:
this is it. But too_short_array:
error had count
parameter. Rails Internalization can recognized if it singular or plural version of count
. And serve one:
or other:
translation. Last thing: In other:
translation we put our count parameter thought %{}
. Then Rails know where put count
value.
This is it. I hope this was useful for you. Please live your comments, questions or any suggestions below. To next time! Bye!
Need help?
If you're looking for a Ruby developer with over a decade of experience, don't hesitate to contact me.
I have experience in a variety of domains, with a focus on short user feedback loops and teamwork. I can help you build a great product.