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