doodle 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog CHANGED
@@ -1,5 +1,41 @@
1
1
  = ChangeLog for doodle
2
2
 
3
+ == 0.0.9 / 2008-04-12
4
+
5
+ - new features:
6
+ - added Doodle.context and Doodle.parent (only valid during
7
+ initialization)
8
+ - use SaveBlock to distinguish between Proc and block args to :init
9
+ - :collect now creates array by default if no :init specified (thanks to
10
+ James Adam :)
11
+ - attributes defined with collect now have :init => [] if no :init
12
+ specified
13
+ - :collect can now initialize from Enumerables by default
14
+ - raise UnknownAttributeError if initialize called with unspecified attribute key
15
+ - new examples:
16
+ - mail example (with gmail smtp support)
17
+ - Doodle.parent
18
+ - datatypes
19
+ - new specs for singletons, Doodle.context and Doodle.parent, factory
20
+ - updated docs
21
+ - removed unused code and tidied up
22
+ - bug fixes:
23
+ - fixed memory leak and provided custom to_yaml and marshal_dump/load
24
+ - fixed regex for factory methods
25
+ - fixed bug where validate! was creating instance variables with defaults
26
+ - fixed errors collection
27
+ - fixed yaml test for JRuby
28
+ - don't define factory method if one by same name already defined
29
+
30
+ == 0.0.8 / 2008-03-25
31
+
32
+ - renamed rake task upload_gem to publish_gem
33
+ - bumped version and updated todo list
34
+ - don't define factory method if one by same name already defined
35
+ - apply validation! to raw instance variables - also, raise Doodle::ValidationError rather than ArgumentError if missing required values
36
+ - tweaked bugs_spec
37
+ - added spec to cover applying conversions to yaml loaded data
38
+
3
39
  == 0.0.7 / 2008-03-22
4
40
 
5
41
  - return self from validate! (so you can use idiom foo = YAML::load(yaml).validate!)
@@ -1,3 +1,4 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
1
2
  require 'rubygems'
2
3
  require 'date'
3
4
  require 'doodle'
@@ -25,11 +26,15 @@ class Event
25
26
  Date.parse(s)
26
27
  end
27
28
  end
28
- has :locations, :init => [], :collect => {:place => "Location"}
29
+ has :locations, :init => [], :collect => {:place => "Location"} do
30
+ from Array do |array|
31
+ array.map{|x| Location(x)}
32
+ end
33
+ end
29
34
  end
30
35
 
31
36
  event = Event "Festival" do
32
- date '2008-04-01'
37
+ date '2018-04-01'
33
38
  place "The muddy field"
34
39
  place "Beer tent" do
35
40
  event "Drinking"
@@ -46,7 +51,46 @@ str =<<EOS
46
51
  name: Glastonbury
47
52
  date: 2000-07-01
48
53
  EOS
49
- event = YAML::load(str).validate! # will raise Doodle::ValidationError
54
+
55
+ def capture(&block)
56
+ begin
57
+ block.call
58
+ rescue Exception => e
59
+ e
60
+ end
61
+ end
62
+
63
+ res = capture {
64
+ event = YAML::load(str).validate! # will raise Doodle::ValidationError
65
+ }
66
+ pp res
67
+
68
+ hash_data = {
69
+ :name => "Festival",
70
+ :date => '2010-04-01',
71
+ :locations =>
72
+ [
73
+ {
74
+ :events => [],
75
+ :name => "The muddy field",
76
+ },
77
+ {
78
+ :name => "Beer tent",
79
+ :events =>
80
+ [
81
+ {
82
+ :name => "Drinking",
83
+ :locations => [],
84
+ }
85
+ ]
86
+ }
87
+ ]
88
+ }
89
+
90
+ pp hash_data
91
+
92
+ e = Event(hash_data)
93
+ pp e
50
94
 
51
95
  __END__
52
96
  --- !ruby/object:Event
@@ -1,30 +1,39 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'rubygems'
1
3
  require 'date'
4
+ require 'pp'
2
5
  require 'doodle'
3
6
 
4
- class Event < Doodle::Base
5
- has :start_date, :kind => Date do
6
- from String do |value|
7
- Date.parse(value)
7
+ class Location < Doodle::Base
8
+ has :name, :kind => String
9
+ has :events, :init => [], :collect => :Event
10
+ end
11
+
12
+ class Event
13
+ # or if you want to inherit from another class
14
+ include Doodle::Helper
15
+ include Doodle::Factory
16
+
17
+ has :name, :kind => String
18
+ has :date do
19
+ kind Date
20
+ default { Date.today }
21
+ must 'be >= today' do |value|
22
+ value >= Date.today
8
23
  end
9
- end
10
- has :end_date, :kind => Date do
11
- from String do |value|
12
- Date.parse(value)
24
+ from String do |s|
25
+ Date.parse(s)
13
26
  end
14
27
  end
15
- from String do |value|
16
- args = value.split(' to ')
17
- new(*args)
18
- end
28
+ has :locations, :init => [], :collect => {:place => "Location"}
19
29
  end
