doodle 0.1.6 → 0.1.7

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,21 @@
1
+ == 0.1.7 / 2008-05-10
2
+ - Features
3
+ - #has now accepts class constant in name position and generates name
4
+ and kind from that, e.g.
5
+ has Person --> has :person, :kind => Person
6
+ has AudioClip --> has :audio_clip, :kind => AudioClip
7
+ - moved datatypes code into main doodle.rb
8
+ - use doodle.parent instead of doodle_parent
9
+
10
+ - Bug fixes:
11
+ - fixed handling of :init blocks so they can refer to user supplied
12
+ attribute values which don't have defaults
13
+ - note: attributes referred to in :init blocks ~must~ be supplied as
14
+ keyword arguments - by the time evaluation gets to the
15
+ initialization block, the :init block has already been evaluated
16
+ so will raise an error if it cannot find the value it's looking for
17
+ - moved more methods into DoodleInfo from Core
18
+
1
19
  == 0.1.6 / 2008-05-08
2
20
  - Features:
3
21
  - short cut syntax for #must - can now specify constraints like
@@ -16,7 +16,7 @@ class Child < Doodle
16
16
  # - somewhat subtle difference (from programmer's point of
17
17
  # - view) between a proc and a block
18
18
  init do
19
- doodle_parent
19
+ doodle.parent
20
20
  end
21
21
  end
22
22
  end
@@ -3,6 +3,7 @@
3
3
  # 2007-11-24 first version
4
4
  # 2008-04-18 latest release 0.0.12
5
5
  # 2008-05-07 0.1.6
6
+ # 2008-05-12 0.1.7
6
7
  $:.unshift(File.dirname(__FILE__)) unless
7
8
  $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
8
9
 
@@ -57,6 +58,7 @@ class Doodle
57
58
  def snake_case(camel_cased_word)
58
59
  camel_cased_word.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
59
60
  end
61
+ # resolve a constant of the form Some::Class::Or::Module
60
62
  def const_resolve(constant)
61
63
  constant.to_s.split(/::/).reject{|x| x.empty?}.inject(Object) { |prev, this| prev.const_get(this) }
62
64
  end
@@ -109,46 +111,6 @@ class Doodle
109
111
  end
110
112
  end
111
113
 
112
- # provide an alternative inheritance chain that works for singleton
113
- # classes as well as modules, classes and instances
114
- module Inherited
115
-
116
- # doodle_parents returns the set of parent classes of an object
117
- def doodle_parents
118
- anc = if respond_to?(:ancestors)
119
- if ancestors.include?(self)
120
- ancestors[1..-1]
121
- else
122
- # singletons have no doodle_parents (they're orphans)
123
- []
124
- end
125
- else
126
- self.class.ancestors
127
- end
128
- anc.select{|x| x.kind_of?(Class)}
129
- end
130
-
131
- # need concepts of
132
- # - attributes
133
- # - instance_attributes
134
- # - singleton_attributes
135
- # - class_attributes
136
-
137
- # send message to all doodle_parents and collect results
138
- def doodle_collect_inherited(message)
139
- result = []
140
- doodle_parents.each do |klass|
141
- if klass.respond_to?(message)
142
- result.unshift(*klass.__send__(message))
143
- else
144
- break
145
- end
146
- end
147
- result
148
- end
149
- private :doodle_collect_inherited
150
- end
151
-
152
114
  # = embrace
153
115
  # the intent of embrace is to provide a way to create directives
154
116
  # that affect all members of a class 'family' without having to
@@ -208,26 +170,134 @@ class Doodle
208
170
 
209
171
  # place to stash bookkeeping info
210
172
  class DoodleInfo
211
- attr_accessor :doodle_local_attributes
212
- attr_accessor :doodle_local_validations
213
- attr_accessor :doodle_local_conversions
173
+ attr_accessor :local_attributes
174
+ attr_accessor :local_validations
175
+ attr_accessor :local_conversions
214
176
  attr_accessor :validation_on
215
177
  attr_accessor :arg_order
216
178
  attr_accessor :errors
217
- attr_accessor :doodle_parent
179
+ attr_accessor :parent
218
180
 
219
181
  def initialize(object)
220
- @doodle_local_attributes = OrderedHash.new
221
- @doodle_local_validations = []
182
+ @this = object
183
+ @local_attributes = OrderedHash.new
184
+ @local_validations = []
222
185
  @validation_on = true
223
- @doodle_local_conversions = {}
186
+ @local_conversions = {}
224
187
  @arg_order = []
225
188
  @errors = []
226
- @doodle_parent = nil
189
+ @parent = nil
227
190
  end
191
+ # hide from inspect
228
192
  def inspect
229
193
  ''
230
194
  end
