|
Peter Marklund's Home |
Rails Custom Validation
Suppose you have validation code for a certain type of data that you want to reuse across your ActiveRecord models. What is the best approach to doing this in Rails? As you probably know, Rails comes with a number of validation macros such as validates_presence_of, validates_format_of, and validates_uniqueness_of etc. One approach is to write your own validation macro that wraps one of the Rails validation macros. The validates_as_email plugin shows us how:
module ActiveRecord
module Validations
module ClassMethods
def validates_as_email(*attr_names)
configuration = {
:message => 'is an invalid email',
:with => RFC822::EmailAddress,
:allow_nil => true }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
validates_format_of attr_names, configuration
end
end
end
end
The RFC822::EmailAddress constant in the code above is a huge regexp that I'm not going to list here... A macro that wraps validates_format_of works well as long as your validation can be done with a regexp. What about validations that are too unwieldly or impossible to do with a single Regexp? Suppose you want to validate phone numbers to make sure they have 10 digits and only contain the characters 0-9()/-.+. Using validates_format_of as a starting point we could write our own macro:
def validates_as_phone(*attr_names)
configuration = {
:message => 'is an invalid phone number, must contain at least 5 digits, only the following characters are allowed: 0-9/-()+',
:on => :save
}
validates_each(attr_names, configuration) do |record, attr_name, value|
n_digits = value.scan(/[0-9]/).size
valid_chars = (value =~ /^[+\/\-() 0-9]+$/)
if !(n_digits > 5 && valid_chars)
record.errors.add(attr_name, configuration[:message])
end
end
end
In the general case we could reduce duplication by hacking validates_format_of and make its :with option accept Proc objects:
def validates_format_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
raise(ArgumentError,
"A regular expression or Proc object / method must be supplied as the :with option of the configuration hash") \
unless (configuration[:with].is_a?(Regexp) || validation_block?(configuration[:with]))
validates_each(attr_names, configuration) do |record, attr_name, value|
if validation_block?(configuration[:with])
value_valid = configuration[:with].send('call', value)
else
value_valid = (value.to_s =~ configuration[:with])
end
record.errors.add(attr_name, configuration[:message]) unless value_valid
end
end
def validation_block?(validation)
validation.respond_to?("call") && (validation.arity == 1 || validation.arity == -1)
end
Using our new validates_format_of could look like:
validates_format_of :phone, :fax, :with => Mcm::Validations.valid_phone
module Mcm
class Validations
def self.valid_phone
Proc.new do |number|
return_value = false
if !number.nil?
n_digits = number.scan(/[0-9]/).size
valid_chars = (number =~ /^[+\/\-() 0-9]+$/)
return_value = (n_digits > 5 && valid_chars)
end
return_value
end
end
end
end
An issue with this approach is that the validation proc object may want to determine what the error message should be. What we could do is adopt the convention that if the Proc object returns a string, then that is the error message.
After all this touching of interal Rails method (potentially making us vulnerable to upgrades), we arrive at the safer and simpler approach that Rails provides for custom validations, namely to implement the validate method:
# In the ActiveRecord class to be validated
def validate
Mcm::Validations.validate_phone(self, 'phone', 'fax')
end
# In lib
class Mcm::Validations
def self.validate_phone(model, *attributes)
error_message = 'is an invalid phone number, must contain at least 5 digits, only the following characters are allowed: 0-9/-()+'
attributes.each do |attribute|
model.errors.add(attribute, error_message) unless Mcm::Validations.valid_phone?(model.send(attribute))
end
end
def self.valid_phone?(number)
return true if number.nil?
n_digits = number.scan(/[0-9]/).size
valid_chars = (number =~ /^[+\/\-() 0-9]+$/)
return n_digits > 5 && valid_chars
end
end
# In config/environemnt.rb (not sure why this is needed)
require 'validations'
We can make the validate method approach more Rails like by using a mixin instead:
# In the ActiveRecord class to be validated
def validate
validate_phone('phone', 'fax')
end
module Mcm::Validations
def validate_phone(*attributes)
error_message = 'is an invalid phone number, must contain at least 5 digits, only the following characters are allowed: 0-9/-()+'
attributes.each do |attribute|
self.errors.add(attribute, error_message) unless valid_phone?(self.send(attribute))
end
end
def valid_phone?(number)
return true if number.nil?
n_digits = number.scan(/[0-9]/).size
valid_chars = (number =~ /^[+\/\-() 0-9]+$/)
return n_digits > 5 && valid_chars
end
end
class ActiveRecord::Base
include Mcm::Validations
end
Which looks clean and I'm reasonably happy with this last approach. Finally...
Comments
Jesse Clark said over 2 years ago:
Nice explanation. Thanks.
I am not exactly clear on where in my rails app to add this bit though:
class ActiveRecord::Base
include Mcm::Validations
end
I tried adding it at the bottom of the validations.rb file I created in /lib but that didn’t seem to do it..
Also just a minor note on the validation:
Adding a constant to the Module for the number of digits allowed makes the final solution very slightly DRYer. Although, it might be nice to add this as a parameter with a default value. This would give the calling code the flexibility of deciding how many digits to require.
Jesse Clark said over 2 years ago:
I was able to get the final listing working by still adding "require ‘validations’ to environment.rb but I had assumed that part of the point of mixing in to ActiveRecord::Base was that this would be unnecessary…?
Peter Marklund said over 2 years ago:
Jesse,
no, you shouldn’t have to require the validation, Rails will automatically load it for you when the Module is first encountered (since the lib dir is in the load path). Just make sure your file has the same name as the module, and also that any namespace has a corresponding directory. In my case the Mcm::Validations module should be in the file lib/mcm/validations.rb
Serene said over 2 years ago:
Hi,
I am learning Ruby on Rails.
I have question/s on creating my own validation rules.
1. Can it be written in this manner?
class Group < ActiveRecord::Base
# Table has id, name, parent_id, created_at
# Prevent user from deleting a group when it is a parent of another group/s
def validate
errors.add_to_base("Cannot delete #{@name} as parent of another group/s" if find_by_parent_id(@id)
end
end
2. How do I access group's attributes within itself (i.e. group)? By @id, @parent_id? Or, by group.id, group.parent_id?
3. What if my validation in 'group' also involved accessing another table? How do I access from, say user, from group model?
4. Could you recommend some websites where I could have access to good documentation and tips?
I would appreciate very much if you could assist. I have been looking around for quite a long time for answer. Hope to hear from you. Thanks.
Jeff Wattenmaker said over 2 years ago:
got it to work, thanks
to allow blanks I had to add the following
return true if number.nil?
return true if number.size == 0
for some reason my number was size 0, not nil
also the error message says it number must be 5, yet it uses >5 which means 6.
I changed it to >= 5 and not it matches the error message
velu said over 2 years ago:
i do not able to understand
fgfd said about 1 year ago:
gdfg
Armen said about 1 year ago:
barev Artur
Armen said about 1 year ago:
heriqa nayes
Arthur said about 1 year ago:
barev inch chka
Karol said about 1 year ago:
Thanks a lot
Matt said about 1 year ago:
Good stuff, thanks!
julie said about 1 year ago:
Very nice information
Harsha vaiashnav said about 1 year ago:
Good information,,,thanks a lot
a.non said 8 months ago:
The question Serene asked pretty much cover what i don't understand too :/
ashu said about 1 month ago:
i need help to create a validtaion on the range of seat numbers of bookings of a show that new entry does not overlap on it can you help on that i am a fresher




evan said over 3 years ago:
Thanks! Needed this information today and your explanations are good.