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