Module: Configurable::Validation

Defined in:
lib/configurable/validation.rb

Overview

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.

integer = Validation.integer
integer.class             # => Proc
integer.call(1)           # => 1
integer.call('1')         # => 1
integer.call(nil)         # => ValidationError

– Developers: note the unusual syntax for declaring constants that are blocks defined by lambda… ex:

block = lambda {}
CONST = block

This syntax plays well with RDoc, which otherwise gets jacked when you do it all in one step.

Defined Under Namespace

Classes: ApiError, ValidationError

Constant Summary collapse

STRING =

default attributes => :string, :example => “string”

string_validation_block
STRING_OR_NIL =
string_or_nil_validation_block
SYMBOL =

default attributes => :symbol, :example => “:sym”

yaml(Symbol)
SYMBOL_OR_NIL =
yaml(Symbol, nil)
BOOLEAN =

default attributes => :boolean, :example => “true, yes”

yaml(true, false, nil)
SWITCH =

default attributes => :switch

yaml(true, false, nil)
FLAG =

default attributes => :flag

yaml(true, false, nil)
ARRAY =

default attributes => :array, :example => “[a, b, c]”

yaml(Array)
ARRAY_OR_NIL =
yaml(Array, nil)
LIST =

default attributes => :list, :split => ‘,’

list_block
HASH =

default attributes => :hash, :example => “{one: 1, two: 2”}

yaml(Hash)
HASH_OR_NIL =
yaml(Hash, nil)
INTEGER =

default attributes => :integer, :example => “2”

yaml(Integer)
INTEGER_OR_NIL =
yaml(Integer, nil)
FLOAT =

default attributes => :float, :example => “2.2, 2.0e+2”

yaml(Float)
FLOAT_OR_NIL =
yaml(Float, nil)
NUMERIC =

default attributes => :numeric, :example => “2, 2.2, 2.0e+2”

yaml(Numeric)
NUMERIC_OR_NIL =
yaml(Numeric, nil)
REGEXP =

default attributes => :regexp, :example => “/regexp/i”

regexp_block
REGEXP_OR_NIL =
regexp_or_nil_block
RANGE =

default attributes => :range, :example => “min..max”

range_block
RANGE_OR_NIL =
range_or_nil_block
TIME =

default attributes => :time, :example => “2008-08-08 08:00:00”

time_block
TIME_OR_NIL =
time_or_nil_block
IO_OR_STRING =

default attributes => :io, :duplicate_default => false, :example => “/path/to/file”

check(IO, String, Integer)
IO_STRING_OR_NIL =
check(IO, String, Integer, nil)

Class Method Summary collapse

Class Method Details

.api(*methods) ⇒ Object

Returns a block that calls validate_api using the block input and methods.



163
164
165
166
167
# File 'lib/configurable/validation.rb', line 163

def api(*methods)
  lambda do |input|
    validate_api(input, methods)
  end
end

.arrayObject

Returns a block that checks the input is an array. String inputs are loaded as yaml first.

array.class               # => Proc
array.call([1,2,3])       # => [1,2,3]
array.call('[1, 2, 3]')   # => [1,2,3]
array.call(nil)           # => ValidationError
array.call('str')         # => ValidationError


300
# File 'lib/configurable/validation.rb', line 300

def array(); ARRAY; end

.array_or_nilObject

Same as array but allows nil:

array_or_nil.call('~')    # => nil
array_or_nil.call(nil)    # => nil


310
# File 'lib/configurable/validation.rb', line 310

def array_or_nil(); ARRAY_OR_NIL; end

.attributes(block) ⇒ Object

Returns the attributes registered to the block.



73
74
75
# File 'lib/configurable/validation.rb', line 73

def attributes(block)
  DEFAULT_ATTRIBUTES[block] || {}
end

.booleanObject

Returns a block that checks the input is true, false or nil. String inputs are loaded as yaml first.

boolean.class             # => Proc
boolean.call(true)        # => true
boolean.call(false)       # => false
boolean.call(nil)         # => nil

boolean.call('true')      # => true
boolean.call('yes')       # => true
boolean.call('FALSE')     # => false

boolean.call(1)           # => ValidationError
boolean.call("str")       # => ValidationError


271
# File 'lib/configurable/validation.rb', line 271

def boolean(); BOOLEAN; end

.check(*validations) ⇒ Object

Returns a block that calls validate using the block input and validations.



157
158
159
# File 'lib/configurable/validation.rb', line 157

def check(*validations)
  lambda {|input| validate(input, validations) }
end

.flagObject

Same as boolean.



285
# File 'lib/configurable/validation.rb', line 285

def flag(); FLAG; end

.floatObject

Returns a block that checks the input is a float. String inputs are loaded as yaml first.

float.class               # => Proc
float.call(1.1)           # => 1.1
float.call('1.1')         # => 1.1
float.call('1.0e+6')      # => 1e6
float.call(1)             # => ValidationError
float.call(nil)           # => ValidationError
float.call('str')         # => ValidationError


408
# File 'lib/configurable/validation.rb', line 408

def float(); FLOAT; end

.float_or_nilObject

Same as float but allows nil:

