Apply ActiveModel Validations by Policy
I'm currently working on a form where a couple attributes of the underlying model can drastically affect which validations are necessary. There are around 13 attributes, and 25 permutations of validations.
I don't really like the idea of the form class topping out at hundreds of lines of conditional methods, so I started looking into what I could accomplish by applying what I'm calling a Validation Policy
on the specific instance.
First, let's go over what didn't work.
Best Laid Plans
My first idea was to simply open up the singleton class of a given instance of the form and include a module containing validation macros. It looked something like this
module ValidationPolicy
def apply_validation_policy(policy)
singleton_class.instance_eval do
include policy
end
end
end
class Form
include ActiveModel::Model
include ValidationPolicy
attr_accessor :foo, :bar
end
module FooPolicy
extends ActiveSupport::Concern
included do
validates :foo, presence: true
end
end
Seemed simple enough, right? Just need to add a method to make @form determine it's policies based on it's attributes and we're good to go!
Unfortunately, that's not the case here.
form = Form.new
form.valid? # => true
form.apply_validation_policy FooPolicy
form.valid? # => true
So started the googling. I discovered a gist of various cuts at doing this sort of thing, none of which seemed to work on my version of Rails. Then I found a issue on the Rails issue tracker that pointed out that the singleton_class does not inherit the necessary state from the superclass for the validations to work.
So the official answer is: Use inheritance.
After messing around with some anonymous class options that felt super hacky, and looking at a number of form and validation gems, I was just about to throw in the towel when I had another idea.
Extend the wheel, don't reinvent it
Rails conditional validations already provide what I want, I just want to mess with the conditions at the instance level without polluting the form class.
So i set out to make this the new class:
class Form
include ActiveModel::Model
include ValidationPolicy
attr_accessor :foo, :bar
validates :foo, presence: true, if: :foo_presence_required_by_policy
end
It's not quite the empty slate I had originally in mind, but the validations are all defined in the class, they are just activated by the individual policy via a clean method-naming convention
So now I just needed a way to define these conditional methods. I decided to stick with the mixin concept with a single DSL method.
module FooPolicy
extend ActiveSupport::Concern
included do
require_validation_of :foo, presence: true
end
end
With this as the goal, I added this to the validation policy module
module ValidationPolicy
extend ActiveSupport::Concern
included do
def self.require_validation_of(name, options)
if options[:presence]
self.send(:define_method, "#{name.to_s}_presence_required_by_policy") do
true
end
end
# other types of validations
end
end
end
Now the bits are all in place, but I do want to avoid blowing up during validations if a policy hasn't been applied.
module ValidationPolicy
# ...
def method_missing(name, *args, &block)
if name.match /required_by_policy$/
return false
end
super
end
end
This will make sure that validations won't blow up with a NoMethodError if a particular policy doesn't apply.
form = Form.new
form.valid? # => true
form.apply_validation_policy FooPolicy
form.valid? # => false
I really like the result. Now I can extract complex validation logic into tiny modules that will be easier to find and maintain than dozens of conditional methods on the Form class. If you have complicated validation needs, I hope you will find this helpful.
EDIT:
I took this one step further, and wrapped up the declarations in another class macro:
module ValidationsPolicy
# ...
included do
def self.validates_presence_of_by_policy(attribute)
self.validates attribute, presence: true, if: "#{attribute.to_s}_presence_required_by_policy".to_sym
end
end
end