Last Updated: February 25, 2016
·
4.187K
· 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.