doodle 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,55 @@
1
+ == 0.1.9 / 2008-08-13
2
+ - Features:
3
+ - to_hash
4
+ - doodle do .. end blocks now support #has, #from, #must and
5
+ #arg_order
6
+ - will now initialize a setter from a block by calling kind.new if
7
+ kind is specified and kind is a Doodle or a Proc, e.g.
8
+
9
+ class Animal
10
+ has :species
11
+ end
12
+
13
+ class Barn
14
+ has :animals, :collect => Animal
15
+ end
16
+
17
+ class Farm
18
+ has Barn
19
+ end
20
+
21
+ farm = Farm do
22
+ # this is new - will call Barn.new(&block)
23
+ barn do
24
+ animal 'chicken'
25
+ animal 'pig'
26
+ end
27
+ end
28
+
29
+ Will not try this for an attribute with :abstract => true
30
+
31
+ - attributes now have :doc option
32
+ - attributes now have :abstract option - will not try to
33
+ auto-instantiate an object from this class
34
+ - attributes now have a :readonly attribute - will not allow setting
35
+ outside initialization
36
+ - Doodle::Utils
37
+ - deep_copy(obj)
38
+ - normalize_keys!(hash, recursive = false, method = :to_sym),
39
+ optionally recurse into child hashes
40
+ - symbolize_keys!(hash, recursive = false)
41
+ - stringify_keys!(hash, recursive = false)
42
+
43
+ - Experimental:
44
+ - Doodle::App for handlng command line application options
45
+ - doodle/datatypes - added more datatypes
46
+
47
+ - Bug fixes:
48
+ - fixed reversion in 0.1.8 which enabled full backtrace from within
49
+ doodle.rb
50
+ - fixed bug where required attributes defined after attributes with
51
+ default values were not being validated (had 'break' instead of 'next')
52
+
1
53
  == 0.1.8 / 2008-05-13
2
54
  - Features:
