sxross/MotionModel

View on GitHub
motion/validatable.rb

Summary

Maintainability
A
1 hr
Test Coverage
module MotionModel
  module Validatable
    class ValidationSpecificationError < RuntimeError;  end
    class RecordInvalid < RuntimeError; end

    def self.included(base)
      base.extend(ClassMethods)
    end

    module ClassMethods
      def validate(field = nil, validation_type = {})
        if field.nil? || field.to_s == ''
          ex = ValidationSpecificationError.new('field not present in validation call')
          raise ex
        end

        unless validation_type.is_a?(Hash)
          ex = ValidationSpecificationError.new('validation_type is not a hash')
          raise ex
        end

        if validation_type == {}
          ex = ValidationSpecificationError.new('validation_type hash is empty')
          raise ex
        end

        validations << {field => validation_type}
      end
      alias_method :validates, :validate

      def validations
        @validations ||= []
      end
    end

    def do_save?(options = {})
      _valid = true
      if options[:validate] != false
        call_hooks 'validation' do
          _valid = valid?
        end
      end
      _valid
    end
    private :do_save?

    def do_insert(options = {})
      return false unless do_save?(options)
      super
    end

    def do_update(options = {})
      return false unless do_save?(options)
      super
    end

    # it fails loudly
    def save!
      raise RecordInvalid.new('failed validation') unless valid?
      save
    end

    # This has two functions:
    #
    # * First, it triggers validations.
    #
    # * Second, it returns the result of performing the validations.
    def before_validation(sender); end
    def after_validation(sender); end

    def valid?
      call_hooks 'validation' do
        @messages = []
        @valid = true
        self.class.validations.each do |validations|
          validate_each(validations)
        end
      end
      @valid
    end

    # Raw array of hashes of error messages.
    def error_messages
      @messages
    end

    # Array of messages for a given field. Results are always an array
    # because a field can fail multiple validations.
    def error_messages_for(field)
      key = field.to_sym
      error_messages.select{|message| message.has_key?(key)}.map{|message| message[key]}
    end

    def validate_each(validations) #nodoc
      validations.each_pair do |field, validation|
        result = validate_one field, validation
        @valid &&= result
      end
    end

    def validation_method(validation_type) #nodoc
      validation_method = "validate_#{validation_type}".to_sym
    end

    def each_validation_for(field) #nodoc
      self.class.validations.select{|validation| validation.has_key?(field)}.each do |validation|
        validation.each_pair do |field, validation_hash|
          yield validation_hash
        end
      end
    end

    # Validates an arbitrary string against a specific field's validators.
    # Useful before setting the value of a model's field. I.e., you get data
    # from a form, do a <tt>validate_for(:my_field, that_data)</tt> and
    # if it succeeds, you do <tt>obj.my_field = that_data</tt>.
    def validate_for(field, value)
      @messages = []
      key = field.to_sym
      result = true
      each_validation_for(key) do |validation|
        validation.each_pair do |validation_type, setting|
          method = validation_method(validation_type)
          if self.respond_to? method
            value.strip! if value.is_a?(String)
            result &&= self.send(method, field, value, setting)
          end
        end
      end
      result
    end

    def validate_one(field, validation) #nodoc
      result = true
      validation.each_pair do |validation_type, setting|
        if self.respond_to? validation_method(validation_type)
          value = self.send(field)
          result &&= self.send(validation_method(validation_type), field, value.is_a?(String) ? value.strip : value, setting)
        else
          ex = ValidationSpecificationError.new("unknown validation type :#{validation_type.to_s}")
        end
      end
      result
    end

    # Validates that something has been endntered in a field.
    # Should catch Fixnums, Bignums and Floats. Nils and Strings should
    # be handled as well, Arrays, Hashes and other datatypes will not.
    def validate_presence(field, value, setting)
      if(value.is_a?(Numeric))
        return true
      elsif value.is_a?(String) || value.nil?
        result = value.nil? || ((value.length == 0) == setting)
        additional_message = setting ? "non-empty" : "non-empty"
        add_message(field, "incorrect value supplied for #{field.to_s} -- should be #{additional_message}.") if result
        return !result
      end
      return false
    end

    # Validates that the length is in a given range of characters. E.g.,
    #
    #     validate :name,   :length => 5..8
    def validate_length(field, value, setting)
      if value.is_a?(String) || value.nil?
        result = value.nil? || (value.length < setting.first || value.length > setting.last)
        add_message(field, "incorrect value supplied for #{field.to_s} -- should be between #{setting.first} and #{setting.last} characters long.") if result
        return !result
      end
      return false
    end

    def validate_email(field, value, setting)
      if value.is_a?(String) || value.nil?
        result = value.nil? || value.match(/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i).nil?
        add_message(field, "#{field.to_s} does not appear to be an email address.") if result
      end
      return !result
    end

    # Validates contents of field against a given Regexp. This can be tricky because you need
    # to anchor both sides in most cases using \A and \Z to get a reliable match.
    def validate_format(field, value, setting)
      result = value.nil? || setting.match(value).nil?
      add_message(field, "#{field.to_s} does not appear to be in the proper format.") if result
      return !result
    end

    # Add a message for <tt>field</tt> to the messages collection.
    def add_message(field, message)
      @messages.push({field.to_sym => message})
    end

    # Stub methods for hook protocols
    def before_validation(sender); end
    def after_validation(sender);  end

  end
end