Validation contexts in ActiveRecord
Validation contexts are a little-appreciated but immensely practical feature of Ruby on Rails’ object-relational mapper, ActiveRecord. I cannot count the number of times I have seen hacks around a problem for which a validation context would have been a perfect fit simply because this feature lives a bit under the radar and isn’t in every Rails developer’s toolbox.
What is a validation context, precisely? It is a way to constrain a model validation to a particular usage context for a record. This is similar to what you might achieve with something like state_machine, but far more lightweight.
Let’s say we have an application where we want to dispense gift cards to select users. Administrators can manage an inventory of gift cards and then invite users to claim them by filling out a form at a tokenized link.
A schema for such a feature might look as follows:
Different validation rules apply in different contexts. In the admin-editing context (which we’ll consider the default context) the record is valid so long as it has a code. In the user-claiming scenario, the gift card is only valid if it has been assigned a name, email and the user has supplied a valid confirmation of its token.
We can tie these contexts to model validations by supplying the :on
option to our validates
and validate
calls. Our gift card model might therefore look as follows:
The beauty of validation contexts for use cases like we have here is how declarative and readable they are and how foolproof they become once we’re further up in the stack. To drive this point home, let’s have a look at how skinny the controller and UI layers we build around this model to handle the full user flow are.
This minimal controller implementation can handle our entire flow of presenting a claim form, processing and validating user input, delivering an email and presenting the user a success page when they are done. The minimal form implementation below is enough to take all the requisite input, as well as keep the token that the user came into the flow with in scope (note: this is using simple_form):
It is worth noting that as of Rails 4.1, the on
option to validate
/validates
can now take multiple contexts. This is welcome flexibility and in my opinion even further reduces the number of real-world use cases for heavyweight solutions like state_machine
.