20
- event = Event.from '2008-03-05 to 2008-03-06'
21
- event.start_date.to_s # => "2008-03-05"
22
- event.end_date.to_s # => "2008-03-06"
23
- event.start_date = '2001-01-01'
24
- event.start_date # =>
25
- event.start_date.to_s # =>
26
30
 
27
- class Date
28
- include Doodle::Factory
31
+ event = Event "Festival" do
32
+ date '2008-04-01'
33
+ place "The muddy field"
34
+ place "Beer tent" do
35
+ event "Drinking"
36
+ end
29
37
  end
30
- date = Date(2008, 03, 01) # =>
38
+
39
+ pp event
@@ -1,3 +1,4 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
1
2
  require 'date'
2
3
  require 'doodle'
3
4
 
@@ -1,3 +1,4 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
1
2
  require 'date'
2
3
  require 'doodle'
3
4
 
@@ -11,6 +12,6 @@ class DateRange < Doodle::Base
11
12
  end
12
13
 
13
14
  dr = DateRange.new
14
- dr.start_date # => #<Date: 4909053/2,0,2299161>
15
- dr.end_date # => #<Date: 4909053/2,0,2299161>
15
+ dr.start_date # => #<Date: 4909137/2,0,2299161>
16
+ dr.end_date # => #<Date: 4909137/2,0,2299161>
16
17
 
@@ -1,3 +1,4 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
1
2
  require 'date'
2
3
  require 'doodle'
3
4
 
@@ -1,3 +1,4 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
1
2
  require 'date'
2
3
  require 'doodle'
3
4
 
@@ -59,4 +60,4 @@ dr.end_date # => #<Date: 4908931/2,0,2299161>
59
60
  dr = DateRange '2008-01-01', '2007-12-31'
60
61
  dr.start_date # =>
61
62
  dr.end_date # =>
62
- # ~> -:59: #<DateRange:0x747fc @start_date=#<Date: 4908933/2,0,2299161>, @end_date=#<Date: 4908931/2,0,2299161>> must have end_date >= start_date (Doodle::ValidationError)
63
+ # ~> -:60: #<DateRange:0xb7a52640 @end_date=#<Date: 4908931/2,0,2299161>, @start_date=#<Date: 4908933/2,0,2299161>> must have end_date >= start_date (Doodle::ValidationError)
@@ -0,0 +1,45 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'doodle'
3
+
4
+ # collect works for anything that provides a :<< method
5
+
6
+ class Text < Doodle::Base
7
+ has :text, :init => "", :collect => :part
8
+ end
9
+
10
+ text = Text do
11
+ part "Hello"
12
+ part " "
13
+ part "World"
14
+ end
15
+
16
+ puts text.text
17
+
18
+ class Lines < Doodle::Base
19
+ has :lines, :init => [], :collect => :line
20
+ end
21
+
22
+ lines = Lines do
23
+ line "Hello"
24
+ line "World"
25
+ end
26
+
27
+ puts lines.lines.join("\n")
28
+
29
+ require 'set'
30
+
31
+ class Thing < Doodle::Base
32
+ has :properties, :init => Set.new, :collect => :prop
33
+ end
34
+
35
+ # this is a contrived example
36
+ thing = Thing do
37
+ prop 'name'
38
+ prop 'size'
39
+ prop 'size'
40
+ prop 'name'
41
+ end
42
+
43
+ thing.prop 'name'
44
+ p thing
45
+
@@ -0,0 +1,55 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'doodle'
3
+
4
+ # collect works for anything that provides a :<< method
5
+
6
+ class Text < Doodle::Base
7
+ has :text, :init => "", :collect => :part
8
+ end
9
+
10
+ text = Text do
11
+ part "Hello"
12
+ part " "
13
+ part "World"
14
+ end
15
+
16
+ puts text.text
17
+
18
+ class Lines < Doodle::Base
19
+ has :lines, :init => [], :collect => :line
20
+ end
21
+
22
+ lines = Lines do
23
+ line "Hello"
24
+ line "World"
25
+ end
26
+
27
+ puts lines.lines.join("\n")
28
+
29
+ require 'set'
30
+
31
+ class Thing < Doodle::Base
32
+ has :properties, :init => Set.new, :collect => :prop
33
+ end
34
+
35
+ # this is a contrived example
36
+ thing = Thing do
37
+ prop 'name'
38
+ prop 'size'
39
+ prop 'size'
40
+ prop 'name'
41
+ end
42
+
43
+ thing.prop 'name'
44
+ p thing
45
+
46
+ # >> [:collector_klass, nil]
47
+ # >> [:collector_klass, nil]
48
+ # >> [:collector_klass, nil]
49
+ # >> [:collector_klass, nil]
50
+ # >> Hello World
51
+ # >> [:collector_klass, nil]
52
+ # >> Hello
53
+ # >> World
54
+ # >> [:collector_klass, nil]
55
+ # >> #<Thing:0xb7b6a6b8 @properties=#<Set: {"name", "size"}>>
@@ -2,6 +2,8 @@
2
2
  # Copyright (C) 2007 by Sean O'Halpin, 2007-11-24
