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