configurable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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