Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Simple DRY Validations

Here’s a handy trick for making custom validations easily reusable.

This is an extract from a customer model with three different street addresses, in which we validate all three of the zip codes. (In this code, the GeoState.valid_zip_code? method answers if something that looks like a zip code is an actual zip code – not all five digit combinations are in use as zip codes, and we want to make sure we’ve got a live one.)

def validate_home_zip_code
  validate_zip_code(:home_zip_code)
end

def validate_mailing_zip_code
  validate_zip_code(:mailing_zip_code)
end

def validate_previous_zip_code
  validate_zip_code(:previous_zip_code)
end

def validate_zip_code(field)
  errors.add(field, :inclusion) if errors.on(field).nil? && !GeoState.valid_zip_code?(send(field))
end

validates_presence_of :home_zip_code
validates_format_of :home_zip_code, :with => /^d{5}(-d{4})?$/, :allow_blank => true
validate :validate_home_zip_code

validates_presence_of :mailing_zip_code
validates_format_of :mailing_zip_code, :with => /^d{5}(-d{4})?$/, :allow_blank => true
validate :validate_mailing_zip_code

validates_presence_of :previous_zip_code
validates_format_of :previous_zip_code, :with => /^d{5}(-d{4})?$/, :allow_blank => true
validate :validate_previous_zip_code

That looks very wet to me. (WET == “Write Every Time”) But it’s not too hard to dry this up using just a tiny bit of knowledge of how ActiveRecord validations work.

Wouldn’t it be nice to get rid of those three different validation methods and just have one thing to use to validate the zip code is real?

def self.validates_zip_code(*attr_names)
  configuration = { }
  configuration.update(attr_names.extract_options!)

  validates_each(attr_names, configuration) do |record, attr_name, value|
    if record.errors.on(attr_name).nil? && !GeoState.valid_zip_code?(record.send(attr_name))
      record.errors.add(attr_name, :invalid_zip, :default => configuration[:message], :value => value)
    end
  end
end

validates_presence_of :home_zip_code
validates_format_of :home_zip_code, :with => /^d{5}(-d{4})?$/, :allow_blank => true
validates_zip_code :home_zip_code

validates_presence_of :mailing_zip_code
validates_format_of :mailing_zip_code, :with => /^d{5}(-d{4})?$/, :allow_blank => true
validates_zip_code :mailing_zip_code

validates_presence_of :previous_zip_code
validates_format_of :previous_zip_code, :with => /^d{5}(-d{4})?$/, :allow_blank => true
validates_zip_code :previous_zip_code

That’s a bit better. Before we had three custom validation methods that did all the same work using an extracted method. We have replaced those three methods with a single custom class-level validation, validates_zip_code, which validates a zip code attribute by name. It’s used just like any other standard validation, and as many times as you want. Nice and dry! The implementation of validates_zip_code is modeled on some of the standard validation methods found in ActiveRecord’s Validations module (in 'active_record/validations.rb'). Take a look at that code for more examples of how you might write your own custom validations.

Now that’s a good start, but we can do even better. We can take the custom validation and fold in the other validations we do on each zip code.

def self.validates_zip_code(*attr_names)
  configuration = { }
  configuration.update(attr_names.extract_options!)

  validates_presence_of(attr_names, configuration)
  validates_format_of(attr_names, configuration.merge(:with => /^d{5}(-d{4})?$/, :allow_blank => true));

  validates_each(attr_names, configuration) do |record, attr_name, value|
    if record.errors.on(attr_name).nil? && !GeoState.valid_zip_code?(record.send(attr_name))
      record.errors.add(attr_name, :invalid_zip, :default => configuration[:message], :value => value)
    end
  end
end

validates_zip_code :home_zip_code
validates_zip_code :mailing_zip_code
validates_zip_code :previous_zip_code

This example was extracted from the version history of the code for a project I worked on recently. It follows the very steps we took to refactor the validations step by step. I know that Jeff Dean has contributed a cool validator object enhancement that will be appearing in Rails 3, but this approach works out of the box with the current version of Rails, so you don’t have to turn blue holding your breath.

Final note: If you’re wondering why we validate presence and then allow blank on the following validations, it’s so that the user only gets one error message at a time.

Comments
Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *