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.
Hello, I'm Casper Fabricius. I have developed for the web for 9 years, and have been enjoying Ruby on Rails for the past 4.
My experience covers communities, shopping solutions, multi-language sites, heavy back-end lifting and a wide selection of more traditional websites. I like to integrate Ruby with Java and .NET through JRuby and IronRuby when it makes sense. I am passionate about test- and behavior-driven development, but at the same time I am pragmatic and believe in getting things done.
I am based in Copenhagen, Denmark, but I take assignments from across the globe. Feel free to study my resumé, featured projects and - of course - to hire me.
Aditya
December 7th, 2008 at 10:35 am
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.
Casper Fabricius
December 7th, 2008 at 12:01 pm
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 :)
Aditya
December 10th, 2008 at 5:52 am
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…
AC
January 23rd, 2009 at 10:19 pm
Looks good. I might have to use this for some crap validation that some plugins/gems have for email which deny valid RFC822 emails.
Will
February 22nd, 2009 at 3:26 pm
Hello Casper. Very interesting that so many of us are working on this. Here’s what I ended up with. It seems a cleaner way but without as much control as your later intervention.
To be included into ActiveRecord::Validations::ClassMethods:
module MultiSite::ScopedValidation
def self.included(base)
def validates_uniqueness_of_with_site(*attr)
if column_names.include?('site_id')
configuration = attr.extract_options!
configuration[:scope] ||= :site_id
attr.push(configuration)
end
validates_uniqueness_of_without_site(*attr)
end
base.alias_method_chain :validates_uniqueness_of, :site
end
end
I really don’t like the site_id assumption, but afaik validations are hit too early in the initialization sequence for anything more properly configurable to work. Would love to be wrong about that.
This is from a fork of multi_site that I’m using to provide site-scoping to other extensions. I’d be very glad of any comments. http://github.com/spanner/radiant-multi-site-extension
Casper Fabricius
February 22nd, 2009 at 5:40 pm
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 Evans
February 12th, 2010 at 3:57 am
I was playing with trying to modify the validation for the slug on radiant pages, it currently does not allow anything but numbers/letters… I could not get your script working with my code, but I was able to get this guy’s solution working. The downside is that in my PageExtension class, I had to put all the validations from the Radiant Page class in the process. http://www.neverlet.be/2009/2/18/active-record-validations-are-callbacks
Ben Evans
February 17th, 2010 at 10:55 pm
After doing some more playing around with your method and the one I linked in my previous comment, I have created a Radiant extension that allows targeted removal of default validators without any “hack” style methods.
Using this extension, you can clear validations, or override targeted validations easily on Radiant core models. It does not take much more to also extend / override validations in extension models.
http://github.com/JediFreeman/Radiant-Validators-Extension
Casper Fabricius
February 18th, 2010 at 10:04 am
Ben, that looks awesome. Any chance you can change it into a plugin so it can be used in any Rails project?