195
+
196
+ # handle errors either by collecting in :errors or raising an exception
197
+ def handle_error(name, *args)
198
+ # don't include duplicates (FIXME: hacky - shouldn't have duplicates in the first place)
199
+ if !errors.include?([name, *args])
200
+ errors << [name, *args]
201
+ end
202
+ if Doodle.raise_exception_on_error
203
+ raise(*args)
204
+ end
205
+ end
206
+
207
+ # provide an alternative inheritance chain that works for singleton
208
+ # classes as well as modules, classes and instances
209
+ def parents
210
+ anc = if @this.respond_to?(:ancestors)
211
+ if @this.ancestors.include?(@this)
212
+ @this.ancestors[1..-1]
213
+ else
214
+ # singletons have no doodle_parents (they're orphans)
215
+ []
216
+ end
217
+ else
218
+ @this.class.ancestors
219
+ end
220
+ anc.select{|x| x.kind_of?(Class)}
221
+ end
222
+
223
+ # send message to all doodle_parents and collect results
224
+ def collect_inherited(message)
225
+ result = []
226
+ parents.each do |klass|
227
+ if klass.respond_to?(message)
228
+ result.unshift(*klass.__send__(message))
229
+ else
230
+ break
231
+ end
232
+ end
233
+ result
234
+ end
235
+
236
+ def handle_inherited_hash(tf, method)
237
+ if tf
238
+ collect_inherited(method).inject(OrderedHash.new){ |hash, item|
239
+ hash.merge(OrderedHash[*item])
240
+ }.merge(@this.__send__(method))
241
+ else
242
+ @this.__send__(method)
243
+ end
244
+ end
245
+
246
+ # returns array of Attributes
247
+ # - if tf == true, returns all inherited attributes
248
+ # - if tf == false, returns only those attributes defined in the current object/class
249
+ def attributes(tf = true)
250
+ results = handle_inherited_hash(tf, :doodle_local_attributes)
251
+ # if an instance, include the singleton_class attributes
252
+ if !@this.kind_of?(Class) && @this.singleton_class.doodle.respond_to?(:attributes)
253
+ results = results.merge(@this.singleton_class.doodle_attributes)
254
+ end
255
+ results
256
+ end
257
+
258
+ def class_attributes
259
+ attrs = OrderedHash.new
260
+ if @this.kind_of?(Class)
261
+ attrs = collect_inherited(:class_attributes).inject(OrderedHash.new){ |hash, item|
262
+ hash.merge(OrderedHash[*item])
263
+ }.merge(@this.singleton_class.respond_to?(:doodle_attributes) ? @this.singleton_class.doodle_attributes : { })
264
+ attrs
265
+ else
266
+ @this.class.class_attributes
267
+ end
268
+ end
269
+
270
+ def validations(tf = true)
271
+ if tf
272
+ # note: validations are handled differently to attributes and
273
+ # conversions because ~all~ validations apply (so are stored
274
+ # as an array), whereas attributes and conversions are keyed
275
+ # by name and kind respectively, so only the most recent
276
+ # applies
277
+
278
+ local_validations + collect_inherited(:doodle_local_validations)
279
+ else
280
+ local_validations
281
+ end
282
+ end
283
+
284
+ def lookup_attribute(name)
285
+ # (look at singleton attributes first)
286
+ # fixme[this smells like a hack to me]
287
+ if @this.class == Class
288
+ class_attributes[name]
289
+ else
290
+ attributes[name]
291
+ end
292
+ end
293
+
294
+ # returns hash of conversions
295
+ # - if tf == true, returns all inherited conversions
296
+ # - if tf == false, returns only those conversions defined in the current object/class
297
+ def conversions(tf = true)
298
+ handle_inherited_hash(tf, :doodle_local_conversions)
299
+ end
300
+
231
301
  end
232
302
 
233
303
  # what it says on the tin :) various hacks to hide @__doodle__ variable
@@ -238,19 +308,36 @@ class Doodle
238
308
  define_method :instance_variables do
239
309
  meth.bind(self).call.reject{ |x| x.to_s =~ /@__doodle__/}
240
310
  end
241
-
242
311
  # hide @__doodle__ from inspect
243
312
  def inspect
244
313
  super.gsub(/\s*@__doodle__=,/,'').gsub(/,?\s*@__doodle__=/,'')
245
314
  end
315
+ # fix for pp
316
+ def pretty_print(q)
317
+ q.pp_object(self)
318
+ end
246
319
  end
247
320
 
321
+ class DataTypeHolder
322
+ attr_accessor :klass
323
+ def initialize(klass, &block)
324
+ @klass = klass
325
+ instance_eval(&block) if block_given?
326
+ end
327
+ def define(name, params, block, type_params, &type_block)
328
+ @klass.class_eval {
329
+ td = has(name, type_params.merge(params), &type_block)
330
+ td.instance_eval(&block) if block
331
+ td
332
+ }
333
+ end
334
+ end
335
+
248
336
  # the core module of Doodle - however, to get most facilities
249
337
  # provided by Doodle without inheriting from Doodle, include
250
338
  # Doodle::Core, not this module
251
339
  module BaseMethods
252
340
  include SelfClass
253
- include Inherited
254
341
  include SmokeAndMirrors
255
342
 
256
343
  # this is the only way to get at internal values. Note: this is
@@ -259,8 +346,30 @@ class Doodle
259
346
  def __doodle__
260
347
  @__doodle__ ||= DoodleInfo.new(self)
261
348
  end
262
- private :__doodle__
349
+ protected :__doodle__
263
350
 
351
+ # set up global datatypes
352
+ def datatypes(*mods)
353
+ mods.each do |mod|
354
+ DataTypeHolder.class_eval { include mod }
355
+ end
356
+ end
357
+
358
+ # vector through this method to get to doodle info or enable global
359
+ # datatypes and provide an interface that allows you to add your own
360
+ # datatypes to this declaration
361
+ def doodle(*mods, &block)
362
+ if mods.size == 0 && !block_given?
363
+ __doodle__
364
+ else
365
+ dh = Doodle::DataTypeHolder.new(self)
366
+ mods.each do |mod|
367
+ dh.extend(mod)
368
+ end
369
+ dh.instance_eval(&block)
370
+ end
371
+ end
372
+
264
373
  # helper for Marshal.dump