3
3
 
4
4
  require 'molic_orderedhash' # todo[replace this with own (required functions only) version]
5
+ require 'pp'
6
+ #require 'bleak_house' if ENV['BLEAK_HOUSE']
5
7
 
6
8
  # *doodle* is my attempt at an eco-friendly metaprogramming framework that does not
7
9
  # have pollute core Ruby objects such as Object, Class and Module.
@@ -12,7 +14,33 @@ require 'molic_orderedhash' # todo[replace this with own (required functions on
12
14
 
13
15
  # Docs at http://doodle.rubyforge.org
14
16
 
17
+ # patch 1.8.5 to add instance_variable_defined?
18
+ # note: this does not seem to work so well with singletons
19
+ if RUBY_VERSION < '1.8.6'
20
+ if !Object.method_defined?(:instance_variable_defined?)
21
+ class Object
22
+ __doodle__inspect = instance_method(:inspect)
23
+ define_method :instance_variable_defined? do |name|
24
+ res = __doodle__inspect.bind(self).call
25
+ rx = /\B#{name}=/
26
+ rx =~ res ? true : false
27
+ end
28
+ end
29
+ end
30
+ end
31
+
15
32
  module Doodle
33
+ VERSION = '0.0.9'
34
+ # where are we?
35
+ class << self
36
+ def context
37
+ Thread.current[:doodle_context] ||= []
38
+ end
39
+ def parent
40
+ context[-2]
41
+ end
42
+ end
43
+
16
44
  module Debug
17
45
  class << self
18
46
  # output result of block if ENV['DEBUG_DOODLE'] set
@@ -34,12 +62,24 @@ module Doodle
34
62
  end
35
63
  end
36
64
 
65
+ # error handling
66
+ @@raise_exception_on_error = true
67
+ def self.raise_exception_on_error
68
+ @@raise_exception_on_error
69
+ end
70
+ def self.raise_exception_on_error=(tf)
71
+ @@raise_exception_on_error = tf
72
+ end
73
+
37
74
  # internal error raised when a default was expected but not found
38
75
  class NoDefaultError < Exception
39
76
  end
40
77
  # raised when a validation rule returns false
41
78
  class ValidationError < Exception
42
79
  end
80
+ # raised when a validation rule returns false
81
+ class UnknownAttributeError < Exception
82
+ end
43
83
  # raised when a conversion fails
44
84
  class ConversionError < Exception
45
85
  end
@@ -59,39 +99,12 @@ module Doodle
59
99
  sc
60
100
  end
61
101
 
62
- # return self if a Module, else the singleton class
63
- def self_class
64
- self.kind_of?(Module) ? self : singleton_class
65
- end
66
-
67
- # frankly a hack to allow init options to work for singleton classes
68
- def class_init(params = {}, &block)
69
- sc = singleton_class(&block)
70
- sc.attributes.select{|n, a| a.init_defined? }.each do |n, a|
71
- send(n, a.init)
72
- end
73
- sc
74
- end
75
102
  end
76
103
 
77
104
  # provide an alternative inheritance chain that works for singleton
78
105
  # classes as well as modules, classes and instances
79
106
  module Inherited
80
107
 
81
- # def supers
82
- # supers = []
83
- # s = superclass rescue nil
84
- # while !s.nil?
85
- # supers << s
86
- # last_s = s.superclass rescue nil
87
- # if last_s == s
88
- # last_s = nil
89
- # end
90
- # s = last_s
91
- # end
92
- # supers
93
- # end
94
-
95
108
  # parents returns the set of parent classes of an object.
96
109
  # note[this is horribly complicated and kludgy - is there a better way?
97
110
  # could do with refactoring]
@@ -102,13 +115,7 @@ module Doodle
102
115
  klasses = []
103
116
  if defined?(superclass)
104
117
  klass = superclass
105
- #p [:klass_superclass, klass]
106
- if self == superclass
107
- # d { [:parents, 'self == superclass'] }
108
- klass = nil
109
- else
110
- #p [:klass_singleton_class, klass]
111
- #p [:parents, 'klass = superclass', self, klass, self.ancestors]
118
+ if self != superclass
112
119
  #
113
120
  # fixme[any other way to do this? seems really clunky to have to hack strings]
114
121
  #
@@ -118,38 +125,21 @@ module Doodle
118
125
  if cap = self.to_s.match(regex)
119
126
  if cap.captures.size > 0
120
127
  k = const_get(cap[1])
128
+ # push onto front of array
121
129
  if k.respond_to?(:superclass) && k.superclass.respond_to?(:singleton_class)
122
130
  klasses.unshift k.superclass.singleton_class
123
131
  end
124
132
  end
