doodle 0.1.8 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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