Home > Articles

  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

8.6 Custom Validation Techniques

When declarative validation doesn’t meet your needs, Rails gives you a few custom techniques.

8.6.1 Custom Validation Macros

Rails has the capability to add custom validation macros (available to all your model classes) by extending ActiveModel::EachValidator.

The following example is silly but demonstrates the functionality.

class ReportLikeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value["Report"]
      record.errors.add(attribute, 'does not appear to be a Report')
    end
  end
end

Now that your custom validator exists, it is available to use with the validates macro in your model.

class Report < ActiveRecord::Base
  validates :name, report_like: true
end

The class name ReportLikeValidator is inferred from the symbol provided (:report_like).

You can receive options via the validates method by adding an initializer method to your custom validator class. For example, let’s make ReportLikeValidator more generic.

class LikeValidator < ActiveModel::EachValidator
  def initialize(options)
    @with = options[:with]
    super
  end

  def validate_each(record, attribute, value)
    unless value[@with]
      record.errors.add(attribute, "does not appear to be like #{@with}")
    end
  end
end

Our model code would change to

class Report < ActiveRecord::Base
  validates :name, like: { with: "Report" }
end

8.6.2 Create a Custom Validator Class

This technique involves inheriting from ActiveModel::Validator and implementing a validate method that takes the record to validate.

I’ll demonstrate with a really wicked example.

class RandomlyValidator < ActiveModel::Validator
  def validate(record)
    record.errors[:base] << "FAIL #1" unless first_hurdle(record)
    record.errors[:base] << "FAIL #2" unless second_hurdle(record)
    record.errors[:base] << "FAIL #3" unless third_hurdle(record)
  end

  private
  def first_hurdle(record)
    rand > 0.3
  end

  def second_hurdle(record)
    rand > 0.6
  end

  def third_hurdle(record)
    rand > 0.9
  end
end

Use your new custom validator in a model with the validates_with macro.

class Report < ActiveRecord::Base
  validates_with RandomlyValidator
end

8.6.3 Add a validate Method to Your Model

Giving your model class a validate instance method might be the way to go if you want to check the state of your object holistically and keep the code for doing so inside of the model class itself.

For example, assume that you are dealing with a model object with a set of three integer attributes (:attr1, :attr2, and :attr3) and a precalculated total attribute (:total). The total must always equal the sum of the three attributes:

class CompletelyLameTotalExample < ActiveRecord::Base
  def validate
    if total != (attr1 + attr2 + attr3)
      errors[:total] << "doesn't add up"
    end
  end
end

You can alternatively add an error message to the whole object instead of just a particular attribute, using the :base key, like this:

errors[:base] << "The total doesn't add up!"

One of the subtleties of writing validations in Rails is that when you are adding errors to a particular attribute, you use a sentence fragment, versus when you are adding to :base, you use an entire sentence. That’s because at some point, you’ll want to expose error messages to the user. The method used to do that for an attribute is full_messages_for, which takes the name of the attribute plus whatever errors have been added to an attribute and strings it all together into a whole sentence using Active Support’s to_sentence method.

  • + Share This
  • 🔖 Save To Your Account