Custom validator class

In Ruby On Rails, most of the model validations are covered with ActiveModel::Validations.
However, there is a need for custom validation.
Then it makes sense to move the validation logic into a separate validation class.

Validation use case

A simplified User model as a starting example:

rails g model User email:string password:string

The password should be present and have a minimum length of 6 characters. In addition, the password complexity has to be validated. Here it should be either a combination of uppercase/ lowercase, uppercase/ number or lowercase/ number:

class User < ApplicationRecord
  validates :password, presence: true
  validates :password, length: { minimum: 6 }, allow_nil: true
  validate :password_complexity

  private

  REQUIRED_PASSWORD_COMPLEXITY = 2
  PASSWORD_COMPLEXITIES = [
    /[A-Z]/,
    /[a-z]/,
    /[0-9]/
  ]

  def password_complexity
    return if password.nil?
    return if implied_complexity(password) >= REQUIRED_PASSWORD_COMPLEXITY
    errors.add(:password, I18n.t('errors.messages.complexity'))
  end

  def implied_complexity(value)
    COMPLEXITIES.select { |complexity| complexity =~ value }
                .size
  end
end

There are several reasons for extracting the validation logic into a Validator class:

  • Improved validation logic testability
  • Validator reusability in other models or form objects

Custom Validatoren einf├╝hren

First, the path to the custom validators have to be configured for the autoloader:

config.autoload_paths += Dir["#{config.root}/lib/custom_validations/**/"]

Custom validator class

The convention for the validator class name applies: Validator name + Validator:

# lib/custom_validations/complexity_validator.rb
class ComplexityValidator < ActiveModel::EachValidator
  MAX_LEVEL = 3
  COMPLEXITIES = [
    /[A-Z]/,
    /[a-z]/,
    /[0-9]/
  ]

  def validate_each(subject, attribute, value)
    return if options[:allow_nil] && value.nil?
    return if implied_complexity(value.to_s) >= required_complexity
    subject.errors.add(attribute, error_message)
  end

  private

  def required_complexity
    options[:with] || MAX_LEVEL
  end

  def implied_complexity(value)
    COMPLEXITIES.select { |complexity| complexity =~ value }
                .size
  end

  def error_message
    options[:message] || I18n.t('errors.messages.complexity')
  end
end

The Validation logic has to be implemented in validate_each. The passed parameters are the object (user), the attribute (password), and the value (the password itself). There is also the possibility to access options. These are the known default options (message, allow_nil, allow_blank, on, if, unless, strict). The with option is for additional parameters. It is passed to the custom validator, as known from standard validators, like the LengthValidator.

Using custom validator

The model readability benefits from moving the validation logic into a custom validator:

class User < ApplicationRecord
  validates :password, presence: true
  validates :password, length: { minimum: 6 },
                       complexity: 2,
                       allow_nil: true

The ComplexityValidator is defined with a complexity of 2.
Furthermore the validator can be combined with other validators (in this case, the LengthValidator).
If a higher password complexity is required for other user roles, like administrators, this is pretty easy to implement. For example, in a Form object that is responsible for creating administrators:

class AdminForm < ApplicationForm
  validates :password, presence: true
  validates :password, length: { minimum: 8 },
                       complexity: 3,
                       allow_nil: true