doodle 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  ############################################################