3
55
  - now applies instance level conversions (class level #from) to
@@ -23,6 +23,7 @@ examples/test-datatypes.rb
23
23
  examples/yaml-example.rb
24
24
  examples/yaml-example2.rb
25
25
  lib/doodle.rb
26
+ lib/doodle/app.rb
26
27
  lib/doodle/datatypes.rb
27
28
  lib/doodle/rfc822.rb
28
29
  lib/doodle/utils.rb
@@ -60,6 +60,7 @@ class Mail < Doodle
60
60
  end
61
61
  end
62
62
 
63
+ require 'rubygems'
63
64
  require 'highline/import'
64
65
  def prompt_for_password
65
66
  ask("Enter your password: ") { |q| q.echo = '*' }
@@ -78,7 +78,7 @@ end
78
78
  m = Mail do
79
79
  subject subject + ': Hi!'
80
80
  if rcpt = prompt
81
- to rcpt
81
+ to rcpt
82
82
  end
83
83
  body "Hello, world!\n"
84
84
  end
@@ -2,7 +2,7 @@ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  require 'doodle'
3
3
 
4
4
  module CommandLine
5
- class Base < Doodle::Base
5
+ class Base < Doodle
6
6
  has :name
7
7
  singleton_class do
8
8
  has :doc
@@ -1,4 +1,5 @@
1
1
  # doodle
2
+ # -*- mode: ruby; ruby-indent-level: 2; tab-width: 2 -*- vim: sw=2 ts=2
2
3
  # Copyright (C) 2007-2008 by Sean O'Halpin
3
4
  # 2007-11-24 first version
4
5
  # 2008-04-18 latest release 0.0.12
@@ -7,13 +8,56 @@
7
8
  $:.unshift(File.dirname(__FILE__)) unless
8
9
  $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
9
10
 
10
- require 'molic_orderedhash' # todo[replace this with own (required functions only) version]
11
+ if RUBY_VERSION < '1.9.0'
12
+ require 'molic_orderedhash' # todo[replace this with own (required functions only) version]
13
+ else
14
+ # 1.9+ hashes are ordered by default
15
+ class Doodle
16
+ OrderedHash = ::Hash
17
+ end
18
+ end
11
19
 
12
20
  # require Ruby 1.8.6 or higher
13
21
  if RUBY_VERSION < '1.8.6'
14
22
  raise Exception, "Sorry - doodle does not work with versions of Ruby below 1.8.6"
15
23
  end
16
24
 
25
+ #
26
+ # instance_exec for ruby 1.8 by Mauricio Fernandez
27
+ # http://eigenclass.org/hiki.rb?bounded+space+instance_exec
28
+ # thread-safe and handles frozen objects in bounded space
29
+ #
30
+ # (tag "ruby instance_exec")
31
+ #
32
+ if !Object.respond_to?(:instance_exec)
33
+ class Object
34
+ module InstanceExecHelper; end
35
+ include InstanceExecHelper
36
+ def instance_exec(*args, &block)
37
+ begin
38
+ old_critical, Thread.critical = Thread.critical, true
39
+ n = 0
40
+ methods = InstanceExecHelper.instance_methods
41
+ # this in order to make the lookup O(1), and name generation O(n) on the
42
+ # number of nested/concurrent instance_exec calls instead of O(n**2)
43
+ table = Hash[*methods.zip(methods).flatten]
44
+ n += 1 while table.has_key?(mname="__instance_exec#{n}")
45
+ ensure
46
+ Thread.critical = old_critical
47
+ end
48
+ InstanceExecHelper.module_eval{ define_method(mname, &block) }
49
+ begin
50
+ ret = send(mname, *args)
51
+ ensure
52
+ InstanceExecHelper.module_eval{ remove_method(mname) } rescue nil
53
+ end
54
+ ret
55
+ end
56
+ end
57
+ end
58
+
59
+ require 'yaml'
60
+
17
61
  # *doodle* is my attempt at an eco-friendly metaprogramming framework that does not
18
62
  # have pollute core Ruby objects such as Object, Class and Module.
19
63
  #
@@ -30,6 +74,9 @@ class Doodle
30
74
  def context
31
75
  Thread.current[:doodle_context] ||= []
32
76
  end
77
+ def parent
78
+ context[-1]
79
+ end
33
80
  end
34
81
 
35
82
  # debugging utilities
@@ -56,27 +103,90 @@ class Doodle
56
103
  end
57
104
  # from facets/string/case.rb, line 80
58
105
  def snake_case(camel_cased_word)
59
- camel_cased_word.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
106
+ camel_cased_word.to_s.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
60
107
  end
61
108
  # resolve a constant of the form Some::Class::Or::Module
62
109
  def const_resolve(constant)
63
110
  constant.to_s.split(/::/).reject{|x| x.empty?}.inject(Object) { |prev, this| prev.const_get(this) }
64
111
  end
65
- # convert keys to symbols - updates target hash
66
- def symbolize_keys!(hash)
112
+ # deep copy of object (unlike shallow copy dup or clone)
113
+ def deep_copy(obj)
114
+ Marshal.load(Marshal.dump(obj))
115
+ end
116
+ # normalize hash keys using method (e.g. :to_sym, :to_s)
117
+ # - updates target hash
118
+ # - optionally recurse into child hashes
119
+ def normalize_keys!(hash, recursive = false, method = :to_sym)
67
120
  hash.keys.each do |key|
68
- sym_key = key.respond_to?(:to_sym) ? key.to_sym : key
69
- hash[sym_key] = hash.delete(key)
121
+ normalized_key = key.respond_to?(method) ? key.send(method) : key
122
+ v = hash.delete(key)
123
+ if recursive
124
+ if v.kind_of?(Hash)
125
+ v = normalize_keys!(v, recursive, method)
126
+ elsif v.kind_of?(Array)
127
+ v = v.map{ |x| normalize_keys!(x, recursive, method) }
128
+ end
129
+ end
130
+ hash[normalized_key] = v
70
131
  end
71
132
  hash
72
133
  end
134
+ # normalize hash keys using method (e.g. :to_sym, :to_s)
135
+ # - returns copy of hash
136
+ # - optionally recurse into child hashes
137
+ def normalize_keys(hash, recursive = false, method = :to_sym)
138
+ if recursive
139
+ h = deep_copy(hash)
140
+ else
141
+ h = hash.dup
142
+ end
143
+ normalize_keys!(h, recursive, method)
144
+ end
73
145
  # convert keys to symbols
74
- def symbolize_keys(hash)
75
- symbolize_keys(hash.dup)
146
+ # - updates target hash in place
147
+ # - optionally recurse into child hashes
148
+ def symbolize_keys!(hash, recursive = false)
149
+ normalize_keys!(hash, recursive, :to_sym)
150
+ end
151
+ # convert keys to symbols
152
+ # - returns copy of hash
153
+ # - optionally recurse into child hashes
154
+ def symbolize_keys(hash, recursive = false)
155
+ normalize_keys(hash, recursive, :to_sym)
156
+ end
157
+ # convert keys to strings
158
+ # - updates target hash in place
159
+ # - optionally recurse into child hashes
160
+ def stringify_keys!(hash, recursive = false)
161
+ normalize_keys!(hash, recursive, :to_s)
162
+ end
163
+ # convert keys to strings
164
+ # - returns copy of hash
165
+ # - optionally recurse into child hashes
166
+ def stringify_keys(hash, recursive = false)
167
+ normalize_keys(hash, recursive, :to_s)
168
+ end
169
+ # simple (!) pluralization - if you want fancier, override this method
170
+ def pluralize(string)
171
+ s = string.to_s
172
+ if s =~ /s$/
173
+ s + 'es'
174
+ else
175
+ s + 's'
176
+ end
177
+ end
178
+
179
+ # caller
180
+ def doodle_caller
181
+ if $DEBUG
182
+ caller
183
+ else
184
+ [caller[-1]]
185
+ end
76
186
  end
77
187
  end
78
188
  end
79
-
189
+
80
190
  # error handling
81
191
  @@raise_exception_on_error = true
82
192
  def self.raise_exception_on_error
@@ -101,6 +211,9 @@ class Doodle
101
211
  # raised when arg_order called with incorrect arguments
102
212
  class InvalidOrderError < Exception
103
213
  end
214
+ # raised when try to set a readonly attribute after initialization
215
+ class ReadOnlyError < Exception
216
+ end
104
217
 
105
218
  # provides more direct access to the singleton class and a way to
106
219
  # treat singletons, Modules and Classes equally in a meta context
@@ -150,7 +263,7 @@ class Doodle
150
263
  #p [:embrace, :inherited, klass]
151
264
  klass.__send__(:embrace, other) # n.b. closure
152
265
  klass.__send__(:include, Factory) # is there another way to do this? i.e. not in embrace
153
- super(klass) if defined?(super)
266
+ #super(klass) if defined?(super)
154
267
  end
155
268
  }
156
269
  sc.module_eval(&block) if block_given?
@@ -164,8 +277,11 @@ class Doodle
164
277
  arg_block = block if block_given?
165
278
  @block = arg_block
166
279
  end
280
+ def call(*a, &b)
281
+ block.call(*a, &b)
282
+ end
167
283
  end
168
-
284
+
169
285
  # A Validation represents a validation rule applied to the instance
170
286
  # after initialization. Generated using the Doodle::BaseMethods#must directive.
171
287
  class Validation
@@ -182,6 +298,7 @@ class Doodle
182
298
 
183
299
  # place to stash bookkeeping info
