The tests shown above are valuable, but also limited. They test a single value against a limited set of possibilities and don’t allow interactions among different values. While Rails makes it easy to do easy things, it fortunately also make it fairly easy to do more complicated things. (You can find these more complicated examples in ch07/guestbook006.)
One of the simplest tests is to require a validation if, and
only if, another condition is met. The :if
parameter, available on every test, lets you define
those conditions. (There’s a corresponding :unless
parameter that works similarly but
in the opposite direction.) The easiest way to use :if
is to point it at a method that returns
a boolean value. That way your code can stay readable, and you can put
whatever complications are involved in the test into a more
maintainable and testable separate method.
This example uses the value of the :can_send_email
field to determine whether
the :description
field must have a
value. Neither is a field that would typically need much validation,
but they can easily be treated as connected:
# if person says 'can send email', then we'd like them to fill their # description in, so we understand who it is we're sending mail to validates_presence_of :description,:if => :require_description_presence?
# we define the supporting condition heredef require_description_presence?
self.can_send_email
end
The validates_presence_of
method will only perform its test if the condition
specified by the :if
parameter
returns true
. The :if
parameter’s value comes from the
require_description_presence?
method, which in this case simply returns the value of can_send_mail
.
Two small things to note about the require_description_presence?
method.
First, its name ends in a question mark, which is an easy way to
flag that a method returns a boolean value. Second, it doesn’t seem
to do anything—but Ruby returns the value of the last thing touched,
so the value of self.can_send_email
becomes the return
value.
While Rails’ built-in validation is very helpful for a broad range of data
checking, there are always going to be times when it’s just not
enough. For example, while Rails can check the length of a string in
characters, it didn’t have a method that checks the length of a string
in words until Rails 2.2 added validates_length_of
.
Performing such checks requires two steps. First, you need to
create a method called validate
.
ActiveRecord will always call the validate method if one is present.
It’s best, however, not to perform your validations directly in that
method. Instead, put calls to readily identifiable methods that
contain your custom logic. To indicate that validation failed, use
self.errors.add
, as shown in Example 7-4. This will tell
Rails that there is an error and which field it applies to, as well as
give you a chance to add a message to the user.
Example 7-4. Custom validation with validate and self.errors
def validate
validate_description end def validate_description # only do this validation if description is providedunless self.description.blank? then
# simple way of calculating words: split the text on whitespace num_words = self.description.split.length if num_words < 5 then self.errors.add(:description, "must be at least 5 words long")
elsif num_words > 50 then self.errors.add(:description, "must be at most 50 words long") end end end
When you perform validation this way, Rails does less work for
you. The unless
self.description.blank?
line is necessary because you can’t
just specify allow_nil
=> true
. Similarly, there aren’t any automatically
generated messages. You have to provide them. And finally, of course,
you’re responsible for all of the validation logic itself.
Rails also offers a validates_each
method that can help you create more descriptively
named validations. For more, see http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates_each.