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.
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.
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
haha – nope, I was going to propose we coerce^Wconvince Sean to add it to Radiant
Great work on the extensions though, Josh Hart clued me in…
Looks good. I might have to use this for some crap validation that some plugins/gems have for email which deny valid RFC822 emails.
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
Ben, that looks awesome. Any chance you can change it into a plugin so it can be used in any Rails project?
Thank you!