doodle 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +21 -0
- data/examples/profile-options.rb +2 -1
- data/lib/doodle.rb +251 -200
- data/lib/doodle/version.rb +1 -1
- data/spec/attributes_spec.rb +24 -24
- data/spec/bugs_spec.rb +3 -3
- data/spec/class_spec.rb +4 -4
- data/spec/collector_spec.rb +36 -0
- data/spec/conversion_spec.rb +1 -1
- data/spec/defaults_spec.rb +8 -8
- data/spec/doodle_spec.rb +21 -21
- data/spec/extra_args_spec.rb +3 -3
- data/spec/inheritance_spec.rb +11 -11
- data/spec/init_spec.rb +6 -6
- data/spec/singleton_spec.rb +12 -12
- data/spec/specialized_attribute_class_spec.rb +5 -5
- data/spec/superclass_spec.rb +6 -6
- data/spec/validation_spec.rb +5 -5
- metadata +2 -2
data/History.txt
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
== 0.1.6 / 2008-05-08
|
2
|
+
- Features:
|
3
|
+
- short cut syntax for #must - can now specify constraints like
|
4
|
+
this:
|
5
|
+
must "size > 0"
|
6
|
+
must "self =~ /[A-Z]"
|
7
|
+
which will be evaluated as if you had written this:
|
8
|
+
must "self =~ /[A-Z]" do |v|
|
9
|
+
v.instance_eval("self =~ /[A-Z]")
|
10
|
+
end
|
11
|
+
- prefixed more public but undocumented methods with 'doodle_' to avoid clashing with
|
12
|
+
common names (e.g. parents, attributes, validations, etc.)
|
13
|
+
- renamed Doodle::Attribute Doodle::DoodleAttribute for same reason
|
14
|
+
- updated specs to reflect name changes
|
15
|
+
- attribute level validation messages now denote containing class
|
16
|
+
- major refactoring of #has method - collections now handled by
|
17
|
+
specialized attribute collector classes
|
18
|
+
|
19
|
+
- Bug fixes:
|
20
|
+
- can now load keyed collection from hash
|
21
|
+
|
1
22
|
== 0.1.5 / 2008-05-06
|
2
23
|
- Bug fixes:
|
3
24
|
- fixed bug where defaults were preventing validation working when loading from
|
data/examples/profile-options.rb
CHANGED
data/lib/doodle.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
# Copyright (C) 2007-2008 by Sean O'Halpin
|
3
3
|
# 2007-11-24 first version
|
4
4
|
# 2008-04-18 latest release 0.0.12
|
5
|
+
# 2008-05-07 0.1.6
|
5
6
|
$:.unshift(File.dirname(__FILE__)) unless
|
6
7
|
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
7
8
|
|
@@ -56,9 +57,12 @@ class Doodle
|
|
56
57
|
def snake_case(camel_cased_word)
|
57
58
|
camel_cased_word.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
|
58
59
|
end
|
60
|
+
def const_resolve(constant)
|
61
|
+
constant.to_s.split(/::/).reject{|x| x.empty?}.inject(Object) { |prev, this| prev.const_get(this) }
|
62
|
+
end
|
59
63
|
end
|
60
64
|
end
|
61
|
-
|
65
|
+
|
62
66
|
# error handling
|
63
67
|
@@raise_exception_on_error = true
|
64
68
|
def self.raise_exception_on_error
|
@@ -109,13 +113,13 @@ class Doodle
|
|
109
113
|
# classes as well as modules, classes and instances
|
110
114
|
module Inherited
|
111
115
|
|
112
|
-
#
|
113
|
-
def
|
116
|
+
# doodle_parents returns the set of parent classes of an object
|
117
|
+
def doodle_parents
|
114
118
|
anc = if respond_to?(:ancestors)
|
115
119
|
if ancestors.include?(self)
|
116
120
|
ancestors[1..-1]
|
117
121
|
else
|
118
|
-
# singletons have no
|
122
|
+
# singletons have no doodle_parents (they're orphans)
|
119
123
|
[]
|
120
124
|
end
|
121
125
|
else
|
@@ -129,11 +133,11 @@ class Doodle
|
|
129
133
|
# - instance_attributes
|
130
134
|
# - singleton_attributes
|
131
135
|
# - class_attributes
|
132
|
-
|
133
|
-
# send message to all
|
134
|
-
def
|
136
|
+
|
137
|
+
# send message to all doodle_parents and collect results
|
138
|
+
def doodle_collect_inherited(message)
|
135
139
|
result = []
|
136
|
-
|
140
|
+
doodle_parents.each do |klass|
|
137
141
|
if klass.respond_to?(message)
|
138
142
|
result.unshift(*klass.__send__(message))
|
139
143
|
else
|
@@ -142,7 +146,7 @@ class Doodle
|
|
142
146
|
end
|
143
147
|
result
|
144
148
|
end
|
145
|
-
private :
|
149
|
+
private :doodle_collect_inherited
|
146
150
|
end
|
147
151
|
|
148
152
|
# = embrace
|
@@ -204,19 +208,19 @@ class Doodle
|
|
204
208
|
|
205
209
|
# place to stash bookkeeping info
|
206
210
|
class DoodleInfo
|
207
|
-
attr_accessor :
|
208
|
-
attr_accessor :
|
209
|
-
attr_accessor :
|
211
|
+
attr_accessor :doodle_local_attributes
|
212
|
+
attr_accessor :doodle_local_validations
|
213
|
+
attr_accessor :doodle_local_conversions
|
210
214
|
attr_accessor :validation_on
|
211
215
|
attr_accessor :arg_order
|
212
216
|
attr_accessor :errors
|
213
217
|
attr_accessor :doodle_parent
|
214
218
|
|
215
219
|
def initialize(object)
|
216
|
-
@
|
217
|
-
@
|
220
|
+
@doodle_local_attributes = OrderedHash.new
|
221
|
+
@doodle_local_validations = []
|
218
222
|
@validation_on = true
|
219
|
-
@
|
223
|
+
@doodle_local_conversions = {}
|
220
224
|
@arg_order = []
|
221
225
|
@errors = []
|
222
226
|
@doodle_parent = nil
|
@@ -241,8 +245,9 @@ class Doodle
|
|
241
245
|
end
|
242
246
|
end
|
243
247
|
|
244
|
-
# the core module of Doodle - to get most facilities
|
245
|
-
# without inheriting from Doodle, include
|
248
|
+
# the core module of Doodle - however, to get most facilities
|
249
|
+
# provided by Doodle without inheriting from Doodle, include
|
250
|
+
# Doodle::Core, not this module
|
246
251
|
module BaseMethods
|
247
252
|
include SelfClass
|
248
253
|
include Inherited
|
@@ -296,7 +301,7 @@ class Doodle
|
|
296
301
|
|
297
302
|
def _handle_inherited_hash(tf, method)
|
298
303
|
if tf
|
299
|
-
|
304
|
+
doodle_collect_inherited(method).inject(OrderedHash.new){ |hash, item|
|
300
305
|
hash.merge(OrderedHash[*item])
|
301
306
|
}.merge(__send__(method))
|
302
307
|
else
|
@@ -306,19 +311,19 @@ class Doodle
|
|
306
311
|
private :_handle_inherited_hash
|
307
312
|
|
308
313
|
# return attributes defined in instance
|
309
|
-
def
|
310
|
-
__doodle__.
|
314
|
+
def doodle_local_attributes
|
315
|
+
__doodle__.doodle_local_attributes
|
311
316
|
end
|
312
|
-
protected :
|
317
|
+
protected :doodle_local_attributes
|
313
318
|
|
314
319
|
# returns array of Attributes
|
315
320
|
# - if tf == true, returns all inherited attributes
|
316
321
|
# - if tf == false, returns only those attributes defined in the current object/class
|
317
|
-
def
|
318
|
-
results = _handle_inherited_hash(tf, :
|
322
|
+
def doodle_attributes(tf = true)
|
323
|
+
results = _handle_inherited_hash(tf, :doodle_local_attributes)
|
319
324
|
# if an instance, include the singleton_class attributes
|
320
|
-
if !kind_of?(Class) && singleton_class.respond_to?(:
|
321
|
-
results = results.merge(singleton_class.
|
325
|
+
if !kind_of?(Class) && singleton_class.respond_to?(:doodle_attributes)
|
326
|
+
results = results.merge(singleton_class.doodle_attributes)
|
322
327
|
end
|
323
328
|
results
|
324
329
|
end
|
@@ -327,9 +332,9 @@ class Doodle
|
|
327
332
|
def class_attributes(tf = true)
|
328
333
|
attrs = OrderedHash.new
|
329
334
|
if self.kind_of?(Class)
|
330
|
-
attrs =
|
335
|
+
attrs = doodle_collect_inherited(:class_attributes).inject(OrderedHash.new){ |hash, item|
|
331
336
|
hash.merge(OrderedHash[*item])
|
332
|
-
}.merge(singleton_class.respond_to?(:
|
337
|
+
}.merge(singleton_class.respond_to?(:doodle_attributes) ? singleton_class.doodle_attributes : { })
|
333
338
|
attrs
|
334
339
|
else
|
335
340
|
self.class.class_attributes
|
@@ -337,28 +342,28 @@ class Doodle
|
|
337
342
|
end
|
338
343
|
|
339
344
|
# the set of conversions defined in the current class (i.e. without inheritance)
|
340
|
-
def
|
341
|
-
__doodle__.
|
345
|
+
def doodle_local_conversions
|
346
|
+
__doodle__.doodle_local_conversions
|
342
347
|
end
|
343
|
-
protected :
|
348
|
+
protected :doodle_local_conversions
|
344
349
|
|
345
350
|
# returns hash of conversions
|
346
351
|
# - if tf == true, returns all inherited conversions
|
347
352
|
# - if tf == false, returns only those conversions defined in the current object/class
|
348
|
-
def
|
349
|
-
_handle_inherited_hash(tf, :
|
353
|
+
def doodle_conversions(tf = true)
|
354
|
+
_handle_inherited_hash(tf, :doodle_local_conversions)
|
350
355
|
end
|
351
356
|
|
352
357
|
# the set of validations defined in the current class (i.e. without inheritance)
|
353
|
-
def
|
354
|
-
__doodle__.
|
358
|
+
def doodle_local_validations
|
359
|
+
__doodle__.doodle_local_validations
|
355
360
|
end
|
356
|
-
protected :
|
361
|
+
protected :doodle_local_validations
|
357
362
|
|
358
363
|
# returns array of Validations
|
359
364
|
# - if tf == true, returns all inherited validations
|
360
365
|
# - if tf == false, returns only those validations defined in the current object/class
|
361
|
-
def
|
366
|
+
def doodle_validations(tf = true)
|
362
367
|
if tf
|
363
368
|
# note: validations are handled differently to attributes and
|
364
369
|
# conversions because ~all~ validations apply (so are stored
|
@@ -366,9 +371,9 @@ class Doodle
|
|
366
371
|
# by name and kind respectively, so only the most recent
|
367
372
|
# applies
|
368
373
|
|
369
|
-
|
374
|
+
doodle_local_validations + doodle_collect_inherited(:doodle_local_validations)
|
370
375
|
else
|
371
|
-
|
376
|
+
doodle_local_validations
|
372
377
|
end
|
373
378
|
end
|
374
379
|
|
@@ -379,7 +384,7 @@ class Doodle
|
|
379
384
|
if self.class == Class
|
380
385
|
class_attributes[name]
|
381
386
|
else
|
382
|
-
|
387
|
+
doodle_attributes[name]
|
383
388
|
end
|
384
389
|
end
|
385
390
|
private :lookup_attribute
|
@@ -424,7 +429,7 @@ class Doodle
|
|
424
429
|
end
|
425
430
|
else
|
426
431
|
# This is an internal error (i.e. shouldn't happen)
|
427
|
-
handle_error name, NoDefaultError, "
|
432
|
+
handle_error name, NoDefaultError, "'#{name}' has no default defined", [caller[-1]]
|
428
433
|
end
|
429
434
|
end
|
430
435
|
end
|
@@ -459,7 +464,7 @@ class Doodle
|
|
459
464
|
if block_given?
|
460
465
|
# set the rule for each arg given
|
461
466
|
args.each do |arg|
|
462
|
-
|
467
|
+
doodle_local_conversions[arg] = block
|
463
468
|
end
|
464
469
|
else
|
465
470
|
convert(self, *args)
|
@@ -467,8 +472,13 @@ class Doodle
|
|
467
472
|
end
|
468
473
|
|
469
474
|
# add a validation
|
470
|
-
def must(
|
471
|
-
|
475
|
+
def must(constraint = 'be valid', &block)
|
476
|
+
if block.nil?
|
477
|
+
# is this really useful? do I really want it?
|
478
|
+
doodle_local_validations << Validation.new(constraint, &proc { |v| v.instance_eval(constraint) })
|
479
|
+
else
|
480
|
+
doodle_local_validations << Validation.new(constraint, &block)
|
481
|
+
end
|
472
482
|
end
|
473
483
|
|
474
484
|
# add a validation that attribute must be of class <= kind
|
@@ -476,7 +486,7 @@ class Doodle
|
|
476
486
|
if args.size > 0
|
477
487
|
# todo[figure out how to handle kind being specified twice?]
|
478
488
|
@kind = args.first
|
479
|
-
|
489
|
+
doodle_local_validations << (Validation.new("be #{@kind}") { |x| x.class <= @kind })
|
480
490
|
else
|
481
491
|
@kind
|
482
492
|
end
|
@@ -484,28 +494,38 @@ class Doodle
|
|
484
494
|
|
485
495
|
# convert a value according to conversion rules
|
486
496
|
def convert(owner, *args)
|
497
|
+
#!p [:convert, 1, owner, args]
|
487
498
|
begin
|
488
499
|
args = args.map do |value|
|
489
|
-
|
490
|
-
|
500
|
+
#!p [:convert, 2, value]
|
501
|
+
if (converter = doodle_conversions[value.class])
|
502
|
+
#!p [:convert, 3, value]
|
491
503
|
value = converter[*args]
|
504
|
+
#!p [:convert, 4, value]
|
492
505
|
else
|
506
|
+
#!p [:convert, 5, value]
|
493
507
|
# try to find nearest ancestor
|
494
508
|
ancestors = value.class.ancestors
|
495
|
-
|
509
|
+
#!p [:convert, 6, ancestors]
|
510
|
+
matches = ancestors & doodle_conversions.keys
|
511
|
+
#!p [:convert, 7, matches]
|
496
512
|
indexed_matches = matches.map{ |x| ancestors.index(x)}
|
513
|
+
#!p [:convert, 8, indexed_matches]
|
497
514
|
if indexed_matches.size > 0
|
515
|
+
#!p [:convert, 9]
|
498
516
|
converter_class = ancestors[indexed_matches.min]
|
499
|
-
|
500
|
-
|
517
|
+
#!p [:convert, 10, converter_class]
|
518
|
+
if converter = doodle_conversions[converter_class]
|
519
|
+
#!p [:convert, 11, converter]
|
501
520
|
value = converter[*args]
|
521
|
+
#!p [:convert, 12, value]
|
502
522
|
end
|
503
523
|
end
|
504
524
|
end
|
505
525
|
value
|
506
526
|
end
|
507
527
|
rescue Exception => e
|
508
|
-
owner.handle_error name, ConversionError, e.to_s, [caller[-1]]
|
528
|
+
owner.handle_error name, ConversionError, "#{owner.kind_of?(Class) ? owner : owner.class} - #{e.to_s}", [caller[-1]]
|
509
529
|
end
|
510
530
|
if args.size > 1
|
511
531
|
args
|
@@ -520,10 +540,10 @@ class Doodle
|
|
520
540
|
#!p [:validate, :before_conversion, args]
|
521
541
|
value = convert(owner, *args)
|
522
542
|
#!p [:validate, :after_conversion, args, :becomes, value]
|
523
|
-
|
543
|
+
doodle_validations.each do |v|
|
524
544
|
##DBG: Doodle::Debug.d { [:validate, self, v, args, value] }
|
525
545
|
if !v.block[value]
|
526
|
-
owner.handle_error name, ValidationError, "#{ name } must #{ v.message } - got #{ value.class }(#{ value.inspect })", [caller[-1]]
|
546
|
+
owner.handle_error name, ValidationError, "#{owner.kind_of?(Class) ? owner : owner.class}.#{ name } must #{ v.message } - got #{ value.class }(#{ value.inspect })", [caller[-1]]
|
527
547
|
end
|
528
548
|
end
|
529
549
|
value
|
@@ -547,50 +567,6 @@ class Doodle
|
|
547
567
|
end
|
548
568
|
private :define_getter_setter
|
549
569
|
|
550
|
-
# define a collector for appendable collections
|
551
|
-
# - collection should provide a :<< method
|
552
|
-
def define_appendable_collector(collection, name, klass = nil, &block)
|
553
|
-
# need to use string eval because passing block
|
554
|
-
if klass.nil?
|
555
|
-
sc_eval("def #{name}(*args, &block); args.unshift(block) if block_given?; #{collection}.<<(*args); end", __FILE__, __LINE__)
|
556
|
-
else
|
557
|
-
sc_eval("def #{name}(*args, &block)
|
558
|
-
if args.size > 0 and args.all?{|x| x.kind_of?(#{klass})}
|
559
|
-
#{collection}.<<(*args)
|
560
|
-
else
|
561
|
-
#{collection} << #{klass}.new(*args, &block)
|
562
|
-
end
|
563
|
-
end", __FILE__, __LINE__)
|
564
|
-
end
|
565
|
-
end
|
566
|
-
private :define_appendable_collector
|
567
|
-
|
568
|
-
# define a collector for keyed collections
|
569
|
-
# - collection should provide a :[] method
|
570
|
-
def define_keyed_collector(collection, name, key_method, klass = nil, &block)
|
571
|
-
#DBG: Doodle::Debug.d { [:define_keyed_collector, collection, name, key_method, klass]}
|
572
|
-
# need to use string eval because passing block
|
573
|
-
if klass.nil?
|
574
|
-
sc_eval("def #{name}(*args, &block)
|
575
|
-
args.each do |arg|
|
576
|
-
#{collection}[arg.send(:#{key_method})] = arg
|
577
|
-
end
|
578
|
-
end", __FILE__, __LINE__)
|
579
|
-
else
|
580
|
-
sc_eval("def #{name}(*args, &block)
|
581
|
-
if args.size > 0 and args.all?{|x| x.kind_of?(#{klass})}
|
582
|
-
args.each do |arg|
|
583
|
-
#{collection}[arg.send(:#{key_method})] = arg
|
584
|
-
end
|
585
|
-
else
|
586
|
-
obj = #{klass}.new(*args, &block)
|
587
|
-
#{collection}[obj.send(:#{key_method})] = obj
|
588
|
-
end
|
589
|
-
end", __FILE__, __LINE__)
|
590
|
-
end
|
591
|
-
end
|
592
|
-
private :define_keyed_collector
|
593
|
-
|
594
570
|
# +has+ is an extended +attr_accessor+
|
595
571
|
#
|
596
572
|
# simple usage - just like +attr_accessor+:
|
@@ -616,6 +592,11 @@ class Doodle
|
|
616
592
|
def has(*args, &block)
|
617
593
|
#DBG: Doodle::Debug.d { [:has, self, self.class, args] }
|
618
594
|
# d { [:has2, name, args] }
|
595
|
+
|
596
|
+
# fixme: this should be in generic Attribute - perhaps class method
|
597
|
+
# how much of this can be handled by initialize_from_hash?
|
598
|
+
# or at least in DoodleAttribute.from(params)
|
599
|
+
|
619
600
|
key_values, positional_args = args.partition{ |x| x.kind_of?(Hash)}
|
620
601
|
if positional_args.size > 0
|
621
602
|
name = positional_args.shift.to_sym
|
@@ -626,94 +607,55 @@ class Doodle
|
|
626
607
|
params = key_values.inject(params){ |acc, item| acc.merge(item)}
|
627
608
|
#DBG: Doodle::Debug.d { [:has, self, self.class, params] }
|
628
609
|
if !params.key?(:name)
|
629
|
-
handle_error name, ArgumentError, "
|
610
|
+
handle_error name, ArgumentError, "#{self.class} must have a name"
|
630
611
|
else
|
631
612
|
name = params[:name].to_sym
|
632
613
|
end
|
633
|
-
handle_error name, ArgumentError, "
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
if !params.key?(:init)
|
641
|
-
if params.key?(:key)
|
642
|
-
#!p [:defining, :hash, name]
|
643
|
-
params[:init] = { }
|
644
|
-
else
|
645
|
-
#!p [:defining, :array, name]
|
646
|
-
params[:init] = []
|
647
|
-
end
|
614
|
+
handle_error name, ArgumentError, "#{self.class} has too many arguments" if positional_args.size > 0
|
615
|
+
|
616
|
+
if params.key?(:collect) && !params.key?(:using)
|
617
|
+
if params.key?(:key)
|
618
|
+
params[:using] = KeyedAttribute
|
619
|
+
else
|
620
|
+
params[:using] = AppendableAttribute
|
648
621
|
end
|
622
|
+
end
|
623
|
+
|
624
|
+
if collector = params.delete(:collect)
|
625
|
+
# this in generic CollectorAttribute class
|
626
|
+
# collector from(Hash)
|
649
627
|
if collector.kind_of?(Hash)
|
650
|
-
collector_name,
|
628
|
+
collector_name, collector_class = collector.to_a[0]
|
651
629
|
else
|
652
630
|
# if Capitalized word given, treat as classname
|
653
631
|
# and create collector for specific class
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
632
|
+
collector_class = collector.to_s
|
633
|
+
#p [:collector_klass, collector_klass]
|
634
|
+
collector_name = Utils.snake_case(collector_class.split(/::/).last)
|
635
|
+
#p [:collector_name, collector_name]
|
636
|
+
if collector_class !~ /^[A-Z]/
|
637
|
+
collector_class = nil
|
658
638
|
end
|
659
639
|
#!p [:collector_klass, collector_klass, params[:init]]
|
660
640
|
end
|
661
|
-
|
662
|
-
|
663
|
-
else
|
664
|
-
define_appendable_collector name, collector_name, collector_klass
|
665
|
-
end
|
641
|
+
params[:collector_class] = collector_class
|
642
|
+
params[:collector_name] = collector_name
|
666
643
|
end
|
667
644
|
|
668
645
|
# get specialized attribute class or use default
|
669
|
-
attribute_class = params.delete(:using) ||
|
670
|
-
|
646
|
+
attribute_class = params.delete(:using) || DoodleAttribute
|
647
|
+
|
648
|
+
# could this be handled in DoodleAttribute?
|
671
649
|
# define getter setter before setting up attribute
|
672
650
|
define_getter_setter name, *args, &block
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
# fixme: this should be in specialized attribute class
|
678
|
-
#!p [:collector_klass2, collector_klass, params[:init]]
|
679
|
-
attribute.instance_eval {
|
680
|
-
# applying map to hashes returns an array, so avoid that by simply passing it through
|
681
|
-
from Hash do |hash|
|
682
|
-
hash
|
683
|
-
end
|
684
|
-
from Enumerable do |enum|
|
685
|
-
if !collector_klass.kind_of?(Class)
|
686
|
-
tmp_klass = self.class.const_get(collector_klass)
|
687
|
-
else
|
688
|
-
tmp_klass = collector_klass
|
689
|
-
end
|
690
|
-
#!p [:enumerating, name, enum]
|
691
|
-
results = enum.map{|x|
|
692
|
-
#!p [:enumerating, x]
|
693
|
-
if x.kind_of?(tmp_klass)
|
694
|
-
x
|
695
|
-
elsif tmp_klass.conversions.key?(x.class)
|
696
|
-
tmp_klass.from(x)
|
697
|
-
else
|
698
|
-
tmp_klass.new(x)
|
699
|
-
end
|
700
|
-
}
|
701
|
-
#!p [:enumerating, :results, results]
|
702
|
-
# fixme: this is all getting a bit too specific to arrays and hashes
|
703
|
-
# figure out a way to do this with specialized Doodle::Attribute?
|
704
|
-
if !key.nil?
|
705
|
-
results = results.inject({ }) do |hash, result|
|
706
|
-
hash[result.send(key)] = result
|
707
|
-
hash
|
708
|
-
end
|
709
|
-
end
|
710
|
-
results
|
711
|
-
end
|
712
|
-
}
|
713
|
-
end
|
651
|
+
params[:doodle_owner] = self
|
652
|
+
#p [:attribute, attribute_class, params]
|
653
|
+
doodle_local_attributes[name] = attribute = attribute_class.new(params, &block)
|
654
|
+
|
714
655
|
attribute
|
715
656
|
end
|
716
657
|
|
658
|
+
|
717
659
|
# define order for positional arguments
|
718
660
|
def arg_order(*args)
|
719
661
|
if args.size > 0
|
@@ -721,19 +663,19 @@ class Doodle
|
|
721
663
|
args = args.uniq
|
722
664
|
args.each do |x|
|
723
665
|
handle_error :arg_order, ArgumentError, "#{x} not a Symbol" if !(x.class <= Symbol)
|
724
|
-
handle_error :arg_order, NameError, "#{x} not an attribute name" if !
|
666
|
+
handle_error :arg_order, NameError, "#{x} not an attribute name" if !doodle_attributes.keys.include?(x)
|
725
667
|
end
|
726
668
|
__doodle__.arg_order = args
|
727
669
|
rescue Exception => e
|
728
670
|
handle_error :arg_order, InvalidOrderError, e.to_s, [caller[-1]]
|
729
671
|
end
|
730
672
|
else
|
731
|
-
__doodle__.arg_order + (
|
673
|
+
__doodle__.arg_order + (doodle_attributes.keys - __doodle__.arg_order)
|
732
674
|
end
|
733
675
|
end
|
734
676
|
|
735
677
|
def get_init_values(tf = true)
|
736
|
-
|
678
|
+
doodle_attributes(tf).select{|n, a| a.init_defined? }.inject({}) {|hash, (n, a)|
|
737
679
|
#!p [:get_init_values, a.init]
|
738
680
|
hash[n] = begin
|
739
681
|
case a.init
|
@@ -770,8 +712,8 @@ class Doodle
|
|
770
712
|
attribs = class_attributes
|
771
713
|
##DBG: Doodle::Debug.d { [:validate!, "using class_attributes", class_attributes] }
|
772
714
|
else
|
773
|
-
attribs =
|
774
|
-
##DBG: Doodle::Debug.d { [:validate!, "using instance_attributes",
|
715
|
+
attribs = doodle_attributes
|
716
|
+
##DBG: Doodle::Debug.d { [:validate!, "using instance_attributes", doodle_attributes] }
|
775
717
|
end
|
776
718
|
attribs.each do |name, att|
|
777
719
|
ivar_name = "@#{att.name}"
|
@@ -793,8 +735,8 @@ class Doodle
|
|
793
735
|
|
794
736
|
# now apply instance level validations
|
795
737
|
|
796
|
-
##DBG: Doodle::Debug.d { [:validate!, "validations",
|
797
|
-
|
738
|
+
##DBG: Doodle::Debug.d { [:validate!, "validations", doodle_validations ]}
|
739
|
+
doodle_validations.each do |v|
|
798
740
|
##DBG: Doodle::Debug.d { [:validate!, self, v ] }
|
799
741
|
begin
|
800
742
|
if !instance_eval(&v.block)
|
@@ -827,49 +769,53 @@ class Doodle
|
|
827
769
|
# helper function to initialize from hash - this is safe to use
|
828
770
|
# after initialization (validate! is called if this method is
|
829
771
|
# called after initialization)
|
830
|
-
def
|
831
|
-
#!p [:
|
772
|
+
def doodle_initialize_from_hash(*args)
|
773
|
+
#!p [:doodle_initialize_from_hash, :args, *args]
|
832
774
|
defer_validation do
|
833
775
|
# hash initializer
|
834
776
|
# separate into array of hashes of form [{:k1 => v1}, {:k2 => v2}] and positional args
|
835
777
|
key_values, args = args.partition{ |x| x.kind_of?(Hash)}
|
836
|
-
#DBG: Doodle::Debug.d { [self.class, :
|
837
|
-
#!p [self.class, :
|
778
|
+
#DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :key_values, key_values, :args, args] }
|
779
|
+
#!p [self.class, :doodle_initialize_from_hash, :key_values, key_values, :args, args]
|
838
780
|
|
839
781
|
# set up initial values with ~clones~ of specified values (so not shared between instances)
|
840
782
|
init_values = get_init_values
|
841
|
-
|
783
|
+
#!p [:init_values, init_values]
|
842
784
|
|
843
785
|
# match up positional args with attribute names (from arg_order) using idiom to create hash from array of assocs
|
844
786
|
arg_keywords = init_values.merge(Hash[*(Utils.flatten_first_level(self.class.arg_order[0...args.size].zip(args)))])
|
845
|
-
|
787
|
+
#!p [self.class, :doodle_initialize_from_hash, :arg_keywords, arg_keywords]
|
846
788
|
|
847
789
|
# merge all hash args into one
|
848
790
|
key_values = key_values.inject(arg_keywords) { |hash, item|
|
849
|
-
|
791
|
+
#!p [self.class, :doodle_initialize_from_hash, :merge, hash, item]
|
850
792
|
hash.merge(item)
|
851
793
|
}
|
852
|
-
|
794
|
+
#!p [self.class, :doodle_initialize_from_hash, :key_values2, key_values]
|
795
|
+
|
796
|
+
# convert keys to symbols (note not recursively - only first level == doodle keywords)
|
797
|
+
key_values.keys.each do |k|
|
798
|
+
sym_key = k.respond_to?(:to_sym) ? k.to_sym : k
|
799
|
+
key_values[sym_key] = key_values.delete(k)
|
800
|
+
end
|
853
801
|
|
854
|
-
#
|
855
|
-
|
856
|
-
#DBG: Doodle::Debug.d { [self.class, :initialize_from_hash, :key_values2, key_values, :args2, args] }
|
857
|
-
#p [self.class, :initialize_from_hash, :key_values3, key_values]
|
802
|
+
#DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :key_values2, key_values, :args2, args] }
|
803
|
+
#!p [self.class, :doodle_initialize_from_hash, :key_values3, key_values]
|
858
804
|
|
859
805
|
# create attributes
|
860
806
|
key_values.keys.each do |key|
|
861
|
-
#DBG: Doodle::Debug.d { [self.class, :
|
862
|
-
|
807
|
+
#DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :setting, key, key_values[key]] }
|
808
|
+
#!p [self.class, :doodle_initialize_from_hash, :setting, key, key_values[key]]
|
863
809
|
if respond_to?(key)
|
864
810
|
__send__(key, key_values[key])
|
865
811
|
else
|
866
812
|
# raise error if not defined
|
867
|
-
handle_error key, Doodle::UnknownAttributeError, "
|
813
|
+
handle_error key, Doodle::UnknownAttributeError, "unknown attribute '#{key}' #{key_values[key].inspect}"
|
868
814
|
end
|
869
815
|
end
|
870
816
|
end
|
871
817
|
end
|
872
|
-
#private :
|
818
|
+
#private :doodle_initialize_from_hash
|
873
819
|
|
874
820
|
# return containing object (set during initialization)
|
875
821
|
# (named doodle_parent to avoid clash with ActiveSupport)
|
@@ -888,7 +834,7 @@ class Doodle
|
|
888
834
|
__doodle__.doodle_parent = Doodle.context[-1]
|
889
835
|
Doodle.context.push(self)
|
890
836
|
defer_validation do
|
891
|
-
|
837
|
+
doodle_initialize_from_hash(*args)
|
892
838
|
instance_eval(&block) if block_given?
|
893
839
|
end
|
894
840
|
Doodle.context.pop
|
@@ -929,13 +875,13 @@ class Doodle
|
|
929
875
|
false
|
930
876
|
end
|
931
877
|
|
932
|
-
if !method_defined && !klass.respond_to?(name) && !eval("respond_to?(:#{name})", TOPLEVEL_BINDING)
|
878
|
+
if name =~ Factory::RX_IDENTIFIER && !method_defined && !klass.respond_to?(name) && !eval("respond_to?(:#{name})", TOPLEVEL_BINDING)
|
933
879
|
eval("def #{ name }(*args, &block); ::#{name}.new(*args, &block); end", ::TOPLEVEL_BINDING, __FILE__, __LINE__)
|
934
880
|
end
|
935
881
|
else
|
936
882
|
klass = names.inject(self) {|c, n| c.const_get(n)}
|
937
883
|
# todo[check how many times this is being called]
|
938
|
-
if !klass.respond_to?(name)
|
884
|
+
if name =~ Factory::RX_IDENTIFIER && !klass.respond_to?(name)
|
939
885
|
klass.module_eval("def self.#{name}(*args, &block); #{name}.new(*args, &block); end", __FILE__, __LINE__)
|
940
886
|
end
|
941
887
|
end
|
@@ -979,7 +925,7 @@ class Doodle
|
|
979
925
|
#
|
980
926
|
# It is used to provide a context for defining #must and #from rules
|
981
927
|
#
|
982
|
-
class
|
928
|
+
class DoodleAttribute < Doodle
|
983
929
|
# todo[want to design Attribute so it's extensible, e.g. to specific datatypes & built-in validations]
|
984
930
|
# must define these methods before using them in #has below
|
985
931
|
|
@@ -997,6 +943,16 @@ class Doodle
|
|
997
943
|
def validate!(all = true)
|
998
944
|
end
|
999
945
|
|
946
|
+
# has default been defined?
|
947
|
+
def default_defined?
|
948
|
+
ivar_defined?(:default)
|
949
|
+
end
|
950
|
+
|
951
|
+
# has default been defined?
|
952
|
+
def init_defined?
|
953
|
+
ivar_defined?(:init)
|
954
|
+
end
|
955
|
+
|
1000
956
|
# is this attribute optional? true if it has a default defined for it
|
1001
957
|
def optional?
|
1002
958
|
default_defined? or init_defined?
|
@@ -1008,14 +964,8 @@ class Doodle
|
|
1008
964
|
!optional?
|
1009
965
|
end
|
1010
966
|
|
1011
|
-
#
|
1012
|
-
|
1013
|
-
ivar_defined?(:default)
|
1014
|
-
end
|
1015
|
-
# has default been defined?
|
1016
|
-
def init_defined?
|
1017
|
-
ivar_defined?(:init)
|
1018
|
-
end
|
967
|
+
# special case - not an attribute
|
968
|
+
define_getter_setter :doodle_owner
|
1019
969
|
|
1020
970
|
# name of attribute
|
1021
971
|
has :name, :kind => Symbol do
|
@@ -1032,6 +982,107 @@ class Doodle
|
|
1032
982
|
|
1033
983
|
end
|
1034
984
|
|
985
|
+
# base class for attribute collector classes
|
986
|
+
class AttributeCollector < DoodleAttribute
|
987
|
+
has :collector_class
|
988
|
+
has :collector_name
|
989
|
+
|
990
|
+
def resolve_collector_class
|
991
|
+
if !collector_class.kind_of?(Class)
|
992
|
+
self.collector_class = Doodle::Utils.const_resolve(collector_class)
|
993
|
+
end
|
994
|
+
end
|
995
|
+
def resolve_value(value)
|
996
|
+
if value.kind_of?(collector_class)
|
997
|
+
value
|
998
|
+
elsif collector_class.doodle_conversions.key?(value.class)
|
999
|
+
collector_class.from(value)
|
1000
|
+
else
|
1001
|
+
collector_class.new(value)
|
1002
|
+
end
|
1003
|
+
end
|
1004
|
+
def initialize(*args, &block)
|
1005
|
+
super
|
1006
|
+
define_collection
|
1007
|
+
from Hash do |hash|
|
1008
|
+
resolve_collector_class
|
1009
|
+
hash.inject({ }) do |h, (key, value)|
|
1010
|
+
h[key] = resolve_value(value)
|
1011
|
+
h
|
1012
|
+
end
|
1013
|
+
end
|
1014
|
+
from Enumerable do |enum|
|
1015
|
+
resolve_collector_class
|
1016
|
+
post_process( enum.map{ |value| resolve_value(value) } )
|
1017
|
+
end
|
1018
|
+
end
|
1019
|
+
def post_process(results)
|
1020
|
+
results
|
1021
|
+
end
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
# define collector methods for array-like attribute collectors
|
1025
|
+
class AppendableAttribute < AttributeCollector
|
1026
|
+
has :init, :init => []
|
1027
|
+
|
1028
|
+
# define a collector for appendable collections
|
1029
|
+
# - collection should provide a :<< method
|
1030
|
+
def define_collection
|
1031
|
+
if collector_class.nil?
|
1032
|
+
doodle_owner.sc_eval("def #{collector_name}(*args, &block)
|
1033
|
+
args.unshift(block) if block_given?
|
1034
|
+
#{name}.<<(*args);
|
1035
|
+
end", __FILE__, __LINE__)
|
1036
|
+
else
|
1037
|
+
doodle_owner.sc_eval("def #{collector_name}(*args, &block)
|
1038
|
+
if args.size > 0 and args.all?{|x| x.kind_of?(#{collector_class})}
|
1039
|
+
#{name}.<<(*args)
|
1040
|
+
else
|
1041
|
+
#{name} << #{collector_class}.new(*args, &block)
|
1042
|
+
end
|
1043
|
+
end", __FILE__, __LINE__)
|
1044
|
+
end
|
1045
|
+
end
|
1046
|
+
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
# define collector methods for hash-like attribute collectors
|
1050
|
+
class KeyedAttribute < AttributeCollector
|
1051
|
+
has :init, :init => { }
|
1052
|
+
has :key
|
1053
|
+
|
1054
|
+
def post_process(results)
|
1055
|
+
results.inject({ }) do |h, result|
|
1056
|
+
h[result.send(key)] = result
|
1057
|
+
h
|
1058
|
+
end
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
# define a collector for keyed collections
|
1062
|
+
# - collection should provide a :[] method
|
1063
|
+
def define_collection
|
1064
|
+
# need to use string eval because passing block
|
1065
|
+
if collector_class.nil?
|
1066
|
+
doodle_owner.sc_eval("def #{collector_name}(*args, &block)
|
1067
|
+
args.each do |arg|
|
1068
|
+
#{name}[arg.send(:#{key})] = arg
|
1069
|
+
end
|
1070
|
+
end", __FILE__, __LINE__)
|
1071
|
+
else
|
1072
|
+
doodle_owner.sc_eval("def #{collector_name}(*args, &block)
|
1073
|
+
if args.size > 0 and args.all?{|x| x.kind_of?(#{collector_class})}
|
1074
|
+
args.each do |arg|
|
1075
|
+
#{name}[arg.send(:#{key})] = arg
|
1076
|
+
end
|
1077
|
+
else
|
1078
|
+
obj = #{collector_class}.new(*args, &block)
|
1079
|
+
#{name}[obj.send(:#{key})] = obj
|
1080
|
+
end
|
1081
|
+
end", __FILE__, __LINE__)
|
1082
|
+
end
|
1083
|
+
end
|
1084
|
+
end
|
1085
|
+
|
1035
1086
|
end
|
1036
1087
|
|
1037
1088
|
############################################################
|