Ruby Science
Extract Validator
Extract Validator is a form of extract class that is used
to remove complex validation details from ActiveRecord
models. This technique also prevents duplication of validation code
across several files.
Uses
- Keeps validation implementation details out of models.
- Encapsulates validation details into a single file, following the single responsibility principle.
- Removes duplication among classes performing the same validation logic.
- Makes validation logic easier to reuse, which makes it easier to avoid duplication.
Example
The Invitation class has validation details in-line. It
checks that the recipient_email matches the formatting of
the regular expression EMAIL_REGEX.
# app/models/invitation.rb
class Invitation < ActiveRecord::Base
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
validates :recipient_email, presence: true, format: EMAIL_REGEX
endWe extract the validation details into a new class
EmailValidator and place the new class into the
app/validators directory:
# app/validators/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
def validate_each(record, attribute, value)
unless value.match EMAIL_REGEX
record.errors.add(attribute, "#{value} is not a valid email")
end
end
endOnce the validator has been extracted, Rails has a convention for
using the new validation class. EmailValidator is used by
setting email: true in the validation arguments:
# app/models/invitation.rb
class Invitation < ActiveRecord::Base
validates :recipient_email, presence: true, email: true
endThe convention is to use the validation class name (in lower case,
and removing Validator from the name). For example, if we
were validating an attribute with ZipCodeValidator, we’d
set zip_code: true as an argument to the validation
call.
When validating an array of data as we do in
SurveyInviter, we use the EnumerableValidator
to loop over the contents of an array.
# app/models/survey_inviter.rb
validates_with EnumerableValidator,
attributes: [:recipients],
unless: 'recipients.nil?',
validator: EmailValidatorThe EmailValidator is passed in as an argument, and each
element in the array is validated against it.
# app/validators/enumerable_validator.rb
class EnumerableValidator < ActiveModel::EachValidator
def validate_each(record, attribute, enumerable)
enumerable.each do |value|
validator.validate_each(record, attribute, value)
end
end
private
def validator
options[:validator].new(validator_options)
end
def validator_options
options.except(:validator).merge(attributes: attributes)
end
endPlease note that in the latest version of the example application the
EmailValidator class was renamed to
EmailAddressValidator to avoid a naming conflict with an
external gem.
Next Steps
- Verify the extracted validator does not have any long methods.
- Check for other models that could use the validator.
Ruby Science
The canonical reference for writing fantastic Rails applications from authors who have created hundreds.