Every validation extends the base class of ActiveModel::Validator (which handles validating the whole record) or ActiveModel::EachValidator (which handles validating the attribute on the record). The latter module also takes care of :allow_nil and :allow_blank options for you, so the validations are not even invoked when the value doesn’t match the given conditions. I’m going to give a quick example of how these two base classes differ.
Any class that inherits from ActiveModel::Validator must implement a method called validate which accepts a record.
# define my validator
class MyValidator < ActiveModel::Validator
# implement the method where the validation logic must reside
def validate(record)
# do my validations on the record and add errors if necessary
record.errors[:base] << "This is some custom error message"
record.errors[:first_name] << "This is some complex validation"
end
end
class Person
# include my validator and validate the record
include ActiveModel::MyValidator
validates_with MyValidator
end
# This method can be used to validate the whole record
# This method below, on the other hand, lets you validate one attribute
class TitleValidator < ActiveModel::EachValidator
# implement the method called during validation
def validate_each(record, attribute, value)
record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value)
end
end
The TitleValidator class inherits from ActiveModel::EachValidator. We have to define one method in the class, validate_each, that takes three parameters called object, attribute and value. The method then checks that the value include in array i.e ['Mr.', 'Mrs.', 'Dr.'] and if not it will add the attribute to the objects errors.
class Person
include ActiveModel::Validations
# validate the :title attribute with PresenceValidator and TitleValidator
validates :title, :presence => true, :title => true
end
You might have noticed that in the second example, the TitleValidator is “magically” invoked upon the :title => true option in the validates :title statement.
This is another really cool feature that ActiveModel::Validator introduces – all the options passed to validates method stripped of reserved keys (:if, :unless, :on, :allow_blank, :allow_nil) and the remaining keys are resolved to class names with a const_get("#{key.to_s.camelize}Validator") call.
Having an title key in the validates hash means that the validator will look for a class called title_validator and passes the validation behaviour into the custom class that we just wrote.
# this is resolved to BarValidator class
validates :name, :bar => true
# this is resolved to FancyValidationWithPoniesValidator class
validates :name, :fancy_validation_with_ponies => true
# and this is what is called internally after resolving the class name
validates_with(FancyValidationWithPoniesValidator, options_and_attributes)
One more thing: if the value of a validation method key is a hash, it will be passed as options to the validator.
validates :foo, :bar => { :amount => 100, :if => :has_bar?, :on => :save, :allow_nil => true }
# this is what BarValidator is passed as options
options => { :amount => 100, :allow_nil => true }
#Email Validators
require 'active_model'
require 'active_support/all'
require 'active_model/validations'
require 'mail'
# http://my.rails-royce.org/2010/07/21/email-validation-in-ruby-on-rails-without-regexp/
class EmailValidator < ActiveModel::EachValidator
def validate_each(record,attribute,value)
begin
return true if value.blank?
m = Mail::Address.new(value)
# We must check that value contains a domain and that value is an email address
r = m.domain && m.address == value
t = m.__send__(:tree)
# We need to dig into treetop
# A valid domain must have dot_atom_text elements size > 1
# user@localhost is excluded
# treetop must respond to domain
# We exclude valid email values like <user@localhost.com>
# Hence we use m.__send__(tree).domain
r &&= (t.domain.dot_atom_text.elements.size > 1)
rescue Exception => e
r = false
end
record.errors[attribute] << (options[:message] || "is invalid") unless r
end
end
#Full Name Validator
class FullNameValidator < ActiveModel::Validator
include NameValidator
def validate(record)
[:first_name, :last_name].each do |attribute|
value = record.send(attribute)
record.errors[attribute] << "can't be blank" if value.blank?
if !value.blank?
record.errors[attribute] << "is too short" if value.size < min_length
record.errors[attribute] << "is too long" if value.size > max_length
record.errors[attribute] << "has invalid characters" unless value =~ /^[a-zA-Z\-\ ]*?$/
end
end
end
end
#NameValidator modules which needed in fullname validator
module NameValidator
mattr_accessor :name_max_length
mattr_accessor :name_min_length
def self.name_length range
@@name_min_length = range.min
@@name_max_length = range.max
end
def min_length
@@name_min_length || 2
end
def max_length
@@name_max_length || 30
end
end
#Account Number validator
class AccountNumberValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
mod_10(value, record, attribute)
end
private
def mod_10 ccNumb, record, attribute
# Created by: David Leppek
#
# Basically, the alorithum takes each digit, from right to left and muliplies each second
# digit by two. If the multiple is two-digits long (i.e.: 6 * 2 = 12) the two digits of
# the multiple are then added together for a new number (1 + 2 = 3). You then add up the
# string of numbers, both unaltered and new values and get a total sum. This sum is then
# divided by 10 and the remainder should be zero if it is a valid credit card. Hense the
# name Mod 10 or Modulus 10.
valid = "0123456789" # Valid digits in a credit card number
len = ccNumb.length # The length of the submitted cc number
iCCN = ccNumb.to_i # integer of ccNumb
sCCN = ccNumb.to_s # string of ccNumb
sCCN = sCCN.strip
iTotal = 0 # integer total set at zero
bNum = true # by default assume it is a number
bResult = false # by default assume it is NOT a valid cc
# Determine if the ccNumb is in fact all numbers
[0..len].each do |j|
temp = sCCN[j, j + 1]
bNum = false if !valid.include?(temp)
end
# if it is NOT a number, you can either alert to the fact, or just pass a failure
bResult = false if !bNum
# Determine if it is the proper length
if len == 0 && bResult
# nothing, field is blank AND passed above # check
bResult = false;
else
#ccNumb is a number and the proper length - let's see if it is a valid card number
if len >= 15
# 15 or 16 for Amex or V/MC
[len..0].each do |i|
# LOOP throught the digits of the card
calc = iCCN.to_i % 10 # right most digit
calc = calc.to_i # assure it is an integer
iTotal += calc # running total of the card number as we loop - Do Nothing to first digit
i-- # decrement the count - move to the next digit in the card
iCCN = iCCN / 10 # subtracts right most digit from ccNumb
calc = iCCN.to_i % 10 # NEXT right most digit
calc = calc * 2 # multiply the digit by two
# Instead of some screwy method of converting 16 to a string and then parsing 1 and 6 and then adding them to make 7,
# I use a simple switch statement to change the value of calc2 to 7 if 16 is the multiple.
case calc
when 10
calc = 1 # 5*2=10 & 1+0 = 1
when 12
calc = 3 # 6*2=12 & 1+2 = 3
when 14
calc = 5 # 7*2=14 & 1+4 = 5
when 16
calc = 7 # 8*2=16 & 1+6 = 7
when 18
calc = 9 # 9*2=18 & 1+8 = 9
else
calc = calc # 4*2= 8 & 8 = 8 -same for all lower numbers
end
iCCN = iCCN / 10 # subtracts right most digit from ccNum
iTotal += calc # running total of the card number as we loop
end
if (iTotal % 10) ==0
# check to see if the sum Mod 10 is zero
bResult = true # This IS (or could be) a valid credit card number.
else
bResult = false # This could NOT be a valid credit card number
end
end
end
# change alert to on-page display or other indication as needed.
if !bResult
record.errors[attribute] << "is not a valid credit card number"
end
end
end