doodle 0.1.5 → 0.1.6
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 +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
|
############################################################
|