validation_scopes for ActiveRecord 2.3.5 and the guilty pleasure of ruby meta programming
January 5, 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.
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):
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 proxyit 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 definedin this case when validation_scope is calledso 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)) doinclude 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)
endendyield 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}") dounless instance_variable_defined?("@#{scope}")
klass = deferred_proxy_class_declaration.call
instance_variable_set("@#{scope}", klass.new(self))
endend
define_method("validation_scope_proxy_for_#{scope}") do
send "init_validation_scope_for_#{scope}"
instance_variable_get("@#{scope}")
endend
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.
validation_scopes for ActiveRecord 2.3.5 and the guilty pleasure of ruby meta programming
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:
Where
:warnings
is an arbitrary name used to create define unique instance methods for arbitrary scopes, andvalidates_presence_of
is any method inActiveRecord::Validations::ClassMethods
.Continuing the
warnings
example, I wanted 3 instance methods (for starters anyway):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. TheValidations
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 proxyit 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 definedin this case when validation_scope is calledso 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 theDelegateClass
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: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 ofValidations
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.