184
300
  class DoodleInfo
301
+ attr_accessor :this
185
302
  attr_accessor :local_attributes
186
303
  attr_accessor :local_validations
187
304
  attr_accessor :local_conversions
@@ -192,15 +309,20 @@ class Doodle
192
309
 
193
310
  def initialize(object)
194
311
  @this = object
195
- @local_attributes = OrderedHash.new
312
+ @local_attributes = Doodle::OrderedHash.new
196
313
  @local_validations = []
197
314
  @validation_on = true
198
315
  @local_conversions = {}
199
316
  @arg_order = []
200
317
  @errors = []
201
- @parent = nil
318
+ #@parent = nil
319
+ @parent = Doodle.parent
202
320
  end
203
321
  # hide from inspect
322
+ m = instance_method(:inspect)
323
+ define_method :__inspect__ do
324
+ m.bind(self).call
325
+ end
204
326
  def inspect
205
327
  ''
206
328
  end
@@ -215,7 +337,7 @@ class Doodle
215
337
  raise(*args)
216
338
  end
217
339
  end
218
-
340
+
219
341
  # provide an alternative inheritance chain that works for singleton
220
342
  # classes as well as modules, classes and instances
221
343
  def parents
@@ -232,7 +354,7 @@ class Doodle
232
354
  anc.select{|x| x.kind_of?(Class)}
233
355
  end
234
356
 
235
- # send message to all doodle_parents and collect results
357
+ # send message to all doodle_parents and collect results
236
358
  def collect_inherited(message)
237
359
  result = []
238
360
  parents.each do |klass|
@@ -247,14 +369,14 @@ class Doodle
247
369
 
248
370
  def handle_inherited_hash(tf, method)
249
371
  if tf
