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