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