module ActiveObject # Raised by save! and create! when the object is invalid. Use the # +object+ method to retrieve the object which did not validate. # begin # complex_operation_that_calls_save!_internally # rescue ActiveObject::ObjectInvalid => invalid # puts invalid.record.errors # end class ObjectInvalid < ActiveObjectError attr_reader :object def initialize(object) @object = object super("Validation failed: #{@object.errors.full_messages.join(", ")}") end end class Errors include Enumerable def initialize(base) # :nodoc: @base, @errors = base, {} end @@default_error_messages = { :inclusion => "is not included in the list", :exclusion => "is reserved", :invalid => "is invalid", :confirmation => "doesn't match confirmation", :accepted => "must be accepted", :empty => "can't be empty", :blank => "can't be blank", :too_long => "is too long (maximum is %d characters)", :too_short => "is too short (minimum is %d characters)", :wrong_length => "is the wrong length (should be %d characters)", :taken => "has already been taken", :not_a_number => "is not a number" } # Holds a hash with all the default error messages, such that they can be replaced by your own copy or localizations. cattr_accessor :default_error_messages # Adds an error to the base object instead of any particular attribute. This is used # to report errors that don't tie to any specific attribute, but rather to the object # as a whole. These error messages don't get prepended with any field name when iterating # with +each_full+, so they should be complete sentences. def add_to_base(msg) add(:base, msg) end # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to on(attribute) # for the same attribute and ensure that this error object returns false when asked if empty?. More than one # error can be added to the same +attribute+ in which case an array will be returned on a call to on(attribute). # If no +msg+ is supplied, "invalid" is assumed. def add(attribute, msg = @@default_error_messages[:invalid]) @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? @errors[attribute.to_s] << msg end # Will add an error message to each of the attributes in +attributes+ that is empty. def add_on_empty(attributes, msg = @@default_error_messages[:empty]) for attr in [attributes].flatten value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] is_empty = value.respond_to?("empty?") ? value.empty? : false add(attr, msg) unless !value.nil? && !is_empty end end # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). def add_on_blank(attributes, msg = @@default_error_messages[:blank]) for attr in [attributes].flatten value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] add(attr, msg) if value.blank? end end # Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+. # If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg. def add_on_boundary_breaking(attributes, range, too_long_msg = @@default_error_messages[:too_long], too_short_msg = @@default_error_messages[:too_short]) for attr in [attributes].flatten value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] add(attr, too_short_msg % range.begin) if value && value.length < range.begin add(attr, too_long_msg % range.end) if value && value.length > range.end end end alias :add_on_boundry_breaking :add_on_boundary_breaking def invalid?(attribute) !@errors[attribute.to_s].nil? end def on(attribute) errors = @errors[attribute.to_s] return nil if errors.nil? errors.size == 1 ? errors.first : errors end alias :[] :on # Returns errors assigned to the base object through +add_to_base+ according to the normal rules of on(attribute). def on_base on(:base) end # Yields each attribute and associated message per error added. # # class Company < ActiveObject::Base # validates_presence_of :name, :address, :email # validates_length_of :name, :in => 5..30 # end # # company = Company.create(:address => '123 First St.') # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # # => name - is too short (minimum is 5 characters) # # name - can't be blank # # address - can't be blank def each @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } end # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned # through iteration as "First name can't be empty". # # class Company < ActiveObject::Base # validates_presence_of :name, :address, :email # validates_length_of :name, :in => 5..30 # end # # company = Company.create(:address => '123 First St.') # company.errors.each_full{|msg| puts msg } # # => Name is too short (minimum is 5 characters) # # Name can't be blank # # Address can't be blank def each_full full_messages.each { |msg| yield msg } end # Returns all the full error messages in an array. # # class Company < ActiveObject::Base # validates_presence_of :name, :address, :email # validates_length_of :name, :in => 5..30 # end # # company = Company.create(:address => '123 First St.') # company.errors.full_messages # => # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] def full_messages(options = {}) full_messages = [] @errors.each_key do |attr| @errors[attr].each do |message| next unless message if attr == "base" full_messages << message else full_messages << attr.to_s + ' ' + message.to_s end end end full_messages end # Returns true if no errors have been added. def empty? @errors.empty? end # Removes all errors that have been added. def clear @errors = {} end # Returns the total number of errors added. Two errors added to the same attribute will be counted as such. def size @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size } end alias_method :count, :size alias_method :length, :size end module Validations VALIDATIONS = %w( validate validate_on_create validate_on_update ) def self.included(base) # :nodoc: base.extend ClassMethods base.class_eval do alias_method_chain :save, :validation alias_method_chain :save!, :validation end base.send :include, ActiveSupport::Callbacks base.define_callbacks *VALIDATIONS end # Ant Mapper classes can implement validations in several ways. The highest level, easiest to read, # and recommended approach is to use the declarative validates_..._of class methods (and # +validates_associated+) documented below. These are sufficient for most model validations. # # Slightly lower level is +validates_each+. It provides some of the same options as the purely declarative # validation methods, but like all the lower-level approaches it requires manually adding to the errors collection # when the record is invalid. # # At a yet lower level, a model can use the class methods +validate+, +validate_on_create+ and +validate_on_update+ # to add validation methods or blocks. These are AntSupport::Callbacks and follow the same rules of inheritance # and chaining. # # The lowest level style is to define the instance methods +validate+, +validate_on_create+ and +validate_on_update+ # as documented in AntObject::Validations. # # == +validate+, +validate_on_create+ and +validate_on_update+ Class Methods # # Calls to these methods add a validation method or block to the class. Again, this approach is recommended # only when the higher-level methods documented below (validates_..._of and +validates_associated+) are # insufficient to handle the required validation. # # This can be done with a symbol pointing to a method: # # class Comment < ActiveObject::Base # validate :must_be_friends # # def must_be_friends # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) # end # end # # Or with a block which is passed the current record to be validated: # # class Comment < ActiveObject::Base # validate do |comment| # comment.must_be_friends # end # # def must_be_friends # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) # end # end # # This usage applies to +validate_on_create+ and +validate_on_update+ as well. module ClassMethods DEFAULT_VALIDATION_OPTIONS = { :on => :save, :allow_nil => false, :allow_blank => false, :message => nil }.freeze ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=', :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=', :odd => 'odd?', :even => 'even?' }.freeze # Validates each attribute against a block. # # class Person < ActiveObject::Base # validates_each :first_name, :last_name do |record, attr, value| # record.errors.add attr, 'starts with z.' if value[0] == ?z # end # end # # Options: # * :on - Specifies when this validation is active (default is :save, other options :create, :update). # * :allow_nil - Skip validation if attribute is +nil+. # * :allow_blank - Skip validation if attribute is blank. # * :if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The # method, proc or string should return or evaluate to a true or false value. # * :unless - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_each(*attrs) options = attrs.extract_options!.symbolize_keys attrs = attrs.flatten # Declare the validation. send(validation_method(options[:on] || :save), options) do |record| attrs.each do |attr| value = record.send(attr) next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) yield record, attr, value end end end # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: # # Model: # class Person < ActiveObject::Base # validates_confirmation_of :user_name, :password # validates_confirmation_of :email_address, :message => "should match confirmation" # end # # View: # <%= password_field "person", "password" %> # <%= password_field "person", "password_confirmation" %> # # The added +password_confirmation+ attribute is virtual; it exists only as an in-memory attribute for validating the password. # To achieve this, the validation adds accessors to the model for the confirmation attribute. NOTE: This check is performed # only if +password_confirmation+ is not +nil+, and by default only on save. To require confirmation, make sure to add a presence # check for the confirmation attribute: # # validates_presence_of :password_confirmation, :if => :password_changed? # # Configuration options: # * :message - A custom error message (default is: "doesn't match confirmation"). # * :on - Specifies when this validation is active (default is :save, other options :create, :update). # * :if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The # method, proc or string should return or evaluate to a true or false value. # * :unless - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_confirmation_of(*attr_names) configuration = { :on => :save } configuration.update(attr_names.extract_options!) attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) validates_each(attr_names, configuration) do |record, attr_name, value| unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") record.errors.add(attr_name, :default => configuration[:message]) end end end # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: # # class Person < ActiveObject::Base # validates_acceptance_of :terms_of_service # validates_acceptance_of :eula, :message => "must be abided" # end # # If the database column does not exist, the +terms_of_service+ attribute is entirely virtual. This check is # performed only if +terms_of_service+ is not +nil+ and by default on save. # # Configuration options: # * :message - A custom error message (default is: "must be accepted"). # * :on - Specifies when this validation is active (default is :save, other options :create, :update). # * :allow_nil - Skip validation if attribute is +nil+ (default is true). # * :accept - Specifies value that is considered accepted. The default value is a string "1", which # makes it easy to relate to an HTML checkbox. This should be set to +true+ if you are validating a database # column, since the attribute is typecast from "1" to +true+ before validation. # * :if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The # method, proc or string should return or evaluate to a true or false value. # * :unless - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_acceptance_of(*attr_names) configuration = { :on => :save, :allow_nil => true, :accept => "1" } configuration.update(attr_names.extract_options!) names = attr_names.reject { |name| self.class.attributes.include?(name.to_s) } attr_accessor(*names) validates_each(attr_names,configuration) do |record, attr_name, value| unless value == configuration[:accept] record.errors.add(attr_name, :default => configuration[:message]) end end end # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example: # # class Person < ActiveObject::Base # validates_presence_of :first_name # end # # The first_name attribute must be in the object and it cannot be blank. # # If you want to validate the presence of a boolean field (where the real values are true and false), # you will want to use validates_inclusion_of :field_name, :in => [true, false] # This is due to the way Object#blank? handles boolean values. false.blank? # => true # # Configuration options: # * message - A custom error message (default is: "can't be blank"). # * on - Specifies when this validation is active (default is :save, other options :create, :update). # * if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The # method, proc or string should return or evaluate to a true or false value. # * unless - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. # def validates_presence_of(*attr_names) configuration = { :message => ActiveObject::Errors.default_error_messages[:blank], :on => :save } configuration.update(attr_names.extract_options!) # can't use validates_each here, because it cannot cope with nonexistent attributes, # while errors.add_on_empty can send(validation_method(configuration[:on]), configuration) do |record| record.errors.add_on_blank(attr_names, configuration[:message]) end end # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: # # class Person < ActiveObject::Base # validates_length_of :first_name, :maximum=>30 # validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind" # validates_length_of :fax, :in => 7..32, :allow_nil => true # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" # validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character" # validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me." # end # # Configuration options: # * minimum - The minimum size of the attribute # * maximum - The maximum size of the attribute # * is - The exact size of the attribute # * within - A range specifying the minimum and maximum size of the attribute # * in - A synonym(or alias) for :within # * allow_nil - Attribute may be nil; skip validation. # # * too_long - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)") # * too_short - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)") # * wrong_length - The error message if using the :is method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)") # * message - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message # * on - Specifies when this validation is active (default is :save, other options :create, :update) # * if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_length_of(*attrs) # Merge given options with defaults. options = { :too_long => ActiveObject::Errors.default_error_messages[:too_long], :too_short => ActiveObject::Errors.default_error_messages[:too_short], :wrong_length => ActiveObject::Errors.default_error_messages[:wrong_length] }.merge(DEFAULT_VALIDATION_OPTIONS) options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash) # Ensure that one and only one range option is specified. range_options = ALL_RANGE_OPTIONS & options.keys case range_options.size when 0 raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' when 1 # Valid number of options; do nothing. else raise ArgumentError, 'Too many range options specified. Choose only one.' end # Get range option and value. option = range_options.first option_value = options[range_options.first] case option when :within, :in raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) too_short = options[:too_short] % option_value.begin too_long = options[:too_long] % option_value.end validates_each(attrs, options) do |record, attr, value| if value.nil? or value.split(//).size < option_value.begin record.errors.add(attr, too_short) elsif value.split(//).size > option_value.end record.errors.add(attr, too_long) end end when :is, :minimum, :maximum raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 # Declare different validations per option. validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } message = (options[:message] || options[message_options[option]]) % option_value validates_each(attrs, options) do |record, attr, value| if value.kind_of?(String) record.errors.add(attr, message) unless !value.nil? and value.split(//).size.method(validity_checks[option])[option_value] else record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] end end end end alias_method :validates_size_of, :validates_length_of # Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression # provided. # # class Person < ActiveObject::Base # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create # end # # Note: use \A and \Z to match the start and end of the string, ^ and $ match the start/end of a line. # # A regular expression must be provided or else an exception will be raised. # # Configuration options: # * message - A custom error message (default is: "is invalid") # * with - The regular expression used to validate the format with (note: must be supplied!) # * on Specifies when this validation is active (default is :save, other options :create, :update) # * if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_format_of(*attr_names) configuration = { :message => ActiveObject::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 must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) validates_each(attr_names, configuration) do |record, attr_name, value| record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with] end end # Validates whether the value of the specified attribute is available in a particular enumerable object. # # class Person < ActiveObject::Base # validates_inclusion_of :gender, :in=>%w( m f ), :message=>"woah! what are you then!??!!" # validates_inclusion_of :age, :in=>0..99 # end # # Configuration options: # * in - An enumerable object of available items # * message - Specifies a customer error message (default is: "is not included in the list") # * allow_nil - If set to true, skips this validation if the attribute is null (default is: false) # * if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_inclusion_of(*attr_names) configuration = { :message => ActiveObject::Errors.default_error_messages[:inclusion], :on => :save } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) enum = configuration[:in] || configuration[:within] raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") validates_each(attr_names, configuration) do |record, attr_name, value| record.errors.add(attr_name, configuration[:message]) unless enum.include?(value) end end # Validates that the value of the specified attribute is not in a particular enumerable object. # # class Person < ActiveObject::Base # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here" # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60" # end # # Configuration options: # * in - An enumerable object of items that the value shouldn't be part of # * message - Specifies a customer error message (default is: "is reserved") # * allow_nil - If set to true, skips this validation if the attribute is null (default is: false) # * if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_exclusion_of(*attr_names) configuration = { :message => ActiveObject::Errors.default_error_messages[:exclusion], :on => :save } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) enum = configuration[:in] || configuration[:within] raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") validates_each(attr_names, configuration) do |record, attr_name, value| record.errors.add(attr_name, configuration[:message]) if enum.include?(value) end end # Validates whether the value of the specified attribute is numeric by trying to convert it to # a float with Kernel.Float (if integer is false) or applying it to the regular expression # /\A[\+\-]?\d+\Z/ (if integer is set to true). # # class Person < ActiveObject::Base # validates_numericality_of :value, :on => :create # end # # Configuration options: # * message - A custom error message (default is: "is not a number") # * on Specifies when this validation is active (default is :save, other options :create, :update) # * only_integer Specifies whether the value has to be an integer, e.g. an integral value (default is false) # * allow_nil Skip validation if attribute is nil (default is false). Notice that for fixnum and float columns empty strings are converted to nil # * if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_numericality_of(*attr_names) configuration = { :message => ActiveObject::Errors.default_error_messages[:not_a_number], :on => :save, :only_integer => false, :allow_nil => false } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) if configuration[:only_integer] validates_each(attr_names,configuration) do |record, attr_name,value| record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_before_type_cast").to_s =~ /\A[+-]?\d+\Z/ end else validates_each(attr_names,configuration) do |record, attr_name,value| next if configuration[:allow_nil] and record.send("#{attr_name}_before_type_cast").nil? begin Kernel.Float(record.send("#{attr_name}_before_type_cast").to_s) rescue ArgumentError, TypeError record.errors.add(attr_name, configuration[:message]) end end end end # Creates an object just like Base.create but calls save! instead of save # so an exception is raised if the record is invalid. def create!(attributes = nil, &block) if attributes.is_a?(Array) attributes.collect { |attr| create!(attr, &block) } else object = new(attributes) yield(object) if block_given? object.save! object end end private def validation_method(on) case on when :save then :validate when :create then :validate_on_create when :update then :validate_on_update end end end # The validation process on save can be skipped by passing false. The regular Base#save method is # replaced with this when the validations module is mixed in, which it is by default. def save_with_validation(perform_validation = true) if perform_validation && valid? || !perform_validation save_without_validation else false end end # Attempts to save the record just like Base#save but will raise a ObjectInvalid exception instead of returning false # if the record is not valid. def save_with_validation! if valid? save_without_validation! else raise ObjectInvalid.new(self) end end # Runs +validate+ and +validate_on_create+ or +validate_on_update+ and returns true if no errors were added otherwise false. def valid? errors.clear run_callbacks(:validate) validate if new_record? run_callbacks(:validate_on_create) validate_on_create else run_callbacks(:validate_on_update) validate_on_update end errors.empty? end # Returns the Errors object that holds all information about attribute error messages. def errors @errors ||= Errors.new(self) end protected # Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes. def validate #:doc: end # Overwrite this method for validation checks used only on creation. def validate_on_create #:doc: end # Overwrite this method for validation checks used only on updates. def validate_on_update # :doc: end end end