overriding rails validations metaprogramatically

I had a need to modify the validations for a model in a Rails 2.3.5 app from a plugin.  I did not want to directly modify the source code; in my case, I was writing my code as a plugin for an existing Rails application so I could upgrade the base code without worrying about losing my changes, or dealing with the hassle of reapplying my patches.  There are several really great Rails frameworks out there that are applications in their own right, such as Redmine and Spree, and writing my customizations as plugins or extensions to them is simply a more flexible solution than modifying the applications directly.

I was modifying a User class, extending the login length to 255 characters. We were using emails as the login, and where I work they can get pretty long because they are usually a concatenation of one’s first and last names. And some of my co-workers have obscenely long names. Directly modifying User would be trivial — you would just increase the value associated with the maximum attribute for the validates_length_of validation we are changing:

validates_length_of :login, :maximum => 255

But, as I mentioned, I did not want to change the original code. So, we need to get at the list of validation callbacks. Luckily, that’s easy: there’s a @validate_callbacks instance method available to us. So, we just need to iterate over it and find the callback that we don’t want.

Each entry in @validate_callbacks is an instance of ActiveSupport::Callbacks::Callback. This class contains an accessor called method which returns the Proc that is executed to run the validation. Unfortunately, we want to get at the values being sent to that proc. We know that the validates_length_of method takes in an attrs parameter, so we just eval that against the callback method’s binding to find the list of attributes that this proc will validate.

Because we know we only passed one attribute in (instead of a chain of them), we just check the first value, and double check that the maximum option was set to 30 (which is the original value). This also allows us to easily filter out the other validations on the login field that we aren’t interested in, such as validates_format_of.  The finished product looks something like this:

require_dependency 'user'
require 'dispatcher'
module UserPatch
def self.included(base)
  base.class_eval do
    @validate_callbacks.reject! { |c| true if Proc === c.method &&
      eval("attrs", c.method.binding).first == :login && c.options[:maximum] == 30 rescue false }
    validates_length_of :login, :maximum => 255 # new value
end end Dispatcher.to_prepare do unless User.included_modules.include?(UserPatch) User.send(:include, UserPatch) end end

Notes

  1. lmcalpin posted this