lib/configurable/validation.rb in configurable-0.1.0 vs lib/configurable/validation.rb in configurable-0.3.0

- old
+ new

@@ -1,8 +1,12 @@ autoload(:YAML, 'yaml') module Configurable + # A hash of (block, default attributes) for config blocks. The + # attributes for nil will be merged with those for the block. + DEFAULT_ATTRIBUTES = Hash.new({}) + DEFAULT_ATTRIBUTES[nil] = {:reader => true, :writer => true} # Validation generates blocks for common validations and transformations of # configurations set through Configurable. In general these blocks load # string inputs as YAML and valdiate the results; non-string inputs are # simply validated. @@ -22,11 +26,11 @@ # # This syntax plays well with RDoc, which otherwise gets jacked when you # do it all in one step. module Validation - # Raised when Validation blocks fail. + # Raised when a Validation block fails. class ValidationError < ArgumentError def initialize(input, validations) super case when validations.empty? "no validations specified" @@ -34,73 +38,62 @@ "expected #{validations.inspect} but was: #{input.inspect}" end end end - # Raised when yamlization fails. - class YamlizationError < ArgumentError - def initialize(input, error) - super "#{error} ('#{input}')" - end - end - module_function - # Yaml conversion and checker. Valid if any of the validations - # match in a case statement. Otherwise raises an error. - - # Returns input if any of the validations match any of the - # inputs, as in a case statement. Raises a ValidationError - # otherwise. For example: + # Registers the default attributes with the specified block + # in Configurable::DEFAULT_ATTRIBUTES. + def register(block, attributes) + DEFAULT_ATTRIBUTES[block] = attributes + end + + # Returns input if it matches any of the validations as in would in a case + # statement. Raises a ValidationError otherwise. For example: # # validate(10, [Integer, nil]) # # Does the same as: # # case 10 # when Integer, nil then input # else raise ValidationError.new(...) # end # - # Note the validations input must be an Array or nil; - # validate will raise an ArgumentError otherwise. - # All inputs are considered VALID if validations == nil. + # A block may be provided to handle invalid inputs; if provided it will be + # called and a ValidationError will not be raised. Note the validations + # input must be an Array or nil; validate will raise an ArgumentError + # otherwise. All inputs are considered VALID if validations == nil. def validate(input, validations) case validations when Array case input when *validations then input - else raise ValidationError.new(input, validations) + else + if block_given? + yield(input) + else + raise ValidationError.new(input, validations) + end end when nil then input - else raise ArgumentError.new("validations must be nil, or an array of valid inputs") + else raise ArgumentError, "validations must be nil, or an array of valid inputs" end end - # Attempts to load the input as YAML. Raises a YamlizationError - # for errors. - def yamlize(input) - begin - YAML.load(input) - rescue - raise YamlizationError.new(input, $!.message) - end - end - # Returns a block that calls validate using the block input - # and the input validations. Raises an error if no validations - # are specified. + # and validations. def check(*validations) - raise ArgumentError.new("no validations specified") if validations.empty? lambda {|input| validate(input, validations) } end # Returns a block that loads input strings as YAML, then - # calls validate with the result and the input validations. - # Non-string inputs are not converted. + # calls validate with the result and validations. Non-string + # inputs are validated directly. # # b = yaml(Integer, nil) # b.class # => Proc # b.call(1) # => 1 # b.call("1") # => 1 @@ -108,23 +101,13 @@ # b.call("str") # => ValidationError # # If no validations are specified, the result will be # returned without validation. def yaml(*validations) + validations = nil if validations.empty? lambda do |input| - res = input.kind_of?(String) ? yamlize(input) : input - validations.empty? ? res : validate(res, validations) - end - end - - # Returns a block loads a String input as YAML then - # validates the result is valid using the input - # validations. If the input is not a String, the - # input is validated directly. - def yamlize_and_check(*validations) - lambda do |input| - input = yamlize(input) if input.kind_of?(String) + input = YAML.load(input) if input.kind_of?(String) validate(input, validations) end end # Returns a block that checks the input is a string. @@ -172,18 +155,18 @@ # symbol.call(':sym') # => :sym # symbol.call(nil) # => ValidationError # symbol.call('str') # => ValidationError # def symbol(); SYMBOL; end - SYMBOL = yamlize_and_check(Symbol) + SYMBOL = yaml(Symbol) # Same as symbol but allows nil: # # symbol_or_nil.call('~') # => nil # symbol_or_nil.call(nil) # => nil def symbol_or_nil(); SYMBOL_OR_NIL; end - SYMBOL_OR_NIL = yamlize_and_check(Symbol, nil) + SYMBOL_OR_NIL = yaml(Symbol, nil) # Returns a block that checks the input is true, false or nil. # String inputs are loaded as yaml first. # # boolean.class # => Proc @@ -197,19 +180,25 @@ # # boolean.call(1) # => ValidationError # boolean.call("str") # => ValidationError # def boolean(); BOOLEAN; end - BOOLEAN = yamlize_and_check(true, false, nil) + BOOLEAN = yaml(true, false, nil) # Same as boolean. def switch(); SWITCH; end - SWITCH = yamlize_and_check(true, false, nil) - + + # default attributes {:type => :switch} + SWITCH = yaml(true, false, nil) + register SWITCH, :type => :switch + # Same as boolean. def flag(); FLAG; end - FLAG = yamlize_and_check(true, false, nil) + + # default attributes {:type => :flag} + FLAG = yaml(true, false, nil) + register FLAG, :type => :flag # Returns a block that checks the input is an array. # String inputs are loaded as yaml first. # # array.class # => Proc @@ -217,18 +206,24 @@ # array.call('[1, 2, 3]') # => [1,2,3] # array.call(nil) # => ValidationError # array.call('str') # => ValidationError # def array(); ARRAY; end - ARRAY = yamlize_and_check(Array) + + # default attributes {:arg_name => "'[a, b, c]'"} + ARRAY = yaml(Array) + register ARRAY, :arg_name => "'[a, b, c]'" # Same as array but allows nil: # # array_or_nil.call('~') # => nil # array_or_nil.call(nil) # => nil def array_or_nil(); ARRAY_OR_NIL; end - ARRAY_OR_NIL = yamlize_and_check(Array, nil) + + # default attributes {:arg_name => "'[a, b, c]'"} + ARRAY_OR_NIL = yaml(Array, nil) + register ARRAY_OR_NIL, :arg_name => "'[a, b, c]'" # Returns a block that checks the input is an array, # then yamlizes each string value of the array. # # list.class # => Proc @@ -238,33 +233,42 @@ # list.call(nil) # => ValidationError # def list(); LIST; end list_block = lambda do |input| validate(input, [Array]).collect do |arg| - arg.kind_of?(String) ? yamlize(arg) : arg + arg.kind_of?(String) ? YAML.load(arg) : arg end end + + # default attributes {:type => :list, :split => ','} LIST = list_block - + register LIST, :type => :list, :split => ',' + # Returns a block that checks the input is a hash. # String inputs are loaded as yaml first. # # hash.class # => Proc # hash.call({'key' => 'value'}) # => {'key' => 'value'} # hash.call('key: value') # => {'key' => 'value'} # hash.call(nil) # => ValidationError # hash.call('str') # => ValidationError # def hash(); HASH; end - HASH = yamlize_and_check(Hash) + + # default attributes {:arg_name => "'{one: 1, two: 2}'"} + HASH = yaml(Hash) + register HASH, :arg_name => "'{one: 1, two: 2}'" # Same as hash but allows nil: # # hash_or_nil.call('~') # => nil # hash_or_nil.call(nil) # => nil def hash_or_nil(); HASH_OR_NIL; end - HASH_OR_NIL = yamlize_and_check(Hash, nil) + + # default attributes {:arg_name => "'{one: 1, two: 2}'"} + HASH_OR_NIL = yaml(Hash, nil) + register HASH_OR_NIL, :arg_name => "'{one: 1, two: 2}'" # Returns a block that checks the input is an integer. # String inputs are loaded as yaml first. # # integer.class # => Proc @@ -273,18 +277,18 @@ # integer.call(1.1) # => ValidationError # integer.call(nil) # => ValidationError # integer.call('str') # => ValidationError # def integer(); INTEGER; end - INTEGER = yamlize_and_check(Integer) + INTEGER = yaml(Integer) # Same as integer but allows nil: # # integer_or_nil.call('~') # => nil # integer_or_nil.call(nil) # => nil def integer_or_nil(); INTEGER_OR_NIL; end - INTEGER_OR_NIL = yamlize_and_check(Integer, nil) + INTEGER_OR_NIL = yaml(Integer, nil) # Returns a block that checks the input is a float. # String inputs are loaded as yaml first. # # float.class # => Proc @@ -294,18 +298,18 @@ # float.call(1) # => ValidationError # float.call(nil) # => ValidationError # float.call('str') # => ValidationError # def float(); FLOAT; end - FLOAT = yamlize_and_check(Float) + FLOAT = yaml(Float) # Same as float but allows nil: # # float_or_nil.call('~') # => nil # float_or_nil.call(nil) # => nil def float_or_nil(); FLOAT_OR_NIL; end - FLOAT_OR_NIL = yamlize_and_check(Float, nil) + FLOAT_OR_NIL = yaml(Float, nil) # Returns a block that checks the input is a number. # String inputs are loaded as yaml first. # # num.class # => Proc @@ -316,99 +320,110 @@ # num.call('1.0e+6') # => 1e6 # num.call(nil) # => ValidationError # num.call('str') # => ValidationError # def num(); NUMERIC; end - NUMERIC = yamlize_and_check(Numeric) + NUMERIC = yaml(Numeric) # Same as num but allows nil: # # num_or_nil.call('~') # => nil # num_or_nil.call(nil) # => nil def num_or_nil(); NUMERIC_OR_NIL; end - NUMERIC_OR_NIL = yamlize_and_check(Numeric, nil) + NUMERIC_OR_NIL = yaml(Numeric, nil) - # Returns a block that checks the input is a regexp. - # String inputs are converted to regexps using - # Regexp#new. + # Returns a block that checks the input is a regexp. String inputs are + # loaded as yaml; if the result is not a regexp, it is converted to + # a regexp using Regexp#new. # # regexp.class # => Proc # regexp.call(/regexp/) # => /regexp/ # regexp.call('regexp') # => /regexp/ # + # yaml_str = '!ruby/regexp /regexp/' + # regexp.call(yaml_str) # => /regexp/ + # # # use of ruby-specific flags can turn on/off # # features like case insensitive matching # regexp.call('(?i)regexp') # => /(?i)regexp/ # def regexp(); REGEXP; end regexp_block = lambda do |input| - input = Regexp.new(input) if input.kind_of?(String) + if input.kind_of?(String) + begin + input = validate(YAML.load(input), [Regexp]) {|obj| input } + rescue(ArgumentError) + end + end + + if input.kind_of?(String) + input = Regexp.new(input) + end + validate(input, [Regexp]) end REGEXP = regexp_block - # Same as regexp but allows nil. Note the special - # behavior of the nil string '~' -- rather than - # being converted to a regexp, it is processed as - # nil to be consistent with the other [class]_or_nil - # methods. + # Same as regexp but allows nil. Note the special behavior of the nil + # string '~' -- rather than being converted to a regexp, it is processed + # as nil to be consistent with the other [class]_or_nil methods. # # regexp_or_nil.call('~') # => nil # regexp_or_nil.call(nil) # => nil def regexp_or_nil(); REGEXP_OR_NIL; end regexp_or_nil_block = lambda do |input| - input = case input + case input when nil, '~' then nil - when String then Regexp.new(input) - else input + else REGEXP[input] end - - validate(input, [Regexp, nil]) end REGEXP_OR_NIL = regexp_or_nil_block - # Returns a block that checks the input is a range. - # String inputs are split into a beginning and - # end if possible, where each part is loaded as - # yaml before being used to construct a Range.a + # Returns a block that checks the input is a range. String inputs are + # loaded as yaml; if the result is still a string, it is split into a + # beginning and end, if possible, and each part is loaded as yaml + # before being used to construct a Range. # # range.class # => Proc # range.call(1..10) # => 1..10 # range.call('1..10') # => 1..10 # range.call('a..z') # => 'a'..'z' # range.call('-10...10') # => -10...10 # range.call(nil) # => ValidationError # range.call('1.10') # => ValidationError # range.call('a....z') # => ValidationError # + # yaml_str = "!ruby/range \nbegin: 1\nend: 10\nexcl: false\n" + # range.call(yaml_str) # => 1..10 + # def range(); RANGE; end range_block = lambda do |input| + if input.kind_of?(String) + begin + input = validate(YAML.load(input), [Range]) {|obj| input } + rescue(ArgumentError) + end + end + if input.kind_of?(String) && input =~ /^([^.]+)(\.{2,3})([^.]+)$/ - input = Range.new(yamlize($1), yamlize($3), $2.length == 3) + input = Range.new(YAML.load($1), YAML.load($3), $2.length == 3) end + validate(input, [Range]) end RANGE = range_block # Same as range but allows nil: # # range_or_nil.call('~') # => nil # range_or_nil.call(nil) # => nil def range_or_nil(); RANGE_OR_NIL; end range_or_nil_block = lambda do |input| - input = case input + case input when nil, '~' then nil - when String - if input =~ /^([^.]+)(\.{2,3})([^.]+)$/ - Range.new(yamlize($1), yamlize($3), $2.length == 3) - else - input - end - else input + else RANGE[input] end - - validate(input, [Range, nil]) end RANGE_OR_NIL = range_or_nil_block # Returns a block that checks the input is a Time. String inputs are # loaded using Time.parse and then converted into times. Parsed times @@ -449,32 +464,17 @@ # Same as time but allows nil: # # time_or_nil.call('~') # => nil # time_or_nil.call(nil) # => nil - def time_or_nil() - # adding this check is a compromise to autoload the parse - # method (autoload doesn't work since Time already exists) - require 'time' unless Time.respond_to?(:parse) - TIME_OR_NIL - end + def time_or_nil(); TIME_OR_NIL; end time_or_nil_block = lambda do |input| - input = case input + case input when nil, '~' then nil - when String then Time.parse(input) - else input + else TIME[input] end - - validate(input, [Time, nil]) end TIME_OR_NIL = time_or_nil_block - - # A hash of default attributes for the validation blocks. - ATTRIBUTES = Hash.new({}) - ATTRIBUTES[SWITCH] = {:type => :switch} - ATTRIBUTES[FLAG] = {:type => :flag} - ATTRIBUTES[LIST] = {:type => :list, :split => ','} - ATTRIBUTES[ARRAY] = {:arg_name => "'[a, b, c]'"} - ATTRIBUTES[HASH] = {:arg_name => "'{one: 1, two: 2}'"} + end end \ No newline at end of file