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 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
  ############################################################