doodle 0.1.8 → 0.1.9
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/History.txt +52 -0
- data/Manifest.txt +1 -0
- data/examples/mail-datatypes.rb +1 -0
- data/examples/mail.rb +1 -1
- data/examples/profile-options.rb +1 -1
- data/lib/doodle.rb +332 -103
- data/lib/doodle/app.rb +445 -0
- data/lib/doodle/datatypes.rb +124 -15
- data/lib/doodle/version.rb +1 -1
- data/lib/molic_orderedhash.rb +99 -162
- data/log/debug.log +1 -0
- data/spec/block_init_spec.rb +52 -0
- data/spec/bugs_spec.rb +114 -18
- data/spec/class_var_spec.rb +28 -14
- data/spec/conversion_spec.rb +36 -38
- data/spec/doodle_spec.rb +23 -23
- data/spec/has_spec.rb +19 -1
- data/spec/init_spec.rb +22 -16
- data/spec/member_init_spec.rb +122 -0
- data/spec/readonly_spec.rb +32 -0
- data/spec/singleton_spec.rb +7 -7
- data/spec/spec_helper.rb +1 -1
- data/spec/symbolize_keys_spec.rb +40 -0
- data/spec/to_hash_spec.rb +35 -0
- metadata +39 -22
data/lib/doodle/app.rb
ADDED
@@ -0,0 +1,445 @@
|
|
1
|
+
# -*- mode: ruby; -*-
|
2
|
+
# command line option handling DSL implemented using Doodle
|
3
|
+
# Sean O'Halpin, 2008-09-29
|
4
|
+
|
5
|
+
=begin
|
6
|
+
|
7
|
+
to do:
|
8
|
+
- add file mode to filename (or have separate file type)
|
9
|
+
- use PathName?
|
10
|
+
- sort out lists of things (with types)
|
11
|
+
- apply match ~before~ from
|
12
|
+
- handle config files
|
13
|
+
|
14
|
+
=end
|
15
|
+
|
16
|
+
require 'doodle'
|
17
|
+
require 'doodle/datatypes'
|
18
|
+
require 'time'
|
19
|
+
require 'date'
|
20
|
+
require 'pp'
|
21
|
+
|
22
|
+
# note that all the datatypes within doodle do ... end blocks are
|
23
|
+
# doodle/datatypes - I then go on to define specialized option types
|
24
|
+
# with many of the same names but they are not the same - could be
|
25
|
+
# confusing :)
|
26
|
+
|
27
|
+
class Doodle
|
28
|
+
class App < Doodle
|
29
|
+
# specialised classes for handling attributes
|
30
|
+
def self.tidy_dir(path)
|
31
|
+
path.to_s.gsub(Regexp.new("^#{ Dir.pwd }/"), './')
|
32
|
+
end
|
33
|
+
|
34
|
+
# generic option
|
35
|
+
class Option < Doodle::DoodleAttribute
|
36
|
+
doodle do
|
37
|
+
string :flag, :max => 1, :doc => "one character abbreviation" do
|
38
|
+
init {
|
39
|
+
mf = name.to_s[0].chr
|
40
|
+
attrs = doodle_owner.doodle.attributes.map{ |k, v| v}
|
41
|
+
chk_attrs = attrs.reject{ |v|
|
42
|
+
v.name == name
|
43
|
+
}
|
44
|
+
# note: cannot check v.flag inside default clause because that
|
45
|
+
# causes an infinite regress (yes, I found out the hard way! :)
|
46
|
+
if chk_attrs.any?{|v|
|
47
|
+
v.respond_to?(:flag) && v.flag == mf
|
48
|
+
}
|
49
|
+
""
|
50
|
+
else
|
51
|
+
mf
|
52
|
+
end
|
53
|
+
}
|
54
|
+
end
|
55
|
+
integer :arity, :default => 1, :doc => "how many arguments are expected"
|
56
|
+
has :values, :default => [], :doc => "valid values for this option"
|
57
|
+
has :match, :default => nil, :doc => "regex to match against"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
# specialied Filename attribute
|
61
|
+
class Filename < Option
|
62
|
+
doodle do
|
63
|
+
boolean :existing, :default => false, :doc => "set to true if file must exist"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
RX_ISODATE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)? ?Z$/
|
68
|
+
|
69
|
+
# App directives
|
70
|
+
class << self
|
71
|
+
public
|
72
|
+
|
73
|
+
has :script_name, :default => File.basename($0)
|
74
|
+
has :doc, :default => $0
|
75
|
+
has :usage do
|
76
|
+
default { "./#{script_name} #{required_args.map{ |a| %[-#{ a.flag } #{format_kind(a.kind)}]}.join(' ')}" + ((required_args.size - doodle.attributes.size) > 0 ? " [OPTIONS]" : '') }
|
77
|
+
end
|
78
|
+
has :examples, :default => nil
|
79
|
+
alias :example :examples
|
80
|
+
|
81
|
+
def required
|
82
|
+
@optional = false
|
83
|
+
end
|
84
|
+
def optional
|
85
|
+
@optional = true
|
86
|
+
end
|
87
|
+
def optional?
|
88
|
+
instance_variable_defined?("@optional") ? @optional : false
|
89
|
+
end
|
90
|
+
def required_args
|
91
|
+
doodle.attributes.select{ |k, v| v.required?}.map{ |k,v| v}
|
92
|
+
end
|
93
|
+
alias :options :optional
|
94
|
+
|
95
|
+
# the generic option - can be any type
|
96
|
+
def option(*args, &block)
|
97
|
+
#p [:option, args, :optional, optional?]
|
98
|
+
key_values, args = args.partition{ |x| x.kind_of?(Hash)}
|
99
|
+
key_values = key_values.inject({ }){ |hash, kv| hash.merge(kv)}
|
100
|
+
|
101
|
+
errors = []
|
102
|
+
|
103
|
+
# handle optional/required flipflop
|
104
|
+
if optional?
|
105
|
+
required = { :default => nil }
|
106
|
+
if key_values.delete(:required)
|
107
|
+
if key_values.key?(:optional)
|
108
|
+
errors << "Can't specify both :required and :optional"
|
109
|
+
end
|
110
|
+
if key_values.key?(:default)
|
111
|
+
errors << "Can't specify both :required and :default"
|
112
|
+
end
|
113
|
+
required = { }
|
114
|
+
elsif key_values.delete(:optional) && !key_values.key?(:default)
|
115
|
+
required = { :default => nil }
|
116
|
+
end
|
117
|
+
else
|
118
|
+
key_values.delete(:required)
|
119
|
+
required = { }
|
120
|
+
end
|
121
|
+
args = [{ :using => Option }.merge(required).merge(key_values), *args]
|
122
|
+
da = has(*args, &block)
|
123
|
+
if errors.size > 0
|
124
|
+
raise ArgumentError, "#{da.name}: #{errors.join(', ')}", [caller[-1]]
|
125
|
+
end
|
126
|
+
da.instance_eval do
|
127
|
+
#p [:checking_values, values, values.class]
|
128
|
+
if da.values.kind_of?(Range)
|
129
|
+
must "be in range #{da.values}" do |s|
|
130
|
+
da.values.include?(s)
|
131
|
+
end
|
132
|
+
elsif da.values.respond_to?(:size) && da.values.size > 0
|
133
|
+
must "be one of #{da.values.join(', ')}" do |s|
|
134
|
+
da.values.include?(s)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
if da.match
|
138
|
+
must "match pattern #{da.match.inspect}" do |s|
|
139
|
+
#p [:matching, s, da.match.inspect]
|
140
|
+
s.to_s =~ da.match
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
da
|
145
|
+
end
|
146
|
+
# expect string
|
147
|
+
def string(*args, &block)
|
148
|
+
args = [{ :using => Option, :kind => String }, *args]
|
149
|
+
da = option(*args, &block)
|
150
|
+
end
|
151
|
+
# expect symbol
|
152
|
+
def symbol(*args, &block)
|
153
|
+
args = [{ :using => Option, :kind => Symbol }, *args]
|
154
|
+
da = option(*args, &block)
|
155
|
+
da.instance_eval do
|
156
|
+
from String do |s|
|
157
|
+
s.to_sym
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
# expect filename
|
162
|
+
# filename :input, :existing => true, :flag => "i", :doc => "input file name"
|
163
|
+
def filename(*args, &block)
|
164
|
+
args = [{ :using => Filename, :kind => String }, *args ]
|
165
|
+
da = option(*args, &block)
|
166
|
+
da.instance_eval do
|
167
|
+
if da.existing
|
168
|
+
must "exist" do |s|
|
169
|
+
File.exist?(s)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
# expect on/off flag, e.g. -b
|
175
|
+
# doesn't take any arguments (mere presence sets it to true)
|
176
|
+
# booleans are false by default
|
177
|
+
def boolean(*args, &block)
|
178
|
+
args = [{ :using => Option, :default => false, :arity => 0}, *args]
|
179
|
+
da = option(*args, &block)
|
180
|
+
da.instance_eval do
|
181
|
+
kind FalseClass, TrueClass
|
182
|
+
from NilClass do
|
183
|
+
false
|
184
|
+
end
|
185
|
+
from Numeric do |n|
|
186
|
+
n == 0 ? false : true
|
187
|
+
end
|
188
|
+
from String do |s|
|
189
|
+
case s
|
190
|
+
when "on", "true", "yes", "1"
|
191
|
+
true
|
192
|
+
when "off", "false", "no", "0"
|
193
|
+
false
|
194
|
+
else
|
195
|
+
raise Doodle::ValidationError, "unknown value for boolean: #{s}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
# whole number, e.g. -n 10
|
201
|
+
# you can use, e.g. :values => [1,2,3] or :values => (0..99) to restrict valid values
|
202
|
+
def integer(*args, &block)
|
203
|
+
args = [{ :using => Option, :kind => Integer }, *args]
|
204
|
+
da = option(*args, &block)
|
205
|
+
da.instance_eval do
|
206
|
+
from String do |s|
|
207
|
+
s =~ /^\d+$/ or raise "must be a whole number"
|
208
|
+
s.to_i
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
# date: -d 2008-09-28
|
213
|
+
def date(*args, &block)
|
214
|
+
args = [{ :using => Option, :kind => Date }, *args]
|
215
|
+
da = option(*args, &block)
|
216
|
+
da.instance_eval do
|
217
|
+
from String do |s|
|
218
|
+
Date.parse(s)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
# time: -d 2008-09-28T18:00:00
|
223
|
+
def time(*args, &block)
|
224
|
+
args = [{ :using => Option, :kind => Time }, *args]
|
225
|
+
da = option(*args, &block)
|
226
|
+
da.instance_eval do
|
227
|
+
from String do |s|
|
228
|
+
Time.parse(s)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
# utcdate: -d 2008-09-28T21:41:29Z
|
233
|
+
# actually uses Time (so restricted range)
|
234
|
+
def utcdate(*args, &block)
|
235
|
+
args = [{ :using => Option, :kind => Time }, *args]
|
236
|
+
da = option(*args, &block)
|
237
|
+
da.instance_eval do
|
238
|
+
from String do |s|
|
239
|
+
if s !~ RX_ISODATE
|
240
|
+
raise ArgumentError, "date must be in ISO format (YYYY-MM-DDTHH:MM:SS, e.g. #{Time.now.utc.xmlschema})"
|
241
|
+
end
|
242
|
+
Time.parse(s)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# use this to include 'standard' flags
|
248
|
+
def std_flags
|
249
|
+
boolean :help, :flag => "h", :doc => "display this help"
|
250
|
+
boolean :verbose, :flag => "v", :doc => "verbose output"
|
251
|
+
boolean :debug, :flag => "D", :doc => "turn on debugging"
|
252
|
+
end
|
253
|
+
|
254
|
+
has :exit_status, :default => 0
|
255
|
+
|
256
|
+
# call App.run to start your application (calls instance.run)
|
257
|
+
def run(argv = ARGV)
|
258
|
+
begin
|
259
|
+
app = from_argv(argv)
|
260
|
+
if app.help
|
261
|
+
puts help_text
|
262
|
+
else
|
263
|
+
app.run
|
264
|
+
end
|
265
|
+
rescue Object => e
|
266
|
+
if exit_status == 0
|
267
|
+
exit_status 1
|
268
|
+
end
|
269
|
+
puts "\nERROR: #{e}"
|
270
|
+
puts
|
271
|
+
puts help_text
|
272
|
+
ensure
|
273
|
+
exit(exit_status)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
|
279
|
+
# helpers
|
280
|
+
def flag_to_attribute(flag)
|
281
|
+
a = doodle.attributes.select do |key, attr|
|
282
|
+
(key.to_s == flag.to_s) || (attr.respond_to?(:flag) && attr.flag.to_s == flag.to_s)
|
283
|
+
end
|
284
|
+
if a.size == 0
|
285
|
+
raise ArgumentError, "Unknown option: #{flag}"
|
286
|
+
elsif a.size > 1
|
287
|
+
#raise ArgumentError, "More than one option matches: #{flag}"
|
288
|
+
end
|
289
|
+
a.first
|
290
|
+
end
|
291
|
+
def key_value(arg, argv)
|
292
|
+
value = nil
|
293
|
+
if arg[0] == ?-
|
294
|
+
# got flag
|
295
|
+
#p [:a, 1, arg]
|
296
|
+
if arg[1] == ?-
|
297
|
+
# got --flag
|
298
|
+
# --key value
|
299
|
+
key = arg[2..-1]
|
300
|
+
#p [:a, 2, arg, key]
|
301
|
+
if key == ""
|
302
|
+
key = "--"
|
303
|
+
end
|
304
|
+
else
|
305
|
+
key = arg[1].chr
|
306
|
+
#p [:a, 4, key]
|
307
|
+
if arg[2]
|
308
|
+
# -kvalue
|
309
|
+
value = arg[2..-1]
|
310
|
+
#p [:a, 5, key, value]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
pkey, attr = flag_to_attribute(key)
|
314
|
+
if pkey.nil?
|
315
|
+
raise Exception, "Internal error: #{key} does not match attribute"
|
316
|
+
end
|
317
|
+
#p [:flag_to_attribute, key, value, pkey, attr]
|
318
|
+
if value.nil?
|
319
|
+
if attr.arity == 0
|
320
|
+
value = true
|
321
|
+
else
|
322
|
+
#p [:args, 5, :getting_args, attr.arity]
|
323
|
+
value = []
|
324
|
+
1.upto(attr.arity) do
|
325
|
+
a = argv.shift
|
326
|
+
break if a.nil?
|
327
|
+
#p [:a, 6, key, value]
|
328
|
+
if a =~ /^-/
|
329
|
+
# got a switch - break? (what should happen here?)
|
330
|
+
#p [:a, 7, key, value]
|
331
|
+
argv.unshift a
|
332
|
+
break
|
333
|
+
else
|
334
|
+
value << a
|
335
|
+
end
|
336
|
+
end
|
337
|
+
if attr.arity == 1
|
338
|
+
value = *value
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
if key.size == 1
|
343
|
+
#p [:finding, key]
|
344
|
+
#p [:found, pkey, attr]
|
345
|
+
key = pkey.to_s
|
346
|
+
end
|
347
|
+
end
|
348
|
+
[key, value]
|
349
|
+
end
|
350
|
+
def params_args(argv)
|
351
|
+
argv = argv.dup
|
352
|
+
params = { }
|
353
|
+
args = []
|
354
|
+
while arg = argv.shift
|
355
|
+
key, value = key_value(arg, argv)
|
356
|
+
if key.nil?
|
357
|
+
args << arg
|
358
|
+
else
|
359
|
+
#p [:setting, key, value]
|
360
|
+
params[key] = value
|
361
|
+
end
|
362
|
+
end
|
363
|
+
[params, args]
|
364
|
+
end
|
365
|
+
def from_argv(argv)
|
366
|
+
params, args = params_args(argv)
|
367
|
+
args << params
|
368
|
+
#pp [:args, args]
|
369
|
+
new(*args)
|
370
|
+
end
|
371
|
+
def format_values(values)
|
372
|
+
case values
|
373
|
+
when Array
|
374
|
+
values.map{ |s| s.to_s }.join(', ')
|
375
|
+
when Range
|
376
|
+
values.inspect
|
377
|
+
else
|
378
|
+
values.inspect
|
379
|
+
end
|
380
|
+
end
|
381
|
+
def format_doc(attr)
|
382
|
+
#p [:doc, attr.doc]
|
383
|
+
doc = attr.doc
|
384
|
+
if doc.kind_of?(Doodle::DeferredBlock)
|
385
|
+
doc = doc.block
|
386
|
+
end
|
387
|
+
if doc.kind_of?(Proc)
|
388
|
+
doc = attr.instance_eval(&doc)
|
389
|
+
end
|
390
|
+
if doc.respond_to?(:call)
|
391
|
+
doc = doc.call
|
392
|
+
end
|
393
|
+
doc = doc.to_s
|
394
|
+
if attr.respond_to?(:values) && (attr.values.kind_of?(Range) || attr.values.size > 0)
|
395
|
+
doc = "#{doc} [#{format_values(attr.values)}]"
|
396
|
+
end
|
397
|
+
doc
|
398
|
+
end
|
399
|
+
def format_kind(kind)
|
400
|
+
if (kind & [TrueClass, FalseClass, NilClass]).size > 0
|
401
|
+
"Boolean"
|
402
|
+
else
|
403
|
+
kind.map{ |k| k.to_s }.join(', ')
|
404
|
+
end.upcase
|
405
|
+
end
|
406
|
+
def help_attributes
|
407
|
+
options, args = doodle.attributes.partition { |key, attr| attr.respond_to?(:flag)}
|
408
|
+
args = args.map { |key, attr|
|
409
|
+
[
|
410
|
+
'',
|
411
|
+
'',
|
412
|
+
format_doc(attr),
|
413
|
+
attr.required?,
|
414
|
+
format_kind(attr.kind),
|
415
|
+
]
|
416
|
+
}
|
417
|
+
options = options.map { |key, attr|
|
418
|
+
[
|
419
|
+
"--#{key}",
|
420
|
+
attr.flag.to_s.size > 0 ? "-#{attr.flag}," : '',
|
421
|
+
format_doc(attr),
|
422
|
+
attr.required?,
|
423
|
+
format_kind(attr.kind),
|
424
|
+
]
|
425
|
+
}
|
426
|
+
args + options
|
427
|
+
end
|
428
|
+
public
|
429
|
+
def help_text
|
430
|
+
format_block = proc {|key, flag, doc, required, kind|
|
431
|
+
sprintf(" %-3s %-14s %-10s %s %s", flag, key, kind, doc, required ? '(REQUIRED)' : '')
|
432
|
+
}
|
433
|
+
required, options = help_attributes.partition{ |a| a[3]}
|
434
|
+
[
|
435
|
+
self.doc,
|
436
|
+
"\n",
|
437
|
+
self.usage ? ["Usage: " + self.usage, "\n"] : [],
|
438
|
+
required.size > 0 ? ["Required args:", required.map(&format_block), "\n"] : [],
|
439
|
+
options.size > 0 ? ["Options:", options.map(&format_block), "\n"] : [],
|
440
|
+
(self.examples && self.examples.size > 0) ? "Examples:\n" + " " + self.examples.join("\n ") : [],
|
441
|
+
]
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
data/lib/doodle/datatypes.rb
CHANGED
@@ -3,7 +3,6 @@ $:.unshift(File.join(File.dirname(__FILE__), '.'))
|
|
3
3
|
|
4
4
|
require 'doodle'
|
5
5
|
|
6
|
-
### user code
|
7
6
|
require 'date'
|
8
7
|
require 'uri'
|
9
8
|
require 'rfc822'
|
@@ -11,28 +10,82 @@ require 'rfc822'
|
|
11
10
|
# note: this doesn't have to be in Doodle namespace
|
12
11
|
class Doodle
|
13
12
|
module DataTypes
|
13
|
+
class DataType < Doodle::DoodleAttribute
|
14
|
+
doodle do
|
15
|
+
has :values, :default => [], :doc => "valid values for this option"
|
16
|
+
has :match, :default => nil, :doc => "regex to match against"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
def datatype(name, params, block, type_params, &type_block)
|
20
|
+
define name, params, block, { :using => DataType }.merge(type_params) do
|
21
|
+
#p [:self, __doodle__.__inspect__]
|
22
|
+
#p [:checking_values, values, values.class]
|
23
|
+
if respond_to?(:values)
|
24
|
+
if values.kind_of?(Range)
|
25
|
+
must "be in range #{values}" do |s|
|
26
|
+
values.include?(s)
|
27
|
+
end
|
28
|
+
# array of values
|
29
|
+
elsif values.respond_to?(:size) && values.size > 0
|
30
|
+
must "be one of #{values.join(', ')}" do |s|
|
31
|
+
values.include?(s)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
if respond_to?(:match) && match
|
36
|
+
must "match pattern #{match.inspect}" do |s|
|
37
|
+
#p [:matching, s, da.match.inspect]
|
38
|
+
s.to_s =~ match
|
39
|
+
end
|
40
|
+
end
|
41
|
+
instance_eval(&type_block) if type_block
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
14
45
|
def integer(name, params = { }, &block)
|
15
|
-
|
46
|
+
if params.key?(:max)
|
47
|
+
max = params.delete(:max)
|
48
|
+
end
|
49
|
+
if params.key?(:min)
|
50
|
+
min = params.delete(:min)
|
51
|
+
end
|
52
|
+
datatype name, params, block, { :kind => Integer } do
|
16
53
|
from Float do |n|
|
17
54
|
n.to_i
|
18
55
|
end
|
19
56
|
from String do |n|
|
20
|
-
n =~ /[0-9]+(.[0-9]+)?/ or raise ArgumentError, "#{name} must be numeric"
|
57
|
+
n =~ /[0-9]+(.[0-9]+)?/ or raise ArgumentError, "#{name} must be numeric", [caller[-1]]
|
21
58
|
n.to_i
|
22
59
|
end
|
60
|
+
if max
|
61
|
+
must "be <= #{max}" do |s|
|
62
|
+
s.size <= max
|
63
|
+
end
|
64
|
+
end
|
65
|
+
if min
|
66
|
+
must "be >= #{min}" do |s|
|
67
|
+
s.size >= min
|
68
|
+
end
|
69
|
+
end
|
23
70
|
end
|
24
71
|
end
|
25
72
|
|
26
73
|
def boolean(name, params = { }, &block)
|
27
|
-
|
74
|
+
datatype name, params, block, { :default => true } do
|
28
75
|
must "be true or false" do |v|
|
29
76
|
[true, false].include?(v)
|
30
77
|
end
|
78
|
+
from NilClass do |v|
|
79
|
+
false
|
80
|
+
end
|
81
|
+
from Integer do |v|
|
82
|
+
v == 0 ? false : true
|
83
|
+
end
|
31
84
|
from String, Symbol do |v|
|
32
85
|
case v.to_s
|
33
|
-
when /^(yes|true|on)$/
|
86
|
+
when /^(yes|true|on|1)$/
|
34
87
|
true
|
35
|
-
when /^(no|false|off)$/
|
88
|
+
when /^(no|false|off|0)$/
|
36
89
|
false
|
37
90
|
else
|
38
91
|
v
|
@@ -40,18 +93,18 @@ class Doodle
|
|
40
93
|
end
|
41
94
|
end
|
42
95
|
end
|
43
|
-
|
96
|
+
|
44
97
|
def symbol(name, params = { }, &block)
|
45
|
-
|
98
|
+
datatype name, params, block, { :kind => Symbol } do
|
46
99
|
from String do |s|
|
47
100
|
s.to_sym
|
48
101
|
end
|
49
102
|
end
|
50
103
|
end
|
51
|
-
|
104
|
+
|
52
105
|
def string(name, params = { }, &block)
|
53
106
|
# must extract non-standard attributes before processing with
|
54
|
-
#
|
107
|
+
# datatype otherwise causes UnknownAttribute error in Attribute definition
|
55
108
|
if params.key?(:max)
|
56
109
|
max = params.delete(:max)
|
57
110
|
end
|
@@ -60,7 +113,7 @@ class Doodle
|
|
60
113
|
# size should be a Range
|
61
114
|
size.kind_of?(Range) or raise ArgumentError, "#{name}: size should be a Range", [caller[-1]]
|
62
115
|
end
|
63
|
-
|
116
|
+
datatype name, params, block, { :kind => String } do
|
64
117
|
from String do |s|
|
65
118
|
s
|
66
119
|
end
|
@@ -84,7 +137,7 @@ class Doodle
|
|
84
137
|
end
|
85
138
|
|
86
139
|
def uri(name, params = { }, &block)
|
87
|
-
|
140
|
+
datatype name, params, block, { :kind => URI } do
|
88
141
|
from String do |s|
|
89
142
|
URI.parse(s)
|
90
143
|
end
|
@@ -100,9 +153,9 @@ class Doodle
|
|
100
153
|
end
|
101
154
|
end
|
102
155
|
end
|
103
|
-
|
156
|
+
|
104
157
|
def date(name, params = { }, &block)
|
105
|
-
|
158
|
+
datatype name, params, block, { :kind => Date } do
|
106
159
|
from String do |s|
|
107
160
|
Date.parse(s)
|
108
161
|
end
|
@@ -115,8 +168,46 @@ class Doodle
|
|
115
168
|
end
|
116
169
|
end
|
117
170
|
|
171
|
+
# defaults to UTC if passed an array
|
172
|
+
# use :timezone => :local if you want local time
|
173
|
+
def time(name, params = { }, &block)
|
174
|
+
timezone_method = params.delete(:timezone) || :utc
|
175
|
+
if timezone_method == :local
|
176
|
+
timezone_method = :mktime
|
177
|
+
end
|
178
|
+
datatype name, params, block, { :kind => Time } do
|
179
|
+
from String do |s|
|
180
|
+
Time.parse(s)
|
181
|
+
end
|
182
|
+
from Array do |*args|
|
183
|
+
Time.send(timezone_method, *args)
|
184
|
+
end
|
185
|
+
# seconds since Thu Jan 01 00:00:00 UTC 1970
|
186
|
+
from Integer do |epoch_seconds|
|
187
|
+
Time.at(epoch_seconds)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
RX_ISODATE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)? ?Z$/
|
193
|
+
|
194
|
+
def utc(name, params = { }, &block)
|
195
|
+
da = time( name, { :kind => Time }.merge(params))
|
196
|
+
da.instance_eval do
|
197
|
+
# override time from String
|
198
|
+
from String do |s|
|
199
|
+
if s !~ RX_ISODATE
|
200
|
+
raise ArgumentError, "date must be in ISO format (YYYY-MM-DDTHH:MM:SS, e.g. #{Time.now.utc.xmlschema})"
|
201
|
+
end
|
202
|
+
Time.parse(s)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
da.instance_eval(&block) if block_given?
|
206
|
+
da
|
207
|
+
end
|
208
|
+
|
118
209
|
def version(name, params = { }, &block)
|
119
|
-
|
210
|
+
datatype name, params, block, { :kind => String } do
|
120
211
|
must "be of form n.n.n" do |str|
|
121
212
|
str =~ /\d+\.\d+.\d+/
|
122
213
|
end
|
@@ -126,6 +217,24 @@ class Doodle
|
|
126
217
|
end
|
127
218
|
end
|
128
219
|
end
|
220
|
+
|
221
|
+
def list(name, params = { }, &block)
|
222
|
+
if name.kind_of?(Class)
|
223
|
+
params[:collect] = name
|
224
|
+
name = Doodle::Utils.pluralize(Doodle::Utils.snake_case(name))
|
225
|
+
end
|
226
|
+
raise ArgumentError, "#{name} must specify what to :collect", [caller[-1]] if !params.key?(:collect)
|
227
|
+
datatype name, params, block, { :using => Doodle::AppendableAttribute }
|
228
|
+
end
|
229
|
+
|
230
|
+
def dictionary(name, params = { }, &block)
|
231
|
+
if name.kind_of?(Class)
|
232
|
+
params[:collect] = name
|
233
|
+
name = Doodle::Utils.pluralize(Doodle::Utils.snake_case(name))
|
234
|
+
end
|
235
|
+
raise ArgumentError, "#{name} must specify what to :collect", [caller[-1]] if !params.key?(:collect)
|
236
|
+
datatype name, params, block, { :using => Doodle::KeyedAttribute }
|
237
|
+
end
|
129
238
|
end
|
130
239
|
end
|
131
240
|
|