265
374
  def marshal_dump
266
375
  # note: perhaps should also dump singleton attribute definitions?
@@ -273,46 +382,17 @@ class Doodle
273
382
  end
274
383
  end
275
384
 
276
- #### ERRORS COLLECTION
277
-
278
- # where should I put this?
279
- def errors
280
- __doodle__.errors
281
- end
282
-
283
- # clear out the errors collection
284
- def clear_errors
285
- #pp [:clear_errors, self, caller]
286
- __doodle__.errors.clear
287
- end
288
-
289
- # handle errors either by collecting in :errors or raising an exception
290
- def handle_error(name, *args)
291
- # don't include duplicates (FIXME: hacky - shouldn't have duplicates in the first place)
292
- if !self.errors.include?([name, *args])
293
- self.errors << [name, *args]
294
- end
295
- if Doodle.raise_exception_on_error
296
- raise(*args)
297
- end
385
+ # deprecated methods
386
+ def doodle_collect_inherited(message)
387
+ __doodle__.collect_inherited(message)
298
388
  end
299
-
300
- #### ERRORS COLLECTION
301
-
302
- def _handle_inherited_hash(tf, method)
303
- if tf
304
- doodle_collect_inherited(method).inject(OrderedHash.new){ |hash, item|
305
- hash.merge(OrderedHash[*item])
306
- }.merge(__send__(method))
307
- else
308
- __send__(method)
309
- end
389
+ def doodle_parents
390
+ __doodle__.parents
310
391
  end
311
- private :_handle_inherited_hash
312
392
 
313
393
  # return attributes defined in instance
314
394
  def doodle_local_attributes
315
- __doodle__.doodle_local_attributes
395
+ __doodle__.local_attributes
316
396
  end
317
397
  protected :doodle_local_attributes
318
398
 
@@ -320,77 +400,47 @@ class Doodle
320
400
  # - if tf == true, returns all inherited attributes
321
401
  # - if tf == false, returns only those attributes defined in the current object/class
322
402
  def doodle_attributes(tf = true)
323
- results = _handle_inherited_hash(tf, :doodle_local_attributes)
324
- # if an instance, include the singleton_class attributes
325
- if !kind_of?(Class) && singleton_class.respond_to?(:doodle_attributes)
326
- results = results.merge(singleton_class.doodle_attributes)
327
- end
328
- results
403
+ __doodle__.attributes(tf)
329
404
  end
330
405
 
331
406
  # return attributes for class
332
- def class_attributes(tf = true)
333
- attrs = OrderedHash.new
334
- if self.kind_of?(Class)
335
- attrs = doodle_collect_inherited(:class_attributes).inject(OrderedHash.new){ |hash, item|
336
- hash.merge(OrderedHash[*item])
337
- }.merge(singleton_class.respond_to?(:doodle_attributes) ? singleton_class.doodle_attributes : { })
338
- attrs
339
- else
340
- self.class.class_attributes
341
- end
407
+ def class_attributes
408
+ __doodle__.class_attributes
342
409
  end
343
410
 
344
411
  # the set of conversions defined in the current class (i.e. without inheritance)
412
+ # deprecated
345
413
  def doodle_local_conversions
346
- __doodle__.doodle_local_conversions
414
+ __doodle__.local_conversions
347
415
  end
348
416
  protected :doodle_local_conversions
349
417
 
350
418
  # returns hash of conversions
351
419
  # - if tf == true, returns all inherited conversions
352
420
  # - if tf == false, returns only those conversions defined in the current object/class
421
+ # deprecated
353
422
  def doodle_conversions(tf = true)
354
- _handle_inherited_hash(tf, :doodle_local_conversions)
423
+ __doodle__.conversions(tf)
355
424
  end
356
425
 
357
426
  # the set of validations defined in the current class (i.e. without inheritance)
427
+ # deprecated
358
428
  def doodle_local_validations
359
- __doodle__.doodle_local_validations
429
+ __doodle__.local_validations
360
430
  end
361
431
  protected :doodle_local_validations
362
432
 
363
433
  # returns array of Validations
364
434
  # - if tf == true, returns all inherited validations
365
435
  # - if tf == false, returns only those validations defined in the current object/class
436
+ # deprecated
366
437
  def doodle_validations(tf = true)
367
- if tf
368
- # note: validations are handled differently to attributes and
369
- # conversions because ~all~ validations apply (so are stored
370
- # as an array), whereas attributes and conversions are keyed
371
- # by name and kind respectively, so only the most recent
372
- # applies
373
-
374
- doodle_local_validations + doodle_collect_inherited(:doodle_local_validations)
375
- else
376
- doodle_local_validations
377
- end
438
+ __doodle__.validations(tf)
378
439
  end
379
440
 
380
- # lookup a single attribute by name, searching the singleton class first
381
- def lookup_attribute(name)
382
- # (look at singleton attributes first)
383
- # fixme[this smells like a hack to me]
384
- if self.class == Class
385
- class_attributes[name]
386
- else
387
- doodle_attributes[name]
388
- end
389
- end
390
- private :lookup_attribute
391
-
392
441
  # either get an attribute value (if no args given) or set it
