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 +18 -0
- data/README +57 -0
- data/examples/event.rb +30 -0
- data/examples/event1.rb +33 -0
- data/examples/event2.rb +21 -0
- data/examples/example-01.rb +16 -0
- data/examples/example-01.rdoc +16 -0
- data/examples/example-02.rb +61 -0
- data/examples/example-02.rdoc +62 -0
- data/lib/doodle.rb +800 -0
- data/lib/molic_orderedhash.rb +243 -0
- data/lib/spec_helper.rb +19 -0
- data/spec/arg_order_spec.rb +125 -0
- data/spec/attributes_spec.rb +106 -0
- data/spec/class_spec.rb +90 -0
- data/spec/conversion_spec.rb +59 -0
- data/spec/defaults_spec.rb +158 -0
- data/spec/doodle_spec.rb +297 -0
- data/spec/flatten_first_level_spec.rb +36 -0
- data/spec/required_spec.rb +25 -0
- data/spec/superclass_spec.rb +27 -0
- data/spec/validation_spec.rb +108 -0
- metadata +74 -0
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) # =>
|
data/examples/event1.rb
ADDED
@@ -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
|
data/examples/event2.rb
ADDED
@@ -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
|
+
############################################################
|