Validations
Last updated
Last updated
One of the original Rails features that drew many to the framework was its powerful business rule validation system for Active Record. Controller actions that call save
and use the return value to drive application behaviour are an enduring part of what makes Rails...Rails.
However, there is some rigidity in the approach Rails uses to test for model validity that frustrate the implementation of reactive applications. Even though Active Model has support for form-level error messages, the standard request-based approach provided by Action Dispatch makes no distinction between field-level and form-level business rules. It's assumed that validation occurs when a form has been completed and the user submits it by clicking a button, 2004 style.
Let's say that you want to incrementally build a model by pushing updates every time the user changes a form input value. Perhaps you don't even have a traditional "submit" button! If your Active Record model has multiple mandatory attributes, it's not currently possible to do reactive validation on a per-attribute basis. Pushing an interim update to Mandatory Attribute A would immediately raise a presence validation error from Mandatory Attribute B.
All Futures makes incremental model creation and validation easy.
Active Record models cannot be saved unless they are in a valid state. save
operations (including methods like update
) call valid?
and if any validations fail, then save
returns false
. ActiveModel::Error
objects are added to the errors
collection of the model.
What's interesting about the Active Record's design is that valid?
is not just some getter that queries an internal state variable; it's actually the method that performs the validations process! If you inspect your model instance before valid?
is called, you'll notice that the errors
collection will be empty even if the model has values that would fail validation.
All Futures does not call valid?
before saving. Instead, valid?
is a tool that you can call when you need it, and it is not tied to the persistance layer in any way.
validate
is aliased to valid?
and might be grammatically satisfying in some contexts.
The All Futures validations are really just Active Entity validations, which means that Jun Jiang did all of the hard work. I am including a slightly edited copy of his instructions here for convenience.
All Futures (and Active Entity) support many 3rd-party Active Model extensions, such as adzap/validates_timeliness.
Defining All Futures validations works just like it does in an Active Record model:
Many Active Record validations are directly supported:
Validation options are supported too:
You can use strict mode, which causes raises an ActiveModel::StrictValidationFailed
exception if you attempt to validate
a model with [failing] strict validations:
You can also include your own custom validator classes, and call custom validator methods with the validates
method.
All Futures also provides several validator methods, courtesy of Active Entity:
subset
validation
All Futures supports array attributes, so you may want to ensure that the elements of an array attribute are included in a given set.
The subset
validation has syntax similar to inclusion
or exclusion
:
uniqueness_in_embeds
validation
The uniqueness_in_embeds
validation ensures that you have only unique virtual records when working with embedded (nested) models.
key
is the attribute name of the nested model. Test multiple attributes by passing an Array.
uniqueness_in_active_record
validation
The uniqueness_in_active_record
validation is All Futures' answer to Active Record's uniqueness validation. It will perform a query against a scope
to ensure that the attribute value is not already present in your relational datastore.
In addition to the standard uniqueness
options, you are required to specify an Active Record model class_name
for it to perform the query.
The only way to implement incremental validations with vanilla Active Record is to use its powerful conditional validation mechanisms. Historically, this has been the only way to build "wizard" style UIs. Use :if
or :unless
to evaluate a function that decides if the validation is applied.
You can pass a Symbol to call a method, or provide a Lambda to evaluate in-line:
It's possible to define groups of validations that are only applied if a condition is met e.g. if the is_author
Boolean attribute is true
:
You can even construct unholy mashups of all these techniques, using :if
and :unless
in combination. There's a "great" example in the Rails Guide:
Unfortunately, the code required to use conditional validation logic in a complex scenario quickly becomes brittle and difficult to maintain. All Futures supports Active Record's conditional mechanisms for compatibility, but you are strongly encouraged to consider a new approach...
Cooking shows would be really boring (and short) if all of the recipes were ready to go into the oven at the beginning of the episode. We trust that the ultimate application of heat will be successful, but watch the chef because the important parts are all in the middle.
Active Record wants fully assembled dishes that are ready to go into the oven. AllFutures is all about how you slice the onions and blend the sauces.
The key design difference that sets All Futures apart from Active Record is that a model instance does not have to be valid to be persisted to Redis.
This means that it's perfectly okay for you to work iteratively, tweaking attributes and providing an infrastructure upon which a reactive UI can be quickly built.
All Futures sits in front of Active Record like a firewall, meaning that it can remove much of the complexity that led to conditional validations in the first place.
While you should still have validations in place to ensure the integrity of your Active Record model, having All Futures in your pipeline means that you'll be passing data to Active Record that's already valid, outside of [literally] exceptional cases.
You will reduce the overall complexity of your Active Record model classes, which can now focus on persistence, while delegating the workflow of your business objects to All Futures.
How cool is that?
When All Futures is in the kitchen, conditional Active Record validations can go in the compost.
Note that any errors on the :base
have no impact individual attribute validity.
Just like calling valid?
, but for one attribute. Returns true
if the specified attribute passes all validation helpers.
You can introspect the validations on a model with the validators
class method, which returns a Hash that is keyed to the attributes. Get an Array of validation objects for the name
attribute with validators_on
class method.
Hopefully, it's self-evident that defining the same validations on both All Futures and Active Record models would be repetitive today and a maintenance burden tomorrow. Instead, create a Concern that you can include in both models.
Let's create a Postable
Concern that we can use in our Post
Active Record model and our PostDraft
All Futures model:
Now you can just include Postable
in both models:
If you have any validations that are only intended to run in one of the models, you can just keep it in the appropriate class. However, there's also another technique that could be helpful in advanced scenarios: you can selectively include class method calls based on the class of the object that is calling it.
All Futures adds the ability to see if a single given attribute is currently valid. This is exceptionally useful for building reactive interfaces.