393
442
  # (using args and/or block)
443
+ # fixme: move
394
444
  def getter_setter(name, *args, &block)
395
445
  name = name.to_sym
396
446
  if block_given? || args.size > 0
@@ -404,52 +454,60 @@ class Doodle
404
454
  private :getter_setter
405
455
 
406
456
  # get an attribute by name - return default if not otherwise defined
457
+ # fixme: init deferred blocks are not getting resolved in all cases
407
458
  def _getter(name, &block)
459
+ #p [:_getter, name]
408
460
  ivar = "@#{name}"
409
461
  if instance_variable_defined?(ivar)
410
- #!p [:_getter, name, ivar, instance_variable_get(ivar)]
462
+ #p [:_getter, :instance_variable_defined, name, ivar, instance_variable_get(ivar)]
411
463
  instance_variable_get(ivar)
412
464
  else
413
465
  # handle default
414
466
  # Note: use :init => value to cover cases where defaults don't work
415
467
  # (e.g. arrays that disappear when you go out of scope)
416
- att = lookup_attribute(name)
468
+ att = __doodle__.lookup_attribute(name)
417
469
  # special case for class/singleton :init
418
- if att.init_defined?
419
- #!p [:_setter, att.init]
420
- _setter(name, att.init)
421
- elsif att.default_defined?
422
- case att.default
470
+ if att.optional?
471
+ optional_value = att.init_defined? ? att.init : att.default
472
+ #p [:optional_value, optional_value]
473
+ case optional_value
423
474
  when DeferredBlock
424
- instance_eval(&att.default.block)
475
+ #p [:deferred_block]
476
+ v = instance_eval(&optional_value.block)
425
477
  when Proc
426
- instance_eval(&att.default)
478
+ v = instance_eval(&optional_value)
427
479
  else
428
- att.default
480
+ v = optional_value
429
481
  end
482
+ if att.init_defined?
483
+ _setter(name, v)
484
+ end
485
+ v
430
486
  else
431
487
  # This is an internal error (i.e. shouldn't happen)
432
- handle_error name, NoDefaultError, "'#{name}' has no default defined", [caller[-1]]
488
+ __doodle__.handle_error name, NoDefaultError, "'#{name}' has no default defined", [caller[-1]]
433
489
  end
434
490
  end
435
491
  end
436
492
  private :_getter
437
493
 
438
494
  # set an attribute by name - apply validation if defined
495
+ # fixme: move
439
496
  def _setter(name, *args, &block)
440
497
  ##DBG: Doodle::Debug.d { [:_setter, name, args] }
498
+ #p [:_setter, name, *args]
441
499
  ivar = "@#{name}"
442
500
  if block_given?
443
501
  args.unshift(DeferredBlock.new(block))
444
502
  end
445
- if att = lookup_attribute(name)
503
+ if att = __doodle__.lookup_attribute(name)
446
504
  ##DBG: Doodle::Debug.d { [:_setter, name, args] }
447
- #!p [:_setter, :got_att, name, *args]
505
+ #p [:_setter, :got_att1, name, ivar, *args]
448
506
  v = instance_variable_set(ivar, att.validate(self, *args))
449
- #!p [:_setter, :got_att, name, :value, v]
507
+ #p [:_setter, :got_att2, name, ivar, :value, v]
450
508
  #v = instance_variable_set(ivar, *args)
451
509
  else
452
- #!p [:_setter, :no_att, name, *args]
510
+ #p [:_setter, :no_att, name, *args]
453
511
  ##DBG: Doodle::Debug.d { [:_setter, "no attribute"] }
454
512
  v = instance_variable_set(ivar, *args)
455
513
  end
@@ -464,7 +522,7 @@ class Doodle
464
522
  if block_given?
465
523
  # set the rule for each arg given
466
524
  args.each do |arg|
467
- doodle_local_conversions[arg] = block
525
+ __doodle__.local_conversions[arg] = block
468
526
  end
469
527
  else
470
528
  convert(self, *args)
@@ -475,9 +533,9 @@ class Doodle
475
533
  def must(constraint = 'be valid', &block)
476
534
  if block.nil?
477
535
  # is this really useful? do I really want it?
478
- doodle_local_validations << Validation.new(constraint, &proc { |v| v.instance_eval(constraint) })
536
+ __doodle__.local_validations << Validation.new(constraint, &proc { |v| v.instance_eval(constraint) })
479
537
  else
480
- doodle_local_validations << Validation.new(constraint, &block)
538
+ __doodle__.local_validations << Validation.new(constraint, &block)
481
539
  end
482
540
  end
483
541
 
@@ -486,19 +544,20 @@ class Doodle
486
544
  if args.size > 0
487
545
  # todo[figure out how to handle kind being specified twice?]
488
546
  @kind = args.first
489
- doodle_local_validations << (Validation.new("be #{@kind}") { |x| x.class <= @kind })
547
+ __doodle__.local_validations << (Validation.new("be #{@kind}") { |x| x.class <= @kind })
490
548
  else
491
549
  @kind
492
550
  end
493
551
  end
494
552
 
495
553
  # convert a value according to conversion rules
554
+ # fixme: move
496
555
  def convert(owner, *args)
497
556
  #!p [:convert, 1, owner, args]
498
557
  begin
499
558
  args = args.map do |value|