250
- collect_inherited(method).inject(OrderedHash.new){ |hash, item|
251
- hash.merge(OrderedHash[*item])
372
+ collect_inherited(method).inject(Doodle::OrderedHash.new){ |hash, item|
373
+ hash.merge(Doodle::OrderedHash[*item])
252
374
  }.merge(@this.doodle.__send__(method))
253
375
  else
254
376
  @this.doodle.__send__(method)
255
377
  end
256
378
  end
257
-
379
+
258
380
  # returns array of Attributes
259
381
  # - if tf == true, returns all inherited attributes
260
382
  # - if tf == false, returns only those attributes defined in the current object/class
@@ -269,10 +391,10 @@ class Doodle
269
391
 
270
392
  # return class level attributes
271
393
  def class_attributes
272
- attrs = OrderedHash.new
394
+ attrs = Doodle::OrderedHash.new
273
395
  if @this.kind_of?(Class)
274
- attrs = collect_inherited(:class_attributes).inject(OrderedHash.new){ |hash, item|
275
- hash.merge(OrderedHash[*item])
396
+ attrs = collect_inherited(:class_attributes).inject(Doodle::OrderedHash.new){ |hash, item|
397
+ hash.merge(Doodle::OrderedHash[*item])
276
398
  }.merge(@this.singleton_class.doodle.respond_to?(:attributes) ? @this.singleton_class.doodle.attributes : { })
277
399
  attrs
278
400
  else
@@ -287,7 +409,7 @@ class Doodle
287
409
  # as an array), whereas attributes and conversions are keyed
288
410
  # by name and kind respectively, so only the most recent
289
411
  # applies
290
-
412
+
291
413
  local_validations + collect_inherited(:local_validations)
292
414
  else
293
415
  local_validations
@@ -311,12 +433,11 @@ class Doodle
311
433
  handle_inherited_hash(tf, :local_conversions)
312
434
  end
313
435
 
314
- # fixme: move
315
436
  def initial_values(tf = true)
316
437
  attributes(tf).select{|n, a| a.init_defined? }.inject({}) {|hash, (n, a)|
317
438
  #p [:initial_values, a.name]
318
439
  hash[n] = case a.init
319
- when NilClass, TrueClass, FalseClass, Fixnum, Float, Bignum
440
+ when NilClass, TrueClass, FalseClass, Fixnum, Float, Bignum, Symbol
320
441
  # uncloneable values
321
442
  #p [:initial_values, :special, a.name, a.init]
322
443
  a.init
@@ -333,7 +454,7 @@ class Doodle
333
454
  begin
334
455
  a.init.clone
335
456
  rescue Exception => e
336
- warn "tried to clone #{a.init.class} in :init option"
457
+ warn "tried to clone #{a.init.class} in :init option (#{e})"
337
458
  #p [:initial_values, :exception, a.name, e]
338
459
  a.init
339
460
  end
@@ -341,10 +462,9 @@ class Doodle
341
462
  hash
342
463
  }
343
464
  end
344
-
465
+
345
466
  # turn off validation, execute block, then set validation to same
346
467
  # state as it was before +defer_validation+ was called - can be nested
347
- # fixme: move
348
468
  def defer_validation(&block)
349
469
  old_validation = self.validation_on
350
470
  self.validation_on = false
@@ -360,12 +480,12 @@ class Doodle
360
480
 
361
481
  # helper function to initialize from hash - this is safe to use
362
482
  # after initialization (validate! is called if this method is
363
- # called after initialization)
483
+ # called after initialization)
364
484
  def initialize_from_hash(*args)
365
- #!p [:doodle_initialize_from_hash, :args, *args]
485
+ # p [:doodle_initialize_from_hash, :args, *args]
366
486
  defer_validation do
367
487
  # hash initializer
368
- # separate into array of hashes of form [{:k1 => v1}, {:k2 => v2}] and positional args
488
+ # separate into array of hashes of form [{:k1 => v1}, {:k2 => v2}] and positional args
369
489
  key_values, args = args.partition{ |x| x.kind_of?(Hash)}
370
490
  #DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :key_values, key_values, :args, args] }
371
491
  #!p [self.class, :doodle_initialize_from_hash, :key_values, key_values, :args, args]
@@ -373,7 +493,7 @@ class Doodle
373
493
  # set up initial values with ~clones~ of specified values (so not shared between instances)
374
494
  #init_values = initial_values
375
495
  #!p [:init_values, init_values]
376
-
496
+
377
497
  # match up positional args with attribute names (from arg_order) using idiom to create hash from array of assocs
378
498
  #arg_keywords = init_values.merge(Hash[*(Utils.flatten_first_level(self.class.arg_order[0...args.size].zip(args)))])
379
499
  arg_keywords = Hash[*(Utils.flatten_first_level(self.class.arg_order[0...args.size].zip(args)))]
@@ -390,7 +510,7 @@ class Doodle
390
510
  Doodle::Utils.symbolize_keys!(key_values)
391
511
  #DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :key_values2, key_values, :args2, args] }
392
512
  #!p [self.class, :doodle_initialize_from_hash, :key_values3, key_values]
393
-
513
+
394
514
  # create attributes
395
515
  key_values.keys.each do |key|
396
516
  #DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :setting, key, key_values[key]] }
@@ -399,19 +519,20 @@ class Doodle
399
519
  __send__(key, key_values[key])
400
520
  else
401
521
  # raise error if not defined
402
- __doodle__.handle_error key, Doodle::UnknownAttributeError, "unknown attribute '#{key}' #{key_values[key].inspect}", (caller)
522
+ __doodle__.handle_error key, Doodle::UnknownAttributeError, "unknown attribute '#{key}' => #{key_values[key].inspect} for #{self} #{doodle.attributes.map{ |k,v| k.inspect}.join(', ')}", Doodle::Utils.doodle_caller
403
523
  end
404
524
  end
405
525
  # do init_values after user supplied values so init blocks can depend on user supplied values
406
526
  #p [:getting_init_values, instance_variables]
407
527
  __doodle__.initial_values.each do |key, value|
408
528
  if !key_values.key?(key) && respond_to?(key)
529
+ #p [:initial_values, key, value]
409
530
  __send__(key, value)
410
531
  end
411
532
  end
412
533
  end
413
534
  end
414
-
535
+
415
536
  end
416
537
 
417
538
  # what it says on the tin :) various hacks to hide @__doodle__ variable
@@ -432,6 +553,7 @@ class Doodle
432
553
  end
433
554
  end
434
555
 
556
+ # implements the #doodle directive
435
557
  class DataTypeHolder
436
558
  attr_accessor :klass
437
559
  def initialize(klass, &block)
@@ -445,8 +567,23 @@ class Doodle
445
567
  td
446
568
  }
447
569
  end
570
+ def has(*args, &block)
571
+ @klass.class_eval { has(*args, &block) }
572
+ end
573
+ def must(*args, &block)
574
+ @klass.class_eval { must(*args, &block) }
575
+ end
576
+ def from(*args, &block)
577
+ @klass.class_eval { from(*args, &block) }
578
+ end
579
+ def arg_order(*args, &block)
580
+ @klass.class_eval { arg_order(*args, &block) }
581
+ end
582
+ def doc(*args, &block)
583
+ @klass.class_eval { doc(*args, &block) }
584
+ end
448
585
  end
449
-
586
+
450
587
  # the core module of Doodle - however, to get most facilities
451
588
  # provided by Doodle without inheriting from Doodle, include
452
589
  # Doodle::Core, not this module
@@ -483,7 +620,7 @@ class Doodle
483
620
  dh.instance_eval(&block)
484
621
  end
485
622
  end
486
-
623
+
487
624
  # helper for Marshal.dump
488
625
  def marshal_dump
489
626
  # note: perhaps should also dump singleton attribute definitions?
@@ -500,6 +637,7 @@ class Doodle
500
637
  # (using args and/or block)
501
638
  # fixme: move
502
639
  def getter_setter(name, *args, &block)
640
+ #p [:getter_setter, name]
503
641
  name = name.to_sym
504
642
  if block_given? || args.size > 0
505
643
  #!p [:getter_setter, :setter, name, *args]
@@ -525,7 +663,7 @@ class Doodle
525
663
  # (e.g. arrays that disappear when you go out of scope)
526
664
  att = __doodle__.lookup_attribute(name)
527
665
  # special case for class/singleton :init
528
- if att.optional?
666
+ if att && att.optional?
529
667
  optional_value = att.init_defined? ? att.init : att.default
530
668
  #p [:optional_value, optional_value]
531
669
  case optional_value
@@ -543,31 +681,81 @@ class Doodle
543
681
  v
544
682
  else
545
683
  # This is an internal error (i.e. shouldn't happen)
546
- __doodle__.handle_error name, NoDefaultError, "'#{name}' has no default defined", (caller)
684
+ __doodle__.handle_error name, NoDefaultError, "'#{name}' has no default defined", Doodle::Utils.doodle_caller
547
685
  end
548
686
  end
549
687
  end
550
688
  private :_getter
551
689
 
690
+ def after_update(params)
691
+ end
692
+
693
+ def ivar_set(name, *args)
694
+ ivar = "@#{name}"
695
+ if instance_variable_defined?(ivar)
696
+ old_value = instance_variable_get(ivar)
697
+ else
698
+ old_value = nil
699
+ end
700
+ instance_variable_set(ivar, *args)
701
+ new_value = instance_variable_get(ivar)
702
+ if new_value != old_value
703
+ #pp [Doodle, :after_update, { :instance => self, :name => name, :old_value => old_value, :new_value => new_value }]
704
+ after_update :instance => self, :name => name, :old_value => old_value, :new_value => new_value
705
+ end
706
+ end
707
+ private :ivar_set
708
+
552
709
  # set an attribute by name - apply validation if defined
553
710
  # fixme: move
554
711
  def _setter(name, *args, &block)
555
712
  ##DBG: Doodle::Debug.d { [:_setter, name, args] }
556
713
  #p [:_setter, name, *args]
557
- ivar = "@#{name}"
714
+ #ivar = "@#{name}"
715
+ att = __doodle__.lookup_attribute(name)
716
+ if att && doodle.validation_on && att.readonly
717
+ raise Doodle::ReadOnlyError, "Trying to set a readonly attribute: #{att.name}", Doodle::Utils.doodle_caller
718
+ end
558
719
  if block_given?
559
- args.unshift(DeferredBlock.new(block))
720
+ # if a class has been defined, let's assume it can take a
721
+ # block initializer (test that it's a Doodle or Proc)
722
+ if att.kind && !att.abstract && klass = att.kind.first
723
+ if [Doodle, Proc].any?{ |c| klass <= c }
724
+ # p [:_setter, '# 1 converting arg to value with kind ' + klass.to_s]
725
+ args = [klass.new(*args, &block)]
726
+ else
727
+ __doodle__.handle_error att.name, ArgumentError, "#{klass} #{att.name} does not take a block initializer", Doodle::Utils.doodle_caller
728
+ end
729
+ else
730
+ # this is used by init do ... block
731
+ args.unshift(DeferredBlock.new(block))
732
+ end
733
+ #elsif
560
734
  end
561
- if att = __doodle__.lookup_attribute(name)
562
- ##DBG: Doodle::Debug.d { [:_setter, name, args] }
735
+ if att # = __doodle__.lookup_attribute(name)
736
+ if att.kind && !att.abstract && klass = att.kind.first
737
+ if !args.first.kind_of?(klass) && [Doodle].any?{ |c| klass <= c }
738
+ #p [:_setter, "#2 converting arg #{att.name} to value with kind #{klass.to_s}"]
739
+ #p [:_setter, args]
740
+ begin
741
+ args = [klass.new(*args, &block)]
742
+ rescue Object => e
743
+ __doodle__.handle_error att.name, e.class, e.to_s, Doodle::Utils.doodle_caller
744
+ end
745
+ end
746
+ end
747
+ # args = [klass.new(*args, &block)] ##DBG: Doodle::Debug.d { [:_setter, name, args] }
563
748
  #p [:_setter, :got_att1, name, ivar, *args]
564
- v = instance_variable_set(ivar, att.validate(self, *args))
749
+ # v = instance_variable_set(ivar, att.validate(self, *args))
750
+ v = ivar_set(name, att.validate(self, *args))
751
+
565
752
  #p [:_setter, :got_att2, name, ivar, :value, v]
566
753
  #v = instance_variable_set(ivar, *args)
567
754
  else
568
755
  #p [:_setter, :no_att, name, *args]
569
756
  ##DBG: Doodle::Debug.d { [:_setter, "no attribute"] }
570
- v = instance_variable_set(ivar, *args)
757
+ # v = instance_variable_set(ivar, *args)
758
+ v = ivar_set(name, *args)
571
759
  end
572
760
  validate!(false)
573
761
  v
@@ -590,41 +778,36 @@ class Doodle
590
778
 
591
779
  # add a validation
592
780
  def must(constraint = 'be valid', &block)
593
- if block.nil?
594
- # is this really useful? do I really want it?
595
- __doodle__.local_validations << Validation.new(constraint, &proc { |v| v.instance_eval(constraint) })
596
- else
597
- __doodle__.local_validations << Validation.new(constraint, &block)
598
- end
781
+ __doodle__.local_validations << Validation.new(constraint, &block)
599
782
  end
600
783
 
601
784
  # add a validation that attribute must be of class <= kind
602
785
  def kind(*args, &block)
603
- @kind ||= []
604
786
  if args.size > 0
605
787
  @kind = [args].flatten
606
788
  # todo[figure out how to handle kind being specified twice?]
607
789
  if @kind.size > 2
608
- kind_text = "a kind of #{ @kind[0..-2].map{ |x| x.to_s }.join(', ') } or #{@kind[-1].to_s}" # =>
790
+ kind_text = "be a kind of #{ @kind[0..-2].map{ |x| x.to_s }.join(', ') } or #{@kind[-1].to_s}" # =>
609
791
  else
610
- kind_text = "a kind of #{@kind.to_s}"
792
+ kind_text = "be a kind of #{@kind.to_s}"
611
793
  end
612
794
  __doodle__.local_validations << (Validation.new(kind_text) { |x| @kind.any? { |klass| x.kind_of?(klass) } })
613
795
  else
614
- @kind
796
+ @kind ||= []
615
797
  end
616
798
  end
617
799
 
618
800
  # convert a value according to conversion rules
619
801
  # fixme: move
620
802
  def convert(owner, *args)
621
- #!p [:convert, 1, owner, args, __doodle__.conversions]
803
+ #pp( { :convert => 1, :owner => owner, :args => args, :conversions => __doodle__.conversions } )
622
804
  begin
623
805
  args = args.map do |value|
624
806
  #!p [:convert, 2, value]
625
807
  if (converter = __doodle__.conversions[value.class])
626
- #!p [:convert, 3, value]
627
- value = converter[*args]
808
+ #p [:convert, 3, value, self, caller]
809
+ value = converter[value]
810
+ #value = instance_exec(value, &converter)
628
811
  #!p [:convert, 4, value]
629
812
  else
630
813
  #!p [:convert, 5, value]
@@ -641,22 +824,24 @@ class Doodle
641
824
  #!p [:convert, 10, converter_class]
642
825
  if converter = __doodle__.conversions[converter_class]
643
826
  #!p [:convert, 11, converter]
644
- value = converter[*args]
827
+ value = converter[value]
828
+ #value = instance_exec(value, &converter)
645
829
  #!p [:convert, 12, value]
646
830
  end
647
831
  else
648
- #!p [:convert, 13, :kind, kind, name, value]
832
+ #!p [:convert, 13, :kind, kind, name, value]
649
833
  mappable_kinds = kind.select{ |x| x <= Doodle::Core }
650
- #!p [:convert, 13.1, :kind, kind, mappable_kinds]
834
+ #!p [:convert, 13.1, :kind, kind, mappable_kinds]
651
835
  if mappable_kinds.size > 0
652
836
  mappable_kinds.each do |mappable_kind|
653
- #!p [:convert, 14, :kind_is_a_doodle, value.class, mappable_kind, mappable_kind.doodle.conversions, args]
837
+ #!p [:convert, 14, :kind_is_a_doodle, value.class, mappable_kind, mappable_kind.doodle.conversions, args]
654
838
  if converter = mappable_kind.doodle.conversions[value.class]
655
- #!p [:convert, 15, value, mappable_kind, args]
656
- value = converter[*args]
839
+ #!p [:convert, 15, value, mappable_kind, args]
840
+ value = converter[value]
841
+ #value = instance_exec(value, &converter)
657
842
  break
658
843
  else
659
- #!p [:convert, 16, :no_conversion_for, value.class]
844
+ #!p [:convert, 16, :no_conversion_for, value.class]
660
845
  end
661
846
  end
662
847
  else
@@ -668,7 +853,7 @@ class Doodle
668
853
  value
669
854
  end
670
855
  rescue Exception => e
671
- owner.__doodle__.handle_error name, ConversionError, "#{e.message}", (caller)
856
+ owner.__doodle__.handle_error name, ConversionError, "#{e.message}", Doodle::Utils.doodle_caller
672
857
  end
673
858
  if args.size > 1
674
859
  args
@@ -681,26 +866,26 @@ class Doodle
681
866
  # fixme: move
682
867
  def validate(owner, *args)
683
868
  ##DBG: Doodle::Debug.d { [:validate, self, :owner, owner, :args, args ] }
684
- #!p [:validate, 1, args]
869
+ #p [:validate, 1, args]
685
870
  begin
686
871
  value = convert(owner, *args)
687
872
  rescue Exception => e
688
- owner.__doodle__.handle_error name, ConversionError, "#{owner.kind_of?(Class) ? owner : owner.class}.#{ name } - #{e.message}", (caller)
873
+ owner.__doodle__.handle_error name, ConversionError, "#{owner.kind_of?(Class) ? owner : owner.class}.#{ name } - #{e.message}", Doodle::Utils.doodle_caller
689
874
  end
690
- #!p [:validate, 2, args, :becomes, value]
875
+ #p [:validate, 2, args, :becomes, value]
691
876
  __doodle__.validations.each do |v|
692
877
  ##DBG: Doodle::Debug.d { [:validate, self, v, args, value] }
693
878
  if !v.block[value]
694
- owner.__doodle__.handle_error name, ValidationError, "#{owner.kind_of?(Class) ? owner : owner.class}.#{ name } must #{ v.message } - got #{ value.class }(#{ value.inspect })", (caller)
879
+ owner.__doodle__.handle_error name, ValidationError, "#{owner.kind_of?(Class) ? owner : owner.class}.#{ name } must #{ v.message } - got #{ value.class }(#{ value.inspect })", Doodle::Utils.doodle_caller
695
880
  end
696
881
  end
697
- #!p [:validate, 3, value]
882
+ #p [:validate, 3, value]
698
883
  value
699
884
  end
700
885
 
701
886
  # define a getter_setter
702
887
  # fixme: move
703
- def define_getter_setter(name, *args, &block)
888
+ def define_getter_setter(name, params = { }, &block)
704
889
  # need to use string eval because passing block
705
890
  sc_eval "def #{name}(*args, &block); getter_setter(:#{name}, *args, &block); end", __FILE__, __LINE__
706
891
  sc_eval "def #{name}=(*args, &block); _setter(:#{name}, *args); end", __FILE__, __LINE__
@@ -717,6 +902,16 @@ class Doodle
717
902
  end
718
903
  private :define_getter_setter
719
904
 
905
+ # +doc+ add docs to doodle class or attribute
906
+ def doc(*args, &block)
907
+ if args.size > 0
908
+ @doc = *args
909
+ else
910
+ @doc
911
+ end
912
+ end
913
+ alias :doc= :doc
914
+
720
915
  # +has+ is an extended +attr_accessor+
721
916
  #
722
917
  # simple usage - just like +attr_accessor+:
@@ -738,33 +933,33 @@ class Doodle
738
933
  # default { Date.today }
739
934
  # end
740
935
  # end
741
- #
936
+ #
742
937
  def has(*args, &block)
743
938
  #DBG: Doodle::Debug.d { [:has, self, self.class, args] }
744
-
939
+
745
940
  params = DoodleAttribute.params_from_args(self, *args)
746
941
  # get specialized attribute class or use default
747
942
  attribute_class = params.delete(:using) || DoodleAttribute
748
943
 
749
944
  # could this be handled in DoodleAttribute?
750
945
  # define getter setter before setting up attribute
751
- define_getter_setter params[:name], *args, &block
946
+ define_getter_setter params[:name], params, &block
752
947
  #p [:attribute, attribute_class, params]
753
- __doodle__.local_attributes[params[:name]] = attribute_class.new(params, &block)
948
+ attr = __doodle__.local_attributes[params[:name]] = attribute_class.new(params, &block)
754
949
  end
755
-
950
+
756
951
  # define order for positional arguments
757
952
  def arg_order(*args)
758
953
  if args.size > 0
759
954
  begin
760
955
  args = args.uniq
761
956
  args.each do |x|
762
- __doodle__.handle_error :arg_order, ArgumentError, "#{x} not a Symbol", (caller) if !(x.class <= Symbol)
763
- __doodle__.handle_error :arg_order, NameError, "#{x} not an attribute name", (caller) if !doodle.attributes.keys.include?(x)
957
+ __doodle__.handle_error :arg_order, ArgumentError, "#{x} not a Symbol", Doodle::Utils.doodle_caller if !(x.class <= Symbol)
958
+ __doodle__.handle_error :arg_order, NameError, "#{x} not an attribute name", Doodle::Utils.doodle_caller if !doodle.attributes.keys.include?(x)
764
959
  end
765
960
  __doodle__.arg_order = args
766
961
  rescue Exception => e
767
- __doodle__.handle_error :arg_order, InvalidOrderError, e.to_s, (caller)
962
+ __doodle__.handle_error :arg_order, InvalidOrderError, e.to_s, Doodle::Utils.doodle_caller
768
963
  end
769
964
  else
770
965
  __doodle__.arg_order + (__doodle__.attributes.keys - __doodle__.arg_order)
@@ -799,29 +994,29 @@ class Doodle
799
994
  # if all == true, reset values so conversions and
800
995
  # validations are applied to raw instance variables
801
996
  # e.g. when loaded from YAML
802
- if all
997
+ if all && !att.readonly
803
998
  ##DBG: Doodle::Debug.d { [:validate!, :sending, att.name, instance_variable_get(ivar_name) ] }
804
999
  __send__("#{att.name}=", instance_variable_get(ivar_name))
805
1000
  end
806
1001
  elsif att.optional? # treat default/init as special case
807
1002
  ##DBG: Doodle::Debug.d { [:validate!, :optional, name ]}
808
- break
1003
+ next
809
1004
  elsif self.class != Class
810
- __doodle__.handle_error name, Doodle::ValidationError, "#{self} missing required attribute '#{name}'", (caller)
1005
+ __doodle__.handle_error name, Doodle::ValidationError, "#{self} missing required attribute '#{name}'", Doodle::Utils.doodle_caller
811
1006
  end
812
1007
  end
813
-
1008
+
814
1009
  # now apply instance level validations
815
-
1010
+
816
1011
  ##DBG: Doodle::Debug.d { [:validate!, "validations", doodle_validations ]}
817
1012
  __doodle__.validations.each do |v|
818
1013
  ##DBG: Doodle::Debug.d { [:validate!, self, v ] }
819
1014
  begin
820
1015
  if !instance_eval(&v.block)
821
- __doodle__.handle_error self, ValidationError, "#{ self.class } must #{ v.message }", (caller)
1016
+ __doodle__.handle_error self, ValidationError, "#{ self.class } must #{ v.message }", Doodle::Utils.doodle_caller
822
1017
  end
823
1018
  rescue Exception => e
824
- __doodle__.handle_error self, ValidationError, e.to_s, (caller)
1019
+ __doodle__.handle_error self, ValidationError, e.to_s, Doodle::Utils.doodle_caller
825
1020
  end
826
1021
  end
827
1022
  end
@@ -837,13 +1032,29 @@ class Doodle
837
1032
  super
838
1033
  end
839
1034
  __doodle__.validation_on = true
840
- __doodle__.parent = Doodle.context[-1]
1035
+ #p [:doodle_parent, Doodle.parent, caller[-1]]
841
1036
  Doodle.context.push(self)
842
1037
  __doodle__.defer_validation do
843
1038
  doodle.initialize_from_hash(*args)
844
1039
  instance_eval(&block) if block_given?
845
1040
  end
846
1041
  Doodle.context.pop
1042
+ #p [:doodle, __doodle__.__inspect__]
1043
+ #p [:doodle, __doodle__.attributes]
1044
+ #p [:doodle_parent, __doodle__.parent]
1045
+ end
1046
+
1047
+ # create 'pure' hash of scalars only from attributes - hacky but works fine
1048
+ def to_hash
1049
+ Doodle::Utils.symbolize_keys!(YAML::load(to_yaml.gsub(/!ruby\/object:.*$/, '')) || { }, true)
1050
+ #begin
1051
+ # YAML::load(to_yaml.gsub(/!ruby\/object:.*$/, '')) || { }
1052
+ #rescue Object => e
1053
+ # doodle.attributes.inject({}) {|hash, (name, attribute)| hash[name] = send(name); hash}
1054
+ #end
1055
+ end
1056
+ def to_string_hash
1057
+ Doodle::Utils.stringify_keys!(YAML::load(to_yaml.gsub(/!ruby\/object:.*$/, '')) || { }, true)
847
1058
  end
848
1059
 
849
1060
  end
@@ -856,10 +1067,10 @@ class Doodle
856
1067
  # As the notion of a factory function is somewhat contentious [xref
857
1068
  # ruby-talk], you need to explicitly ask for them by including Factory
858
1069
  # in your base class:
859
- # class Base < Doodle::Root
1070
+ # class Animal < Doodle
860
1071
  # include Factory
861
1072
  # end
862
- # class Dog < Base
1073
+ # class Dog < Animal
863
1074
  # end
864
1075
  # stimpy = Dog(:name => 'Stimpy')
865
1076
  # etc.
@@ -880,8 +1091,8 @@ class Doodle
880
1091
  rescue Object
881
1092
  false
882
1093
  end
883
-
884
- if name =~ Factory::RX_IDENTIFIER && !method_defined && !klass.respond_to?(name) && !eval("respond_to?(:#{name})", TOPLEVEL_BINDING)
1094
+
1095
+ if name =~ Factory::RX_IDENTIFIER && !method_defined && !klass.respond_to?(name) && !eval("respond_to?(:#{name})", TOPLEVEL_BINDING)
885
1096
  eval("def #{ name }(*args, &block); ::#{name}.new(*args, &block); end", ::TOPLEVEL_BINDING, __FILE__, __LINE__)
886
1097
  end
887
1098
  else
@@ -915,11 +1126,9 @@ class Doodle
915
1126
  }
