configurable 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +19 -0
- data/README +237 -0
- data/lib/config_parser.rb +216 -0
- data/lib/config_parser/option.rb +52 -0
- data/lib/config_parser/switch.rb +29 -0
- data/lib/config_parser/utils.rb +133 -0
- data/lib/configurable.rb +155 -0
- data/lib/configurable/class_methods.rb +308 -0
- data/lib/configurable/delegate.rb +75 -0
- data/lib/configurable/delegate_hash.rb +165 -0
- data/lib/configurable/indifferent_access.rb +22 -0
- data/lib/configurable/validation.rb +480 -0
- metadata +84 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
module Configurable
|
2
|
+
|
3
|
+
# Delegates are used by DelegateHash to determine how to map read/write
|
4
|
+
# operations to a receiver.
|
5
|
+
class Delegate
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Determines if the value is duplicable. Non-duplicable
|
9
|
+
# values include nil, true, false, Symbol, Numeric, and
|
10
|
+
# any object that does not respond to dup.
|
11
|
+
def duplicable_value?(value)
|
12
|
+
case value
|
13
|
+
when nil, true, false, Symbol, Numeric, Method then false
|
14
|
+
else value.respond_to?(:dup)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# The reader method, by default key
|
20
|
+
attr_reader :reader
|
21
|
+
|
22
|
+
# The writer method, by default key=
|
23
|
+
attr_reader :writer
|
24
|
+
|
25
|
+
# An array of metadata for self, used to present the
|
26
|
+
# delegate in different contexts (ex on the command
|
27
|
+
# line or web).
|
28
|
+
attr_reader :attributes
|
29
|
+
|
30
|
+
# Initializes a new Delegate with the specified key
|
31
|
+
# and default value.
|
32
|
+
def initialize(reader, writer="#{reader}=", default=nil, attributes={})
|
33
|
+
self.default = default
|
34
|
+
self.reader = reader
|
35
|
+
self.writer = writer
|
36
|
+
|
37
|
+
@attributes = attributes
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets the default value for self.
|
41
|
+
def default=(value)
|
42
|
+
@duplicable = Delegate.duplicable_value?(value)
|
43
|
+
@default = value.freeze
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the default value, or a duplicate of the default
|
47
|
+
# value if specified and the default value is duplicable
|
48
|
+
# (see Delegate.duplicable_value?)
|
49
|
+
def default(duplicate=true)
|
50
|
+
duplicate && @duplicable ? @default.dup : @default
|
51
|
+
end
|
52
|
+
|
53
|
+
# Sets the reader for self. The reader is symbolized,
|
54
|
+
# but may also be set to nil.
|
55
|
+
def reader=(value)
|
56
|
+
@reader = value == nil ? value : value.to_sym
|
57
|
+
end
|
58
|
+
|
59
|
+
# Sets the writer for self. The writer is symbolized,
|
60
|
+
# but may also be set to nil.
|
61
|
+
def writer=(value)
|
62
|
+
@writer = value == nil ? value : value.to_sym
|
63
|
+
end
|
64
|
+
|
65
|
+
# True if another is a kind of Delegate with the same
|
66
|
+
# reader, writer, and default value. Attributes are
|
67
|
+
# not considered.
|
68
|
+
def ==(another)
|
69
|
+
another.kind_of?(Delegate) &&
|
70
|
+
self.reader == another.reader &&
|
71
|
+
self.writer == another.writer &&
|
72
|
+
self.default(false) == another.default(false)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'configurable/delegate'
|
2
|
+
|
3
|
+
module Configurable
|
4
|
+
|
5
|
+
# DelegateHash delegates get and set operations to instance methods on a receiver.
|
6
|
+
#
|
7
|
+
# class Sample
|
8
|
+
# attr_accessor :key
|
9
|
+
# end
|
10
|
+
# sample = Sample.new
|
11
|
+
#
|
12
|
+
# dhash = DelegateHash.new
|
13
|
+
# dhash.delegates[:key] = Delegate.new(:key)
|
14
|
+
# dhash.bind(sample)
|
15
|
+
#
|
16
|
+
# sample.key = 'value'
|
17
|
+
# dhash[:key] # => 'value'
|
18
|
+
#
|
19
|
+
# dhash[:key] = 'another'
|
20
|
+
# sample.key # => 'another'
|
21
|
+
#
|
22
|
+
# Non-delegate keys are sent to an underlying data store:
|
23
|
+
#
|
24
|
+
# dhash[:not_delegated] = 'value'
|
25
|
+
# dhash[:not_delegated] # => 'value'
|
26
|
+
#
|
27
|
+
# dhash.store # => {:not_delegated => 'value'}
|
28
|
+
# dhash.to_hash # => {:key => 'another', :not_delegated => 'value'}
|
29
|
+
#
|
30
|
+
class DelegateHash
|
31
|
+
|
32
|
+
# The bound receiver
|
33
|
+
attr_reader :receiver
|
34
|
+
|
35
|
+
# The underlying data store for non-delegate keys
|
36
|
+
attr_reader :store
|
37
|
+
|
38
|
+
# A hash of (key, Delegate) pairs identifying which
|
39
|
+
# keys to delegate to the receiver
|
40
|
+
attr_reader :delegates
|
41
|
+
|
42
|
+
# Initializes a new DelegateHash. Note that initialize simply sets the
|
43
|
+
# receiver, it does NOT map stored values the same way bind does.
|
44
|
+
# This allows quick, implicit binding where the bound store is set
|
45
|
+
# up beforehand.
|
46
|
+
#
|
47
|
+
# For more standard binding use: DelegateHash.new.bind(receiver)
|
48
|
+
def initialize(delegates={}, store={}, receiver=nil)
|
49
|
+
@receiver = nil
|
50
|
+
@store = store
|
51
|
+
@delegates = delegates
|
52
|
+
@receiver = receiver
|
53
|
+
end
|
54
|
+
|
55
|
+
# Binds self to the specified receiver. Mapped keys are
|
56
|
+
# removed from store and sent to their writer method on
|
57
|
+
# receiver.
|
58
|
+
def bind(receiver)
|
59
|
+
raise ArgumentError, "receiver cannot be nil" if receiver == nil
|
60
|
+
raise ArgumentError, "already bound to: #{@receiver}" if bound? && @receiver != receiver
|
61
|
+
|
62
|
+
store.keys.each do |key|
|
63
|
+
next unless delegate = delegates[key]
|
64
|
+
receiver.send(delegate.writer, store.delete(key)) if delegate.writer
|
65
|
+
end
|
66
|
+
@receiver = receiver
|
67
|
+
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns true if self is bound to a receiver
|
72
|
+
def bound?
|
73
|
+
receiver != nil
|
74
|
+
end
|
75
|
+
|
76
|
+
# Unbinds self from the specified receiver. Mapped values
|
77
|
+
# are stored in store. Returns the unbound receiver.
|
78
|
+
def unbind
|
79
|
+
delegates.each_pair do |key, delegate|
|
80
|
+
store[key] = receiver.send(delegate.reader) if delegate.reader
|
81
|
+
end
|
82
|
+
current_receiver = receiver
|
83
|
+
@receiver = nil
|
84
|
+
|
85
|
+
current_receiver
|
86
|
+
end
|
87
|
+
|
88
|
+
# Retrieves the value corresponding to the key. If bound?
|
89
|
+
# and the key is a delegates key, then the value is
|
90
|
+
# obtained from the delegate.reader method on the receiver.
|
91
|
+
def [](key)
|
92
|
+
case
|
93
|
+
when bound? && delegate = delegates[key]
|
94
|
+
delegate.reader ? receiver.send(delegate.reader) : store[key]
|
95
|
+
else store[key]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Associates the value the key. If bound? and the key
|
100
|
+
# is a delegates key, then the value will be forwarded
|
101
|
+
# to the delegate.writer method on the receiver.
|
102
|
+
def []=(key, value)
|
103
|
+
case
|
104
|
+
when bound? && delegate = delegates[key]
|
105
|
+
delegate.writer ? receiver.send(delegate.writer, value) : store[key] = value
|
106
|
+
else store[key] = value
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# True if the key is assigned in self.
|
111
|
+
def has_key?(key)
|
112
|
+
(bound? && delegates.has_key?(key)) || store.has_key?(key)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Calls block once for each key-value pair stored in self.
|
116
|
+
def each_pair # :yields: key, value
|
117
|
+
delegates.each_pair do |key, delegate|
|
118
|
+
yield(key, receiver.send(delegate.reader)) if delegate.reader
|
119
|
+
end if bound?
|
120
|
+
|
121
|
+
store.each_pair do |key, value|
|
122
|
+
yield(key, value)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Updates self to ensure that each delegates key
|
127
|
+
# has a value in self; the delegate.default value is
|
128
|
+
# set if a value does not already exist.
|
129
|
+
#
|
130
|
+
# Returns self.
|
131
|
+
def update
|
132
|
+
delegates.each_pair do |key, delegate|
|
133
|
+
self[key] ||= delegate.default
|
134
|
+
end
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
# Duplicates self, returning an unbound DelegateHash.
|
139
|
+
def dup
|
140
|
+
duplicate = super()
|
141
|
+
duplicate.instance_variable_set(:@receiver, nil)
|
142
|
+
duplicate.instance_variable_set(:@store, @store.dup)
|
143
|
+
duplicate
|
144
|
+
end
|
145
|
+
|
146
|
+
# Equal if the to_hash values of self and another are equal.
|
147
|
+
def ==(another)
|
148
|
+
another.respond_to?(:to_hash) && to_hash == another.to_hash
|
149
|
+
end
|
150
|
+
|
151
|
+
# Returns self as a hash.
|
152
|
+
def to_hash
|
153
|
+
hash = store.dup
|
154
|
+
delegates.keys.each do |key|
|
155
|
+
hash[key] = self[key]
|
156
|
+
end if bound?
|
157
|
+
hash
|
158
|
+
end
|
159
|
+
|
160
|
+
# Overrides default inspect to show the to_hash values.
|
161
|
+
def inspect
|
162
|
+
"#<#{self.class}:#{object_id} to_hash=#{to_hash.inspect}>"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Configurable
|
2
|
+
module IndifferentAccess
|
3
|
+
|
4
|
+
def [](key)
|
5
|
+
super(convert(key))
|
6
|
+
end
|
7
|
+
|
8
|
+
def []=(key, value)
|
9
|
+
super(convert(key), value)
|
10
|
+
end
|
11
|
+
|
12
|
+
def key?(key)
|
13
|
+
super(convert(key))
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def convert(key)
|
19
|
+
key.kind_of?(String) ? key.to_sym : key
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,480 @@
|
|
1
|
+
autoload(:YAML, 'yaml')
|
2
|
+
|
3
|
+
module Configurable
|
4
|
+
|
5
|
+
# Validation generates blocks for common validations and transformations of
|
6
|
+
# configurations set through Configurable. In general these blocks load
|
7
|
+
# string inputs as YAML and valdiate the results; non-string inputs are
|
8
|
+
# simply validated.
|
9
|
+
#
|
10
|
+
# integer = Validation.integer
|
11
|
+
# integer.class # => Proc
|
12
|
+
# integer.call(1) # => 1
|
13
|
+
# integer.call('1') # => 1
|
14
|
+
# integer.call(nil) # => ValidationError
|
15
|
+
#
|
16
|
+
#--
|
17
|
+
# Developers: note the unusual syntax for declaring constants that are
|
18
|
+
# blocks defined by lambda... ex:
|
19
|
+
#
|
20
|
+
# block = lambda {}
|
21
|
+
# CONST = block
|
22
|
+
#
|
23
|
+
# This syntax plays well with RDoc, which otherwise gets jacked when you
|
24
|
+
# do it all in one step.
|
25
|
+
module Validation
|
26
|
+
|
27
|
+
# Raised when Validation blocks fail.
|
28
|
+
class ValidationError < ArgumentError
|
29
|
+
def initialize(input, validations)
|
30
|
+
super case
|
31
|
+
when validations.empty?
|
32
|
+
"no validations specified"
|
33
|
+
else
|
34
|
+
"expected #{validations.inspect} but was: #{input.inspect}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Raised when yamlization fails.
|
40
|
+
class YamlizationError < ArgumentError
|
41
|
+
def initialize(input, error)
|
42
|
+
super "#{error} ('#{input}')"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module_function
|
47
|
+
|
48
|
+
# Yaml conversion and checker. Valid if any of the validations
|
49
|
+
# match in a case statement. Otherwise raises an error.
|
50
|
+
|
51
|
+
# Returns input if any of the validations match any of the
|
52
|
+
# inputs, as in a case statement. Raises a ValidationError
|
53
|
+
# otherwise. For example:
|
54
|
+
#
|
55
|
+
# validate(10, [Integer, nil])
|
56
|
+
#
|
57
|
+
# Does the same as:
|
58
|
+
#
|
59
|
+
# case 10
|
60
|
+
# when Integer, nil then input
|
61
|
+
# else raise ValidationError.new(...)
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# Note the validations input must be an Array or nil;
|
65
|
+
# validate will raise an ArgumentError otherwise.
|
66
|
+
# All inputs are considered VALID if validations == nil.
|
67
|
+
def validate(input, validations)
|
68
|
+
case validations
|
69
|
+
when Array
|
70
|
+
|
71
|
+
case input
|
72
|
+
when *validations then input
|
73
|
+
else raise ValidationError.new(input, validations)
|
74
|
+
end
|
75
|
+
|
76
|
+
when nil then input
|
77
|
+
else raise ArgumentError.new("validations must be nil, or an array of valid inputs")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Attempts to load the input as YAML. Raises a YamlizationError
|
82
|
+
# for errors.
|
83
|
+
def yamlize(input)
|
84
|
+
begin
|
85
|
+
YAML.load(input)
|
86
|
+
rescue
|
87
|
+
raise YamlizationError.new(input, $!.message)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns a block that calls validate using the block input
|
92
|
+
# and the input validations. Raises an error if no validations
|
93
|
+
# are specified.
|
94
|
+
def check(*validations)
|
95
|
+
raise ArgumentError.new("no validations specified") if validations.empty?
|
96
|
+
lambda {|input| validate(input, validations) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns a block that loads input strings as YAML, then
|
100
|
+
# calls validate with the result and the input validations.
|
101
|
+
# Non-string inputs are not converted.
|
102
|
+
#
|
103
|
+
# b = yaml(Integer, nil)
|
104
|
+
# b.class # => Proc
|
105
|
+
# b.call(1) # => 1
|
106
|
+
# b.call("1") # => 1
|
107
|
+
# b.call(nil) # => nil
|
108
|
+
# b.call("str") # => ValidationError
|
109
|
+
#
|
110
|
+
# If no validations are specified, the result will be
|
111
|
+
# returned without validation.
|
112
|
+
def yaml(*validations)
|
113
|
+
lambda do |input|
|
114
|
+
res = input.kind_of?(String) ? yamlize(input) : input
|
115
|
+
validations.empty? ? res : validate(res, validations)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns a block loads a String input as YAML then
|
120
|
+
# validates the result is valid using the input
|
121
|
+
# validations. If the input is not a String, the
|
122
|
+
# input is validated directly.
|
123
|
+
def yamlize_and_check(*validations)
|
124
|
+
lambda do |input|
|
125
|
+
input = yamlize(input) if input.kind_of?(String)
|
126
|
+
validate(input, validations)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns a block that checks the input is a string.
|
131
|
+
# Moreover, strings are re-evaluated as string
|
132
|
+
# literals using %Q.
|
133
|
+
#
|
134
|
+
# string.class # => Proc
|
135
|
+
# string.call('str') # => 'str'
|
136
|
+
# string.call('\n') # => "\n"
|
137
|
+
# string.call("\n") # => "\n"
|
138
|
+
# string.call("%s") # => "%s"
|
139
|
+
# string.call(nil) # => ValidationError
|
140
|
+
# string.call(:sym) # => ValidationError
|
141
|
+
#
|
142
|
+
def string(); STRING; end
|
143
|
+
string_validation_block = lambda do |input|
|
144
|
+
input = validate(input, [String])
|
145
|
+
eval %Q{"#{input}"}
|
146
|
+
end
|
147
|
+
STRING = string_validation_block
|
148
|
+
|
149
|
+
# Same as string but allows nil. Note the special
|
150
|
+
# behavior of the nil string '~' -- rather than
|
151
|
+
# being treated as a string, it is processed as nil
|
152
|
+
# to be consistent with the other [class]_or_nil
|
153
|
+
# methods.
|
154
|
+
#
|
155
|
+
# string_or_nil.call('~') # => nil
|
156
|
+
# string_or_nil.call(nil) # => nil
|
157
|
+
def string_or_nil(); STRING_OR_NIL; end
|
158
|
+
string_or_nil_validation_block = lambda do |input|
|
159
|
+
input = validate(input, [String, nil])
|
160
|
+
case input
|
161
|
+
when nil, '~' then nil
|
162
|
+
else eval %Q{"#{input}"}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
STRING_OR_NIL = string_or_nil_validation_block
|
166
|
+
|
167
|
+
# Returns a block that checks the input is a symbol.
|
168
|
+
# String inputs are loaded as yaml first.
|
169
|
+
#
|
170
|
+
# symbol.class # => Proc
|
171
|
+
# symbol.call(:sym) # => :sym
|
172
|
+
# symbol.call(':sym') # => :sym
|
173
|
+
# symbol.call(nil) # => ValidationError
|
174
|
+
# symbol.call('str') # => ValidationError
|
175
|
+
#
|
176
|
+
def symbol(); SYMBOL; end
|
177
|
+
SYMBOL = yamlize_and_check(Symbol)
|
178
|
+
|
179
|
+
# Same as symbol but allows nil:
|
180
|
+
#
|
181
|
+
# symbol_or_nil.call('~') # => nil
|
182
|
+
# symbol_or_nil.call(nil) # => nil
|
183
|
+
def symbol_or_nil(); SYMBOL_OR_NIL; end
|
184
|
+
SYMBOL_OR_NIL = yamlize_and_check(Symbol, nil)
|
185
|
+
|
186
|
+
# Returns a block that checks the input is true, false or nil.
|
187
|
+
# String inputs are loaded as yaml first.
|
188
|
+
#
|
189
|
+
# boolean.class # => Proc
|
190
|
+
# boolean.call(true) # => true
|
191
|
+
# boolean.call(false) # => false
|
192
|
+
# boolean.call(nil) # => nil
|
193
|
+
#
|
194
|
+
# boolean.call('true') # => true
|
195
|
+
# boolean.call('yes') # => true
|
196
|
+
# boolean.call('FALSE') # => false
|
197
|
+
#
|
198
|
+
# boolean.call(1) # => ValidationError
|
199
|
+
# boolean.call("str") # => ValidationError
|
200
|
+
#
|
201
|
+
def boolean(); BOOLEAN; end
|
202
|
+
BOOLEAN = yamlize_and_check(true, false, nil)
|
203
|
+
|
204
|
+
# Same as boolean.
|
205
|
+
def switch(); SWITCH; end
|
206
|
+
SWITCH = yamlize_and_check(true, false, nil)
|
207
|
+
|
208
|
+
# Same as boolean.
|
209
|
+
def flag(); FLAG; end
|
210
|
+
FLAG = yamlize_and_check(true, false, nil)
|
211
|
+
|
212
|
+
# Returns a block that checks the input is an array.
|
213
|
+
# String inputs are loaded as yaml first.
|
214
|
+
#
|
215
|
+
# array.class # => Proc
|
216
|
+
# array.call([1,2,3]) # => [1,2,3]
|
217
|
+
# array.call('[1, 2, 3]') # => [1,2,3]
|
218
|
+
# array.call(nil) # => ValidationError
|
219
|
+
# array.call('str') # => ValidationError
|
220
|
+
#
|
221
|
+
def array(); ARRAY; end
|
222
|
+
ARRAY = yamlize_and_check(Array)
|
223
|
+
|
224
|
+
# Same as array but allows nil:
|
225
|
+
#
|
226
|
+
# array_or_nil.call('~') # => nil
|
227
|
+
# array_or_nil.call(nil) # => nil
|
228
|
+
def array_or_nil(); ARRAY_OR_NIL; end
|
229
|
+
ARRAY_OR_NIL = yamlize_and_check(Array, nil)
|
230
|
+
|
231
|
+
# Returns a block that checks the input is an array,
|
232
|
+
# then yamlizes each string value of the array.
|
233
|
+
#
|
234
|
+
# list.class # => Proc
|
235
|
+
# list.call([1,2,3]) # => [1,2,3]
|
236
|
+
# list.call(['1', 'str']) # => [1,'str']
|
237
|
+
# list.call('str') # => ValidationError
|
238
|
+
# list.call(nil) # => ValidationError
|
239
|
+
#
|
240
|
+
def list(); LIST; end
|
241
|
+
list_block = lambda do |input|
|
242
|
+
validate(input, [Array]).collect do |arg|
|
243
|
+
arg.kind_of?(String) ? yamlize(arg) : arg
|
244
|
+
end
|
245
|
+
end
|
246
|
+
LIST = list_block
|
247
|
+
|
248
|
+
# Returns a block that checks the input is a hash.
|
249
|
+
# String inputs are loaded as yaml first.
|
250
|
+
#
|
251
|
+
# hash.class # => Proc
|
252
|
+
# hash.call({'key' => 'value'}) # => {'key' => 'value'}
|
253
|
+
# hash.call('key: value') # => {'key' => 'value'}
|
254
|
+
# hash.call(nil) # => ValidationError
|
255
|
+
# hash.call('str') # => ValidationError
|
256
|
+
#
|
257
|
+
def hash(); HASH; end
|
258
|
+
HASH = yamlize_and_check(Hash)
|
259
|
+
|
260
|
+
# Same as hash but allows nil:
|
261
|
+
#
|
262
|
+
# hash_or_nil.call('~') # => nil
|
263
|
+
# hash_or_nil.call(nil) # => nil
|
264
|
+
def hash_or_nil(); HASH_OR_NIL; end
|
265
|
+
HASH_OR_NIL = yamlize_and_check(Hash, nil)
|
266
|
+
|
267
|
+
# Returns a block that checks the input is an integer.
|
268
|
+
# String inputs are loaded as yaml first.
|
269
|
+
#
|
270
|
+
# integer.class # => Proc
|
271
|
+
# integer.call(1) # => 1
|
272
|
+
# integer.call('1') # => 1
|
273
|
+
# integer.call(1.1) # => ValidationError
|
274
|
+
# integer.call(nil) # => ValidationError
|
275
|
+
# integer.call('str') # => ValidationError
|
276
|
+
#
|
277
|
+
def integer(); INTEGER; end
|
278
|
+
INTEGER = yamlize_and_check(Integer)
|
279
|
+
|
280
|
+
# Same as integer but allows nil:
|
281
|
+
#
|
282
|
+
# integer_or_nil.call('~') # => nil
|
283
|
+
# integer_or_nil.call(nil) # => nil
|
284
|
+
def integer_or_nil(); INTEGER_OR_NIL; end
|
285
|
+
INTEGER_OR_NIL = yamlize_and_check(Integer, nil)
|
286
|
+
|
287
|
+
# Returns a block that checks the input is a float.
|
288
|
+
# String inputs are loaded as yaml first.
|
289
|
+
#
|
290
|
+
# float.class # => Proc
|
291
|
+
# float.call(1.1) # => 1.1
|
292
|
+
# float.call('1.1') # => 1.1
|
293
|
+
# float.call('1.0e+6') # => 1e6
|
294
|
+
# float.call(1) # => ValidationError
|
295
|
+
# float.call(nil) # => ValidationError
|
296
|
+
# float.call('str') # => ValidationError
|
297
|
+
#
|
298
|
+
def float(); FLOAT; end
|
299
|
+
FLOAT = yamlize_and_check(Float)
|
300
|
+
|
301
|
+
# Same as float but allows nil:
|
302
|
+
#
|
303
|
+
# float_or_nil.call('~') # => nil
|
304
|
+
# float_or_nil.call(nil) # => nil
|
305
|
+
def float_or_nil(); FLOAT_OR_NIL; end
|
306
|
+
FLOAT_OR_NIL = yamlize_and_check(Float, nil)
|
307
|
+
|
308
|
+
# Returns a block that checks the input is a number.
|
309
|
+
# String inputs are loaded as yaml first.
|
310
|
+
#
|
311
|
+
# num.class # => Proc
|
312
|
+
# num.call(1.1) # => 1.1
|
313
|
+
# num.call(1) # => 1
|
314
|
+
# num.call(1e6) # => 1e6
|
315
|
+
# num.call('1.1') # => 1.1
|
316
|
+
# num.call('1.0e+6') # => 1e6
|
317
|
+
# num.call(nil) # => ValidationError
|
318
|
+
# num.call('str') # => ValidationError
|
319
|
+
#
|
320
|
+
def num(); NUMERIC; end
|
321
|
+
NUMERIC = yamlize_and_check(Numeric)
|
322
|
+
|
323
|
+
# Same as num but allows nil:
|
324
|
+
#
|
325
|
+
# num_or_nil.call('~') # => nil
|
326
|
+
# num_or_nil.call(nil) # => nil
|
327
|
+
def num_or_nil(); NUMERIC_OR_NIL; end
|
328
|
+
NUMERIC_OR_NIL = yamlize_and_check(Numeric, nil)
|
329
|
+
|
330
|
+
# Returns a block that checks the input is a regexp.
|
331
|
+
# String inputs are converted to regexps using
|
332
|
+
# Regexp#new.
|
333
|
+
#
|
334
|
+
# regexp.class # => Proc
|
335
|
+
# regexp.call(/regexp/) # => /regexp/
|
336
|
+
# regexp.call('regexp') # => /regexp/
|
337
|
+
#
|
338
|
+
# # use of ruby-specific flags can turn on/off
|
339
|
+
# # features like case insensitive matching
|
340
|
+
# regexp.call('(?i)regexp') # => /(?i)regexp/
|
341
|
+
#
|
342
|
+
def regexp(); REGEXP; end
|
343
|
+
regexp_block = lambda do |input|
|
344
|
+
input = Regexp.new(input) if input.kind_of?(String)
|
345
|
+
validate(input, [Regexp])
|
346
|
+
end
|
347
|
+
REGEXP = regexp_block
|
348
|
+
|
349
|
+
# Same as regexp but allows nil. Note the special
|
350
|
+
# behavior of the nil string '~' -- rather than
|
351
|
+
# being converted to a regexp, it is processed as
|
352
|
+
# nil to be consistent with the other [class]_or_nil
|
353
|
+
# methods.
|
354
|
+
#
|
355
|
+
# regexp_or_nil.call('~') # => nil
|
356
|
+
# regexp_or_nil.call(nil) # => nil
|
357
|
+
def regexp_or_nil(); REGEXP_OR_NIL; end
|
358
|
+
regexp_or_nil_block = lambda do |input|
|
359
|
+
input = case input
|
360
|
+
when nil, '~' then nil
|
361
|
+
when String then Regexp.new(input)
|
362
|
+
else input
|
363
|
+
end
|
364
|
+
|
365
|
+
validate(input, [Regexp, nil])
|
366
|
+
end
|
367
|
+
REGEXP_OR_NIL = regexp_or_nil_block
|
368
|
+
|
369
|
+
# Returns a block that checks the input is a range.
|
370
|
+
# String inputs are split into a beginning and
|
371
|
+
# end if possible, where each part is loaded as
|
372
|
+
# yaml before being used to construct a Range.a
|
373
|
+
#
|
374
|
+
# range.class # => Proc
|
375
|
+
# range.call(1..10) # => 1..10
|
376
|
+
# range.call('1..10') # => 1..10
|
377
|
+
# range.call('a..z') # => 'a'..'z'
|
378
|
+
# range.call('-10...10') # => -10...10
|
379
|
+
# range.call(nil) # => ValidationError
|
380
|
+
# range.call('1.10') # => ValidationError
|
381
|
+
# range.call('a....z') # => ValidationError
|
382
|
+
#
|
383
|
+
def range(); RANGE; end
|
384
|
+
range_block = lambda do |input|
|
385
|
+
if input.kind_of?(String) && input =~ /^([^.]+)(\.{2,3})([^.]+)$/
|
386
|
+
input = Range.new(yamlize($1), yamlize($3), $2.length == 3)
|
387
|
+
end
|
388
|
+
validate(input, [Range])
|
389
|
+
end
|
390
|
+
RANGE = range_block
|
391
|
+
|
392
|
+
# Same as range but allows nil:
|
393
|
+
#
|
394
|
+
# range_or_nil.call('~') # => nil
|
395
|
+
# range_or_nil.call(nil) # => nil
|
396
|
+
def range_or_nil(); RANGE_OR_NIL; end
|
397
|
+
range_or_nil_block = lambda do |input|
|
398
|
+
input = case input
|
399
|
+
when nil, '~' then nil
|
400
|
+
when String
|
401
|
+
if input =~ /^([^.]+)(\.{2,3})([^.]+)$/
|
402
|
+
Range.new(yamlize($1), yamlize($3), $2.length == 3)
|
403
|
+
else
|
404
|
+
input
|
405
|
+
end
|
406
|
+
else input
|
407
|
+
end
|
408
|
+
|
409
|
+
validate(input, [Range, nil])
|
410
|
+
end
|
411
|
+
RANGE_OR_NIL = range_or_nil_block
|
412
|
+
|
413
|
+
# Returns a block that checks the input is a Time. String inputs are
|
414
|
+
# loaded using Time.parse and then converted into times. Parsed times
|
415
|
+
# are local unless specified otherwise.
|
416
|
+
#
|
417
|
+
# time.class # => Proc
|
418
|
+
#
|
419
|
+
# now = Time.now
|
420
|
+
# time.call(now) # => now
|
421
|
+
#
|
422
|
+
# time.call('2008-08-08 20:00:00.00 +08:00').getutc.strftime('%Y/%m/%d %H:%M:%S')
|
423
|
+
# # => '2008/08/08 12:00:00'
|
424
|
+
#
|
425
|
+
# time.call('2008-08-08').strftime('%Y/%m/%d %H:%M:%S')
|
426
|
+
# # => '2008/08/08 00:00:00'
|
427
|
+
#
|
428
|
+
# time.call(1) # => ValidationError
|
429
|
+
# time.call(nil) # => ValidationError
|
430
|
+
#
|
431
|
+
# Warning: Time.parse will parse a valid time (Time.now)
|
432
|
+
# even when no time is specified:
|
433
|
+
#
|
434
|
+
# time.call('str').strftime('%Y/%m/%d %H:%M:%S')
|
435
|
+
# # => Time.now.strftime('%Y/%m/%d %H:%M:%S')
|
436
|
+
#
|
437
|
+
def time()
|
438
|
+
# adding this here is a compromise to lazy-load the parse
|
439
|
+
# method (autoload doesn't work since Time already exists)
|
440
|
+
require 'time' unless Time.respond_to?(:parse)
|
441
|
+
TIME
|
442
|
+
end
|
443
|
+
|
444
|
+
time_block = lambda do |input|
|
445
|
+
input = Time.parse(input) if input.kind_of?(String)
|
446
|
+
validate(input, [Time])
|
447
|
+
end
|
448
|
+
TIME = time_block
|
449
|
+
|
450
|
+
# Same as time but allows nil:
|
451
|
+
#
|
452
|
+
# time_or_nil.call('~') # => nil
|
453
|
+
# time_or_nil.call(nil) # => nil
|
454
|
+
def time_or_nil()
|
455
|
+
# adding this check is a compromise to autoload the parse
|
456
|
+
# method (autoload doesn't work since Time already exists)
|
457
|
+
require 'time' unless Time.respond_to?(:parse)
|
458
|
+
TIME_OR_NIL
|
459
|
+
end
|
460
|
+
|
461
|
+
time_or_nil_block = lambda do |input|
|
462
|
+
input = case input
|
463
|
+
when nil, '~' then nil
|
464
|
+
when String then Time.parse(input)
|
465
|
+
else input
|
466
|
+
end
|
467
|
+
|
468
|
+
validate(input, [Time, nil])
|
469
|
+
end
|
470
|
+
TIME_OR_NIL = time_or_nil_block
|
471
|
+
|
472
|
+
# A hash of default attributes for the validation blocks.
|
473
|
+
ATTRIBUTES = Hash.new({})
|
474
|
+
ATTRIBUTES[SWITCH] = {:type => :switch}
|
475
|
+
ATTRIBUTES[FLAG] = {:type => :flag}
|
476
|
+
ATTRIBUTES[LIST] = {:type => :list, :split => ','}
|
477
|
+
ATTRIBUTES[ARRAY] = {:arg_name => "'[a, b, c]'"}
|
478
|
+
ATTRIBUTES[HASH] = {:arg_name => "'{one: 1, two: 2}'"}
|
479
|
+
end
|
480
|
+
end
|