configurable 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|