125
- #p [:klass_self_klass, klass]
126
- #p [:klasses, klasses]
127
- loop do
128
- if klass.nil?
129
- break
130
- end
133
+ until klass.nil?
131
134
  klasses.unshift klass
132
- #p [:loop_klasses, klasses]
133
135
  if klass == klass.superclass
134
- #p [:HERE_HERE_BEFORE, klasses]
135
- #break
136
136
  return klasses # oof
137
137
  end
138
138
  klass = klass.superclass
139
139
  end
140
- #p [:HERE_HERE, klasses]
141
140
  else
142
- #p [:klass_self_klass, klass]
143
- #p [:klasses, klasses]
144
- loop do
145
- if klass.nil?
146
- break
147
- end
141
+ until klass.nil?
148
142
  klasses << klass
149
- #p [:loop_klasses, klasses]
150
- if klass == klass.superclass
151
- break
152
- end
153
143
  klass = klass.superclass
154
144
  end
155
145
  end
@@ -157,21 +147,11 @@ module Doodle
157
147
  end
158
148
  else
159
149
  klass = self.class
160
- #p [:klass_self_klass, klass]
161
- #p [:klasses, klasses]
162
- loop do
163
- if klass.nil?
164
- break
165
- end
150
+ until klass.nil?
166
151
  klasses << klass
167
- #p [:loop_klasses, klasses]
168
- if klass == klass.superclass
169
- break
170
- end
171
152
  klass = klass.superclass
172
153
  end
173
154
  end
174
- #p [:HERE_HERE_END, klasses]
175
155
  klasses
176
156
  end
177
157
 
@@ -181,7 +161,6 @@ module Doodle
181
161
  klasses = parents
182
162
  #p [:parents, parents]
183
163
  # d { [:collect_inherited, :parents, message, klasses] }
184
- #klasses = self_class.ancestors # this produces quite different behaviour
185
164
  klasses.each do |klass|
186
165
  #p [:testing, klass]
187
166
  if klass.respond_to?(message)
@@ -202,7 +181,7 @@ module Doodle
202
181
  #
203
182
  # this works down to third level <tt>class << self</tt> - in practice, this is
204
183
  # perfectly good - it would be great to have a completely general
205
- # solution but I'm doubt whether the payoff is worth the time
184
+ # solution but I'm doubt whether the payoff is worth the effort
206
185
 
207
186
  module Embrace
208
187
  # fake module inheritance chain
@@ -219,8 +198,8 @@ module Doodle
219
198
  # ensure that subclasses are also embraced
220
199
  define_method :inherited do |klass|
221
200
  #p [:embrace, :inherited, klass]
222
- klass.send(:embrace, other)
223
- klass.send(:include, Factory) # yikes!
201
+ klass.send(:embrace, other) # n.b. closure
202
+ klass.send(:include, Factory) # is there another way to do this? i.e. not in embrace
224
203
  super(klass) if defined?(super)
225
204
  end
226
205
  }
@@ -228,14 +207,14 @@ module Doodle
228
207
  end
229
208
  end
230
209
 
231
- # Lazy is a Proc that caches the result of a call
232
- class Lazy < Proc
233
- # return the result of +call+ing this Proc - cached after first +call+
234
- def value
235
- @value ||= call
210
+ class SaveBlock
211
+ attr_accessor :block
212
+ def initialize(arg_block = nil, &block)
213
+ arg_block = block if block_given?
214
+ @block = arg_block
236
215
  end
237
216
  end
238
-
217
+
239
218
  # A Validation represents a validation rule applied to the instance
240
219
  # after initialization. Generated using the Doodle::BaseMethods#must directive.
241
220
  class Validation
@@ -251,22 +230,13 @@ module Doodle
251
230
  end
252
231
 
253
232
  class DoodleInfo
254
- DOODLES = {}
255
- @@raise_exception_on_error = true
256
233
  attr_accessor :local_attributes
257
234
  attr_accessor :local_validations
258
235
  attr_accessor :local_conversions
259
236
  attr_accessor :validation_on
260
237
  attr_accessor :arg_order
261
238
  attr_accessor :errors
262
-
263
- def self.raise_exception_on_error
264
- @@raise_exception_on_error
265
- end
266
- def self.raise_exception_on_error=(tf)
267
- @@raise_exception_on_error = tf
268
- end
269
-
239
+
270
240
  def initialize(object)
271
241
  @local_attributes = OrderedHash.new
272
242
  @local_validations = []
@@ -274,13 +244,6 @@ module Doodle
274
244
  @local_conversions = {}
275
245
  @arg_order = []
276
246
  @errors = []
277
-
278
- oid = object.object_id
279
- ObjectSpace.define_finalizer(object) do
280
- # this seems to be called only on exit
281
- Doodle::Debug.d { "finalizing #{oid}" }
282
- DOODLES.delete(oid)
283
- end
284
247
  end
285
248
  end
286
249
 
@@ -290,36 +253,53 @@ module Doodle
290
253
  include SelfClass
291
254
  include Inherited
