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.