916
1127
  end
917
1128
  end
918
- # deprecated
919
- Helper = Core
920
1129
 
921
- # deprecated
922
- class Base
1130
+ # wierd 1.9 shit
1131
+ class IAmNotUsedBut1_9GoesIntoAnInfiniteRegressInInspectIfIAmNotDefined
923
1132
  include Core
924
1133
  end
925
1134
  include Core
@@ -934,6 +1143,7 @@ class Doodle
934
1143
  class DoodleAttribute < Doodle
935
1144
  # note: using extend with a module causes an infinite loop in 1.9
936
1145
  # hence the inline
1146
+
937
1147
  class << self
938
1148
  # rewrite rules for the argument list to #has
939
1149
  def params_from_args(owner, *args)
@@ -953,15 +1163,15 @@ class Doodle
953
1163
  params = key_values.inject(params){ |acc, item| acc.merge(item)}
954
1164
  #DBG: Doodle::Debug.d { [:has, self, self.class, params] }
955
1165
  if !params.key?(:name)
956
- __doodle__.handle_error name, ArgumentError, "#{self.class} must have a name", (caller)
1166
+ __doodle__.handle_error name, ArgumentError, "#{self.class} must have a name", Doodle::Utils.doodle_caller
957
1167
  params[:name] = :__ERROR_missing_name__
