The Rails 4 Way (2014)
Chapter 8. Validations
I have bought this wonderful machine- a computer. Now I am rather an authority on gods, so I identified the machine- it seems to me to be an Old Testament god with a lot of rules and no mercy.
—Joseph Campbell
The Validations API in Active Model, along with its supplementary functionality in Active Record allows you to declaratively define valid states for your model objects. The validation methods hook into the life cycle of an Active Record model object and are able to inspect the object to determine whether certain attributes are set, have values in a given range, or pass any other logical hurdles that you specify.
In this chapter, we’ll describe the validation methods available and how to use them effectively. We’ll also explore how those validation methods interact with your model’s attributes and how the built-in error-messaging system messages can be used effectively in your application’s user interface to provide descriptive feedback.
Finally, we’ll cover how to use Active Model’s validation functionality in your own, non-Active Record classes.
8.1 Finding Errors
Validation problems are also known as (drumroll please…) errors! Every Active Record model object contains a collection of errors, accessible (unsurprisingly) as the errors attribute.
When a model object is valid, the errors collection is empty. In fact, when you call valid? on a model object, a series of steps to find errors is taken as follows (slightly simplified):
1. Clear the errors collection.
2. Run validations.
3. Return whether the model’s errors collection is now empty or not.
If the errors collection ends up empty, the object is valid. In cases where you have to write actual validation logic yourself, you mark an object invalid by adding items to the errors collection using its add methods. Simple as that.
We’ll cover the methods of the Errors class in some more detail later on. It makes more sense to look at the validation methods themselves first.
8.2 The Simple Declarative Validations
Whenever possible, you should set validations for your models declaratively by using one or more of the following class methods available to all Active Record classes. Unless otherwise noted, all of the validates methods accept a variable number of attributes, plus options. There are some options for these validation methods that are common to all of them, and we’ll cover them at the end of the section.
8.2.1 validates_absence_of
New to Rails 4, the validates_absence_of method ensures specified attributes are blank, It uses the blank? method, defined on Object, which returns true for values that are nil or a blank string "". It is the polar opposite of the commonly used validates_presence_of validation method, covered later in this section.
1 classAccount < ActiveRecord::Base
2 validates_absence_of :something_unwanted
3 end
When the validates_absence_of validation fails, an error message is stored in the model object reading “attribute must be blank.”
8.2.2 validates_acceptance_of
Many web applications have screens in which the user is prompted to agree to terms of service or some similar concept, usually involving a check box. No actual database column matching the attribute declared in the validation is required. When you call this method, it will create virtual attributes automatically for each named attribute you specify. I see this validation as a type of syntax sugar since it is so specific to web application programming.
1 classAccount < ActiveRecord::Base
2 validates_acceptance_of :privacy_policy, :terms_of_service
3 end
You can use this validation with or without a boolean columns on the table backing your model. A transient attribute will be created if necessary. Choose to store the value in the database only if you need to keep track of whether the user accepted the term, for auditing or other reasons. Mind you, not accepting the term would prevent creation of the record, but it’s good to know what is supported.
When the validates_acceptance_of validation fails, an error message is stored in the model object reading “attribute must be accepted.”
The :accept option makes it easy to change the value considered acceptance. The default value is "1", which matches the value supplied by check boxes generated using Rails helper methods.
1 classCancellation < ActiveRecord::Base
2 validates_acceptance_of :account_cancellation, accept: 'YES'
3 end
If you use the preceding example in conjunction with a text field connected to the account_cancellation attribute, the user would have to type the word YES in order for the cancellation object to be valid.
8.2.3 validates_associated
Used to ensure that all associated objects are valid on save. Works with any kind of association and is specific to Active Record (not Active Model.) We emphasize all because the default behavior of has_many associations is to ensure the validity of their new child records on save.
Suggestion
You probably don’t need to use this particular validation nowadays since has_many associations default to validate: true. Additionally note that one of the implications of that default is that setting validate: true carelessly on a belongs_to association can cause infinite loop problems.
A validates_associated on belongs_to will not fail if the association is nil. If you want to make sure that the association is populated and valid, you have to use validates_associated in conjunction with validates_presence_of.
Tim says… It’s possible to get similar behavior by using a combination of the :autosave and :validate options on a has_many. |
8.2.4 validates_confirmation_of
The validates_confirmation_of method is another case of syntactic sugar for web applications, since it is so common to include dual-entry text fields to make sure that the user entered critical data such as passwords and e-mail address correctly. This validation will create a virtual attribute for the confirmation value and compare the two attributes to make sure they match in order for the model to be valid.
Here’s an example, using our fictional Account model again:
1 classAccount < ActiveRecord::Base
2 validates_confirmation_of :password
3 end
The user interface used to set values for the Account model would need to include extra text fields named with a _confirmation suffix, and when submitted, the value of those fields would have to match in order for this validation to pass. A simplified example of matching view code is provided.
1 = form_for account do |f|
2 = f.label :login
3 = f.text_field :login
4 = f.label :password
5 = f.password_field :password
6 = f.label :password_confirmation
7 = f.password_field :password_confirmation
8 = f.submit
8.2.5 validates_each
The validates_each method is a little more free-form than its companions in the validation family in that it doesn’t have a predefined validation function. Instead, you give it an array of attribute names to check, and supply a Ruby block to be used in checking each attribute’s validity. Notice that parameters for the model instance (record), the name of the attribute as a symbol, and the value to check are passed as block parameters. The block function designates the model object as valid or not by merit of adding to its errors array or not. The return value of the block is ignored.
There aren’t too many situations where this method is necessary, but one plausible example is when interacting with external services for validation. You might wrap the external validation in a faรงade specific to your application, and then call it using a validates_each block:
1 classInvoice < ActiveRecord::Base
2 validates_each :supplier_id, :purchase_order do |record, attr, value|
3 record.errors.add(attr) unless PurchasingSystem.validate(attr, value)
4 end
5 end
8.2.6 validates_format_of
To use validates_format_of, you’ll have to know how to use Ruby regular expressions.19 Pass the method one or more attributes to check, and a regular expression as the (required) :with option. A good example, as shown in the Rails docs, is checking for a valid e-mail address format:
1 classPerson < ActiveRecord::Base
2 validates_format_of :email,
3 with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
4 end
By the way, that example is totally not an RFC-compliant email address format checker.20
Courtenay says… Regular expressions are awesome but can get very complex, particularly when validating domain names or email addresses. You can use #{} inside regular expressions, so split up your regex into chunks like this: validates_format_of :name, with: /\A((localhost)|#{DOMAIN}|#{NUMERIC_IP})#{PORT}\z/ That expression is pretty straightforward and easy to understand. The constants themselves are not so easy to understand but easier than if they were all jumbled in together: 1 PORT = /(([:]\d+)?)/ 2 DOMAIN = /([a-z0-9\-]+\.?)*([a-z0-9]{2,})\.[a-z]{2,}/ 3 NUMERIC_IP = /(?>(?:1?\d?\d|2[0-4]\d|25[0-5])\.){3} 4 (?:1?\d?\d|2[0-4]\d|25[0-5])(?:\/(?:[12]?\d|3[012])|-(?>(?:1?\d?\d| 5 2[0-4]\d|25[0-5])\.){3}(?:1?\d?\d|2[0-4]\d|25[0-5]))?/ |
Lark says… I’ll take your readability Courtenay, and raise you test isolation. Your regular expression should itself be in a constant so you can test it. |
8.2.7 validates_inclusion_of and validates_exclusion_of
These methods take a variable number of attribute names and an :in option. When they run, they check to make sure that the value of the attribute is included (or excluded, respectively) in the enumerable object passed as the :in option.
The examples in the Rails docs are probably some of the best illustrations of their use, so I’ll take inspiration from them:
1 classPerson < ActiveRecord::Base
2 validates_inclusion_of :gender, in: %w( m f ), message: 'O RLY?'
3 ...
4
5 classAccount < ActiveRecord::Base
6 validates_exclusion_of :username, in: %w( admin superuser ),
7 message: 'Borat says "Naughty, naughty!"'
8 ...
Notice that in the examples I’ve introduced usage of the :message option, common to all validation methods, to customize the error message constructed and added to the Errors collection when the validation fails. We’ll cover the default error messages and how to effectively customize them a little further along in the chapter.
8.2.8 validates_length_of
The validates_length_of method takes a variety of different options to let you concisely specify length constraints for a given attribute of your model.
1 classAccount < ActiveRecord::Base
2 validates_length_of :login, minimum: 5
3 end
8.2.8.1 Constraint Options
The :minimum and :maximum options work as expected, but don’t use them together. To specify a range, use the :within option and pass it a Ruby range, as in the following example:
1 classAccount < ActiveRecord::Base
2 validates_length_of :username, within: 5..20
3 end
To specify an exact length of an attribute, use the :is option:
1 classAccount < ActiveRecord::Base
2 validates_length_of :account_number, is: 16
3 end
8.2.8.2 Error Message Options
Rails gives you the ability to generate detailed error messages for validates_length_of via the :too_long, :too_short, and :wrong_length options. Use %{count} in your custom error message as a placeholder for the number corresponding to the constraint.
1 classAccount < ActiveRecord::Base
2 validates_length_of :account_number, is: 16,
3 wrong_length: "should be %{count} characters long"
4 end
8.2.9 validates_numericality_of
The somewhat clumsily named validates_numericality_of method is used to ensure that an attribute can only hold a numeric value.
The :only_integer option lets you further specify that the value should only be an integer value and defaults to false.
1 classAccount < ActiveRecord::Base
2 validates_numericality_of :account_number, only_integer: true
3 end
The :even and :odd options do what you would expect and are useful for things like, I don’t know, checking electron valences. (Actually, I’m not creative enough to think of what you would use this validation for, but there you go.)
The following comparison options are also available:
· :equal_to
· :greater_than
· :greater_than_or_equal_to
· :less_than
· :less_than_or_equal_to
· :other_than
8.2.9.1 Infinity and Other Special Float Values
Interestingly, Ruby has the concept of infinity built-in. If you haven’t seen infinity before, try the following in a console:
>> (1.0/0.0)
=> Infinity
Infinity is considered a number by validates_numericality_of. Databases (like PostgreSQL) with support for the IEEE 754 standard should allow special float values like Infinity to be stored. The other special values are positive infinity (+INF), negative infinity (-INF), and not-a-number (NaN). IEEE 754 also distinguishes between positive zero (+0) and negative zero (-0). NaN is used to represent results of operations that are undefined.
8.2.10 validates_presence_of
One of the more common validation methods, validates_presence_of, is used to denote mandatory attributes. This method checks whether the attribute is blank using the blank? method, defined on Object, which returns true for values that are nil or a blank string "".
1 classAccount < ActiveRecord::Base
2 validates_presence_of :username, :email, :account_number
3 end
A common mistake is to use validates_presence_of with a boolean attribute, like the backing field for a checkbox. If you want to make sure that the attribute is true, use validates_acceptance_of instead. The boolean value false is considered blank, so if you want to make sure that only trueor false values are set on your model, use the following pattern:
validates_inclusion_of :protected, in: [true, false]
8.2.10.1 Validating the Presence and/or Existence of Associated Objects
When you’re trying to ensure that an association is present, pass validates_presence_of its foreign key attribute, not the association variable itself. Note that the validation will fail in cases when both the parent and child object are unsaved (since the foreign key will be blank).
Many developers try to use this validation with the intention of ensuring that associated objects actually exist in the database. Personally, I think that would be a valid use case for an actual foreign-key constraint in the database, but if you want to do the check in your Rails code then emulate the following example:
1 classTimesheet < ActiveRecord::Base
2 belongs_to :user
3 validates_presence_of :user_id
4 validate :user_exists
5
6 protected
7
8 def user_exists
9 errors.add(:user_id, "doesn't exist") unless User.exists?(user_id)
10 end
11 end
Without a validation, if your application violates a database foreign key constraint, you will get an Active Record exception.
8.2.11 validates_uniqueness_of
The validates_uniqueness_of method, also exclusive to Active Record, ensures that the value of an attribute is unique for all models of the same type. This validation does not work by adding a uniqueness constraint at the database level. It does work by constructing and executing a query looking for a matching record in the database. If any record is returned when this method does its query, the validation fails.
1 classAccount < ActiveRecord::Base
2 validates_uniqueness_of :username
3 end
By specifying a :scope option, additional attributes can be used to determine uniqueness. You may pass :scope one or more attribute names as symbols (putting multiple symbols in an array).
1 classAddress < ActiveRecord::Base
2 validates_uniqueness_of :line_two, scope: [:line_one, :city, :zip]
3 end
It’s also possible to specify whether to make the uniqueness constraint case-sensitive or not, via the :case_sensitive option (ignored for nontextual attributes).
With the addition of support for PostgreSQL array columns in Rails 4, the validates_uniqueness_of method can be used to validate that all items in the array are unique. PostgreSQL array columns are topic was covered in detail in Chapter 9, “Advanced Active Record”.
Tim says… This validation is not foolproof due to a potential race condition between the SELECT query that checks for duplicates and the INSERT or UPDATE which persists the record. An Active Record exception could be generated as a result, so be prepared to handle that failure in your controller. I recommend that you use a unique index constraint in the database if you absolutely must make sure that a column value is unique. |
8.2.11.1 Enforcing Uniqueness of Join Models
In the course of using join models (with has_many :through), it seems pretty common to need to make the relationship unique. Consider an application that models students, courses, and registrations with the following code:
1 classStudent < ActiveRecord::Base
2 has_many :registrations
3 has_many :courses, through: :registrations
4 end
5
6 classRegistration < ActiveRecord::Base
7 belongs_to :student
8 belongs_to :course
9 end
10
11 classCourse < ActiveRecord::Base
12 has_many :registrations
13 has_many :students, through: :registrations
14 end
How do you make sure that a student is not registered more than once for a particular course? The most concise way is to use validates_uniqueness_of with a :scope constraint. The important thing to remember with this technique is to reference the foreign keys, not the names of the associations themselves:
1 classRegistration < ActiveRecord::Base
2 belongs_to :student
3 belongs_to :course
4
5 validates_uniqueness_of :student_id, scope: :course_id,
6 message: "can only register once per course"
7 end
Notice that since the default error message generated when this validation fails would not make sense, I’ve provided a custom error message that will result in the expression: “Student can only register once per course.”
Tim says…
Astute readers will notice that the validation was on student_id but the error message references “Student.” Rails special cases this to do what you mean.
8.2.11.2 Limit Constraint Lookup
As of Rails 4, one can specify criteria that constraints a uniqueness validation against a set of records by setting the :conditions option.
To illustrate, let’s assume we have an article that requires titles to be unique against all published articles in the database. We can achieve this using validates_uniqueness_of by doing the following:
1 classArticle < ActiveRecord::Base
2 validates_uniqueness_of :title,
3 conditions: -> { where.not(published_at: nil) }
4 ...
5 end
When the model is saved, Active Record will query for title against all articles in the database that are published. If no results are returned, the model is valid.
8.2.12 validates_with
All of the validation methods we’ve covered so far are essentially local to the class in which they are used. If you want to develop a suite of custom, reusable validation classes, then you need a way to apply them to your models, and that is what the validates_with method allows you to do.
To implement a custom validator, extend ActiveRecord::Validator and implement the validate method. The record being validated is available as record and you manipulate its errors hash to log validation errors.
The following examples, from Ryan Daigle’s excellent post on this feature, demonstrate a reusable email field validator:
1 classEmailValidator < ActiveRecord::Validator
2 def validate()
3 record.errors[:email] << "is not valid" unless
4 record.email =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
5 end
6 end
7
8 classAccount < ActiveRecord::Base
9 validates_with EmailValidator
10 end
The example assumes the existence of an email attribute on the record. If you need to make your reusable validator more flexible, you can access validation options at runtime via the options hash, like this:
1 classEmailValidator < ActiveRecord::Validator
2 def validate()
3 email_field = options[:attr]
4 record.errors[email_field] << "is not valid" unless
5 record.send(email_field) =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
6 end
7 end
8
9 classAccount < ActiveRecord::Base
10 validates_with EmailValidator, attr: :email
11 end
8.2.13 RecordInvalid
Whenever you do so-called bang operations (such as save!) and a validation fails, you should be prepared to rescue ActiveRecord::RecordInvalid. Validation failures will cause RecordInvalid to be raised and its message will contain a description of the failures.
Here’s a quick example from one of my applications that has pretty restrictive validations on its User model:
>> u = User.new
=> #<User ...>
>> u.save!
ActiveRecord::RecordInvalid: Validation failed: Name can't be blank,
Password confirmation can't be blank, Password is too short (minimum
is 5 characters), Email can't be blank, Email address format is bad
8.3 Common Validation Options
The following options apply to all of the validation methods.
8.3.1 :allow_blank and :allow_nil
In some cases, you only want to trigger a validation if a value is present, in other words the attribute is optional. There are two options that provide this functionality.
The :allow_blank option skips validation if the value is blank according to the blank? method. Similarly, the :allow_nil option skips the validation if the value of the attribute is nil; it only checks for nil, and empty strings "" are not considered nil, but they are considered blank.
8.3.2 :if and :unless
The :if and :unless options is covered in the next section, “Conditional Validation.”
8.3.3 :message
As we’ve discussed earlier in the chapter, the way that the validation process registers failures is by adding items to the Errors collection of the model object being checked. Part of the error item is a specific message describing the validation failure. All of the validation methods accept a:message option so that you can override the default error message format.
1 classAccount < ActiveRecord::Base
2 validates_uniqueness_of :username, message: "is already taken"
3 end
The default English locale file in ActiveModel defines most of the standard error message templates.
inclusion: "is not included in the list"
exclusion: "is reserved"
invalid: "is invalid"
confirmation: "doesn't match %{attribute}"
accepted: "must be accepted"
empty: "can't be empty"
blank: "can't be blank"
present: "must be blank"
too_long: "is too long (maximum is %{count} characters)"
too_short: "is too short (minimum is %{count} characters)"
wrong_length: "is the wrong length (should be %{count} characters)"
not_a_number: "is not a number"
not_an_integer: "must be an integer"
greater_than: "must be greater than %{count}"
greater_than_or_equal_to: "must be greater than or equal to %{count}"
equal_to: "must be equal to %{count}"
less_than: "must be less than %{count}"
less_than_or_equal_to: "must be less than or equal to %{count}"
other_than: "must be other than %{count}"
odd: "must be odd"
even: "must be even"
The default messages only use the count variable for interpolation, where appropriate, but model, attribute, and value are always available.
validates_uniqueness_of username, message: "%{value} is already registered"
8.3.4 :on
By default, validations are run on save (both create and update operations). If you need to do so, you can limit a given validation to just one of those operations by passing the :on option either :create or :update.
Assuming that your application does not support changing emails, one good use for on: :create might be in conjunction with validates_uniqueness_of, since checking uniqueness with a query on large datasets can be time-consuming.
1 classAccount < ActiveRecord::Base
2 validates_uniqueness_of :email, on: :create
3 end
8.3.5 :strict
New to Rails 4 is the :strict validation option. Setting :strict to true causes an exception ActiveModel::StrictValidationFailed to be raised when an model is invalid.
1 classAccount < ActiveRecord::Base
2 validates :email, presence: { strict: true }
3 end
To override the type of exception raised on error, pass the custom exception to the :strict option.
8.4 Conditional Validation
Since all validation methods are implemented via the Active Model Callback API, they also accept :if and :unless options, to determine at runtime (and not during the class definition) whether the validation needs to be run or not. The following three types of arguments can be supplied as an:if and :unless options:
Symbol
The name of a method to invoke as a symbol. This is probably the most common option, and offers the best performance.
String
A snippet of Ruby code to eval might be useful when the condition is really short, but keep in mind that eval’ing statements is relatively slow.
Proc
A block of code to be instance_eval‘d, so that self is the current record. Perhaps the most elegant choice for one-line conditionals.
validates_presence_of :approver, if: -> { approved? && !legacy? }
8.4.1 Usage and Considerations
When does it make sense to use conditional validations? The answer is: whenever an object can be validly persisted in more than one state. A very common example involves the User (or Person) model, used for login and authentication.
1 validates_presence_of :password, if: :password_required?
2 validates_presence_of :password_confirmation, if: :password_required?
3 validates_length_of :password, within: 4..40, if: :password_required?
4 validates_confirmation_of :password, if: :password_required?
This code is not DRY (meaning that it is repetitive). You can refactor it to make it a little dryer using the with_options method that Rails mixes into Object.
1 with_options if: :password_required? do |user|
2 user.validates_presence_of :password
3 user.validates_presence_of :password_confirmation
4 user.validates_length_of :password, within: 4..40
5 user.validates_confirmation_of :password
6 end
All of the example validations check for the two cases when a (plaintext) password field should be required in order for the model to be valid.
1 def password_required?
2 encrypted_password.blank? || !password.blank?
3 end
The first case is if the encrypted_password attribute is blank, because that means we are dealing with a new User instance that has not been given a password yet. The other case is when the password attribute itself is not blank; perhaps this is happening during an update operation and the user is attempting to reset her password.
8.4.2 Validation Contexts
Another way to accomplish conditional validation leverages support for validation contexts. Declare a validation and pass the name of an application-specific validation context as the value of the :on option. That validation will now only be checked when explicitly invoked usingrecord.valid?(context_name).
Consider the following example involving a report generation app. Saving a report without a name is fine, but publishing one without a name is not.
1 classReport < ActiveRecord::Base
2 validates_presence_of :name, on: :publish
3 end
4
5 classReportsController < ApplicationController
6 expose(:report)
7
8 # POST /reports/1/publish
9 def publish
10 if report.valid? :publish
11 redirect_to report, notice: "Report published"
12 else
13 flash.now.alert = "Can't publish unnamed reports!"
14 render :show
15 end
16 end
17 end
8.5 Short-form Validation
Introduced in Rails 3, the validates method identifies an attribute and accepts options that correspond to the validators we’ve already covered in the chapter. Using validates can tighten up your model code nicely.
1 validates :username, presence: true,
2 format: { with: /[A-Za-z0-9]+/ },
3 length: { minimum: 3 },
4 uniqueness: true
The following options are available for use with the validates method.
absence: true
Alias for validates_absence_of. Supply additional options by replacing true with a hash.
validates :unwanted, absence: { message: "You shouldn't have set that" }
acceptance: true
Alias for validates_acceptance_of, typically used with checkboxes that indicate acceptance of terms. Supply additional options by replacing true with a hash.
validates :terms, acceptance: { message: 'You must accept terms.' }
confirmation: true
Alias for validates_confirmation_of, typically used to ensure that email and password confirmation fields match up correctly. Supply additional options by replacing true with a hash.
validates :email, confirmation: { message: 'Try again.' }
exclusion: { in: [1,2,3] }
Alias for validates_exclusion_of. If your only option is the array to exclude against, you can shorten the syntax further by supplying an array as the value.
validates :username, exclusion: %w(admin superuser)
format: { with: /.*/ }
Alias for validates_format_of. If your only option is the regular expression, you can shorten the syntax further by making it the value like:
format: /[A-Za-z0-9]+/
inclusion: { in: [1,2,3] }
Alias for validates_inclusion_of. If your only option is the inclusion array, you can shorten the syntax further by making the array the value.
validates :gender, inclusion: %w(male female)
length: { minimum: 0, maximum: 1000 }
Alias for validates_length_of. If your only options are minimum and maximum lengths, you can shorten the syntax further by supplying a Ruby range as the value.
validates :username, length: 3..20
numericality: true
Alias for validates_numericality_of. Supply additional options by replacing true with a hash.
validates :quantity, numericality: { message: 'Supply a number.' }
presence: true
Alias for validates_presence_of. Supply additional options by replacing true with a hash.
validates :username, presence: { message: 'How do you expect to login?' }
uniqueness: true
Alias for validates_uniqueness_of. Supply additional options by replacing true with a hash.
validates :quantity, uniqueness: { message: "You're SOL on that login choice, buddy!" }
8.6 Custom Validation Techniques
When the existing declarative validation macros are not enough for your application needs Rails gives you a few custom techniques.
8.6.1 Add custom validation macros to your application
Rails has the ability to add custom validation macros (available to all your model classes) by extending ActiveModel::EachValidator.
The following example is silly, but demonstrates the functionality nicely.
1 classReportLikeValidator < ActiveModel::EachValidator
2 def validate_each(record, attribute, value)
3 unless value["Report"]
4 record.errors.add(attribute, 'does not appear to be a Report')
5 end
6 end
7 end
Now that your custom validator exists, it is available to use with the validates macro in your model.
1 classReport < ActiveRecord::Base
2 validates :name, report_like: true
3 end
The key :report_like is inferred from the name of the validator class, which in this case was ReportLikeValidator.
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.
1 classLikeValidator < ActiveModel::EachValidator
2 def initialize(options)
3 @with = options[:with]
4 super
5 end
6
7 def validate_each(record, attribute, value)
8 unless value[@with]
9 record.errors.add(attribute, "does not appear to be like #{@with}")
10 end
11 end
12 end
Our model code would change to
1 classReport < ActiveRecord::Base
2 validates :name, like: { with: "Report" }
3 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.
1 classRandomlyValidator < ActiveModel::Validator
2 def validate(record)
3 record.errors[:base] << "FAIL #1" unless first_hurdle(record)
4 record.errors[:base] << "FAIL #2" unless second_hurdle(record)
5 record.errors[:base] << "FAIL #3" unless third_hurdle(record)
6 end
7
8 private
9
10 def first_hurdle(record)
11 rand > 0.3
12 end
13
14 def second_hurdle(record)
15 rand > 0.6
16 end
17
18 def third_hurdle(record)
19 rand > 0.9
20 end
21 end
Use your new custom validator in a model with the validates_with macro.
1 classReport < ActiveRecord::Base
2 validates_with RandomlyValidator
3 end
8.6.3 Add a validate method to your model
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. (This is an older technique that I can’t fully endorse; it adds complexity to your model class unnecessarily given how easy it is to create custom validator classes.)
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:
1 classCompletelyLameTotalExample < ActiveRecord::Base
2 def validate
3 if total != (attr1 + attr2 + attr3)
4 errors[:total] << "The total doesn't add up!"
5 end
6 end
7 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!"
Remember: The way to mark an object as invalid is to add to its Errors object. The return value of a custom validation method is not used.
8.7 Skipping Validations
The methods update_attribute and update_column don’t invoke validations, yet their companion method update does. Whoever wrote the API docs believes that this behavior is “especially useful for Boolean flags on existing records.””
I don’t know if that is entirely true or not, but I do know that it is the source of ongoing contention in the community. Unfortunately, I don’t have much more to add other than some simple common-sense advice: Be very careful using the update_attribute or update_column methods. It can easily persist your model objects in invalid states.
8.8 Working with the Errors Hash
Some methods are provided to allow you to add validation errors to the collection manually and alter the state of the Errors hash.
8.8.0.1 errors[:base] = msg
Adds an error message related to the overall object state itself and not the value of any particular attribute. Make your error messages complete sentences, because Rails does not do any additional processing of them to make them readable.
8.8.0.2 errors[:attribute] = msg
Adds an error message related to a particular attribute. The message should be a sentence fragment that reads naturally when prepended with the capitalized name of the attribute.
8.8.0.3 clear
As you might expect, the clear method clears the Errors collection.
8.8.1 Checking for Errors
It’s also possible to check the Errors object for validation failures on specific attributes with a couple of methods, just using square brackets notation. An array is always returned; an empty one when there aren’t any validation errors for the attribute specified.
>> user.errors[:login]
=> ["zed is already registered"]
>> user.errors[:password]
=> []
Alternatively, one could also access full error messages for a specific attribute using the full_messages_for method. Just like accessing validation failures for attributes using bracket notation, an array is always returned.
>> user.errors.full_messages_for(:email)
=> ["Email can't be blank"]
8.9 Testing Validations with Shoulda
Even though validations are declarative code, if you’re doing TDD then you’ll want to specify them before writing them. Luckily, Thoughtbot’s Shoulda Matchers library contains a number of matchers designed to easily test validations.
1 describe Post do
2 it { should validate_uniqueness_of(:title) }
3 it { should validate_presence_of(:body).with_message(/wtf/) }
4 it { should validate_presence_of(:title) }
5 it { should validate_numericality_of(:user_id) }
6 end
7
8 describe User do
9 it { should_not allow_value("blah").for(:email) }
10 it { should_not allow_value("b lah").for(:email) }
11 it { should allow_value("a@b.com").for(:email) }
12 it { should allow_value("asdf@asdf.com").for(:email) }
13 it { should ensure_length_of(:email).is_at_least(1).is_at_most(100) }
14 it { should ensure_inclusion_of(:age).in_range(1..100) }
15 end
8.10 Conclusion
In this (relatively speaking) short chapter, we covered the ActiveRecord Validations API in-depth. One of the most appealing aspects of Rails is how we can declaratively specify the criteria for determining the validity of model objects.