Fun with Fancy Validations

When you normally think about validations on a model, it’s in the context of a model whose structure you control. For example, your app may have an Entry model and you might want to validate that each entry has a title.

class Entry < ActiveRecord::Base
  validates :title, presence: true
end

The situation gets more complex when you start letting users control the structure of data in your application. Perhaps you allow your users to add their own custom fields to entries. Or maybe you have a generic Document model and allow your users to define whatever structure they want. Your users will certainly want some assurance that their data is valid, but you cannot write validations in the usual way for data that has an unknown structure.

In this post, I want to present one method I’ve come up with for allowing custom validations of this kind.

How Rails handles validations

Under the hood, Rails handles validations using a module from ActiveModel called ActiveModel::Validations. This module provides the validates et al. macros, as well as a set of validator classes. These have names like PresenceValidator, FormatValidator, etc.

When you use one of the validation macros, Rails translates your options into a list of validators which are added to the model’s internal list of validators. When the validate callback is fired, the validators are instantiated and told to apply themselves to the model record.

How does a validator add an error to a record? Simply by adding to the record’s errors object.

The important thing to note is that the validates macros are merely a convenience for attaching validators to a model’s validate callback. The validators provided by ActiveModel::Validations are nothing special themselves and nothing is stopping us from using them directly in our own code.

Custom validation rules

Let’s say we have a Review model in our application. It stores custom fields in a serialized hash. How these are managed is not important for now, but I may cover it in a future post. We want to allow users to add custom validations to these custom fields. But first, let’s lay out some boilerplate code:

class Review < ActiveRecord::Base
  store :custom_fields, coder: JSON

  def read_attribute_for_validation(attribute)
    if !self.has_attribute?(attribute) && self.custom_fields.has_key?(attribute)
      self.custom_fields[attribute.to_s]
    else
      super
    end
  end
end

We give Review a custom implementation of read_attribute_for_validation so that the validators can easily find the custom field attributes. This method is called by ActiveModel in order to get the value of an attribute during validation.

We’ll start out with letting users mark custom fields as required. This is just like letting them add a presence validation to the model, so the PresenceValidator seems like a good choice.

We’ll create a generic class called Rule that will hold the name of a Validator class, an attribute to apply the validation to, and the name of the class that this rule should apply to.

class CreateRules < ActiveRecord::Migration
  def change
    create_table :rules do |t|
      t.string :validator_class
      t.string :attribute_name
      t.string :validatable_type
  end
end
class Rule < ActiveRecord::Base
  def validate(record)
    v = self.validator_class.constantize.new(attributes: [self.attribute_name.to_sym])
    v.validate(record)
  end
end

A rule to validate that the custom field “review_score” is always included might look like the following:

validator_class attribute_name validatable_type
ActiveModel::Validations::
PresenceValidator
review_score Review

Applying the rules

To apply the rules to our Review class, the Review needs to know which Rules apply to it and how to apply them.

To handle this, we’ll make a CustomValidations concern that will handle adding the appropriate methods and logic:

module CustomValidations
  extend ActiveSupport::Concern

  included do
    before_validation :apply_validation_rules
  end

  def applicable_validation_rules
    Rule.where(validatable_type: self.class.to_s)
  end

  def apply_validation_rules
    self.applicable_validation_rules.each do |rule|
      rule.validate(self)
    end
  end
end

The applicable_validation_rules method selects Rules with validatable_type equal to the object’s class. apply_validation_rules then applies these rules and their validators to the record.

Now we can include this in the Review class:

class Review < ActiveRecord::Base
  include CustomValidations
end

Now whenever we call valid? on a Review, the apply_validation_rules method will be called, the applicable Rules will be fetched, and their validators will be applied to the record. If any of the validators add errors the record, then valid? will return false, just as though we had specified those validators with the usual validates macro!

More complicated validations

The PresenceValidator is pretty simple and doesn’t require any more information than what field it should check. For a validator such as the FormatValidator or the InclusionValidator, however, we need to supply more information: a regex format, or a minimum and maximum, respectively. Other standard ActiveModel validators may also take other options.

Since the options for each validator are different, we’ll again serialize them into a hash and store that in a column on the Rule:

class AddOptionsToRules < ActiveRecord::Migration
  def change
    add_column :rules, :options, :text
  end
end

Then we’ll modify the validate method to pass the options on to the Validator:

class Rule < ActiveRecord::Base
  store :options, coder: JSON

  def validate(record)
    opts = { attributes: [self.attribute_name.to_sym] }
    opts.merge!(self.options.symbolize_keys)
    v = self.validator_class.constantize.new(opts)
    v.validate(record)
  end
end

A rule requiring all review scores to be written as fractions of 10 might look like:

validator_class attribute_name validatable_type options
ActiveModel::Validations::
FormatValidator
review_score Review {“with”: “\d+\/10”}

This will also allow you to specify optional parameters to validators, such as telling the PresenceValidator to ignore nils.

Wrapping up

This pattern is not restricted to just the built-in ActiveModel validator classes but can also be used with our own custom Validator classes. In fact anything that conforms to the ActiveModel::Validations can fit, including custom validators that you write.

One possible improvement to the Rule model might be to hold a constant listing the allowed validator classes. Since the rule object must constantize the validator_class attribute, it’s a good idea to validate what we allow in there.

class Rule < ActiveRecord::Base
  ALLOWED_VALIDATORS = [
    ActiveModel::Validations::PresenceValidator,
    ActiveModel::Validations::AbsenceValidator,
    ActiveModel::Validations::FormatValidator,
    # ... and so on
  ]
end

I haven’t presented any sort of UI for this pattern, but I don’t think it would be too hard to figure out. A crucial part would be to make a mapping between the ALLOWED_VALIDATORS and “friendly” human names. You could then use this to create a <select> tag in a form to allow the user to choose what kind of validation.

Another difficult problem would be how to present a good UI to allow your users to enter any extra options, such as minimum or maximum length or a regex format, that a particular validator might want. One possible solution to this would be creating an AJAX action in your form’s controller that would return an HTML partial containing the extra fields when the user changes what type of validator they are constructing.

The pattern I’ve described here and the problem it’s intended to solve are not ones that you would typically run across in a regular Rails application. However, I think that thinking about user customization and giving your users more say into what is an acceptable format for their data is important. Hopefully it can be helpful to you!