Last Updated: February 25, 2016
·
4.364K
· jeanmertz

uniqueness validations in ActiveModel

I've been trying to follow Bryan Helmkamp's 7 Patterns to Refactor Fat ActiveRecord Models, working my way to writing better code.

One of the things I liked – and recently tried to implement – where specialized Form Objects. These objects handle everything (validation, saving, etc) related to a specific (large'ish) form.

One thing he left out, was how to handle uniqueness constraints often required for form validations (and unavailable outside of ActiveRecord based classes). Here's my take on overwriting the default UniquenessValidator to handle such cases:

class UniquenessValidator < ActiveRecord::Validations::UniquenessValidator
 def setup(klass)
 super
 @klass = options[:model] if options[:model]
 end

 def validate_each(record, attribute, value)
 # UniquenessValidator can't be used outside of ActiveRecord instances, here
 # we return the exact same error, unless the 'model' option is given.
 #
 if ! options[:model] && ! record.class.ancestors.include?(ActiveRecord::Base)
 raise ArgumentError, "Unknown validator: 'UniquenessValidator'"

 # If we're inside an ActiveRecord class, and `model` isn't set, use the
 # default behaviour of the validator.
 #
 elsif ! options[:model]
 super

 # Custom validator options. The validator can be called in any class, as
 # long as it includes `ActiveModel::Validations`. You can tell the validator
 # which ActiveRecord based class to check against, using the `model`
 # option. Also, if you are using a different attribute name, you can set the
 # correct one for the ActiveRecord class using the `attribute` option.
 #
 else
 record_org, attribute_org = record, attribute

 attribute = options[:attribute].to_sym if options[:attribute]
 record = options[:model].new(attribute => value)

 super

 if record.errors.any?
 record_org.errors.add(attribute_org, :taken,
 options.except(:case_sensitive, :scope).merge(value: value))
 end
 end
 end
end

Now you can use this specific validator along all the others provided by ActiveModel as long as you include ActiveModel::Validations.