Validations

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.

Validations 101

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.

validate is aliased to valid? and might be grammatically satisfying in some contexts.

Implementing validations

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:

class Book < AllFutures::Base
  attribute :title, :string
  validates :title, presence: true
end

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:

validates :title, presence: {strict: true}

You can also include your own custom validator classes, and call custom validator methods with the validates method.

All Futures validations

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:

class Steak < AllFutures::Base
  attribute :side_dishes, :string, array: true, default: []
  validates :side_dishes, subset: { in: %w(chips mashed_potato salad) }
end

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.

class Category < AllFutures::Base
  attribute :name, :string
end

class Reviewer < AllFutures::Base
  attribute :first_name, :string
  attribute :last_name, :string
end

class Book < AllFutures::Base
  embeds_many :categories
  validates :categories, uniqueness_in_embeds: {key: :name}

  embeds_many :reviewers
  validates :categories, uniqueness_in_embeds: {key: [:first_name, :last_name]}
end

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.

class Candidate < AllFutures::Base
  attribute :name, :string

  validates :name,
            uniqueness_on_active_record: {
              class_name: "Staff"
            }
end

Conditional validations

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:

class Order < AllFutures::Base
  validates :password, confirmation: true, unless: -> { password.blank? }
  validates :card_number, presence: true, if: :paid_with_card?

  def paid_with_card?
    payment_type == "card"
  end
end

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:

class DraftPost < AllFutures::Base
  with_options if: :is_author? do
    validates :title, presence: true
    validates :body, length: { minimum: 10 }
  end
end

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:

class Computer < ApplicationRecord
  validates :mouse, presence: true,
                    if: [Proc.new { |c| c.market.retail? }, :desktop?],
                    unless: Proc.new { |c| c.trackpad.present? }
end

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...

The All Futures way

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.

Programmatic validations

Note that any errors on the :base have no impact individual attribute validity.

attribute_valid?(attribute), ATTR_valid?

Just like calling valid?, but for one attribute. Returns true if the specified attribute passes all validation helpers.

post_draft.attribute_valid? :name
post_draft.name_valid?

Meta-programming validations

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.

PostDraft.validators
PostDraft.validators_on :name

Sharing validations with Active Record

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:

app/models/concerns/postable.rb
module Postable
  extend ActiveSupport::Concern

  included do
    validates :name, presence: true
  end
end

Now you can just include Postable in both models:

class Post < ApplicationModel
  include Postable
end

class PostDraft < AllFutures::Base
  include Postable
end

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.

module Postable
  extend ActiveSupport::Concern

  included do |base|
    validates :name, presence: true
    if base < ActiveRecord::Base
      validates :name, uniqueness: true
    end
  end
end

Last updated