Hi, welcome to Word of Mike, my little corner of the internet. I am a Software/Web Developer working in North Yorkshire. I mainly write about programming but my other passion is politics so beware. (click to hide)

2013-07-19 17:52:37 UTC

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.