Darwinweb

validation_scopes for ActiveRecord 2.3.5 and the guilty pleasure of ruby meta programming

January 4, 2010     

Update Feb 5, 2013: This whole deferred class definition thing led to a memory leak and was definitely ill-advised as I suspected. However in Ruby 1.9.x it turned out to be completely unnecessary. See the simplified implementation.

Recently I found myself wanting to use Rails validations semantics outside the context of saving an object. It wasn’t enough to force save without validations because I actually wanted to have some validations applied to the life-cycle of the object, and other validations for various other purposes including user feedback and control flow of other object modifications.

I ran a few searches on Github and came up with a few interesting projects including soft_validations, validation_groups, validation_scenarios (the closest to what I was looking for), and partially_valid. However none of these really hit the nail on the head. Either they didn’t expose enough of validations goodness, or the syntax just didn’t fit what I was looking for.

My approach was to start with the Validations module itself in ActiveRecord 2.3.5, and figure out how to create completely distinct sets of validations. The goal was to avoid depending on implementation details or doing any kind of monkey patching to validations which would certainly create upgrade headaches. I also think that distinct sets of validations is nice primitive functionality on top of which more domain-specific plugins could be built. The domain I’m working on is not clearly defined, so I hedged in favor of generality.

The result is the newly-released validation_scopes gem

API

After spending a few hours thinking about what I needed, I came up with an ideal API that looks like this:

validation_scope :warnings do |s|
  s.validates_presence_of :title
end

Where :warnings is an arbitrary name used to create define unique instance methods for arbitrary scopes, and validates_presence_of is any method in ActiveRecord::Validations::ClassMethods.

Continuing the warnings example, I wanted 3 instance methods (for starters anyway):

def warnings… # Calls Validations#errors
def has_warnings?… # Calls Validations#invalid?
def no_warnings?… # Calls Validations#valid?

My hunch was that such an API could be built dynamically with a minimum of code.

The Proxy

The first issue I tackled was where to store the alternate @errors instance variable. The Validations module accesses this directly and frequently, so I figured pretty early on that I’d need some sort of dynamically defined class to separately hold each validation_scope’s @errors instance var.

Of course all Rails validations, need to access the models attributes directly, and possibly any of its methods, so the dynamically defined class would have to be a proxy—it would have the Validations module included, and the custom methods defined, but everything else would delegate back to the original ActiveRecord object. DelegateClass turned out to serve brilliantly for this with one caveat.

The methods that a DelegateClass will successfully delegate are defined at the time the class is defined—in this case when validation_scope is called—so if you were to define your validation_scope at the top of the of your class definition then any methods defined after that point would not be available to validations leading to the most infuriating sort of bug.

Deferring the DelegateClass definition

After digging through the delegate.rb to see if there was some sort of hack to force it to pick up methods defined after the DelegateClass definition. I started googling the crap out of “late-binding”, “ruby”, and “delegate”, but could only find Java references. I guess late-binding isn’t such a core concept in a language like Ruby.

Pretty soon I started daydreaming about Prototype’s defer method and how tidy that would leave everything. Suddenly I had the thought… can I just wrap this in a Proc and call it some time after the base class is defined? Now that is pretty standard Ruby and sounds easy on paper, but keep in mind this is a class method where I’m dynamically defining a class and then yielding it back and later referencing it in the closure of a dynamically defined method. The meta-programming magic was already past the point of conscious design (for me anyway), it was more like evolutionary magic now. I plugged it in. The results:

def validation_scope(scope)
  base_class = self
  deferred_proxy_class_declaration = Proc.new do
    proxy_class = Class.new(DelegateClass(base_class)) do
      include ActiveRecord::Validations

      def initialize(record)
        @base_record = record
        super(record)
      end

      # Hack since DelegateClass doesn't seem to be making AR::Base class methods available.
      def errors
        @errors ||= ActiveRecord::Errors.new(@base_record)
      end
    end

    yield proxy_class

    proxy_class
  end

  define_method(scope) do
    send("validation_scope_proxy_for_#{scope}").errors
  end

  define_method("no_#{scope}?") do
    send("validation_scope_proxy_for_#{scope}").valid?
  end

  define_method("has_#{scope}?") do
    send("validation_scope_proxy_for_#{scope}").invalid?
  end

  define_method("init_validation_scope_for_#{scope}") do
    unless instance_variable_defined?("@#{scope}")
      klass = deferred_proxy_class_declaration.call
      instance_variable_set("@#{scope}", klass.new(self))
    end
  end

  define_method("validation_scope_proxy_for_#{scope}") do
    send "init_validation_scope_for_#{scope}"
    instance_variable_get("@#{scope}")
  end
end

And by golly this bastard actually works. I think the part that got me nervous was relying on going through two closures to yield to the original validation_scope block. I’m sure lisp guys do this kind of thing all day and night with nary a second though, but it was pretty satisfying to me for this to just work.

Too Much Meta?

Looking at the result, my gut instinct is that this is too much magic. It’s difficult to parse what is actually going on here without understanding the justifications at each step. In the end I required minimal knowledge of Validations internals. It’s dependent on the module hierarchy but that’s about it. I’m confident that I’ll be able to port this to Rails 3 without too much hassle and that it will remain relatively stable after that. Also compared to similar plugins, this has a good power to weight ratio by multiplexing all the power of Validations with only 50 lines of code.

Yet even despite those benefits, I think the only thing that allows me to use this in good conscience is having a test suite that clearly documents what is expected out of this code with concrete examples.