958
1168
  else
959
1169
  # ensure that :name is a symbol
960
1170
  params[:name] = params[:name].to_sym
961
1171
  end
962
1172
  name = params[:name]
963
- __doodle__.handle_error name, ArgumentError, "#{self.class} has too many arguments", (caller) if positional_args.size > 0
964
-
1173
+ __doodle__.handle_error name, ArgumentError, "#{self.class} has too many arguments", Doodle::Utils.doodle_caller if positional_args.size > 0
1174
+
965
1175
  if collector = params.delete(:collect)
966
1176
  if !params.key?(:using)
967
1177
  if params.key?(:key)
@@ -1035,6 +1245,15 @@ class Doodle
1035
1245
  # special case - not an attribute
1036
1246
  define_getter_setter :doodle_owner
1037
1247
 
1248
+ # temporarily fake existence of abstract attribute - later has
1249
+ # :abstract overrides this
1250
+ def abstract
1251
+ @abstract = false
1252
+ end
1253
+ def readonly
1254
+ false
1255
+ end
1256
+
1038
1257
  # name of attribute
1039
1258
  has :name, :kind => Symbol do
1040
1259
  from String do |s|
@@ -1048,13 +1267,21 @@ class Doodle
1048
1267
  # initial value
1049
1268
  has :init, :default => nil
