Removing Rails validations with metaprogramming

I recently found myself facing a metaprogramming challenge. I solved it by combining two terrible ugly hacks, and as such I won’t say I found a solution that is anywhere near to be elegant.

My problem was this: I was developing a Radiant extension called tags_multi_site, which allows the tags extension to play nice with the multi_site extension. This required me to scope all tags within a site, so that tags with the same name could exist in different sites, but in the same physical database table.

The tags extension has this validation:

class MetaTag < ActiveRecord::Base
  validates_uniqueness_of :name, :case_sensitive => false
end

I needed to add :scope => :site_id, but I couldn’t touch the code of tags extension itself, since that would terribly un-DRY and not reusable for anyone else. I had to either modify the existing validation programmatically from my own extension or to remove it and add my own.

I went for the last solution. I quickly discovered that validations are saved in an array available as an inheritable attribute on the model (read_inheritable_attribute(:validate)), and that the built-in Rails validation are stored as Procs in this array. One could remove all validations added so far by emptying this array, but I only wanted to remove validates_uniqueness_of to stay as loosely coupled as possible.

Procs can’t tell much about themselves – they are mostly just there to be called. But I knew from the Rails code that each validation Proc is added from inside the class method of the validation. So, I just had to figure out a way to determine the method context the Proc had been declared in to be able to remove the right one.

I realized I could read variables from the Proc‘s context by doing an eval with the Proc‘s binding applied. I also found an expression somewhere that returned the name of current method by using the stacktrace information in caller.

All in all, the solution ended up like this:

module TagsMultiSite
  module MetaTagExtensions
    def self.included(base)
      base.extend(ClassMethods)
      base.class_eval {
        # HACK: Remove the existing validates_uniqueness_of block
        read_inheritable_attribute(:validate).reject! do |proc|
          if proc.is_a?(Proc)
            method = eval("caller[0] =~ /`([^']*)'/ and $1", proc.binding).to_sym rescue nil # Returns the name of method the proc was declared in
            :validates_uniqueness_of == method
          else
            false
          end
        end

        # Add new validates_uniqueness_of with correct scope
        validates_uniqueness_of :name, :case_sensitive => false, :scope => :site_id
      }
    end
  end
end

It would be easy to make this into a generalized method for removing Rails validations, but I think this issue is pretty rare. Usually people can just change or remove the original validation. Still, this example demonstrates fairly well how the trusting nature of Ruby allow us to make far-fetched metaprogramming hacks to solve our problems.

7 thoughts on “Removing Rails validations with metaprogramming

  1. Whoa. I was running into the exact same thing with mutli_site – login seems to be unque in the User model and there’s no good way of removing the validation. The thing I’m proposing is to add an :if => should_validate_unique_login but I’m not so sure if that’s the best option…

    Overriding validations just seems like a hack.

  2. It is a hack, and an ugly one at that, but since I was writing an extension to be released, I could not rely on changing someone else’s code.

    All changes had to be done from my code, but if you have a trick up your sleeve for adding that :if => without modifying existing code I’d like to see it :)

  3. Looks good. I might have to use this for some crap validation that some plugins/gems have for email which deny valid RFC822 emails.

  4. Nice solution, Will – I actually like that better than mine for this particular case. It is cleaner as you say, and easier to read and understand – less magic. Still, I mostly blogged about my solution because I found it an interesting study into how far you can go with Ruby :)

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>