292
255
 
293
- # this is the only way to get at internal values
294
- # FIXME: this is going to leak memory (I think)
295
-
256
+ # this is the only way to get at internal values. Note: this is
257
+ # initialized on the fly rather than in #initialize because
258
+ # classes and singletons don't call #initialize
296
259
  def __doodle__
297
- DoodleInfo::DOODLES[object_id] ||= DoodleInfo.new(self)
260
+ @__doodle__ ||= DoodleInfo.new(self)
298
261
  end
299
262
  private :__doodle__
300
-
301
- # handle errors either by collecting in :errors or raising an exception
302
- def handle_error(name, *args)
303
- __doodle__.errors << [name, *args]
304
- if DoodleInfo.raise_exception_on_error
305
- raise(*args)
263
+
264
+ # hack to fool yaml (and anything else that queries instance_variables)
265
+ # pick a name that no-one else is likely to use
266
+ alias :seoh_doodle_instance_variables_8b016735_bd60_44d9_bda5_5f9d0aade5a6 :instance_variables
267
+ # redefine instance_variables to ignore our private @__doodle__ variable
268
+ def instance_variables
269
+ seoh_doodle_instance_variables_8b016735_bd60_44d9_bda5_5f9d0aade5a6.reject{ |x| x == '@__doodle__'}
270
+ end
271
+
272
+ def marshal_dump
273
+ instance_variables.map{|x| [x, instance_variable_get(x)] }
274
+ end
275
+ def marshal_load(data)
276
+ data.each do |name, value|
277
+ instance_variable_set(name, value)
306
278
  end
307
279
  end
308
280
 
309
- # get list of errors
281
+ # where should I put this?
310
282
  def errors
311
- errs = attributes.inject([]) do |e, (n, a)|
312
- e.push(*a.send(:__doodle__).errors)
313
- e
314
- end
315
- errs.push(__doodle__.errors).reject{|x| x.size == 0}
283
+ __doodle__.errors
316
284
  end
285
+
317
286
  def clear_errors
318
- attributes.each do |n, a|
319
- a.send(:__doodle__).errors.clear
320
- end
287
+ #pp [:clear_errors, self, caller]
321
288
  __doodle__.errors.clear
322
289
  end
290
+
291
+ # handle errors either by collecting in :errors or raising an exception
292
+ def handle_error(name, *args)
293
+ #pp [:caller, self, caller]
294
+ #pp [:handle_error, name, args]
295
+ # don't include duplicates (FIXME: hacky - shouldn't have duplicates in the first place)
296
+ if !self.errors.include?([name, *args])
297
+ self.errors << [name, *args]
298
+ end
299
+ if Doodle.raise_exception_on_error
300
+ raise(*args)
301
+ end
302
+ end
323
303
 
324
304
  # return attributes defined in instance
325
305
  def local_attributes
@@ -413,13 +393,7 @@ module Doodle
413
393
  ivar = "@#{name}"
414
394
  if instance_variable_defined?(ivar)
415
395
  ## d { [:_getter, 2, name, block] }
416
- v = instance_variable_get(ivar)
417
- #d { [:_getter, :defined, name, v] }
418
- # if v.kind_of?(Lazy)
419
- # p [name, self, self.class, v]
420
- # v = instance_eval &v.block
421
- # end
422
- v
396
+ instance_variable_get(ivar)
423
397
  else
424
398
  # handle default
425
399
  # Note: use :init => value to cover cases where defaults don't work
@@ -427,14 +401,17 @@ module Doodle
427
401
  att = lookup_attribute(name)
428
402
  #d { [:getter, name, att, block] }
429
403
  if att.default_defined?
430
- if att.default.kind_of?(Proc)
404
+ case att.default
405
+ when SaveBlock
406
+ instance_eval(&att.default.block)
407
+ when Proc
431
408
  instance_eval(&att.default)
432
409
  else
433
410
  att.default
434
411
  end
435
412
  else
436
413
  # This is an internal error (i.e. shouldn't happen)
437
- handle_error name, NoDefaultError, "Internal error - '#{name}' has no default defined", [caller[-1]]
414
+ handle_error name, NoDefaultError, "Error - '#{name}' has no default defined", [caller[-1]]
438
415
  end
439
416
  end
440
417
  end
@@ -442,15 +419,18 @@ module Doodle
442
419
 
443
420
  # set an attribute by name - apply validation if defined
444
421
  def _setter(name, *args, &block)
445
- # d { [:_setter, self, self.class, name, args, block] }
422
+ #pp [:_setter, self, self.class, name, args, block]
446
423
  ivar = "@#{name}"
447
- args.unshift(block) if block_given?
424
+ if block_given?
425
+ args.unshift(SaveBlock.new(block))
426
+ #p [:instance_eval_block, self, block]
427
+ end
448
428
  # d { [:_setter, 3, :setting, name, ivar, args] }
449
429
  att = lookup_attribute(name)
450
430
  # d { [:_setter, 4, :setting, name, att] }
