Modifying Rails Validations at Run Time
I'm in the process of writing my scoped multi-tenancy gem for Rails. One of the advantages of it over the gems out there now is that it applies scopes to your validations. (As well as providing rake tasks for migrating single-tenant databases into a multi-tenant one, which is no mean feat I assure you!)
01/08/2013: Fixed it now, phew.
Rails doesn't make it easy to play with validations at run time. It provides `.validators` so that you can inspect them, but if you try and modify the options hash you'll find that you get a:
RuntimeError: can't modify frozen Hash
Which is no fun, is it? On realising that I couldn't edit validators this way, I thought I'd just remove it and add a new one with the options that I want. However, .validators doesn't provide any way of modifying the array it returns. On inspecting .validators it turns out it simply calls this:
# File activemodel/lib/active_model/validations.rb, line 168 def validators _validators.values.flatten.uniq end
Oh-ho-ho, hello there _validators. Looks like you're the array we want to be playing around with. Nope .. spoke too soon. This just stores the validators, for the purpose of reflection I assume, but deleting these won't actually stop the validation running.
Validations are actually just callbacks like before_save, etc. And you can access model callbacks with the _type_callbacks method. Type in our case will be validate.
For the purpose of my multi-tenancy gem, I needed to scope all uniqueness validations with the tenant_id. You should only have to tell your models that they don't have a table to themselves once, and the rest should be taken care of. It defeats the object of having a gem to handle this stuff if you have to remember to scope your validations still. So, I wrote the following method for my mixin which will do just that, combining the methods and stuff I've just been talking about:
def scope_validators(foreign_key) _validators.each do |attribute, validations| validations.reject!{|v| v.kind == :uniqueness} end new_callback_chain = self._validate_callbacks.reject do |callback| callback.raw_filter.is_a?(ActiveRecord::Validations::UniquenessValidator) end deleted = self._validate_callbacks - new_callback_chain (self._validate_callbacks.clear << new_callback_chain).flatten! deleted.each do |c| v = c.raw_filter v.attributes.each do |a| validates_uniqueness_of *v.attributes, v.options.merge(scope: foreign_key) end end end
It's pretty ugly, actually, but it works a dream and frankly that's all I really care about.
Happy hacking.