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 validate 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)
STRBOL =

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

lambda do |input|
  if input.kind_of?(String)
    input = input.to_sym
  end
  
  validate(input, [Symbol])
end
STRBOL_OR_NIL =
lambda do |input|
  input = case input
  when "~"    then nil
  when String then input.to_sym
  else input
  end
  
  validate(input, [Symbol, nil])
end
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, :dup => 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.



178
179
180
181
182
# File 'lib/configurable/validation.rb', line 178

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


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

def array(); ARRAY; end

.array_or_nilObject

Same as array but allows nil:

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


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

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


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

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.



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

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


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

def float(); FLOAT; end

.float_or_nilObject

Same as float but allows nil:

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


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

def float_or_nil(); FLOAT_OR_NIL; end

.guess(value) ⇒ Object

Guesses and returns a block for the example value.



162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/configurable/validation.rb', line 162

def guess(value)
  case value
  when true    then switch
  when false   then flag
  when Numeric then numeric
  when Array   then list
  when String  then string
  when Time    then time
  when Range   then range
  when Regexp  then regexp
  else yaml
  end
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


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

def hash(); HASH; end

.hash_or_nilObject

Same as hash but allows nil:

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


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

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


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

def integer(); INTEGER; end

.integer_or_nilObject

Same as integer but allows nil:

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


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

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).



729
730
731
732
733
734
735
736
737
738
739
740
741
# File 'lib/configurable/validation.rb', line 729

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


751
752
753
754
755
756
757
758
759
760
761
762
763
# File 'lib/configurable/validation.rb', line 751

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


373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/configurable/validation.rb', line 373

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 => ','}


695
696
697
698
699
700
701
702
703
704
705
706
# File 'lib/configurable/validation.rb', line 695

def list_select(*options, &validation)
  register( 
    :type => :list_select, 
    :options => options, 
    :split => ',',
    :validation => attributes(validation)
  ) do |input|
    args = validate(input, [Array])
    args.collect! {|arg| validation.call(arg) } if validation
    args.each {|arg| validate(arg, options) }
  end
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


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

def numeric(); NUMERIC; end

.numeric_or_nilObject

Same as numeric but allows nil:

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


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

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


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

def range(); RANGE; end

.range_or_nilObject

Same as range but allows nil:

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


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

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/


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

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


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

def regexp_or_nil(); REGEXP_OR_NIL; end

.register(attributes = {}, &block) ⇒ 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(attributes={}, &block)
  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}


665
666
667
668
669
670
671
672
673
674
# File 'lib/configurable/validation.rb', line 665

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

.strbolObject

Returns a block that checks the input is a symbol. String inputs are directly converted to a symbol.

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


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

def strbol(); STRBOL; end

.strbol_or_nilObject

Same as strbol but allows nil. Tilde is considered a string equivalent of nil (this behavior is consistent with the YAML methods but obviously inconsistent with the strbol behavior).

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


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

def strbol_or_nil(); STRBOL_OR_NIL; end

.stringObject

Returns a block that checks the input is a string.

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


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

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


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

def string_or_nil(); STRING_OR_NIL; end

.switchObject

Same as boolean.



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

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


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

def symbol(); SYMBOL; end

.symbol_or_nilObject

Same as symbol but allows nil:

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


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

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')


617
618
619
620
621
622
# File 'lib/configurable/validation.rb', line 617

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


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

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.



197
198
199
200
201
202
203
# File 'lib/configurable/validation.rb', line 197

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