451
431
  if att
452
432
  #d { [:_setter, :instance_variable_set, :ivar, ivar, :args, args, :att_validate, att.validate(*args) ] }
453
- v = instance_variable_set(ivar, att.validate(*args))
433
+ v = instance_variable_set(ivar, att.validate(self, *args))
454
434
  else
455
435
  #d { [:_setter, :instance_variable_set, ivar, args ] }
456
436
  v = instance_variable_set(ivar, *args)
@@ -471,7 +451,7 @@ module Doodle
471
451
  end
472
452
  # d { [:from, conversions] }
473
453
  else
474
- convert(*args)
454
+ convert(self, *args)
475
455
  end
476
456
  end
477
457
 
@@ -493,10 +473,11 @@ module Doodle
493
473
  end
494
474
 
495
475
  # convert a value according to conversion rules
496
- def convert(value)
476
+ def convert(owner, *args)
497
477
  begin
478
+ value = args.first
498
479
  if (converter = conversions[value.class])
499
- value = converter[value]
480
+ value = converter[*args]
500
481
  else
501
482
  # try to find nearest ancestor
502
483
  ancestors = value.class.ancestors
@@ -507,24 +488,25 @@ module Doodle
507
488
  converter_class = ancestors[indexed_matches.min]
508
489
  #p [:converter, converter_class]
509
490
  if converter = conversions[converter_class]
510
- value = converter[value]
491
+ value = converter[*args]
511
492
  end
512
493
  end
513
494
  end
514
495
  rescue => e
515
- handle_error name, ConversionError, e.to_s, [caller[-1]]
496
+ owner.handle_error name, ConversionError, e.to_s, [caller[-1]]
516
497
  end
517
498
  value
518
499
  end
519
500
 
520
501
  # validate that args meet rules defined with +must+
521
- def validate(*args)
522
- value = convert(*args)
502
+ def validate(owner, *args)
503
+ Doodle::Debug.d { [:validate, self, :owner, owner, :args, args ] }
504
+ value = convert(owner, *args)
523
505
  #d { [:validate, self, :args, args, :value, value ] }
524
506
  validations.each do |v|
525
- Doodle::Debug.d { [:validate, self, v, args ] }
507
+ Doodle::Debug.d { [:validate, self, v, args, value] }
526
508
  if !v.block[value]
527
- handle_error name, ValidationError, "#{ name } must #{ v.message } - got #{ value.class }(#{ value.inspect })", [caller[-1]]
509
+ owner.handle_error name, ValidationError, "#{ name } must #{ v.message } - got #{ value.class }(#{ value.inspect })", [caller[-1]]
528
510
  end
529
511
  end
530
512
  #d { [:validate, :value, value ] }
@@ -533,11 +515,21 @@ module Doodle
533
515
 
534
516
  # define a getter_setter
535
517
  def define_getter_setter(name, *args, &block)
536
- # d { [:define_getter_setter, [self, self.class, self_class], name, args, block] }
518
+ # d { [:define_getter_setter, [self, self.class], name, args, block] }
537
519
 
538
520
  # need to use string eval because passing block
539
- module_eval "def #{name}(*args, &block); getter_setter(:#{name}, *args, &block); end"
540
- module_eval "def #{name}=(*args, &block); _setter(:#{name}, *args); end"
521
+ module_eval "def #{name}(*args, &block); getter_setter(:#{name}, *args, &block); end", __FILE__, __LINE__
522
+ module_eval "def #{name}=(*args, &block); _setter(:#{name}, *args); end", __FILE__, __LINE__
523
+
524
+ # this is how it should be done (in 1.9)
525
+ # module_eval {
526
+ # define_method name do |*args, &block|
527
+ # getter_setter(name.to_sym, *args, &block)
528
+ # end
529
+ # define_method "#{name}=" do |*args, &block|
530
+ # _setter(name.to_sym, *args, &block)
531
+ # end
532
+ # }
541
533
  end
542
534
  private :define_getter_setter
543
535
 
@@ -546,9 +538,9 @@ module Doodle
546
538
  def define_collector(collection, name, klass = nil, &block)
547
539
  # need to use string eval because passing block
548
540
  if klass.nil?
549
- module_eval "def #{name}(*args, &block); args.unshift(block) if block_given?; #{collection}.<<(*args); end"
541
+ module_eval "def #{name}(*args, &block); args.unshift(block) if block_given?; #{collection}.<<(*args); end", __FILE__, __LINE__
550
542
  else
551
- module_eval "def #{name}(*args, &block); #{collection} << #{klass}.new(*args, &block); end"
543
+ module_eval "def #{name}(*args, &block); #{collection} << #{klass}.new(*args, &block); end", __FILE__, __LINE__
552
544
  end
553
545
  end
554
546
  private :define_collector
@@ -576,7 +568,7 @@ module Doodle
576
568
  # end
577
569
  #
578
570
  def has(*args, &block)