500
559
  #!p [:convert, 2, value]
501
- if (converter = doodle_conversions[value.class])
560
+ if (converter = __doodle__.conversions[value.class])
502
561
  #!p [:convert, 3, value]
503
562
  value = converter[*args]
504
563
  #!p [:convert, 4, value]
@@ -507,7 +566,7 @@ class Doodle
507
566
  # try to find nearest ancestor
508
567
  ancestors = value.class.ancestors
509
568
  #!p [:convert, 6, ancestors]
510
- matches = ancestors & doodle_conversions.keys
569
+ matches = ancestors & __doodle__.conversions.keys
511
570
  #!p [:convert, 7, matches]
512
571
  indexed_matches = matches.map{ |x| ancestors.index(x)}
513
572
  #!p [:convert, 8, indexed_matches]
@@ -515,7 +574,7 @@ class Doodle
515
574
  #!p [:convert, 9]
516
575
  converter_class = ancestors[indexed_matches.min]
517
576
  #!p [:convert, 10, converter_class]
518
- if converter = doodle_conversions[converter_class]
577
+ if converter = __doodle__.conversions[converter_class]
519
578
  #!p [:convert, 11, converter]
520
579
  value = converter[*args]
521
580
  #!p [:convert, 12, value]
@@ -525,7 +584,7 @@ class Doodle
525
584
  value
526
585
  end
527
586
  rescue Exception => e
528
- owner.handle_error name, ConversionError, "#{owner.kind_of?(Class) ? owner : owner.class} - #{e.to_s}", [caller[-1]]
587
+ owner.__doodle__.handle_error name, ConversionError, "#{owner.kind_of?(Class) ? owner : owner.class} - #{e.to_s}", [caller[-1]]
529
588
  end
530
589
  if args.size > 1
531
590
  args
@@ -535,21 +594,23 @@ class Doodle
535
594
  end
536
595
 
537
596
  # validate that args meet rules defined with +must+
597
+ # fixme: move
538
598
  def validate(owner, *args)
539
599
  ##DBG: Doodle::Debug.d { [:validate, self, :owner, owner, :args, args ] }
540
600
  #!p [:validate, :before_conversion, args]
541
601
  value = convert(owner, *args)
542
602
  #!p [:validate, :after_conversion, args, :becomes, value]
543
- doodle_validations.each do |v|
603
+ __doodle__.validations.each do |v|
544
604
  ##DBG: Doodle::Debug.d { [:validate, self, v, args, value] }
545
605
  if !v.block[value]
546
- owner.handle_error name, ValidationError, "#{owner.kind_of?(Class) ? owner : owner.class}.#{ name } must #{ v.message } - got #{ value.class }(#{ value.inspect })", [caller[-1]]
606
+ owner.__doodle__.handle_error name, ValidationError, "#{owner.kind_of?(Class) ? owner : owner.class}.#{ name } must #{ v.message } - got #{ value.class }(#{ value.inspect })", [caller[-1]]
547
607
  end
548
608
  end
549
609
  value
550
610
  end
551
611
 
552
612
  # define a getter_setter
613
+ # fixme: move
553
614
  def define_getter_setter(name, *args, &block)
554
615
  # need to use string eval because passing block
555
616
  sc_eval "def #{name}(*args, &block); getter_setter(:#{name}, *args, &block); end", __FILE__, __LINE__
@@ -591,70 +652,17 @@ class Doodle
591
652
  #
592
653
  def has(*args, &block)
593
654
  #DBG: Doodle::Debug.d { [:has, self, self.class, args] }
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
-
600
- key_values, positional_args = args.partition{ |x| x.kind_of?(Hash)}
601
- if positional_args.size > 0
602
- name = positional_args.shift.to_sym
603
- params = { :name => name }
604
- else
605
- params = { }
606
- end
607
- params = key_values.inject(params){ |acc, item| acc.merge(item)}
608
- #DBG: Doodle::Debug.d { [:has, self, self.class, params] }
609
- if !params.key?(:name)
610
- handle_error name, ArgumentError, "#{self.class} must have a name"
611
- else
612
- name = params[:name].to_sym
613
- 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
621
- end
622
- end
623
655
 
624
- if collector = params.delete(:collect)
625
- # this in generic CollectorAttribute class
626
- # collector from(Hash)
627
- if collector.kind_of?(Hash)
628
- collector_name, collector_class = collector.to_a[0]
629
- else
630
- # if Capitalized word given, treat as classname
631
- # and create collector for specific class
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
638
- end
639
- #!p [:collector_klass, collector_klass, params[:init]]
640
- end
641
- params[:collector_class] = collector_class
642
- params[:collector_name] = collector_name
643
- end
644
-
656
+ params = DoodleAttribute.params_from_args(self, *args)
645
657
  # get specialized attribute class or use default
646
658
  attribute_class = params.delete(:using) || DoodleAttribute
647
659
 
648
660
  # could this be handled in DoodleAttribute?
649
661
  # define getter setter before setting up attribute
650
- define_getter_setter name, *args, &block
651
- params[:doodle_owner] = self
662
+ define_getter_setter params[:name], *args, &block
652
663
  #p [:attribute, attribute_class, params]
653
- doodle_local_attributes[name] = attribute = attribute_class.new(params, &block)
654
-
655
- attribute
664
+ __doodle__.local_attributes[params[:name]] = attribute_class.new(params, &block)
656
665
  end
