doodle 0.0.1

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/COPYING ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2008 Sean O'Halpin <monkeymind.textdriven.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,57 @@
1
+ = README
2
+ == doodle
3
+
4
+ Version 0.0.1
5
+
6
+ *doodle* is my attempt at a metaprogramming framework that tries not to
7
+ have to inject methods into core Ruby objects such as Object, Class
8
+ and Module.
9
+
10
+ While doodle itself is useful for defining classes, my main goal is to
11
+ come up with a useful DSL notation for class definitions which can be
12
+ reused in many contexts.
13
+
14
+ Note that this is very much the first version of a
15
+ work-in-progress. Despite a fair number of specifications, you can
16
+ expect there to be bugs and unexpected behaviours.
17
+
18
+ Read more at http://doodle.rubyforge.org
19
+
20
+ == Examples
21
+ === Simple example
22
+
23
+ :include: examples/example-01.rdoc
24
+
25
+ === More complex example
26
+
27
+ :include: examples/example-02.rdoc
28
+
29
+ == Known bugs
30
+
31
+ * Not compatible with ruby 1.9
32
+
33
+ == To do
34
+
35
+ * Better documentation
36
+ * Make compatible with ruby 1.9
37
+ * Add examples showing other uses of DSL aspect
38
+ * More specs
39
+
40
+ == Similar and related libraries
41
+
42
+ * traits[http://www.codeforpeople.com/lib/ruby/traits/]
43
+ * attributes[http://www.codeforpeople.com/lib/ruby/attributes/]
44
+
45
+ == Thanks
46
+
47
+ *doodle* was developed using
48
+ BDD[http://en.wikipedia.org/wiki/Behavior_driven_development] with
49
+ RSpec[http://rspec.rubyforge.org/], autotest (part of the
50
+ ZenTest[http://www.zenspider.com/ZSS/Products/ZenTest/] suite) and
51
+ rcov[http://eigenclass.org/hiki.rb?rcov] - fantastic tools.
52
+
53
+ == Confessions of a crap artist
54
+
55
+ There is at least one horrible hack in there (see
56
+ Doodle::Inherited#parents[classes/Doodle/Inherited.html#M000021]) -
57
+ though some may consider the whole thing a horrible hack :)
data/examples/event.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'date'
2
+ require 'doodle'
3
+
4
+ class Event < Doodle::Base
5
+ has :start_date, :kind => Date do
6
+ from String do |value|
7
+ Date.parse(value)
8
+ end
9
+ end
10
+ has :end_date, :kind => Date do
11
+ from String do |value|
12
+ Date.parse(value)
13
+ end
14
+ end
15
+ from String do |value|
16
+ args = value.split(' to ')
17
+ new(*args)
18
+ end
19
+ 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
+
27
+ class Date
28
+ include Doodle::Factory
29
+ end
30
+ date = Date(2008, 03, 01) # =>
@@ -0,0 +1,33 @@
1
+ require 'date'
2
+ require 'pp'
3
+ require 'doodle'
4
+
5
+ class Location < Doodle::Base
6
+ has :name, :kind => String
7
+ has :events, :init => [], :collect => :Event # forward reference, so use symbol
8
+ end
9
+
10
+ class Event < Doodle::Base
11
+ has :name, :kind => String
12
+ has :date do
13
+ kind Date
14
+ default { Date.today }
15
+ must 'be >= today' do |value|
16
+ value >= Date.today
17
+ end
18
+ from String do |s|
19
+ Date.parse(s)
20
+ end
21
+ end
22
+ has :locations, :init => [], :collect => {:place => "Location"}
23
+ end
24
+
25
+ event = Event "Festival" do
26
+ date '2008-04-01'
27
+ place "The muddy field"
28
+ place "Beer tent" do
29
+ event "Drinking"
30
+ end
31
+ end
32
+
33
+ pp event
@@ -0,0 +1,21 @@
1
+ require 'date'
2
+ require 'doodle'
3
+
4
+ class Event < Doodle::Base
5
+ has :start_date, :kind => Date do
6
+ from String do |value|
7
+ Date.parse(value)
8
+ end
9
+ end
10
+ has :end_date, :kind => Date do
11
+ from String do |value|
12
+ Date.parse(value)
13
+ end
14
+ end
15
+ end
16
+ event = Event '2008-03-05', '2008-03-06'
17
+ event.start_date.to_s # => "2008-03-05"
18
+ event.end_date.to_s # => "2008-03-06"
19
+ event.start_date = '2001-01-01'
20
+ event.start_date # => #<Date: 4903821/2,0,2299161>
21
+ event.start_date.to_s # => "2001-01-01"
@@ -0,0 +1,16 @@
1
+ require 'date'
2
+ require 'doodle'
3
+
4
+ class DateRange < Doodle::Base
5
+ has :start_date do
6
+ default { Date.today }
7
+ end
8
+ has :end_date do
9
+ default { start_date }
10
+ end
11
+ end
12
+
13
+ dr = DateRange.new
14
+ dr.start_date # => #<Date: 4908855/2,0,2299161>
15
+ dr.end_date # => #<Date: 4908855/2,0,2299161>
16
+
@@ -0,0 +1,16 @@
1
+ require 'date'
2
+ require 'doodle'
3
+
4
+ class DateRange < Doodle::Base
5
+ has :start_date do
6
+ default { Date.today }
7
+ end
8
+ has :end_date do
9
+ default { start_date }
10
+ end
11
+ end
12
+
13
+ dr = DateRange.new
14
+ dr.start_date # => #<Date: 4909053/2,0,2299161>
15
+ dr.end_date # => #<Date: 4909053/2,0,2299161>
16
+
@@ -0,0 +1,61 @@
1
+ require 'date'
2
+ require 'doodle'
3
+
4
+ class DateRange < Doodle::Base
5
+ has :start_date, :kind => Date do
6
+ default { Date.today }
7
+ from String do |s|
8
+ Date.parse(s)
9
+ end
10
+ must "be >= 2000-01-01" do |d|
11
+ d >= Date.parse('2000-01-01')
12
+ end
13
+ end
14
+ has :end_date do
15
+ default { start_date }
16
+ from String do |s|
17
+ Date.parse(s)
18
+ end
19
+ end
20
+ must 'have end_date >= start_date' do
21
+ end_date >= start_date
22
+ end
23
+ from String do |s|
24
+ m = /(\d{4}-\d{2}-\d{2})\s*(?:to|-|\s)\s*(\d{4}-\d{2}-\d{2})/.match(s)
25
+ if m
26
+ self.new(*m.captures)
27
+ end
28
+ end
29
+ end
30
+
31
+ dr = DateRange.new '2007-12-31', '2008-01-01'
32
+ dr.start_date # =>
33
+ dr.end_date # =>
34
+
35
+ dr = DateRange '2007-12-31', '2008-01-01'
36
+ dr.start_date # =>
37
+ dr.end_date # =>
38
+
39
+ dr = DateRange :start_date => '2007-12-31', :end_date => '2008-01-01'
40
+ dr.start_date # =>
41
+ dr.end_date # =>
42
+
43
+ dr = DateRange do
44
+ start_date '2007-12-31'
45
+ end_date '2008-01-01'
46
+ end
47
+ dr.start_date # =>
48
+ dr.end_date # =>
49
+
50
+
51
+ dr = DateRange.from '2007-01-01 to 2008-12-31'
52
+ dr.start_date # =>
53
+ dr.end_date # =>
54
+
55
+ dr = DateRange.from '2007-01-01 2007-12-31'
56
+ dr.start_date # =>
57
+ dr.end_date # =>
58
+
59
+ dr = DateRange '2008-01-01', '2007-12-31'
60
+ dr.start_date # =>
61
+ dr.end_date # =>
@@ -0,0 +1,62 @@
1
+ require 'date'
2
+ require 'doodle'
3
+
4
+ class DateRange < Doodle::Base
5
+ has :start_date, :kind => Date do
6
+ default { Date.today }
7
+ from String do |s|
8
+ Date.parse(s)
9
+ end
10
+ must "be >= 2000-01-01" do |d|
11
+ d >= Date.parse('2000-01-01')
12
+ end
13
+ end
14
+ has :end_date do
15
+ default { start_date }
16
+ from String do |s|
17
+ Date.parse(s)
18
+ end
19
+ end
20
+ must 'have end_date >= start_date' do
21
+ end_date >= start_date
22
+ end
23
+ from String do |s|
24
+ m = /(\d{4}-\d{2}-\d{2})\s*(?:to|-|\s)\s*(\d{4}-\d{2}-\d{2})/.match(s)
25
+ if m
26
+ self.new(*m.captures)
27
+ end
28
+ end
29
+ end
30
+
31
+ dr = DateRange.new '2007-12-31', '2008-01-01'
32
+ dr.start_date # => #<Date: 4908931/2,0,2299161>
33
+ dr.end_date # => #<Date: 4908933/2,0,2299161>
34
+
35
+ dr = DateRange '2007-12-31', '2008-01-01'
36
+ dr.start_date # => #<Date: 4908931/2,0,2299161>
37
+ dr.end_date # => #<Date: 4908933/2,0,2299161>
38
+
39
+ dr = DateRange :start_date => '2007-12-31', :end_date => '2008-01-01'
40
+ dr.start_date # => #<Date: 4908931/2,0,2299161>
41
+ dr.end_date # => #<Date: 4908933/2,0,2299161>
42
+
43
+ dr = DateRange do
44
+ start_date '2007-12-31'
45
+ end_date '2008-01-01'
46
+ end
47
+ dr.start_date # => #<Date: 4908931/2,0,2299161>
48
+ dr.end_date # => #<Date: 4908933/2,0,2299161>
49
+
50
+
51
+ dr = DateRange.from '2007-01-01 to 2008-12-31'
52
+ dr.start_date # => #<Date: 4908203/2,0,2299161>
53
+ dr.end_date # => #<Date: 4909663/2,0,2299161>
54
+
55
+ dr = DateRange.from '2007-01-01 2007-12-31'
56
+ dr.start_date # => #<Date: 4908203/2,0,2299161>
57
+ dr.end_date # => #<Date: 4908931/2,0,2299161>
58
+
59
+ dr = DateRange '2008-01-01', '2007-12-31'
60
+ dr.start_date # =>
61
+ 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)
data/lib/doodle.rb ADDED
@@ -0,0 +1,800 @@
1
+ # doodle
2
+ # Copyright (C) 2007 by Sean O'Halpin, 2007-11-24
3
+
4
+ require 'molic_orderedhash' # todo[replace this with own (required function only) version]
5
+
6
+ # *doodle* is my attempt at a metaprogramming framework that does not
7
+ # have to inject methods into core Ruby objects such as Object, Class
8
+ # and Module.
9
+
10
+ # While doodle itself is useful for defining classes, my main goal is to
11
+ # come up with a useful DSL notation for class definitions which can be
12
+ # reused in many contexts.
13
+
14
+ # Docs at http://doodle.rubyforge.org
15
+
16
+ module Doodle
17
+ module Debug
18
+ class << self
19
+ # output result of block if DEBUG_DOODLE set
20
+ def d(&block)
21
+ p(block.call) if ENV['DEBUG_DOODLE']
22
+ end
23
+ end
24
+ end
25
+
26
+ module Utils
27
+ # Unnest arrays by one level of nesting - for example, [1, [[2], 3]] => [1, [2], 3].
28
+ # This is a function to avoid changing base classes.
29
+ def self.flatten_first_level(enum)
30
+ enum.inject([]) {|arr, i| if i.kind_of? Array then arr.push(*i) else arr.push(i) end }
31
+ end
32
+ end
33
+
34
+ # internal error raised when a default was expected but not found
35
+ class NoDefaultError < Exception
36
+ end
37
+ # raised when a validation rule returns false
38
+ class ValidationError < Exception
39
+ end
40
+ # raised when a conversion fails
41
+ class ConversionError < Exception
42
+ end
43
+ # raised when arg_order called with incorrect arguments
44
+ class InvalidOrderError < Exception
45
+ end
46
+
47
+ # provides more direct access to the singleton class and a way to
48
+ # treat Modules and Classes equally in a meta context
49
+ module SelfClass
50
+ # return self if a Module, else the singleton class
51
+ def self_class
52
+ self.kind_of?(Module) ? self : singleton_class
53
+ end
54
+ # return the 'singleton class' of an object, optionally executing
55
+ # a block argument in the (module/class) context of that object
56
+ def singleton_class(&block)
57
+ sc = (class << self; self; end)
58
+ sc.module_eval(&block) if block_given?
59
+ sc
60
+ end
61
+ # an alias for singleton_class
62
+ alias :meta :singleton_class
63
+ def class_init(params = {}, &block)
64
+ sc = singleton_class &block
65
+ sc.attributes.select{|n, a| a.init_defined? }.each do |n, a|
66
+ send(n, a.init)
67
+ end
68
+ sc
69
+ end
70
+ end
71
+
72
+ # provide an alternative inheritance chain that works for singleton
73
+ # classes as well as modules, classes and instances
74
+ module Inherited
75
+
76
+ # def supers
77
+ # supers = []
78
+ # s = superclass rescue nil
79
+ # while !s.nil?
80
+ # supers << s
81
+ # last_s = s.superclass rescue nil
82
+ # if last_s == s
83
+ # last_s = nil
84
+ # end
85
+ # s = last_s
86
+ # end
87
+ # supers
88
+ # end
89
+
90
+ # parents returns the set of parent classes of an object.
91
+ # note[this is horribly complicated and kludgy - is there a better way?
92
+ # could do with refactoring]
93
+
94
+ # this function is a ~mess~ - refactor!!!
95
+ def parents
96
+ # d { [:parents, self.to_s, defined?(superclass)] }
97
+ klasses = []
98
+ if defined?(superclass)
99
+ klass = superclass
100
+ #p [:klass_superclass, klass]
101
+ if self == superclass
102
+ # d { [:parents, 'self == superclass'] }
103
+ klass = nil
104
+ else
105
+ #p [:klass_singleton_class, klass]
106
+ #p [:parents, 'klass = superclass', self, klass, self.ancestors]
107
+ #
108
+ # fixme[any other way to do this? seems really clunky to have to hack strings]
109
+ #
110
+ # What's this doing? Finding the class of which this is the singleton class
111
+ regexen = [/Class:(?:#<)?([A-Z_][A-Za-z_]+)/, /Class:(([A-Z_][A-Za-z_]+))/]
112
+ regexen.each do |regex|
113
+ if cap = self.to_s.match(regex)
114
+ if cap.captures.size > 0
115
+ k = const_get(cap[1])
116
+ if k.respond_to?(:superclass) && k.superclass.respond_to?(:meta)
117
+ klasses.unshift k.superclass.meta
118
+ end
119
+ end
120
+ #p [:klass_self_klass, klass]
121
+ #p [:klasses, klasses]
122
+ loop do
123
+ if klass.nil?
124
+ break
125
+ end
126
+ klasses.unshift klass
127
+ #p [:loop_klasses, klasses]
128
+ if klass == klass.superclass
129
+ #p [:HERE_HERE_BEFORE, klasses]
130
+ #break
131
+ return klasses # oof
132
+ end
133
+ klass = klass.superclass
134
+ end
135
+ #p [:HERE_HERE, klasses]
136
+ else
137
+ #p [:klass_self_klass, klass]
138
+ #p [:klasses, klasses]
139
+ loop do
140
+ if klass.nil?
141
+ break
142
+ end
143
+ klasses << klass
144
+ #p [:loop_klasses, klasses]
145
+ if klass == klass.superclass
146
+ break
147
+ end
148
+ klass = klass.superclass
149
+ end
150
+ end
151
+ end
152
+ end
153
+ else
154
+ klass = self.class
155
+ #p [:klass_self_klass, klass]
156
+ #p [:klasses, klasses]
157
+ loop do
158
+ if klass.nil?
159
+ break
160
+ end
161
+ klasses << klass
162
+ #p [:loop_klasses, klasses]
163
+ if klass == klass.superclass
164
+ break
165
+ end
166
+ klass = klass.superclass
167
+ end
168
+ end
169
+ #p [:HERE_HERE_END, klasses]
170
+ klasses
171
+ end
172
+
173
+ # send message to all parents and collect results
174
+ def collect_inherited(message)
175
+ result = []
176
+ klasses = parents
177
+ #p [:parents, parents]
178
+ # d { [:collect_inherited, :parents, message, klasses] }
179
+ #klasses = self_class.ancestors # this produces quite different behaviour
180
+ klasses.each do |klass|
181
+ #p [:testing, klass]
182
+ if klass.respond_to?(message)
183
+ # d { [:collect_inherited, :responded, message, klass] }
184
+ result.unshift(*klass.send(message))
185
+ else
186
+ break
187
+ end
188
+ end
189
+ # d { [:collect_inherited, :result, message, result] }
190
+ if self.class.respond_to?(message)
191
+ result.unshift(*self.class.send(message))
192
+ end
193
+ result
194
+ end
195
+ private :collect_inherited
196
+ end
197
+
198
+ # the intent of embrace is to provide a way to create directives that
199
+ # affect all members of a class 'family' without having to modify
200
+ # Module, Class or Object - in some ways, it's similar to Ara Howard's mixable[http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/197296]
201
+ #
202
+ # this works down to third level <tt>class << self</tt> - in practice, this is
203
+ # perfectly good - it would be great to have a completely general
204
+ # solution but I'm doubt whether the payoff is worth the time
205
+
206
+ module Embrace
207
+ # fake module inheritance chain
208
+ def embrace(other, &block)
209
+ # include in instance method chain
210
+ include other
211
+ #extend other
212
+ sc = class << self; self; end
213
+ sc.class_eval {
214
+ # class method chain
215
+ include other
216
+ # singleton method chain
217
+ extend other
218
+ # ensure that subclasses are also embraced
219
+ define_method :inherited do |klass|
220
+ #p [:embrace, :inherited, klass]
221
+ klass.send(:embrace, other)
222
+ klass.send(:include, Factory) # yikes!
223
+ super(klass) if defined?(super)
224
+ end
225
+ }
226
+ sc.class_eval(&block) if block_given?
227
+ end
228
+ end
229
+
230
+ # Lazy is a Proc that caches the result of a call
231
+ class Lazy < Proc
232
+ # return the result of +call+ing this Proc - cached after first +call+
233
+ def value
234
+ @value ||= call
235
+ end
236
+ end
237
+
238
+ # A Validation represents a validation rule applied to the instance
239
+ # after initialization. Generated using the Doodle::BaseMethods#must directive.
240
+ class Validation
241
+ attr_accessor :message
242
+ attr_accessor :block
243
+ # create a new validation rule. This is typically a result of
244
+ # calling +must+ so the text should work following the word
245
+ # "must", e.g. "must not be nil", "must be >= 10", etc.
246
+ def initialize(message = 'not be nil', &block)
247
+ @message = message
248
+ @block = block_given? ? block : proc { |x| !self.nil? }
249
+ end
250
+ end
251
+
252
+ class DoodleInfo
253
+ DOODLES = {}
254
+ attr_accessor :local_attributes
255
+ attr_accessor :local_validations
256
+ attr_accessor :local_conversions
257
+ attr_accessor :validation_on
258
+ attr_accessor :arg_order
259
+
260
+ def initialize(object)
261
+ @local_attributes = OrderedHash.new
262
+ @local_validations = []
263
+ @validation_on = true
264
+ @local_conversions = {}
265
+ @arg_order = []
266
+ oid = object.object_id
267
+ ObjectSpace.define_finalizer(object) do
268
+ DOODLES.delete(oid)
269
+ end
270
+ end
271
+ end
272
+
273
+ # the core module of Doodle - to get most facilities provided by Doodle
274
+ # without inheriting from Doodle::Base, include Doodle::Helper, not this module
275
+ module BaseMethods
276
+ include SelfClass
277
+ include Inherited
278
+
279
+ # this is the only way to get at internal values
280
+ # FIXME: this is going to leak memory
281
+
282
+ def __doodle__
283
+ DoodleInfo::DOODLES[object_id] ||= DoodleInfo.new(self)
284
+ end
285
+ private :__doodle__
286
+
287
+ # return attributes defined in instance
288
+ def local_attributes
289
+ __doodle__.local_attributes
290
+ end
291
+ protected :local_attributes
292
+
293
+ # returns array of Attributes
294
+ # - if tf == true, returns all inherited attributes
295
+ # - if tf == false, returns only those attributes defined in the current object/class
296
+ def attributes(tf = true)
297
+ if tf
298
+ a = collect_inherited(:local_attributes).inject(OrderedHash.new){ |hash, item|
299
+ #p [:hash, hash, :item, item]
300
+ hash.merge(OrderedHash[*item])
301
+ }.merge(local_attributes)
302
+ # d { [:attributes, self.to_s, a] }
303
+ a
304
+ else
305
+ local_attributes
306
+ end
307
+ end
308
+
309
+ # the set of validations defined in the current class (i.e. without inheritance)
310
+ def local_validations
311
+ __doodle__.local_validations
312
+ end
313
+ protected :local_validations
314
+
315
+ # returns array of Validations
316
+ # - if tf == true, returns all inherited validations
317
+ # - if tf == false, returns only those validations defined in the current object/class
318
+ def validations(tf = true)
319
+ if tf
320
+ local_validations.push(*collect_inherited(:local_validations))
321
+ else
322
+ local_validations
323
+ end
324
+ end
325
+
326
+ # the set of conversions defined in the current class (i.e. without inheritance)
327
+ def local_conversions
328
+ __doodle__.local_conversions
329
+ end
330
+ protected :local_conversions
331
+
332
+ # returns array of conversions
333
+ # - if tf == true, returns all inherited conversions
334
+ # - if tf == false, returns only those conversions defined in the current object/class
335
+ def conversions(tf = true)
336
+ if tf
337
+ a = collect_inherited(:local_conversions).inject(OrderedHash.new){ |hash, item|
338
+ #p [:hash, hash, :item, item]
339
+ hash.merge(Hash[*item])
340
+ }.merge(self.local_conversions)
341
+ # d { [:conversions, self.to_s, a] }
342
+ a
343
+ else
344
+ local_conversions
345
+ end
346
+ end
347
+
348
+ # lookup a single attribute by name, searching the singleton class first
349
+ def lookup_attribute(name)
350
+ # (look at singleton attributes first)
351
+ # fixme[this smells like a hack to me - why not handled in attributes?]
352
+ att = meta.attributes[name] || attributes[name]
353
+ end
354
+ private :lookup_attribute
355
+
356
+ # either get an attribute value (if no args given) or set it
357
+ # (using args and/or block)
358
+ def getter_setter(name, *args, &block)
359
+ # d { [:getter_setter, name, args, block] }
360
+ name = name.to_sym
361
+ if block_given? || args.size > 0
362
+ # setter
363
+ _setter(name, *args, &block)
364
+ else
365
+ _getter(name)
366
+ end
367
+ end
368
+ private :getter_setter
369
+
370
+ # get an attribute by name - return default if not otherwise defined
371
+ def _getter(name, &block)
372
+ ## d { [:_getter, 1, self.to_s, name, block, instance_variables] }
373
+ # getter
374
+ ivar = "@#{name}"
375
+ if instance_variable_defined?(ivar)
376
+ ## d { [:_getter, 2, name, block] }
377
+ v = instance_variable_get(ivar)
378
+ #d { [:_getter, :defined, name, v] }
379
+ # if v.kind_of?(Lazy)
380
+ # p [name, self, self.class, v]
381
+ # v = instance_eval &v.block
382
+ # end
383
+ v
384
+ else
385
+ # handle default
386
+ att = lookup_attribute(name)
387
+ #d { [:getter, name, att, block] }
388
+ if att.default_defined?
389
+ if att.default.kind_of?(Proc)
390
+ default = instance_eval(&att.default)
391
+ else
392
+ default = att.default
393
+ end
394
+ #d { [:_getter, :default, name, default] } Note: once the
395
+ # default is accessed, the instance variable is set. I think
396
+ # I would prefer not to do this and to have :init => value
397
+ # instead to cover cases where defaults don't work
398
+ # (e.g. arrays that disappear when you go out of scope)
399
+ #instance_variable_set("@#{name}", default)
400
+ default
401
+ else
402
+ raise NoDefaultError, "'#{name}' has no default defined", [caller[-1]]
403
+ end
404
+ end
405
+ end
406
+ private :_getter
407
+
408
+ # set an attribute by name - apply validation if defined
409
+ def _setter(name, *args, &block)
410
+ # d { [:_setter, self, self.class, name, args, block] }
411
+ ivar = "@#{name}"
412
+ args.unshift(block) if block_given?
413
+ # d { [:_setter, 3, :setting, name, ivar, args] }
414
+ att = lookup_attribute(name)
415
+ # d { [:_setter, 4, :setting, name, att] }
416
+ if att
417
+ #d { [:_setter, :instance_variable_set, :ivar, ivar, :args, args, :att_validate, att.validate(*args) ] }
418
+ v = instance_variable_set(ivar, att.validate(*args))
419
+ else
420
+ #d { [:_setter, :instance_variable_set, ivar, args ] }
421
+ v = instance_variable_set(ivar, *args)
422
+ end
423
+ validate!
424
+ v
425
+ end
426
+ private :_setter
427
+
428
+ # if block passed, define a conversion from class
429
+ # if no args, apply conversion to arguments
430
+ def from(*args, &block)
431
+ # d { [:from, self, self.class, self.name, args, block] }
432
+ if block_given?
433
+ # setting rule
434
+ local_conversions[*args] = block
435
+ # d { [:from, conversions] }
436
+ else
437
+ convert(*args)
438
+ end
439
+ end
440
+
441
+ # add a validation
442
+ def must(message = 'be valid', &block)
443
+ local_validations << Validation.new(message, &block)
444
+ end
445
+
446
+ # add a validation that attribute must be of class <= kind
447
+ def kind(*args, &block)
448
+ # d { [:kind, args, block] }
449
+ if args.size > 0
450
+ # todo[figure out how to handle kind being specified twice?]
451
+ @kind = args.first
452
+ local_validations << (Validation.new("be #{@kind}") { |x| x.class <= @kind })
453
+ else
454
+ @kind
455
+ end
456
+ end
457
+
458
+ # convert a value according to conversion rules
459
+ def convert(value)
460
+ begin
461
+ if (converter = conversions[value.class])
462
+ value = converter[value]
463
+ else
464
+ # try to find nearest ancestor
465
+ ancestors = value.class.ancestors
466
+ matches = ancestors & conversions.keys
467
+ indexed_matches = matches.map{ |x| ancestors.index(x)}
468
+ #p [matches, indexed_matches, indexed_matches.min]
469
+ if indexed_matches.size > 0
470
+ converter_class = ancestors[indexed_matches.min]
471
+ #p [:converter, converter_class]
472
+ if converter = conversions[converter_class]
473
+ value = converter[value]
474
+ end
475
+ end
476
+ end
477
+ rescue => e
478
+ raise ValidationError, e.to_s, [caller[-1]]
479
+ end
480
+ value
481
+ end
482
+
483
+ # validate that args meet rules defined with +must+
484
+ def validate(*args)
485
+ value = convert(*args)
486
+ #d { [:validate, self, :args, args, :value, value ] }
487
+ validations.each do |v|
488
+ Doodle::Debug.d { [:validate, self, v, args ] }
489
+ if !v.block[value]
490
+ raise ValidationError, "#{ name } must #{ v.message } - got #{ value.class }(#{ value.inspect })", [caller[-1]]
491
+ end
492
+ end
493
+ #d { [:validate, :value, value ] }
494
+ value
495
+ end
496
+
497
+ # define a getter_setter
498
+ def define_getter_setter(name, *args, &block)
499
+ # d { [:define_getter_setter, [self, self.class, self_class], name, args, block] }
500
+
501
+ # need to use string eval because passing block
502
+ module_eval "def #{name}(*args, &block); getter_setter(:#{name}, *args, &block); end"
503
+ module_eval "def #{name}=(*args, &block); _setter(:#{name}, *args); end"
504
+ end
505
+ private :define_getter_setter
506
+
507
+ # define a collector
508
+ # - collection should provide a :<< method
509
+ def define_collector(collection, klass, name, &block)
510
+ # need to use string eval because passing block
511
+ module_eval "def #{name}(*args, &block); #{collection} << #{klass}.new(*args, &block); end"
512
+ end
513
+ private :define_collector
514
+
515
+ # +has+ is an extended +attr_accessor+
516
+ #
517
+ # simple usage - just like +attr_accessor+:
518
+ #
519
+ # class Event
520
+ # has :date
521
+ # end
522
+ #
523
+ # set default value:
524
+ #
525
+ # class Event
526
+ # has :date, :default => Date.today
527
+ # end
528
+ #
529
+ # set lazily evaluated default value:
530
+ #
531
+ # class Event
532
+ # has :date do
533
+ # default { Date.today }
534
+ # end
535
+ # end
536
+ #
537
+ def has(*args, &block)
538
+ Doodle::Debug.d { [:has, self, self.class, self_class, args] }
539
+ name = args.shift.to_sym
540
+ # d { [:has2, name, args] }
541
+ key_values, positional_args = args.partition{ |x| x.kind_of?(Hash)}
542
+ raise ArgumentError, "Too many arguments" if positional_args.size > 0
543
+ # d { [:has_args, self, key_values, positional_args, args] }
544
+ params = { :name => name }
545
+ params = key_values.inject(params){ |acc, item| acc.merge(item)}
546
+
547
+ # don't pass collector params through to Attribute
548
+ if collector = params.delete(:collect)
549
+ if collector.kind_of?(Hash)
550
+ collector_name, klass = collector.to_a[0]
551
+ else
552
+ klass = collector.to_s
553
+ collector_name = klass.downcase
554
+ end
555
+ define_collector name, klass, collector_name
556
+ end
557
+
558
+ # d { [:has_args, :params, params] }
559
+ # fixme[this is a little fragile - depends on order of local_attributes in Attribute - should convert to hash args]
560
+ # self_class.local_attributes[name] = attribute = Attribute.new(params, &block)
561
+ local_attributes[name] = attribute = Attribute.new(params, &block)
562
+ define_getter_setter name, *args, &block
563
+
564
+ #super(*args, &block) if defined?(super)
565
+ attribute
566
+ end
567
+
568
+ # define order for positional arguments
569
+ def arg_order(*args)
570
+ if args.size > 0
571
+ begin
572
+ #p [:arg_order, 1, self, self.class, args]
573
+ args.uniq!
574
+ args.each do |x|
575
+ raise Exception, "#{x} not a Symbol" if !(x.class <= Symbol)
576
+ raise Exception, "#{x} not an attribute name" if !attributes.keys.include?(x)
577
+ end
578
+ __doodle__.arg_order = args
579
+ rescue Exception => e
580
+ #p [InvalidOrderError, e.to_s]
581
+ raise InvalidOrderError, e.to_s, [caller[-1]]
582
+ end
583
+ else
584
+ #p [:arg_order, 3, self, self.class, :default]
585
+ __doodle__.arg_order + (attributes.keys - __doodle__.arg_order)
586
+ end
587
+ end
588
+
589
+ # helper function to initialize from hash - this is safe to use
590
+ # after initialization (validate! is called if this method is
591
+ # called after initialization)
592
+ def initialize_from_hash(*args)
593
+ defer_validation do
594
+ # hash initializer
595
+ # separate into positional args and hashes (keyword => value)
596
+ key_values, args = args.partition{ |x| x.kind_of?(Hash)}
597
+ # d { [:initialize, :key_values, key_values, :args, args] }
598
+
599
+ # use idiom to create hash from array of assocs
600
+ arg_keywords = Hash[*(Utils.flatten_first_level(self.class.arg_order[0...args.size].zip(args)))]
601
+ # d { [:initialize, :arg_keywords, arg_keywords] }
602
+
603
+ # set up initial values with ~clones~ of specified values (so not shared between instances)
604
+ init_values = attributes.select{|n, a| a.init_defined? }.inject({}) {|hash, (n, a)| hash[n] = a.init.clone; hash }
605
+
606
+ # add to start of key_values (so can be overridden by params)
607
+ key_values.unshift(init_values)
608
+
609
+ # merge all hash args into one
610
+ key_values = key_values.inject(arg_keywords){ |hash, item| hash.merge(item)}
611
+ # d { [:initialize, :key_values, key_values] }
612
+ key_values.keys.each do |key|
613
+ key = key.to_sym
614
+ # d { [:initialize_from_hash, :setting, key, key_values[key]] }
615
+ if respond_to?(key)
616
+ send(key, key_values[key])
617
+ else
618
+ _setter(key, key_values[key])
619
+ end
620
+ end
621
+ end
622
+ end
623
+ #private :initialize_from_hash
624
+
625
+ # return true if instance variable +name+ defined
626
+ def ivar_defined?(name)
627
+ instance_variable_defined?("@#{name}")
628
+ end
629
+ private :ivar_defined?
630
+
631
+ # validate this object by applying all validations in sequence
632
+ def validate!
633
+ #d# d { [:validate!, self] }
634
+ if __doodle__.validation_on
635
+ attributes.each do |name, att|
636
+ # d { [:validate!, self, self.class, att.name, att.default_defined? ] }
637
+ #p collect_inherited(:attributes)
638
+ # treat default as special case
639
+ if att.name == :default || att.default_defined?
640
+ elsif !ivar_defined?(att.name)
641
+ raise ArgumentError, "#{self} missing required attribute '#{name}'", [caller[-1]]
642
+ end
643
+ end
644
+
645
+ validations.each do |v|
646
+ #d# d { [:validate!, self, v ] }
647
+ if !instance_eval(&v.block)
648
+ # if !instance_eval{ v.block.call(self) }
649
+ raise ValidationError, "#{ self.inspect } must #{ v.message }", [caller[-1]]
650
+ end
651
+ end
652
+ end
653
+ end
654
+ private :validate!
655
+
656
+ # turn off validation, execute block, then set validation to same
657
+ # state as it was before +defer_validation+ was called - can be nested
658
+ def defer_validation(&block)
659
+ old_validation = __doodle__.validation_on
660
+ __doodle__.validation_on = false
661
+ v = nil
662
+ begin
663
+ v = instance_eval(&block)
664
+ ensure
665
+ __doodle__.validation_on = old_validation
666
+ end
667
+ validate!
668
+ v
669
+ end
670
+
671
+ # object can be initialized from a mixture of positional arguments,
672
+ # hash of keyword value pairs and a block which is instance_eval'd
673
+ def initialize(*args, &block)
674
+ __doodle__.validation_on = true
675
+
676
+ defer_validation do
677
+ # d { [:initialize, self.to_s, args, block] }
678
+ initialize_from_hash(*args)
679
+ # d { [:initialize, self.to_s, args, block, :calling_block] }
680
+ instance_eval(&block) if block_given?
681
+ end
682
+ end
683
+
684
+ end
685
+
686
+ # A factory function is a function that has the same name as
687
+ # a class which acts just like class.new. For example:
688
+ # Cat(:name => 'Ren')
689
+ # is the same as:
690
+ # Cat.new(:name => 'Ren')
691
+ # As the notion of a factory function is somewhat contentious [xref
692
+ # ruby-talk], you need to explicitly ask for them by including Factory
693
+ # in your base class:
694
+ # class Base < Doodle::Root
695
+ # include Factory
696
+ # end
697
+ # class Dog < Base
698
+ # end
699
+ # stimpy = Dog(:name => 'Stimpy')
700
+ # etc.
701
+ module Factory
702
+ # create a factory function called +name+ for the current class
703
+ def factory(name = self)
704
+ name = self.to_s
705
+ names = name.split(/::/)
706
+ name = names.pop
707
+ if names.empty?
708
+ # top level class - should be available to all
709
+ mklass = klass = Object
710
+ #p [:names_empty, klass, mklass]
711
+ eval src = "def #{ name }(*args, &block); ::#{name}.new(*args, &block); end", ::TOPLEVEL_BINDING
712
+ else
713
+ klass = names.inject(self) {|c, n| c.const_get(n)}
714
+ mklass = class << klass; self; end
715
+ #p [:names, klass, mklass]
716
+ #eval src = "def #{ names.join('::') }::#{name}(*args, &block); #{ names.join('::') }::#{name}.new(*args, &block); end"
717
+ klass.class_eval src = "def self.#{name}(*args, &block); #{name}.new(*args, &block); end"
718
+ end
719
+ #p [:factory, mklass, klass, src]
720
+ end
721
+ # inherit the factory function capability
722
+ def self.included(other)
723
+ #p [:factory, :included, self, other ]
724
+ super
725
+ #raise Exception, "#{self} can only be included in a Class" if !other.kind_of? Class
726
+ # make +factory+ method available
727
+ other.extend self
728
+ other.module_eval {
729
+ factory
730
+ }
731
+ end
732
+ end
733
+
734
+ # Include Doodle::Helper if you want to derive from another class
735
+ # but still get Doodle goodness in your class (including Factory
736
+ # methods).
737
+ module Helper
738
+ def self.included(other)
739
+ #p [:Helper, :included, self, other ]
740
+ super
741
+ other.module_eval {
742
+ extend Embrace
743
+ embrace BaseMethods
744
+ }
745
+ end
746
+ end
747
+
748
+ # derive from Base if you want all the Doodle goodness
749
+ class Base
750
+ include Helper
751
+ end
752
+
753
+ # todo[need to extend this]
754
+ class Attribute < Doodle::Base
755
+ # must define these methods before using them in #has below
756
+
757
+ # bump off +validate!+ for Attributes - maybe better way of doing
758
+ # this however, without this, tries to validate Attribute to :kind
759
+ # specified, e.g. if you have
760
+ #
761
+ # has :date, :kind => Date
762
+ #
763
+ # it will fail because Attribute is not a kind of Date -
764
+ # obviously, I have to think about this some more :S
765
+ #
766
+ def validate!
767
+ end
768
+
769
+ # is this attribute optional? true if it has a default defined for it
770
+ def optional?
771
+ !self.required?
772
+ end
773
+
774
+ # an attribute is required if it has no default or initial value defined for it
775
+ def required?
776
+ # d { [:default?, self.class, self.name, instance_variable_defined?("@default"), @default] }
777
+ !(default_defined? or init_defined?)
778
+ end
779
+
780
+ # has default been defined?
781
+ def default_defined?
782
+ ivar_defined?(:default)
783
+ end
784
+ # has default been defined?
785
+ def init_defined?
786
+ ivar_defined?(:init)
787
+ end
788
+
789
+ # name of attribute
790
+ has :name
791
+ # default value (can be a block)
792
+ has :default
793
+ # initial value
794
+ has :init
795
+ end
796
+ end
797
+
798
+ ############################################################
799
+ # and we're bootstrapped! :)
800
+ ############################################################