What's New in Edge Rails: Independent Model Validators

Posted by ryan
at 10:13 PM on Monday, August 10, 2009

This feature is schedule for: Rails v3.0

ActiveRecord validations, ground zero for anybody learning about Rails, got a lil’ bit of decoupling mojo today with the introduction of validator classes. Until today, the only options you had to define a custom validation was by overriding the validate method or by using validates_each, both of which pollute your models with gobs of validation logic.

ActiveRecord Validators

Validators remedy this by containing granular levels of validation logic that can be reused across your models. For instance, for that classic email validation example we can create a single validator:

1
2
3
4
5
6
class EmailValidator < ActiveRecord::Validator
  def validate()
    record.errors[:email] << "is not valid" unless
      record.email =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
  end
end

Each validator should implement a validate method, within which it has access to the model instance in question as record. Validation errors can then be added to the base model by adding to the errors collection as in this example.

So how do you tell a validator to operate on a model? validates_with that takes the class of the validator:

1
2
3
class User < ActiveRecord::Base
  validates_with EmailValidator
end

Validation Arguments

This is all well and good, but is a pretty brittle solution in this example as the validator is assuming an email field. We need a way to pass in the name of the field to validate against for a model class that is unknown until runtime. We can do this by passing in options to validates_with which are then made available to the validator at runtime as the options hash. So let’s update our email validator to operate on an email field that can be set by the model requiring validation:

1
2
3
4
5
6
7
class EmailValidator < ActiveRecord::Validator
  def validate()
    email_field = options[:attr]
    record.errors[email_field] << "is not valid" unless
      record.send(email_field) =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
  end
end

And to wire it up from the user model:

1
2
3
class User < ActiveRecord::Base
  validates_with EmailValidator, :attr => :email_address
end

Any arguments can be passed into your validators by hitching a ride onto this options hash of validates_with.

Options & Notes

There are also some built-in options that you’ll be very familiar with, namely :on, :if and :unless that define when the validation will occur. They’re all the same as the options to built-in validations like validates_presence_of.

1
2
3
4
class User < ActiveRecord::Base
  validates_with EmailValidator, :if => Proc.new  { |u| u.signup_step > 2 },
    :attr => :email_address
end

It’s also possible to specify more than one validator with validates_with:

1
2
3
class User < ActiveRecord::Base
  validates_with EmailValidator, ZipCodeValidator, :on => :create
end

While this might seem like a pretty minor update, it allows for far better reusability of custom validation logic than what’s available now. So enjoy.

tags: ruby, rubyonrails

Comments

Leave a response

  1. Prem SichanugristAugust 10, 2009 @ 11:40 PM

    One quick question,

    I can also use the validation helper methods (such as validates_uniqueness_of) in the model along with validates_with right?

  2. CarlAugust 11, 2009 @ 12:59 AM

    This looks like a welcome change to me. So where should the validator class files be stored? Under Model, or lib, or will there be a new folder?

  3. PeteAugust 11, 2009 @ 01:09 AM

  4. Derek P.August 11, 2009 @ 01:32 AM

    It would be really cool if I could define a to_js method that allowed me to define my javascript validation here as well which would be able to understand and execute it, as long as my JS validator conformed to some sort of regular interface (ie: returns true/false). I’d love to have all my validator code in 1 class.

  5. Vít OndruchAugust 11, 2009 @ 01:58 AM

    I just wondering why the options hash is not used by more convenient way, i.e. your example could looks better as:

    class EmailValidator < ActiveRecord::Validator def validate(options) email_field = options[:attr] record.errors[email_field] << “is not valid” unless record.send(email_field) =~ /([\s]+)((?:[-a-z0-9]\.)[a-z]{2,})$/i end end

  6. KieranAugust 11, 2009 @ 01:59 AM

    I second Prem’s question. Is this possible:

    class EmailValidator < ActiveRecord::Validator validates_presence_if options[:attr].to_sym validates_uniqueness_of options[:attr].to_sym def validate() # rest of logic end end

  7. SebanAugust 11, 2009 @ 03:37 AM

    What if I forget to define validate method in ancestor of ActiveRecord::Validator? Sometime I miss for Java interfaces. But only sometimes ;-)

  8. SebanAugust 11, 2009 @ 03:43 AM

    Once again I. I’ve checked http://github.com/rails/rails/blob/0a558b36eb3858ceeb926ada1388b0bd41da11f7/activerecord/lib/active_record/validator.rb. RuntimeError “You must to override this method” – I like it. :-)

  9. HeikoAugust 11, 2009 @ 03:49 AM

    One off-topic thing: The e-mail regular expression is wrong. ^ and $ match the line beginning and end, use \A and \z like here: http://www.rorsecurity.info/journal/2007/4/16/ruby-regular-expression-fun.html

  10. DanAugust 11, 2009 @ 02:13 PM

    My biggest issue with the way validators currently behave is that they display the model name as the first word in the error message. There are plenty of ways around it but on the applications I’ve worked with it’s been rare that displaying error messages of that format to the user makes sense. Now that validators are getting some love, hopefully they’ll address that as well.

  11. Maurício LinharesAugust 11, 2009 @ 02:25 PM

    The only thing missing is a funky way of registering validations, like:

    ActiveRecord::Base.register_validator :validates_email_format_of, EmailValidator

  12. nonaAugust 11, 2009 @ 11:16 PM

    Can they be mixed with ActiveResource or other non-DB/non-ActiveRecord models?

  13. alaaAugust 13, 2009 @ 01:43 PM

    thanks for the post,

    It will be more fun to have

    class User < ActiveRecord::Base validate :email_address, EmailValidator end

    aka

    class User < ActiveRecord::Base validate attribute, validator_class [, other extara options like :on or :if ] end

  14. David BlackAugust 16, 2009 @ 06:06 AM

    I should dust off my Singleton Validations plugin… it’s validations at the instance level. I haven’t checked whether it’s working with very recent Railses.

  15. Jamie HillAugust 17, 2009 @ 01:46 PM

    I have written a patch that includes some of the wishes in these comments: https://rails.lighthouseapp.com/projects/8994/tickets/3058-patch-sexy-validations

    Could anyone who wants this, please give it a +1. Thanks.