657
-
658
666
 
659
667
  # define order for positional arguments
660
668
  def arg_order(*args)
@@ -662,32 +670,44 @@ class Doodle
662
670
  begin
663
671
  args = args.uniq
664
672
  args.each do |x|
665
- handle_error :arg_order, ArgumentError, "#{x} not a Symbol" if !(x.class <= Symbol)
666
- handle_error :arg_order, NameError, "#{x} not an attribute name" if !doodle_attributes.keys.include?(x)
673
+ __doodle__.handle_error :arg_order, ArgumentError, "#{x} not a Symbol", [caller[-1]] if !(x.class <= Symbol)
674
+ __doodle__.handle_error :arg_order, NameError, "#{x} not an attribute name", [caller[-1]] if !doodle_attributes.keys.include?(x)
667
675
  end
668
676
  __doodle__.arg_order = args
669
677
  rescue Exception => e
670
- handle_error :arg_order, InvalidOrderError, e.to_s, [caller[-1]]
678
+ __doodle__.handle_error :arg_order, InvalidOrderError, e.to_s, [caller[-1]]
671
679
  end
672
680
  else
673
- __doodle__.arg_order + (doodle_attributes.keys - __doodle__.arg_order)
681
+ __doodle__.arg_order + (__doodle__.attributes.keys - __doodle__.arg_order)
674
682
  end
675
683
  end
676
684
 
685
+ # fixme: move
677
686
  def get_init_values(tf = true)
