Building Custom Rails Attribute Validators
The validation that ships with Rails is useful, albeit generic. It leaves us to construct our own validators as dictated by our domains. Most of our domains share some common data types like emails or phone numbers. Individually they might require SSNs, SINs, credit card numbers, URIs, or any other of a million types of data. The good news is that Rails gives us the tools necessary to build our own validators.
We’re going to validate user-added HTML (we can debate the merits of letting users enter HTML at a later time). In particular we want to know if the HTML has text in it. Is there something there to see? For example, if a user inputs
<p></p> we’re going to fail it.
Basically we want the ability to check for
presence like we would with regular text. If we have an email model with a message body, the validation would look like this:
Let’s start with the basic structure of a validator. It inherits from
ActiveModel::EachValidator and defines a
validate_each instance method. The method is provided with a record (an instance of the model), the name of the attribute, and the value being set.
The class also contains an
options attribute which represents everything that was passed to
:html in the
validates call. Let’s fill in
validate_each so it checks for the presence of HTML.
The guts of
validate_each are straight forward. We check to see if the
presence key is used and then check to see if our HTML is blank. If the HTML is blank then we add an error to the record using the standard Rails blank message.
Now we need to define
blank?. We’ll use Nokogiri to grab text blocks and see if we find any visible text (it could still be hidden by styling, but let’s keep it simple). Nokogiri is fast, but even so, let’s make sure there’s something there before we start parsing.
We’ve implemented our check and things are going well. What if we don’t like the default message? Sometimes it’s helpful to pass in a message tailored to the situation.
Since the error message code is about to get a little more complicated, let’s pull it out of
validate_each. Now we check the
options hash for a
message key and return the custom message or the default error message.
Putting all of that together we get:
Like all code in our application we want to test our validator. Let’s go over a few example tests. We’ll check that
<p> </p> fails and
<p>A</p> passes. We’ll also check that the default message is right and make sure custom messages work.
You might have noticed that I left
test_model empty. We need to build a class that we can use to test our validator. Actually, we’re going to need two so we can test one with the
:message option set and one without it set. What if we add new options and we need to test those? Hard coding classes for each test feels cumbersome.
What we need is a way to easily build classes with different validation options. To do this we’ll add a method to our spec file that takes an
options hash and returns a custom built class. The class will quack like a non-persisted
ActiveRecord::Base. Anonymous classes don’t have names and Rails is going to expect the class to respond to
name. Fixing this is as easy as adding a class method
Odds are good that you’re going to build more than one validator so, you’ll want to extract the generic parts of your class. It’ll also help to expose the parts of the custom class that are important to the tests.
Now we go back and fill in
test_model using our new
Validate All the Things
Validators are a critical part of any application. They provide a way to ensure the accuracy and consistency of your data. Using the same validator across models stops your team from littering your application with different ideas about what constitutes a valid phone number. Validators are worth every bit of effort you put into them. Anyone who’s written migrations to fix bad data can attest to that.
Aaron Lasseigne strives to design comprehensive solutions that provide structural stability and a positive customer experience. He is a graduate of The Ohio State University with a degree in Computer and Information Science. Before coming to OrgSync, Aaron worked as a Developer for Monster Worldwide in their FastWeb division and then as a Manager of Application Development at HRsmart Inc. Outside of work Aaron likes to travel, read, cook, and is an avid fan of Chicago Bears and Buckeye football.
comments powered by Disqus