lib/configurable/validation.rb in configurable-0.4.2 vs lib/configurable/validation.rb in configurable-0.5.0
- old
+ new
@@ -37,10 +37,22 @@
else
"expected #{validations.inspect} but was: #{input.inspect}"
end
end
end
+
+ # Raised when validate_api fails.
+ class ApiError < ArgumentError
+ def initialize(input, methods)
+ super case
+ when methods.empty?
+ "no api specified"
+ else
+ "expected api #{methods.inspect} for: #{input.inspect}"
+ end
+ end
+ end
module_function
# Registers the default attributes with the specified block
# in Configurable::DEFAULT_ATTRIBUTES.
@@ -48,16 +60,22 @@
DEFAULT_ATTRIBUTES[block] = attributes
block
end
# Registers the default attributes of the source as the attributes
- # of the target. Attributes are duplicated so they may be modifed.
- def register_as(source, target)
- DEFAULT_ATTRIBUTES[target] = DEFAULT_ATTRIBUTES[source].dup
+ # of the target. Overridding or additional attributes are merged
+ # to the defaults.
+ def register_as(source, target, attributes={})
+ DEFAULT_ATTRIBUTES[target] = DEFAULT_ATTRIBUTES[source].dup.merge!(attributes)
target
end
+ # Returns the attributes registered to the block.
+ def attributes(block)
+ DEFAULT_ATTRIBUTES[block] || {}
+ 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])
#
@@ -67,13 +85,14 @@
# when Integer, nil then input
# else raise ValidationError.new(...)
# end
#
# 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.
+ # called with the input and a ValidationError will not be raised unless the
+ # block returns false. 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
@@ -85,14 +104,42 @@
raise ValidationError.new(input, validations)
end
end
when nil then input
- else raise ArgumentError, "validations must be nil, or an array of valid inputs"
+ else raise ArgumentError, "validations must be an array of valid inputs or nil"
end
end
+ # Returns input if it responds to all of the specified methods. Raises an
+ # ApiError otherwise. For example:
+ #
+ # validate_api(10, [:to_s, :to_f]) # => 10
+ # validate_api(Object.new, [:to_s, :to_f]) # !> ApiError
+ #
+ # A block may be provided to handle invalid inputs; if provided it will be
+ # called with the input and an ApiError will not be raised unless the
+ # block returns false. Note the methods input must be an Array or nil;
+ # validate_api will raise an ArgumentError otherwise. All inputs are
+ # considered VALID if methods == nil.
+ def validate_api(input, methods)
+ case methods
+ when Array
+ unless methods.all? {|m| input.respond_to?(m) }
+ if block_given? && yield(input)
+ input
+ else
+ raise ApiError.new(input, methods)
+ end
+ end
+ when nil
+ else raise ArgumentError, "methods must be an array or nil"
+ end
+
+ input
+ end
+
# Helper to load the input into a valid object. If a valid object is not
# loaded as YAML, or if an error occurs, the original input is returned.
def load_if_yaml(input, *validations)
begin
yaml = YAML.load(input)
@@ -108,11 +155,19 @@
# Returns a block that calls validate using the block input
# and validations.
def check(*validations)
lambda {|input| validate(input, validations) }
end
-
+
+ # Returns a block that calls validate_api using the block input
+ # and methods.
+ def api(*methods)
+ lambda do |input|
+ validate_api(input, methods)
+ end
+ end
+
# Returns a block that loads input strings as YAML, then
# calls validate with the result and validations. Non-string
# inputs are validated directly.
#
# b = yaml(Integer, nil)
@@ -264,11 +319,24 @@
# list.call([1,2,3]) # => [1,2,3]
# list.call(['1', 'str']) # => [1,'str']
# list.call('str') # => ValidationError
# list.call(nil) # => ValidationError
#
- def list(); LIST; end
+ def list(&validation)
+ return LIST unless validation
+
+ block = lambda do |input|
+ args = validate(input, [Array]).collect do |arg|
+ arg.kind_of?(String) ? YAML.load(arg) : arg
+ end
+ args.collect! {|arg| validation.call(arg) }
+ args
+ end
+
+ register_as(LIST, block, :validation => attributes(validation))
+ end
+
list_block = lambda do |input|
validate(input, [Array]).collect do |arg|
arg.kind_of?(String) ? YAML.load(arg) : arg
end
end
@@ -353,30 +421,30 @@
register_as FLOAT, FLOAT_OR_NIL
# Returns a block that checks the input is a number.
# String inputs are loaded as yaml first.
#
- # num.class # => Proc
- # num.call(1.1) # => 1.1
- # num.call(1) # => 1
- # num.call(1e6) # => 1e6
- # num.call('1.1') # => 1.1
- # num.call('1.0e+6') # => 1e6
- # num.call(nil) # => ValidationError
- # num.call('str') # => ValidationError
+ # numeric.class # => Proc
+ # numeric.call(1.1) # => 1.1
+ # numeric.call(1) # => 1
+ # numeric.call(1e6) # => 1e6
+ # numeric.call('1.1') # => 1.1
+ # numeric.call('1.0e+6') # => 1e6
+ # numeric.call(nil) # => ValidationError
+ # numeric.call('str') # => ValidationError
#
- def num(); NUMERIC; end
+ def numeric(); NUMERIC; end
- # default attributes {:type => :num, :example => "2, 2.2, 2.0e+2"}
+ # default attributes {:type => :numeric, :example => "2, 2.2, 2.0e+2"}
NUMERIC = yaml(Numeric)
- register NUMERIC, :type => :num, :example => "2, 2.2, 2.0e+2"
+ register NUMERIC, :type => :numeric, :example => "2, 2.2, 2.0e+2"
- # Same as num but allows nil:
+ # Same as numeric 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.call('~') # => nil
+ # numeric_or_nil.call(nil) # => nil
+ def numeric_or_nil(); NUMERIC_OR_NIL; end
NUMERIC_OR_NIL = yaml(Numeric, nil)
register_as NUMERIC, NUMERIC_OR_NIL
# Returns a block that checks the input is a regexp. String inputs are
@@ -427,13 +495,12 @@
REGEXP_OR_NIL = regexp_or_nil_block
register_as REGEXP, REGEXP_OR_NIL
# 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.
+ # loaded as yaml (a '!ruby/range' prefix is added before loading if
+ # if it is not specified).
#
# range.class # => Proc
# range.call(1..10) # => 1..10
# range.call('1..10') # => 1..10
# range.call('a..z') # => 'a'..'z'
@@ -446,17 +513,14 @@
# range.call(yaml_str) # => 1..10
#
def range(); RANGE; end
range_block = lambda do |input|
if input.kind_of?(String)
+ input = "!ruby/range #{input}" unless input =~ /\A\s*!ruby\/range\s/
input = load_if_yaml(input, Range)
end
- if input.kind_of?(String) && input =~ /^([^.]+)(\.{2,3})([^.]+)$/
- input = Range.new(YAML.load($1), YAML.load($3), $2.length == 3)
- end
-
validate(input, [Range])
end
# default attributes {:type => :range, :example => "min..max"}
RANGE = range_block
@@ -531,12 +595,12 @@
end
TIME_OR_NIL = time_or_nil_block
register_as TIME, TIME_OR_NIL
- # Returns a block that only allows the specified values. Select can take
- # a block that will validate each individual value.
+ # Returns a block that only allows the specified options. Select can take
+ # a block that will validate the input individual value.
#
# s = select(1,2,3, &integer)
# s.class # => Proc
# s.call(1) # => 1
# s.call('3') # => 3
@@ -545,24 +609,27 @@
# s.call(0) # => ValidationError
# s.call('4') # => ValidationError
#
# The select block is registered with these default attributes:
#
- # {:type => :select, :values => values}
+ # {:type => :select, :options => options}
#
- def select(*values, &validation)
+ def select(*options, &validation)
block = lambda do |input|
input = validation.call(input) if validation
- validate(input, values)
+ validate(input, options)
end
- register(block, :type => :select, :values => values)
+ register(block,
+ :type => :select,
+ :options => options,
+ :validation => attributes(validation))
end
# Returns a block that checks the input is an array, and that each member
- # of the array is one of the specified values. A block may be provided
- # to validate each individual value.
+ # of the array is included in options. A block may be provided to validate
+ # the individual values.
#
# s = list_select(1,2,3, &integer)
# s.class # => Proc
# s.call([1]) # => [1]
# s.call([1, '3']) # => [1, 3]
@@ -573,75 +640,82 @@
# s.call([0]) # => ValidationError
# s.call(['4']) # => ValidationError
#
# The list_select block is registered with these default attributes:
#
- # {:type => :list_select, :values => values, :split => ','}
+ # {:type => :list_select, :options => options, :split => ','}
#
- def list_select(*values, &validation)
+ def list_select(*options, &validation)
block = lambda do |input|
args = validate(input, [Array])
args.collect! {|arg| validation.call(arg) } if validation
- args.each {|arg| validate(arg, values) }
+ args.each {|arg| validate(arg, options) }
end
- register(block, :type => :list_select, :values => values, :split => ',')
+ register(block,
+ :type => :list_select,
+ :options => options,
+ :split => ',',
+ :validation => attributes(validation))
end
- # Returns a block validating the input is an IO or a string. String inputs
- # are expected to be filepaths, but io does not open a file immediately.
+ # Returns a block validating the input is an IO, a string, or an integer.
+ # String inputs are expected to be filepaths and integer inputs are expected
+ # to be valid file descriptors, but io does not open an IO immediately.
#
# io.class # => Proc
# io.call($stdout) # => $stdout
# io.call('/path/to/file') # => '/path/to/file'
- #
+ # io.call(1) # => 1
# io.call(nil) # => ValidationError
- # io.call(10) # => ValidationError
#
# An IO api can be specified to allow other objects to be validated. This
# is useful for duck-typing an IO when a known subset of methods are needed.
#
# array_io = io(:<<)
# array_io.call($stdout) # => $stdout
# array_io.call([]) # => []
- # array_io.call(nil) # => ValidationError
+ # array_io.call(nil) # => ApiError
#
+ # Note that by default io configs will not be duplicated (duplicate IOs
+ # flush separately, and this can result in disorder. see
+ # http://gist.github.com/88808).
def io(*api)
if api.empty?
IO_OR_STRING
else
block = lambda do |input|
- validate(input, [IO, String]) do
- api.all? {|m| input.respond_to?(m) }
+ validate(input, [IO, String, Integer]) do
+ validate_api(input, api)
end
end
register_as IO_OR_STRING, block
end
end
- # default attributes {:type => :io, :example => "/path/to/file"}
- IO_OR_STRING = check(IO, String)
- register IO_OR_STRING, :type => :io, :example => "/path/to/file"
+ # default attributes {:type => :io, :duplicate_default => false, :example => "/path/to/file"}
+ IO_OR_STRING = check(IO, String, Integer)
+ register IO_OR_STRING, :type => :io, :duplicate_default => false, :example => "/path/to/file"
# Same as io but allows nil:
#
# io_or_nil.call(nil) # => nil
#
def io_or_nil(*api)
if api.empty?
IO_STRING_OR_NIL
else
block = lambda do |input|
- validate(input, [IO, String, nil]) do
- api.all? {|m| input.respond_to?(m) }
+ validate(input, [IO, String, Integer, nil]) do
+ validate_api(input, api)
end
end
register_as IO_STRING_OR_NIL, block
end
end
- IO_STRING_OR_NIL = check(IO, String, nil)
+ IO_STRING_OR_NIL = check(IO, String, Integer, nil)
register_as IO_OR_STRING, IO_STRING_OR_NIL
end
end
\ No newline at end of file