579
- Doodle::Debug.d { [:has, self, self.class, self_class, args] }
571
+ Doodle::Debug.d { [:has, self, self.class, args] }
580
572
  name = args.shift.to_sym
581
573
  # d { [:has2, name, args] }
582
574
  key_values, positional_args = args.partition{ |x| x.kind_of?(Hash)}
@@ -586,24 +578,42 @@ module Doodle
586
578
  params = key_values.inject(params){ |acc, item| acc.merge(item)}
587
579
 
588
580
  # don't pass collector params through to Attribute
581
+ collector_klass = nil
589
582
  if collector = params.delete(:collect)
583
+ if !params.key?(:init)
584
+ params[:init] = []
585
+ end
590
586
  if collector.kind_of?(Hash)
591
- collector_name, klass = collector.to_a[0]
587
+ collector_name, collector_klass = collector.to_a[0]
592
588
  else
593
589
  # if Capitalized word given, treat as classname
594
590
  # and create collector for specific class
595
- klass = collector.to_s
596
- collector_name = Utils.snake_case(klass)
597
- if klass !~ /^[A-Z]/
598
- klass = nil
591
+ collector_klass = collector.to_s
592
+ collector_name = Utils.snake_case(collector_klass)
593
+ if collector_klass !~ /^[A-Z]/
594
+ collector_klass = nil
599
595
  end
600
596
  end
601
- define_collector name, collector_name, klass
597
+ define_collector name, collector_name, collector_klass
602
598
  end
603
599
 
604
600
  # d { [:has_args, :params, params] }
605
- local_attributes[name] = attribute = Attribute.new(params, &block)
601
+ # define getter setter before setting up attribute
606
602
  define_getter_setter name, *args, &block
603
+ local_attributes[name] = attribute = Attribute.new(params, &block)
604
+ # if a collector has been defined and has a specific class, then you can pass in an array of hashes
605
+ if collector_klass
606
+ attribute.instance_eval {
607
+ from Enumerable do |enum|
608
+ if !collector_klass.kind_of?(Class)
609
+ tmp_klass = self.class.const_get(collector_klass)
610
+ else
611
+ tmp_klass = collector_klass
612
+ end
613
+ enum.map{|x| tmp_klass.new(x)}
614
+ end
615
+ }
616
+ end
607
617
  attribute
608
618
  end
609
619
 
@@ -647,11 +657,17 @@ module Doodle
647
657
  hash[n] = begin
648
658
  case a.init
649
659
  when NilClass, TrueClass, FalseClass, Fixnum
660
+ #p [:init, :no_clone]
650
661
  a.init
662
+ when SaveBlock
663
+ #p [:init, :save_block]
664
+ instance_eval(&a.init.block)
651
665
  else
666
+ #p [:init, :clone]
652
667
  a.init.clone
653
668
  end
654
- rescue
669
+ rescue Exception => e
670
+ #p [:init, :rescue, e]
655
671
  a.init
656
672
  end
657
673
  ; hash }
@@ -672,7 +688,8 @@ module Doodle
672
688
  if respond_to?(key)
673
689
  send(key, key_values[key])
674
690
  else
675
- _setter(key, key_values[key])
691
+ # raise error if not defined
692
+ handle_error key, Doodle::UnknownAttributeError, "Unknown attribute '#{key}' #{key_values[key].inspect}"
676
693
  end
677
694
  end
678
695
  end
@@ -688,6 +705,10 @@ module Doodle
688
705
  # validate this object by applying all validations in sequence
689
706
  # - if all == true, validate all attributes, e.g. when loaded from YAML, else validate at object level only
690
707
  def validate!(all = true)
708
+ #pp [:validate!, all, caller]
709
+ if all
710
+ clear_errors
711
+ end
691
712
  #Doodle::Debug.d { [:validate!, self] }
692
713
  #Doodle::Debug.d { [:validate!, self, __doodle__.validation_on] }
693
714
  if __doodle__.validation_on
@@ -700,13 +721,14 @@ module Doodle
700
721
  end
701
722
  # if all == true, reset values so conversions and validations are applied to raw instance variables
702
723
  # e.g. when loaded from YAML
703
- if all
704
- send(att.name, instance_variable_get("@#{att.name}"))
724
+ att_name = "@#{att.name}"
725
+ if all && instance_variable_defined?(att_name)
726
+ send(att.name, instance_variable_get(att_name))
705
727
  end
706
728
  end
707
729
  # now apply instance level validations
708
730
  validations.each do |v|
709
- #Doodle::Debug.d { [:validate!, self, v ] }
731
+ Doodle::Debug.d { [:validate!, self, v ] }
710
732
  if !instance_eval(&v.block)
711
733
  handle_error :validate!, ValidationError, "#{ self.inspect } must #{ v.message }", [caller[-1]]
712
734
  end
@@ -735,13 +757,16 @@ module Doodle
735
757
  # hash of keyword value pairs and a block which is instance_eval'd
736
758
  def initialize(*args, &block)
737
759
  __doodle__.validation_on = true
