原创作者: hideto   阅读:1539次   评论:0条   更新时间:2011-05-26    
Validations相关的源码全在validations.rb文件里:
module ActiveRecord
  class Errors
    include Enumerable

    @@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"
    }

    def add(attribute, msg = @@default_error_messages[:invalid])
      @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
      @errors[attribute.to_s] << msg
    end

    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

  end

  module Validations
    def self.included(base) # :nodoc:
      base.extend ClassMethods
      base.class_eval do
        alias_method_chain :save, :validation
        alias_method_chain :save!, :validation
        alias_method_chain :update_attribute, :validation_skipping
      end
    end

    module ClassMethods

      def validate(*methods, &block)
        methods << block if block_given?
        write_inheritable_set(:validate, methods)
      end

      def validate_on_create(*methods, &block)
        methods << block if block_given?
        write_inheritable_set(:validate_on_create, methods)
      end

      def validate_on_update(*methods, &block)
        methods << block if block_given?
        write_inheritable_set(:validate_on_update, methods)
      end

      def validates_each(*attrs)
        options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
        attrs   = attrs.flatten

        # Declare the validation.
        send(validation_method(options[:on] || :save)) do |record|
          # Don't validate when there is an :if condition and that condition is false
          unless options[:if] && !evaluate_condition(options[:if], record)
            attrs.each do |attr|
              value = record.send(attr)
              next if value.nil? && options[:allow_nil]
              yield record, attr, value
            end
          end
        end
      end

      def validates_confirmation_of(*attr_names)
        configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

        attr_accessor *(attr_names.map { |n| "#{n}_confirmation" })

        validates_each(attr_names, configuration) do |record, attr_name, value|
          record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
        end
      end

      def validates_presence_of(*attr_names)
        configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

        # can't use validates_each here, because it cannot cope with nonexistent attributes,
        # while errors.add_on_empty can
        attr_names.each do |attr_name|
          send(validation_method(configuration[:on])) do |record|
            unless configuration[:if] and not evaluate_condition(configuration[:if], record)
              record.errors.add_on_blank(attr_name,configuration[:message])
            end
          end
        end
      end

      def validates_length_of(*attrs)
        # Merge given options with defaults.
        options = {
          :too_long     => ActiveRecord::Errors.default_error_messages[:too_long],
          :too_short    => ActiveRecord::Errors.default_error_messages[:too_short],
          :wrong_length => ActiveRecord::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

      def validates_uniqueness_of(*attr_names)
        configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

        validates_each(attr_names,configuration) do |record, attr_name, value|
          if value.nil? || (configuration[:case_sensitive] || !columns_hash[attr_name.to_s].text?)
            condition_sql = "#{record.class.table_name}.#{attr_name} #{attribute_condition(value)}"
            condition_params = [value]
          else
            condition_sql = "LOWER(#{record.class.table_name}.#{attr_name}) #{attribute_condition(value)}"
            condition_params = [value.downcase]
          end
          if scope = configuration[:scope]
            Array(scope).map do |scope_item|
              scope_value = record.send(scope_item)
              condition_sql << " AND #{record.class.table_name}.#{scope_item} #{attribute_condition(scope_value)}"
              condition_params << scope_value
            end
          end
          unless record.new_record?
            condition_sql << " AND #{record.class.table_name}.#{record.class.primary_key} <> ?"
            condition_params << record.send(:id)
          end
          if record.class.find(:first, :conditions => [condition_sql, *condition_params])
            record.errors.add(attr_name, configuration[:message])
          end
        end
      end

      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 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

      def validates_numericality_of(*attr_names)
        configuration = { :message => ActiveRecord::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
    end

    def valid?
      errors.clear

      run_validations(:validate)
      validate

      if new_record?
        run_validations(:validate_on_create)
        validate_on_create
      else
        run_validations(:validate_on_update)
        validate_on_update
      end

      errors.empty?
    end

    def errors
      @errors ||= Errors.new(self)
    end
  end
end      

从上述代码我们可以看出以下几点:
1,save、save!方法会使用validation,而update_attribute则skip validation
2,record.errors方法得到该record经过validate后的所有errors
3,save(false)会skip validation

overwriting Base#validate的例子:
class Person < ActiveRecord::Base
  protected
    def validate
      errors.add_on_empty %w( first_name last_name )
      errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
    end

    def validate_on_create # is only run the first time a new object is saved
      unless valid_discount?(membership_discount)
        errors.add("membership_discount", "has expired")
      end
    end

    def validate_on_update
      errors.add_to_base("No changes have occurred") if unchanged_attributes?
    end
end


view中:
<% unless @post.errors.empty? %>
  The post couldn't be saved due to these errors:
  <ul>
  <% @post.errors.each_full do |message| %>
    <li><%= message %></li>
  <% end %>
  </ul>
<% end %>

或者
<%= error_messages_for 'post' %>


看了validations的代码,我们甚至可以写自己的Validation关键字:
module ActiveRecord
  module Validations
    module ClassMethods
      def validates_oak_id(*attr_names)
        configuration = { :message => "Invalid Oak ID" }
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

        validates_each (attr_names, configuration) do |record, attr_name, value|
          record.errors.add(attr_name, configuration[:message]) unless FAUtils.valid_oakid?(value)
        end
      end
    end
  end
end

这个例子使用FAUtils库来验证oakid的合法性,一切都很简单吧?
评论 共 0 条 请登录后发表评论

发表评论

您还没有登录,请您登录后再发表评论

文章信息

Global site tag (gtag.js) - Google Analytics