1050
1269
 
1270
+ # documentation
1271
+ has :doc, :default => ""
1272
+
1273
+ # don't try to initialize from this class
1274
+ remove_method(:abstract) # because we faked it earlier - remove to avoid redefinition warning
1275
+ has :abstract, :default => false
1276
+ remove_method(:readonly) # because we faked it earlier - remove to avoid redefinition warning
1277
+ has :readonly, :default => false
1051
1278
  end
1052
1279
 
1053
1280
  # base class for attribute collector classes
1054
1281
  class AttributeCollector < DoodleAttribute
1055
1282
  has :collector_class
1056
1283
  has :collector_name
1057
-
1284
+
1058
1285
  def resolve_collector_class
1059
1286
  if !collector_class.kind_of?(Class)
1060
1287
  self.collector_class = Doodle::Utils.const_resolve(collector_class)
@@ -1074,7 +1301,7 @@ class Doodle
1074
1301
  define_collection
1075
1302
  from Hash do |hash|
1076
1303
  resolve_collector_class
1077
- hash.inject({ }) do |h, (key, value)|
1304
+ hash.inject(self.init.clone) do |h, (key, value)|
1078
1305
  h[key] = resolve_value(value)
1079
1306
  h
1080
1307
  end
@@ -1085,12 +1312,13 @@ class Doodle
1085
1312
  end
1086
1313
  end
1087
1314
  def post_process(results)
1088
- results
1315
+ self.init.clone.replace(results)
1089
1316
  end
1090
1317
  end
1091
1318
 
1092
1319
  # define collector methods for array-like attribute collectors
1093
1320
  class AppendableAttribute < AttributeCollector
1321
+ # has :init, :init => DoodleArray.new
1094
1322
  has :init, :init => []
1095
1323
 
1096
1324
  # define a collector for appendable collections
@@ -1113,16 +1341,17 @@ class Doodle
1113
1341
  end", __FILE__, __LINE__)
1114
1342
  end
1115
1343
  end
1116
-
1344
+
1117
1345
  end
1118
1346
 
1119
1347
  # define collector methods for hash-like attribute collectors
1120
1348
  class KeyedAttribute < AttributeCollector
1349
+ # has :init, :init => DoodleHash.new
1121
1350
  has :init, :init => { }
1122
1351
  has :key
1123
1352
 
1124
1353
  def post_process(results)
1125
- results.inject({ }) do |h, result|
1354
+ results.inject(self.init.clone) do |h, result|
1126
1355
  h[result.send(key)] = result
1127
1356
  h
1128
1357
  end