738
-
760
+ #p [:Doodle_context_push, self]
761
+ Doodle.context.push(self)
739
762
  defer_validation do
740
763
  # d { [:initialize, self.to_s, args, block] }
741
764
  initialize_from_hash(*args)
742
765
  # d { [:initialize, self.to_s, args, block, :calling_block] }
743
766
  instance_eval(&block) if block_given?
744
767
  end
768
+ Doodle.context.pop
769
+ #p [:Doodle_context_pop, ]
745
770
  end
746
771
 
747
772
  end
@@ -762,38 +787,40 @@ module Doodle
762
787
  # stimpy = Dog(:name => 'Stimpy')
763
788
  # etc.
764
789
  module Factory
765
- # create a factory function called +name+ for the current class
766
- def factory(name = self)
767
- name = self.to_s
768
- names = name.split(/::/)
769
- name = names.pop
770
- if names.empty?
771
- # top level class - should be available to all
772
- mklass = klass = Object
773
- #p [:names_empty, klass, mklass]
774
- eval src = "def #{ name }(*args, &block); ::#{name}.new(*args, &block); end", ::TOPLEVEL_BINDING
775
- else
776
- klass = names.inject(self) {|c, n| c.const_get(n)}
777
- mklass = class << klass; self; end
778
- #p [:names, klass, mklass]
779
- #eval src = "def #{ names.join('::') }::#{name}(*args, &block); #{ names.join('::') }::#{name}.new(*args, &block); end"
780
- # TODO: check how many times this is being called
781
- if !klass.respond_to?(name)
782
- klass.class_eval(src = "def self.#{name}(*args, &block); #{name}.new(*args, &block); end")
790
+ RX_IDENTIFIER = /^[A-Za-z_][A-Za-z_0-9]+\??$/
791
+ class << self
792
+ # create a factory function in appropriate module for the specified class
793
+ def factory(konst)
794
+ #p [:factory, name]
795
+ name = konst.to_s
796
+ names = name.split(/::/)
797
+ name = names.pop
798
+ if names.empty?
799
+ # top level class - should be available to all
800
+ klass = Object
801
+ #p [:names_empty, klass, mklass]
802
+ if !klass.respond_to?(name) && name =~ Factory::RX_IDENTIFIER
803
+ eval("def #{ name }(*args, &block); ::#{name}.new(*args, &block); end", ::TOPLEVEL_BINDING, __FILE__, __LINE__)
804
+ end
805
+ else
806
+ klass = names.inject(self) {|c, n| c.const_get(n)}
807
+ mklass = class << klass; self; end
808
+ #p [:names, klass, mklass]
809
+ # TODO: check how many times this is being called
810
+ if !klass.respond_to?(name) && name =~ Factory::RX_IDENTIFIER
811
+ klass.class_eval("def self.#{name}(*args, &block); #{name}.new(*args, &block); end", __FILE__, __LINE__)
812
+ end
783
813
  end
814
+ #p [:factory, mklass, klass, src]
815
+ end
816
+
817
+ # inherit the factory function capability
818
+ def included(other)
819
+ #p [:factory, :included, self, other ]
820
+ super
821
+ # make +factory+ method available
822
+ factory other
784
823
  end
785
- #p [:factory, mklass, klass, src]
786
- end
787
- # inherit the factory function capability
788
- def self.included(other)
789
- #p [:factory, :included, self, other ]
790
- super
791
- #raise Exception, "#{self} can only be included in a Class" if !other.kind_of? Class
792
- # make +factory+ method available
793
- other.extend self
794
- other.module_eval {
795
- factory
796
- }
797
824
  end
798
825
  end
799
826
 
@@ -807,6 +834,7 @@ module Doodle
807
834
  other.module_eval {
808
835
  extend Embrace
809
836
  embrace BaseMethods
837
+ include Factory
810
838
  }
811
839
  end
812
840
  end
@@ -834,13 +862,13 @@ module Doodle
834
862
 
835
863
  # is this attribute optional? true if it has a default defined for it
836
864
  def optional?
837
- !self.required?
865
+ default_defined? or init_defined?
838
866
  end
839
867
 
840
868
  # an attribute is required if it has no default or initial value defined for it
841
869
  def required?
842
870
  # d { [:default?, self.class, self.name, instance_variable_defined?("@default"), @default] }
843
- !(default_defined? or init_defined?)
871
+ !optional?
844
872
  end
845
873
 
846
874
  # has default been defined?
@@ -853,12 +881,18 @@ module Doodle
853
881
  end
854
882
 
855
883
  # name of attribute
856
- has :name
884
+ has :name, :kind => Symbol do
885
+ from String do |s|
886
+ s.to_sym
887
+ end
888
+ end
857
889
  # default value (can be a block)
858
- has :default
890
+ has :default, :default => nil
859
891
  # initial value
860
- has :init
892
+ has :init, :default => nil
893
+
861
894
  end
895
+
862
896
  end
863
897
 
864
898
  ############################################################