float_or_nil.call('~')    # => nil
float_or_nil.call(nil)    # => nil


418
# File 'lib/configurable/validation.rb', line 418

def float_or_nil(); FLOAT_OR_NIL; end

.hashObject

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


357
# File 'lib/configurable/validation.rb', line 357

def hash(); HASH; end

.hash_or_nilObject

Same as hash but allows nil:

hash_or_nil.call('~')          # => nil
hash_or_nil.call(nil)          # => nil


367
# File 'lib/configurable/validation.rb', line 367

def hash_or_nil(); HASH_OR_NIL; end

.integerObject

Returns a block that checks the input is an integer. String inputs are loaded as yaml first.

integer.class             # => Proc
integer.call(1)           # => 1
integer.call('1')         # => 1
integer.call(1.1)         # => ValidationError
integer.call(nil)         # => ValidationError
integer.call('str')       # => ValidationError


382
# File 'lib/configurable/validation.rb', line 382

def integer(); INTEGER; end

.integer_or_nilObject

Same as integer but allows nil:

integer_or_nil.call('~')  # => nil
integer_or_nil.call(nil)  # => nil


392
# File 'lib/configurable/validation.rb', line 392

def integer_or_nil(); INTEGER_OR_NIL; end

.io(*api) ⇒ Object

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

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)           # => ApiError

Note that by default io configs will not be duplicated (duplicate IOs flush separately, and this can result in disorder. see gist.github.com/88808).



682
683
684
685
686
687
688
689
690
691
692
693
694
# File 'lib/configurable/validation.rb', line 682

def io(*api)
  if api.empty?
    IO_OR_STRING
  else
    block = lambda do |input|
      validate(input, [IO, String, Integer]) do
        validate_api(input, api)
      end
    end
    
    register_as IO_OR_STRING, block
  end
end

.io_or_nil(*api) ⇒ Object

Same as io but allows nil:

io_or_nil.call(nil)          # => nil


704
705
706
707
708
709
710
711
712
713
714
715
716
# File 'lib/configurable/validation.rb', line 704

def io_or_nil(*api)
  if api.empty?
    IO_STRING_OR_NIL
  else
    block = lambda do |input|
      validate(input, [IO, String, Integer, nil]) do
        validate_api(input, api)
      end
    end
    
    register_as IO_STRING_OR_NIL, block
  end  
end

.list(&validation) ⇒ Object

Returns a block that checks the input is an array, then yamlizes each string value of the array.

list.class                # => Proc
list.call([1,2,3])        # => [1,2,3]
list.call(['1', 'str'])   # => [1,'str']
list.call('str')          # => ValidationError
list.call(nil)            # => ValidationError


324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/configurable/validation.rb', line 324

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_select(*options, &validation) ⇒ Object

Returns a block that checks the input is an array, and that each member 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]
s.call([])                   # => []

s.call(1)                    # => ValidationError
s.call([nil])                # => ValidationError
s.call([0])                  # => ValidationError
s.call(['4'])                # => ValidationError

The list_select block is registered with these default attributes:

{:type => :list_select, :options => options, :split => ','}


647
648
649
650
651
652
653
654
655
656
657
658
659
# File 'lib/configurable/validation.rb', line 647

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, options) }
  end
  
  register(block, 
    :type => :list_select, 
    :options => options, 
    :split => ',',
    :validation => attributes(validation))
end

.load_if_yaml(input, *validations) ⇒ Object

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.



143
144
145
146
147
148
149
150
151
152
153
# File 'lib/configurable/validation.rb', line 143

def load_if_yaml(input, *validations)
  begin
    yaml = YAML.load(input)
    case yaml
    when *validations then yaml
    else input
    end
  rescue(ArgumentError)
    input
  end
end

.numericObject

Returns a block that checks the input is a number. String inputs are loaded as yaml first.

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


435
# File 'lib/configurable/validation.rb', line 435

def numeric(); NUMERIC; end

.numeric_or_nilObject

Same as numeric but allows nil:

numeric_or_nil.call('~')    # => nil
numeric_or_nil.call(nil)    # => nil


445
# File 'lib/configurable/validation.rb', line 445

def numeric_or_nil(); NUMERIC_OR_NIL; end

.rangeObject

Returns a block that checks the input is a range. String inputs are 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'
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


515
# File 'lib/configurable/validation.rb', line 515

def range(); RANGE; end

.range_or_nilObject

Same as range but allows nil:

range_or_nil.call('~')    # => nil
range_or_nil.call(nil)    # => nil


533
# File 'lib/configurable/validation.rb', line 533

def range_or_nil(); RANGE_OR_NIL; end

.regexpObject

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/


465
# File 'lib/configurable/validation.rb', line 465

def regexp(); REGEXP; end

.regexp_or_nilObject

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


488
# File 'lib/configurable/validation.rb', line 488

def regexp_or_nil(); REGEXP_OR_NIL; end

.register(block, attributes) ⇒ Object

Registers the default attributes with the specified block in Configurable::DEFAULT_ATTRIBUTES.



59
60
61
62
# File 'lib/configurable/validation.rb', line 59

