doodle 0.0.1

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