validatable_hash
January 24th, 2008 • Uncategorized
# ----------------------------------------------------------------------- # 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