678
- doodle_attributes(tf).select{|n, a| a.init_defined? }.inject({}) {|hash, (n, a)|
679
- #!p [:get_init_values, a.init]
680
- hash[n] = begin
681
- case a.init
682
- when NilClass, TrueClass, FalseClass, Fixnum
683
- a.init
684
- when DeferredBlock
687
+ __doodle__.attributes(tf).select{|n, a| a.init_defined? }.inject({}) {|hash, (n, a)|
688
+ #p [:get_init_values, a.name]
689
+ hash[n] = case a.init
690
+ when NilClass, TrueClass, FalseClass, Fixnum, Float, Bignum
691
+ # uncloneable values
692
+ #p [:get_init_values, :special, a.name, a.init]
693
+ a.init
694
+ when DeferredBlock
695
+ #p [:get_init_values, self, DeferredBlock, a.name]
696
+ begin
685
697
  instance_eval(&a.init.block)
686
- else
687
- a.init.clone
698
+ rescue Object => e
699
+ #p [:exception_in_deferred_block, e]
700
+ raise
701
+ end
702
+ else
703
+ #p [:get_init_values, :clone, a.name]
704
+ begin
705
+ a.init.clone
706
+ rescue Exception => e
707
+ warn "tried to clone #{a.init.class} in :init option"
708
+ #p [:get_init_values, :exception, a.name, e]
709
+ a.init
688
710
  end
689
- rescue Exception => e
690
- a.init
691
711
  end
692
712
  hash
693
713
  }
@@ -695,6 +715,7 @@ class Doodle
695
715
  private :get_init_values
696
716
 
697
717
  # return true if instance variable +name+ defined
718
+ # fixme: move
698
719
  def ivar_defined?(name)
699
720
  instance_variable_defined?("@#{name}")
700
721
  end
@@ -705,14 +726,14 @@ class Doodle
705
726
  def validate!(all = true)
706
727
  ##DBG: Doodle::Debug.d { [:validate!, all, caller] }
707
728
  if all
708
- clear_errors
729
+ __doodle__.errors.clear
709
730
  end
710
731
  if __doodle__.validation_on
711
732
  if self.class == Class
712
- attribs = class_attributes
733
+ attribs = __doodle__.class_attributes
713
734
  ##DBG: Doodle::Debug.d { [:validate!, "using class_attributes", class_attributes] }
714
735
  else
715
- attribs = doodle_attributes
736
+ attribs = __doodle__.attributes
716
737
  ##DBG: Doodle::Debug.d { [:validate!, "using instance_attributes", doodle_attributes] }
717
738
  end
718
739
  attribs.each do |name, att|
@@ -729,21 +750,21 @@ class Doodle
729
750
  ##DBG: Doodle::Debug.d { [:validate!, :optional, name ]}
730
751
  break
731
752
  elsif self.class != Class
732
- handle_error name, Doodle::ValidationError, "#{self} missing required attribute '#{name}'", [caller[-1]]
753
+ __doodle__.handle_error name, Doodle::ValidationError, "#{self} missing required attribute '#{name}'", [caller[-1]]
733
754
  end
734
755
  end
735
756
 
736
757
  # now apply instance level validations
737
758
 
738
759
  ##DBG: Doodle::Debug.d { [:validate!, "validations", doodle_validations ]}
739
- doodle_validations.each do |v|
760
+ __doodle__.validations.each do |v|
740
761
  ##DBG: Doodle::Debug.d { [:validate!, self, v ] }
741
762
  begin
742
763
  if !instance_eval(&v.block)
743
- handle_error self, ValidationError, "#{ self.class } must #{ v.message }", [caller[-1]]
764
+ __doodle__.handle_error self, ValidationError, "#{ self.class } must #{ v.message }", [caller[-1]]
744
765
  end
745
766
  rescue Exception => e
746
- handle_error self, ValidationError, e.to_s, [caller[-1]]
767
+ __doodle__.handle_error self, ValidationError, e.to_s, [caller[-1]]
747
768
  end
748
769
  end
749
770
  end
@@ -753,6 +774,7 @@ class Doodle
753
774
 
754
775
  # turn off validation, execute block, then set validation to same
755
776
  # state as it was before +defer_validation+ was called - can be nested
777
+ # fixme: move
756
778
  def defer_validation(&block)
757
779
  old_validation = __doodle__.validation_on
758
780
  __doodle__.validation_on = false
@@ -769,6 +791,7 @@ class Doodle
769
791
  # helper function to initialize from hash - this is safe to use
770
792
  # after initialization (validate! is called if this method is
771
793
  # called after initialization)
794
+ # fixme?
772
795
  def doodle_initialize_from_hash(*args)
773
796
  #!p [:doodle_initialize_from_hash, :args, *args]
774
797
  defer_validation do
@@ -779,11 +802,12 @@ class Doodle
779
802
  #!p [self.class, :doodle_initialize_from_hash, :key_values, key_values, :args, args]
780
803
 
781
804
  # set up initial values with ~clones~ of specified values (so not shared between instances)
782
- init_values = get_init_values
805
+ #init_values = get_init_values
783
806
  #!p [:init_values, init_values]
784
807
 
785
808
  # match up positional args with attribute names (from arg_order) using idiom to create hash from array of assocs
786
- arg_keywords = init_values.merge(Hash[*(Utils.flatten_first_level(self.class.arg_order[0...args.size].zip(args)))])
809
+ #arg_keywords = init_values.merge(Hash[*(Utils.flatten_first_level(self.class.arg_order[0...args.size].zip(args)))])
810
+ arg_keywords = Hash[*(Utils.flatten_first_level(self.class.arg_order[0...args.size].zip(args)))]
787
811
  #!p [self.class, :doodle_initialize_from_hash, :arg_keywords, arg_keywords]
788
812
 
789
813
  # merge all hash args into one
@@ -805,12 +829,20 @@ class Doodle
805
829
  # create attributes
806
830
  key_values.keys.each do |key|
807
831
  #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]]
832
+ #p [self.class, :doodle_initialize_from_hash, :setting, key, key_values[key]]
809
833
  if respond_to?(key)
810
834
  __send__(key, key_values[key])
811
835
  else
812
836
  # raise error if not defined
813
- handle_error key, Doodle::UnknownAttributeError, "unknown attribute '#{key}' #{key_values[key].inspect}"
837
+ __doodle__.handle_error key, Doodle::UnknownAttributeError, "unknown attribute '#{key}' #{key_values[key].inspect}", [caller[-1]]
838
+ end
839
+ end
840
+ # do init_values after user supplied values so init blocks can depend on user supplied values
841
+ #p [:getting_init_values, instance_variables]
842
+ init_values = get_init_values
843
+ init_values.each do |key, value|
844
+ if !key_values.key?(key) && respond_to?(key)
845
+ __send__(key, value)
814
846
  end
815
847
  end
816
848
  end
@@ -819,8 +851,9 @@ class Doodle
819
851
 
820
852
  # return containing object (set during initialization)
821
853
  # (named doodle_parent to avoid clash with ActiveSupport)
854
+ # fixme: move
822
855
  def doodle_parent
823
- __doodle__.doodle_parent
856
+ __doodle__.parent
824
857
  end
825
858
 
826
859
  # object can be initialized from a mixture of positional arguments,
@@ -831,7 +864,7 @@ class Doodle
831
864
  super
832
865
  end
833
866
  __doodle__.validation_on = true
834
- __doodle__.doodle_parent = Doodle.context[-1]
867
+ __doodle__.parent = Doodle.context[-1]
835
868
  Doodle.context.push(self)
836
869
  defer_validation do
837
870
  doodle_initialize_from_hash(*args)
@@ -871,10 +904,10 @@ class Doodle
871
904
  method_defined = begin
872
905
  method(name)
873
906
  true
874
- rescue
907
+ rescue Object
875
908
  false
876
909
  end
877
-
910
+
878
911
  if name =~ Factory::RX_IDENTIFIER && !method_defined && !klass.respond_to?(name) && !eval("respond_to?(:#{name})", TOPLEVEL_BINDING)
879
912
  eval("def #{ name }(*args, &block); ::#{name}.new(*args, &block); end", ::TOPLEVEL_BINDING, __FILE__, __LINE__)
880
913
  end
@@ -926,7 +959,65 @@ class Doodle
926
959
  # It is used to provide a context for defining #must and #from rules
927
960
  #
928
961
  class DoodleAttribute < Doodle
929
- # todo[want to design Attribute so it's extensible, e.g. to specific datatypes & built-in validations]
962
+ # note: using extend with a module causes an infinite loop in 1.9
963
+ # hence the inline
964
+ class << self
965
+ # rewrite rules for the argument list to #has
966
+ def params_from_args(owner, *args)
967
+ key_values, positional_args = args.partition{ |x| x.kind_of?(Hash)}
968
+ params = { }
969
+ if positional_args.size > 0
970
+ name = positional_args.shift
971
+ case name
972
+ # has Person --> has :person, :kind => Person
973
+ when Class
974
+ params[:name] = Utils.snake_case(name.to_s.split(/::/).last)
975
+ params[:kind] = name
976
+ else
977
+ params[:name] = name.to_s.to_sym
978
+ end
979
+ end
980
+ params = key_values.inject(params){ |acc, item| acc.merge(item)}
981
+ #DBG: Doodle::Debug.d { [:has, self, self.class, params] }
982
+ if !params.key?(:name)
983
+ __doodle__.handle_error name, ArgumentError, "#{self.class} must have a name", [caller[-1]]
984
+ else
985
+ name = params[:name].to_sym
986
+ end
987
+ __doodle__.handle_error name, ArgumentError, "#{self.class} has too many arguments", [caller[-1]] if positional_args.size > 0
988
+
989
+ if collector = params.delete(:collect)
990
+ if !params.key?(:using)
991
+ if params.key?(:key)
992
+ params[:using] = KeyedAttribute
993
+ else
994
+ params[:using] = AppendableAttribute
995
+ end
996
+ end
997
+ # this in generic CollectorAttribute class
998
+ # collector from(Hash)
999
+ if collector.kind_of?(Hash)
1000
+ collector_name, collector_class = collector.to_a[0]
1001
+ else
1002
+ # if Capitalized word given, treat as classname
1003
+ # and create collector for specific class
1004
+ collector_class = collector.to_s
1005
+ #p [:collector_klass, collector_klass]
1006
+ collector_name = Utils.snake_case(collector_class.split(/::/).last)
1007
+ #p [:collector_name, collector_name]
1008
+ if collector_class !~ /^[A-Z]/
1009
+ collector_class = nil
1010
+ end
1011
+ #!p [:collector_klass, collector_klass, params[:init]]
1012
+ end
1013
+ params[:collector_class] = collector_class
1014
+ params[:collector_name] = collector_name
1015
+ end
1016
+ params[:doodle_owner] = owner
1017
+ params
1018
+ end
1019
+ end
1020
+
930
1021
  # must define these methods before using them in #has below
931
1022
 
932
1023
  # hack: bump off +validate!+ for Attributes - maybe better way of doing
@@ -995,7 +1086,7 @@ class Doodle
995
1086
  def resolve_value(value)
996
1087
  if value.kind_of?(collector_class)
997
1088
  value
998
- elsif collector_class.doodle_conversions.key?(value.class)
1089
+ elsif collector_class.__doodle__.conversions.key?(value.class)
999
1090
  collector_class.from(value)
1000
1091
  else
1001
1092
  collector_class.new(value)
@@ -3,40 +3,6 @@ $:.unshift(File.join(File.dirname(__FILE__), '.'))
3
3
 
4
4
  require 'doodle'
5
5
 
6
- class Doodle
7
- class DataTypeHolder
8
- attr_accessor :klass
9
- def initialize(klass, &block)
10
- @klass = klass
11
- instance_eval(&block) if block_given?
12
- end
13
- def define(name, params, block, type_params, &type_block)
14
- @klass.class_eval {
15
- td = has(name, type_params.merge(params), &type_block)
16
- td.instance_eval(&block) if block
17
- td
18
- }
19
- end
20
- end
21
-
22
- # set up global datatypes
23
- def self.datatypes(*mods)
24
- mods.each do |mod|
25
- DataTypeHolder.class_eval { include mod }
26
- end
27
- end
28
-
29
- # enable global datatypes and provide an interface that allows you
30
- # to add your own datatypes to this declaration
31
- def self.doodle(*mods, &block)
32
- dh = Doodle::DataTypeHolder.new(self)
33
- mods.each do |mod|
34
- dh.extend(mod)
35
- end
36
- dh.instance_eval(&block)
37
- end
38
- end
39
-
40
6
  ### user code
41
7
  require 'date'
42
8
  require 'uri'
@@ -2,7 +2,7 @@ class Doodle #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
4
  MINOR = 1
5
- TINY = 6
5
+ TINY = 7
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe Doodle, 'has Class' do
4
+ temporary_constant :Foo, :Bar do
5
+ it "should convert 'has Bar' into 'has :bar, :kind => Bar'" do
6
+ class Bar
7
+ end
8
+ class Foo < Doodle
9
+ has Bar
10
+ end
11
+ att = Foo.doodle_attributes.values.first
12
+ att.name.should_be :bar
13
+ att.kind.should_be Bar
14
+ end
15
+ it "should allow overriding name of attribute when using 'has Bar'" do
16
+ class Bar
17
+ end
18
+ class Foo < Doodle
19
+ has Bar, :name => :baz
20
+ end
21
+ att = Foo.doodle_attributes.values.first
22
+ att.name.should_be :baz
23
+ att.kind.should_be Bar
24
+ end
25
+ it "should convert class name to snakecase when using CamelCase class constant" do
26
+ class AudioClip
27
+ end
28
+ class Foo < Doodle
29
+ has AudioClip
30
+ end
31
+ att = Foo.doodle_attributes.values.first
32
+ att.name.should_be :audio_clip
33
+ att.kind.should_be AudioClip
34
+ end
35
+ end
36
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doodle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean O'Halpin
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-05-08 00:00:00 +01:00
12
+ date: 2008-05-12 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -145,4 +145,5 @@ test_files:
145
145
  - spec/conversion_spec.rb
146
146
  - spec/class_validation_spec.rb
147
147
  - spec/doodle_spec.rb
148
+ - spec/has_spec.rb
148
149
  - spec/doodle_context_spec.rb