validatable_hash

  # -----------------------------------------------------------------------
  # Don't like nasty conditionals when doing option checking ?
  # Good news! They have already been done for u ... by snusnu
  # -----------------------------------------------------------------------
  #
  # How does it work?
  #
  # 1) instance method acts_as_option_hash! gets defined on Hash
  # 2) You call acts_as_option_hash! on any old Hash instance
  # 3) acts_as_option_hash! yields an instance of OptionSpec
  # 4) This instance IS ONLY ADDED TO YOUR Hash INSTANCE
  #    (not to class Hash itself => it becomes a singleton method)
  # 5) You spec your options using methods on OptionSpec
  #    (you will be for
  # 6) Once acts_as_option_hash! returns it evaluates the passed options
  # 7) a) Everything is fine you go use your options
  #       without any further checks
  # 7) b) An InvalidOptions exception is thrown because the passed options
  #       don't match your spec.
  #
  # ----------------------------------------------------------------------
  #
  # DESIRED USAGE
  #
  # ----------------------------------------------------------------------
  #
  # class Foo
  #
  #   options_for :foo do |spec|
  #     spec.only_one_of do |group|
  #       group.option :only do |o|
  #         o.allow [ :sleep, 1, String, Float ]
  #         o.reject [ :bar, 2, 3.14 ]
  #       end
  #       group.option :only do |o|
  #         o.allow [ :sleep, 1, String, Float ]
  #         o.reject [ :bar, 2, 3.14 ]
  #       end
  #     end
  #
  #     spec.option :only do |o|
  #       o.allow [ :sleep, 1, String, Float ], :unless => :except
  #       o.reject [ :bar, 2, 3.14 ]
  #
  #     spec.option :foo do |o|
  #       if spec.present [ :bam ] do ||
  #         o.allow => [ :sleep, 1, String, Float ]
  #         o.reject => [ :bar, 2, 3.14 ]
  #       end
  #       o.if_present [ :bam ]
  #       o.unless_present [ [ "test", :spec ], "fun" ]
  #     end
  #   end
  #
  #   OPTION_SPEC = lambda do |spec|
  #     spec.option :foo do |o|
  #       if spec.present [ :bam ] do ||
  #         o.allow => [ :sleep, 1, String, Float ]
  #         o.reject => [ :bar, 2, 3.14 ]
  #       end
  #       o.if_present [ :bam ]
  #       o.unless_present [ [ "test", :spec ], "fun" ]
  #     end
  #   end
  #
  # end

module Snusnu # :nodoc:
  module ValidatableHash

    class ValidatableHashSpec

      attr_reader :expectations

      def entry(key, value_spec)
        (@expectations ||= {})[key] = {
          :pattern => value_spec[:pattern],
          :optional => value_spec[:optional] || false
        }

        @optional_entry_count ||= 0
        @optional_entry_count += 1 if value_spec[:optional]

        @methods_defined || instance_eval do
          def entry_count=(v)
            @entry_count = v
            @fixed_count = true
          end

          def entry_count
            @entry_count ||= @expectations.size
          end

          def maximal_entry_count
            @expectations.size
          end

          def minimal_entry_count
            @fixed_count ? entry_count : entry_count - @optional_entry_count
          end
        end
        @methods_defined = true
      end

    end

    class InvalidHash < Exception; end

    module InstanceMethods
      def self.included(base)
        base.extend(Snusnu::ValidatableHash::InstanceMethods)
      end
      def acts_as_validatable_hash!
        yield(@validatable_hash_spec = ValidatableHashSpec.new)
        extend(SingletonInstanceMethods)
        unless valid?
          msg = "Your passed hash doesn't match your spec!"
          raise InvalidHash, msg
        end
      end
    end

    module SingletonInstanceMethods

      def valid?
        valid_size? && all? { |k,v| valid_value?(k, v) }
      rescue InvalidHash => e
        false # swallow here but raise if called after creation
      end

      private

      def expectations
        @validatable_hash_spec.expectations
      end

      def valid_value?(k,v)
        return false if expectations[k].nil?
        return true if expectations[k][:optional]
        return true if expectations[k][:pattern].nil?
        pattern = expectations[k][:pattern]

        if pattern.is_a?(Class)
          v.is_a?(pattern)
        elsif pattern.is_a?(Hash)
          has_match_in_hash_pattern?(pattern, v)
        elsif pattern.is_a?(Array)
          has_match_in_array?(pattern, v)
        else
          v == pattern
        end
      end

      def has_match_in_array?(a, v)
        simple_patterns = []; hash_patterns = []
        a.each { |p| (p.is_a?(Hash) ? hash_patterns : simple_patterns) << p }
        hash_patterns.any? { |p| has_match_in_hash_pattern?(p, v) } ||
        simple_patterns.any? { |p| has_match_in_simple_pattern?(p, v) }
      end

      def has_match_in_simple_pattern?(p, v)
        (p.class == Class && v.is_a?(p)) ||
        (p.class != Class && v.instance_of?(p.class) && v == p)
      end

      def has_match_in_hash_pattern?(h, v)
        h.has_key?(v.class) ? h[v.class].any? { |x| v == x } : false
      end

      def valid_size?
        size >= @validatable_hash_spec.minimal_entry_count &&
        size <= @validatable_hash_spec.maximal_entry_count
      end

    end

  end
end


Leave a Reply

Formatting: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>