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
:recipient_email, presence: true, format: EMAIL_REGEX
validates end
We 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
.errors.add(attribute, "#{value} is not a valid email")
recordend
end
end
Once 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
:recipient_email, presence: true, email: true
validates end
The 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
EnumerableValidator,
validates_with attributes: [:recipients],
unless: 'recipients.nil?',
validator: EmailValidator
The 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)
.each do |value|
enumerable.validate_each(record, attribute, value)
validatorend
end
private
def validator
[:validator].new(validator_options)
optionsend
def validator_options
.except(:validator).merge(attributes: attributes)
optionsend
end
Please 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.