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 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
@@ -1,4 +1,5 @@
1
- require 'lib/doodle'
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'doodle'
2
3
 
3
4
  module CommandLine
4
5
  class Base < Doodle::Base
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
- # parents returns the set of parent classes of an object
113
- def parents
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 parents (they're orphans)
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 parents and collect results
134
- def collect_inherited(message)
136
+
137
+ # send message to all doodle_parents and collect results
138
+ def doodle_collect_inherited(message)
135
139
  result = []
136
- parents.each do |klass|
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 :collect_inherited
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 :local_attributes
208
- attr_accessor :local_validations
209
- attr_accessor :local_conversions
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
- @local_attributes = OrderedHash.new
217
- @local_validations = []
220
+ @doodle_local_attributes = OrderedHash.new
221
+ @doodle_local_validations = []
218
222
  @validation_on = true
219
- @local_conversions = {}
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 provided by Doodle
245
- # without inheriting from Doodle, include Doodle::Core, not this module
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
- collect_inherited(method).inject(OrderedHash.new){ |hash, item|
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 local_attributes
310
- __doodle__.local_attributes
314
+ def doodle_local_attributes
315
+ __doodle__.doodle_local_attributes
311
316
  end
312
- protected :local_attributes
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 attributes(tf = true)
318
- results = _handle_inherited_hash(tf, :local_attributes)
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?(:attributes)
321
- results = results.merge(singleton_class.attributes)
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 = collect_inherited(:class_attributes).inject(OrderedHash.new){ |hash, item|
335
+ attrs = doodle_collect_inherited(:class_attributes).inject(OrderedHash.new){ |hash, item|
331
336
  hash.merge(OrderedHash[*item])
332
- }.merge(singleton_class.respond_to?(:attributes) ? singleton_class.attributes : { })
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 local_conversions
341
- __doodle__.local_conversions
345
+ def doodle_local_conversions
346
+ __doodle__.doodle_local_conversions
342
347
  end
343
- protected :local_conversions
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 conversions(tf = true)
349
- _handle_inherited_hash(tf, :local_conversions)
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 local_validations
354
- __doodle__.local_validations
358
+ def doodle_local_validations
359
+ __doodle__.doodle_local_validations
355
360
  end
356
- protected :local_validations
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 validations(tf = true)
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
- local_validations + collect_inherited(:local_validations)
374
+ doodle_local_validations + doodle_collect_inherited(:doodle_local_validations)
370
375
  else
371
- local_validations
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
- attributes[name]
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, "Error - '#{name}' has no default defined", [caller[-1]]
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
- local_conversions[arg] = block
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(message = 'be valid', &block)
471
- local_validations << Validation.new(message, &block)
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
- local_validations << (Validation.new("be #{@kind}") { |x| x.class <= @kind })
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
- if (converter = conversions[value.class])
490
- #p [:convert, :using, value.class]
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
- matches = ancestors & conversions.keys
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
- if converter = conversions[converter_class]
500
- #p [:convert, :using, converter_class]
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
- validations.each do |v|
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, "Must have a name"
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, "Too many arguments" if positional_args.size > 0
634
-
635
- # don't pass collector params through to Attribute
636
- collector_klass = nil
637
- # fixme: this should be in specialized attribute class
638
- # (and also distinction between appendable and keyed collections
639
- if collector = params.delete(:collect)
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, collector_klass = collector.to_a[0]
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
- collector_klass = collector.to_s
655
- collector_name = Utils.snake_case(collector_klass)
656
- if collector_klass !~ /^[A-Z]/
657
- collector_klass = nil
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
- if key = params.delete(:key)
662
- define_keyed_collector name, collector_name, key, collector_klass
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) || Attribute
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
- local_attributes[name] = attribute = attribute_class.new(params, &block)
674
-
675
- # if a collector has been defined and has a specific class, then you can pass in an array of hashes
676
- if collector_klass
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 !attributes.keys.include?(x)
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 + (attributes.keys - __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
- attributes(tf).select{|n, a| a.init_defined? }.inject({}) {|hash, (n, a)|
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 = attributes
774
- ##DBG: Doodle::Debug.d { [:validate!, "using instance_attributes", 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", validations ]}
797
- validations.each do |v|
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 initialize_from_hash(*args)
831
- #!p [:initialize_from_hash, :args, *args]
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, :initialize_from_hash, :key_values, key_values, :args, args] }
837
- #!p [self.class, :initialize_from_hash, :key_values, key_values, :args, args]
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
- #p [:init_values, init_values]
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
- #p [self.class, :initialize_from_hash, :arg_keywords, arg_keywords]
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
- #p [self.class, :initialize_from_hash, :merge, hash, item]
791
+ #!p [self.class, :doodle_initialize_from_hash, :merge, hash, item]
850
792
  hash.merge(item)
851
793
  }
852
- #p [self.class, :initialize_from_hash, :key_values2, key_values]
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
- # convert key names to symbols
855
- key_values = key_values.inject({}) {|h, (k, v)| h[k.to_sym] = v; h}
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, :initialize_from_hash, :setting, key, key_values[key]] }
862
- #p [self.class, :initialize_from_hash, :setting, key, key_values[key]]
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, "Unknown attribute '#{key}' #{key_values[key].inspect}"
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 :initialize_from_hash
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
- initialize_from_hash(*args)
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) && name =~ Factory::RX_IDENTIFIER
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) && name =~ Factory::RX_IDENTIFIER
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 Attribute < Doodle
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
- # has default been defined?
1012
- def default_defined?
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
  ############################################################