def register(block, attributes)
  DEFAULT_ATTRIBUTES[block] = attributes
  block
end

.register_as(source, target, attributes = {}) ⇒ Object

Registers the default attributes of the source as the attributes of the target. Overridding or additional attributes are merged to the defaults.



67
68
69
70
# File 'lib/configurable/validation.rb', line 67

def register_as(source, target, attributes={})
  DEFAULT_ATTRIBUTES[target] = DEFAULT_ATTRIBUTES[source].dup.merge!(attributes)
  target
end

.select(*options, &validation) ⇒ Object

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

s.call(nil)                  # => ValidationError
s.call(0)                    # => ValidationError
s.call('4')                  # => ValidationError

The select block is registered with these default attributes:

{:type => :select, :options => options}


616
617
618
619
620
621
622
623
624
625
626
# File 'lib/configurable/validation.rb', line 616

def select(*options, &validation)
  block = lambda do |input|
    input = validation.call(input) if validation
    validate(input, options)
  end
  
  register(block, 
    :type => :select, 
    :options => options,
    :validation => attributes(validation))
end

.stringObject

Returns a block that checks the input is a string. Moreover, strings are re-evaluated as string literals using %Q.

string.class              # => Proc
string.call('str')        # => 'str'
string.call('\n')         # => "\n"
string.call("\n")         # => "\n"
string.call("%s")         # => "%s"
string.call(nil)          # => ValidationError
string.call(:sym)         # => ValidationError


202
# File 'lib/configurable/validation.rb', line 202

def string(); STRING; end

.string_or_nilObject

Same as string but allows nil. Note the special behavior of the nil string ‘~’ – rather than being treated as a string, it is processed as nil to be consistent with the other [class]_or_nil methods.

string_or_nil.call('~')   # => nil
string_or_nil.call(nil)   # => nil


220
# File 'lib/configurable/validation.rb', line 220

def string_or_nil(); STRING_OR_NIL; end

.switchObject

Same as boolean.



278
# File 'lib/configurable/validation.rb', line 278

def switch(); SWITCH; end

.symbolObject

Returns a block that checks the input is a symbol. String inputs are loaded as yaml first.

symbol.class              # => Proc
symbol.call(:sym)         # => :sym
symbol.call(':sym')       # => :sym
symbol.call(nil)          # => ValidationError
symbol.call('str')        # => ValidationError


241
# File 'lib/configurable/validation.rb', line 241

def symbol(); SYMBOL; end

.symbol_or_nilObject

Same as symbol but allows nil:

symbol_or_nil.call('~')   # => nil
symbol_or_nil.call(nil)   # => nil


251
# File 'lib/configurable/validation.rb', line 251

def symbol_or_nil(); SYMBOL_OR_NIL; end

.timeObject

Returns a block that checks the input is a Time. String inputs are loaded using Time.parse and then converted into times. Parsed times are local unless specified otherwise.

time.class               # => Proc

now = Time.now
time.call(now)           # => now

time.call('2008-08-08 20:00:00.00 +08:00').getutc.strftime('%Y/%m/%d %H:%M:%S')
#  => '2008/08/08 12:00:00'

time.call('2008-08-08').strftime('%Y/%m/%d %H:%M:%S')
#  => '2008/08/08 00:00:00'

time.call(1)             # => ValidationError
time.call(nil)           # => ValidationError

Warning: Time.parse will parse a valid time (Time.now) even when no time is specified:

time.call('str').strftime('%Y/%m/%d %H:%M:%S')      
# => Time.now.strftime('%Y/%m/%d %H:%M:%S')


568
569
570
571
572
573
# File 'lib/configurable/validation.rb', line 568

def time()
  # adding this here is a compromise to lazy-load the parse
  # method (autoload doesn't work since Time already exists)
  require 'time' unless Time.respond_to?(:parse)
  TIME
end

.time_or_nilObject

Same as time but allows nil:

time_or_nil.call('~')    # => nil
time_or_nil.call(nil)    # => nil


588
# File 'lib/configurable/validation.rb', line 588

def time_or_nil(); TIME_OR_NIL; end

.validate(input, validations) ⇒ Object

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

A block may be provided to handle invalid inputs; if provided it will be 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.



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/configurable/validation.rb', line 94

def validate(input, validations)
  case validations
  when Array
  
    case input
    when *validations then input
    else 
      if block_given? && yield(input)
        input
      else
        raise ValidationError.new(input, validations)
      end
    end

  when nil then input
  else raise ArgumentError, "validations must be an array of valid inputs or nil"
  end
end

.validate_api(input, methods) ⇒ Object

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.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/configurable/validation.rb', line 124

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

.yaml(*validations) ⇒ Object

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)
b.class                 # => Proc
b.call(1)               # => 1
b.call("1")             # => 1
b.call(nil)             # => nil
b.call("str")           # => ValidationError

If no validations are specified, the result will be returned without validation.



182
183
184
185
186
187
188
# File 'lib/configurable/validation.rb', line 182

def yaml(*validations)
  validations = nil if validations.empty?
  lambda do |input|
    input = YAML.load(input) if input.kind_of?(String)
    validate(input, validations)
  end
end