# * George Moschovitis # (c) 2004-2005 Navel, all rights reserved. # $Id$ module N # Implements a meta-language for validating managed # objects. Typically used in Validator objects but can be # included in managed objects too. # # === Example # # class User # prop_accessor :name, String # prop_accessor :level, Fixnum # # validate_length :name, :range => 2..6 # validate_unique :name, :msg => :name_allready_exists # validate_format :name, :format => /[a-z]*/, :msg => 'invalid format', :on => :create # end # # class N::CustomUserValidator # include N::Validation # validate_length :name, :range => 2..6, :msg_short => :name_too_short, :msg_long => :name_too_long # end # # user = @request.fill(User.new) # user.level = 15 # # unless user.valid? # user.save # else # p user.errors[:name] # end # # unless user.save # p user.errors.on(:name) # end # # unless errors = N::CustomUserValidator.errors(user) # user.save # else # p errors[:name] # end # #-- # TODO: all validation methods should imply validate_value # TODO: add validate_unique #++ module Validation # Encapsulates a list of validation errors. class Errors attr_accessor :errors cattr_accessor :no_value, 'No value provided' cattr_accessor :no_confirmation, 'Invalid confirmation' cattr_accessor :invalid_format, 'Invalid format' cattr_accessor :too_short, 'Too short, must be more than %d characters long' cattr_accessor :too_long, 'Too long, must be less than %d characters long' cattr_accessor :invalid_length, 'Must be %d characters long' cattr_accessor :no_inclusion, 'The value is invalid' def initialize @errors = {} end def add(attr, message) (@errors[attr] ||= []) << message end def on(attr) @errors[attr] end alias_method :[], :on # Yields each attribute and associated message. def each @errors.each_key do |attr| @errors[attr].each { |msg| yield attr, msg } end end def size @errors.size end alias_method :count, :size def empty? @errors.empty? end def clear @errors.clear end end # If the validate method returns true, this # attributes holds the errors found. attr_accessor :errors # Call the #validate method for this object. # If validation errors are found, sets the # @errors attribute to the Errors object and # returns true. def valid? begin @errors = self.class.validate(self) return @errors.empty? rescue NoMethodError => e # gmosx: hmm this is potentially dangerous. N::Validation.eval_validate(self.class) retry end end # Evaluate the 'validate' method for the calling # class. # # WARNING: for the moment only evaluates for # on == :save def self.eval_validate(klass) code = %{ def self.validate(obj, on = :save) errors = Errors.new } for val, on in klass.__meta[:validations] code << %{ #{val} } end code << %{ return errors end } # puts '-->', code, '<--' klass.module_eval(code) end def self.append_features(base) super base.module_eval <<-"end_eval", __FILE__, __LINE__ meta :validations, [] end_eval base.extend(MetaLanguage) end # Implements the Validation meta-language. module MetaLanguage # the postfix attached to confirmation attributes. mattr_accessor :confirmation_postfix, '_confirmation' # Validates that the attributes have a values, ie they are # neither nil or empty. # # === Example # # validate_value :param, :msg => 'No confirmation' def validate_value(*params) c = { :msg => N::Validation::Errors.no_value, :on => :save } c.update(params.pop) if params.last.is_a?(Hash) idx = 0 for name in params code = %{ if obj.#{name}.nil? errors.add(:#{name}, '#{c[:msg]}') elsif obj.#{name}.respond_to?(:empty?) errors.add(:#{name}, '#{c[:msg]}') if obj.#{name}.empty? end } __meta[:validations] << [code, c[:on]] end end # Validates the confirmation of +String+ attributes. # # === Example # # validate_confirmation :password, :msg => 'No confirmation' def validate_confirmation(*params) c = { :msg => N::Validation::Errors.no_confirmation, :postfix => N::Validation::MetaLanguage.confirmation_postfix, :on => :save } c.update(params.pop) if params.last.is_a?(Hash) for name in params confirm_name = "#{name}#{c[:postfix]}" eval "attr_accessor :#{confirm_name}" code = %{ if obj.#{confirm_name}.nil? or (obj.#{confirm_name} != obj.#{name}) errors.add(:#{name}, '#{c[:msg]}') end } __meta[:validations] << [code, c[:on]] end end # Validates the format of +String+ attributes. # # === Example # # validate_format :name, :format => /$A*/, :msg => 'My error', :on => :create def validate_format(*params) c = { :format => nil, :msg_no_value => N::Validation::Errors.no_value, :msg => N::Validation::Errors.invalid_format, :on => :save } c.update(params.pop) if params.last.is_a?(Hash) unless c[:format].is_a?(Regexp) raise(ArgumentError, 'A regular expression must be supplied as the :format option') end for name in params code = %{ if obj.#{name}.nil? errors.add(:#{name}, '#{c[:msg_no_value]}') else unless obj.#{name}.to_s.match(/#{Regexp.quote(c[:format].source)}/) errors.add(:#{name}, '#{c[:msg]}') end end; } __meta[:validations] << [code, c[:on]] end end # Validates the length of +String+ attributes. # # === Example # # validate_length :name, :max => 30, :msg => 'Too long' # validate_length :name, :min => 2, :msg => 'Too sort' # validate_length :name, :range => 2..30 # validate_length :name, :length => 15, :msg => 'Name should be %d chars long' def validate_length(*params) c = { :min => nil, :max => nil, :range => nil, :length => nil, :msg => nil, :msg_no_value => N::Validation::Errors.no_value, :msg_short => N::Validation::Errors.too_short, :msg_long => N::Validation::Errors.too_long, :on => :save } c.update(params.pop) if params.last.is_a?(Hash) min, max = c[:min], c[:max] range, length = c[:range], c[:length] count = 0 [min, max, range, length].each { |o| count += 1 if o } if count == 0 raise(ArgumentError, 'One of :min, :max, :range, :length must be provided!') end if count > 1 raise(ArgumentError, 'The :min, :max, :range, :length options are mutually exclusive!') end for name in params if min c[:msg] ||= N::Validation::Errors.too_short code = %{ if obj.#{name}.nil? errors.add(:#{name}, '#{c[:msg_no_value]}') elsif obj.#{name}.to_s.length < #{min} msg = '#{c[:msg]}' msg = (msg % #{min}) rescue msg errors.add(:#{name}, msg) end; } elsif max c[:msg] ||= N::Validation::Errors.too_long code = %{ if obj.#{name}.nil? errors.add(:#{name}, '#{c[:msg_no_value]}') elsif obj.#{name}.to_s.length > #{max} msg = '#{c[:msg]}' msg = (msg % #{max}) rescue msg errors.add(:#{name}, msg) end; } elsif range code = %{ if obj.#{name}.nil? errors.add(:#{name}, '#{c[:msg_no_value]}') elsif obj.#{name}.to_s.length < #{range.first} msg = '#{c[:msg_short]}' msg = (msg % #{range.first}) rescue msg errors.add(:#{name}, msg) elsif obj.#{name}.to_s.length > #{range.last} msg = '#{c[:msg_long]}' msg = (msg % #{range.last}) rescue msg errors.add(:#{name}, msg) end; } elsif length c[:msg] ||= N::Validation::Errors.invalid_length code = %{ if obj.#{name}.nil? errors.add(:#{name}, '#{c[:msg_no_value]}') elsif obj.#{name}.to_s.length != #{length} msg = '#{c[:msg]}' msg = (msg % #{length}) rescue msg errors.add(:#{name}, msg) end; } end __meta[:validations] << [code, c[:on]] end end # Validates that the attributes are included in # an enumeration. # # === Example # # validate_inclusion :sex, :in => %w{ Male Female }, :msg => 'huh??' # validate_inclusion :age, :in => 5..99 def validate_inclusion(*params) c = { :in => nil, :msg => N::Validation::Errors.no_inclusion, :allow_nil => false, :on => :save } c.update(params.pop) if params.last.is_a?(Hash) unless c[:in].respond_to?('include?') raise(ArgumentError, 'An object that responds to #include? must be supplied as the :in option') end for name in params if c[:allow_nil] code = %{ unless obj.#{name}.nil? or (#{c[:in].inspect}).include?(obj.#{name}) errors.add(:#{name}, '#{c[:msg]}') end; } else code = %{ unless (#{c[:in].inspect}).include?(obj.#{name}) errors.add(:#{name}, '#{c[:msg]}') end; } end __meta[:validations] << [code, c[:on]] end end end end end