module VirtualBox
  class AbstractModel
    # Provides validation methods for a class. Unlike ActiveRecord,
    # validations are instance-level rather than class-level.
    module Validatable
      # Returns the errors on a model. The structure of this is a hash, keyed
      # by the field name. The value of each member in the hash is an array of
      # error messages.
      #
      # @return [Hash]
      def errors
        @errors ||= {}
      end

      # Returns the errors on a specific field. This returns nil if there are
      # no errors, otherwise it returns an array of error messages.
      def errors_on(field)
        @errors[field.to_sym]
      end

      # Adds an error to a field. The error is a message.
      def add_error(field, error)
        errors[field] ||= []
        errors[field].push(error)
      end

      # Clears all the errors from a model.
      def clear_errors
        @errors = {}
      end

      def full_error_messages
        full_error_messages = Array.new
        errors.each do |field_name, messages|
          messages.each do |message|
            human_field_name = field_name.to_s.gsub('_', ' ').capitalize
            full_error_messages << "#{human_field_name} #{message}"
          end
        end
        full_error_messages
      end

      # This method calls the validate method on the model (which any subclass
      # is expected to implement), and then checks that the validations didn't
      # add any errors.
      #
      # @return [Boolean]
      def valid?
        validate
        errors.empty?
      end

      # Subclasses should override this method. Validation can be done any
      # way an implementer feels. Helper methods such as {#validates_presence_of},
      # {#validates_inclusion_of}, etc. exist, but they're use isn't required.
      # {#add_error} can be used to add an error to any field. By convention
      # this method should return `true` or `false` to signal any errors.
      #
      # @return [Boolean]
      def validate
        true
      end

      # Validates the presence (non-emptiness) of a field or fields. This
      # validation fails if the specified fields are either blank ("") or
      # nil.
      #
      # Additionally, a custom error message can be specified:
      #
      #     validates_presence_of :foo, :bar
      #     validates_presence_of :baz, :message => "must not be blank!"
      #
      # @return [Boolean]
      def validates_presence_of(*fields)
        options = __validates_extract_options(fields, {
          :message => "can't be blank."
        })

        fields.collect { |field|
          value = send(field)
          if value.nil? || value.to_s.empty?
            add_error(field, options[:message])
            false
          else
            true
          end
        }.compact.all? { |v| v == true }
      end

      # Validates the format of a field with a given regular expression.
      #
      #     validates_format_of :foo, :with => /\d+/
      #
      def validates_format_of(*fields)
        options = __validates_extract_options(fields, {
          :with => nil,
          :message => "is not properly formatted."
        })

        fields.collect { |field|
          value = send(field)
          # Use validates_presence_of if you need it to be set
          next if value.nil? || value.to_s.empty?
          if options[:with] && value =~ options[:with]
            true
          else
            add_error(field, options[:message])
            false
          end
        }.compact.all? { |v| v == true }
      end

      # Validates the numericality of a specific field.
      #
      #     validates_numericality_of :field
      #
      def validates_numericality_of(*fields)
        options = __validates_extract_options(fields, {
          :message => "is not a number."
        })

        fields.collect { |field|
          value = send(field)
          # Use validates_presence_of if you need it to be set
          next if value.nil? || value.to_s.empty?
          if value.is_a?(Numeric) && value.to_s =~ /^\d$/
            true
          else
            add_error(field, options[:message])
            false
          end
        }.compact.all? { |v| v == true }
      end

      # Validates that a field's value is within a specific range,
      # array, etc.
      #
      #     validates_inclusion_of :foo, :in => [1,2,3]
      #     validates_inclusion_of :bar, :in => (1..6)
      #
      def validates_inclusion_of(*fields)
        options = __validates_extract_options(fields, {
          :in => nil,
          :message => "value %s is not included in the list."
        })

        fields.collect { |field|
          value = send(field)
          # Use validates_presence_of if you need it to be set
          return if value.nil? || value.to_s.empty?
          if options[:in] && options[:in].include?(value)
            true
          else
            message = options[:message] % value
            add_error(field, message)
            false
          end
        }.compact.all? { |v| v == true }
      end

      # Internal method. Should never be called.
      def __validates_extract_options(fields, defaults)
        defaults.merge(fields.last.is_a?(Hash) ? fields.pop : {})
      end
    end
  end
end
