doodle 0.2.2 → 0.2.3
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/History.txt +24 -0
- data/Manifest.txt +26 -1
- data/README.txt +9 -8
- data/lib/doodle.rb +43 -1496
- data/lib/doodle/app.rb +6 -0
- data/lib/doodle/attribute.rb +165 -0
- data/lib/doodle/base.rb +180 -0
- data/lib/doodle/collector-1.9.rb +72 -0
- data/lib/doodle/collector.rb +191 -0
- data/lib/doodle/comparable.rb +8 -0
- data/lib/doodle/conversion.rb +80 -0
- data/lib/doodle/core.rb +42 -0
- data/lib/doodle/datatype-holder.rb +39 -0
- data/lib/doodle/debug.rb +20 -0
- data/lib/doodle/deferred.rb +13 -0
- data/lib/doodle/equality.rb +21 -0
- data/lib/doodle/exceptions.rb +29 -0
- data/lib/doodle/factory.rb +91 -0
- data/lib/doodle/getter-setter.rb +154 -0
- data/lib/doodle/info.rb +298 -0
- data/lib/doodle/inherit.rb +40 -0
- data/lib/doodle/json.rb +38 -0
- data/lib/doodle/marshal.rb +16 -0
- data/lib/doodle/normalized_array.rb +512 -0
- data/lib/doodle/normalized_hash.rb +356 -0
- data/lib/doodle/ordered-hash.rb +8 -0
- data/lib/doodle/singleton.rb +23 -0
- data/lib/doodle/smoke-and-mirrors.rb +23 -0
- data/lib/doodle/to_hash.rb +17 -0
- data/lib/doodle/utils.rb +173 -11
- data/lib/doodle/validation.rb +122 -0
- data/lib/doodle/version.rb +1 -1
- data/lib/molic_orderedhash.rb +24 -10
- data/spec/assigned_spec.rb +45 -0
- data/spec/attributes_spec.rb +7 -7
- data/spec/collector_spec.rb +100 -13
- data/spec/doodle_context_spec.rb +5 -5
- data/spec/from_spec.rb +43 -3
- data/spec/json_spec.rb +232 -0
- data/spec/member_init_spec.rb +11 -11
- data/spec/modules_spec.rb +4 -4
- data/spec/multi_collector_spec.rb +91 -0
- data/spec/must_spec.rb +32 -0
- data/spec/spec_helper.rb +14 -4
- data/spec/specialized_attribute_class_spec.rb +2 -2
- data/spec/typed_collector_spec.rb +57 -0
- data/spec/xml_spec.rb +8 -8
- metadata +33 -3
data/History.txt
CHANGED
@@ -1,3 +1,27 @@
|
|
1
|
+
== 0.2.3 / 2009-03-06
|
2
|
+
|
3
|
+
- Features:
|
4
|
+
- collect can now take multiple types, e.g.
|
5
|
+
has :shapes, :collect => [Circle, Square]
|
6
|
+
or
|
7
|
+
has :shapes, :collect => {:circle => Circle, :square => Square}
|
8
|
+
- to_json and from_json - see http:://doodle.rubyforge.org/doodle-json.html for details
|
9
|
+
- can now specify :must and :from directly in #has params hash, e.g.
|
10
|
+
has :answer, :from => { String => proc {|c| c.to_i } },
|
11
|
+
:must => { "be 42" => proc {|c| c == 42 } }
|
12
|
+
- added #assigned? for attributes, e.g.
|
13
|
+
if obj.assigned?(:name) # => true if @name exists
|
14
|
+
- added key_values:
|
15
|
+
class Foo < Doodle
|
16
|
+
has :name
|
17
|
+
has :count
|
18
|
+
end
|
19
|
+
Foo.new('a', 1).doodle.key_values # => [[:name, "a"], [:count, 1]]
|
20
|
+
- and key_values_without_defaults
|
21
|
+
- added :expand => [false|true] to Doodle::App::Filename - expands path if set to true. Default = false
|
22
|
+
- all specs pass on jruby-1.2.0RC2 and ruby 1.8.7p72
|
23
|
+
- plus more docs + specs
|
24
|
+
|
1
25
|
== 0.2.2 / 2009-03-02
|
2
26
|
|
3
27
|
- Features:
|
data/Manifest.txt
CHANGED
@@ -22,13 +22,38 @@ examples/smtp_tls.rb
|
|
22
22
|
examples/test-datatypes.rb
|
23
23
|
examples/yaml-example.rb
|
24
24
|
examples/yaml-example2.rb
|
25
|
-
lib/doodle.rb
|
26
25
|
lib/doodle/app.rb
|
26
|
+
lib/doodle/attribute.rb
|
27
|
+
lib/doodle/base.rb
|
28
|
+
lib/doodle/collector-1.9.rb
|
29
|
+
lib/doodle/collector.rb
|
30
|
+
lib/doodle/comparable.rb
|
31
|
+
lib/doodle/conversion.rb
|
32
|
+
lib/doodle/core.rb
|
33
|
+
lib/doodle/datatype-holder.rb
|
27
34
|
lib/doodle/datatypes.rb
|
35
|
+
lib/doodle/debug.rb
|
36
|
+
lib/doodle/deferred.rb
|
37
|
+
lib/doodle/equality.rb
|
38
|
+
lib/doodle/exceptions.rb
|
39
|
+
lib/doodle/factory.rb
|
40
|
+
lib/doodle/getter-setter.rb
|
41
|
+
lib/doodle/info.rb
|
42
|
+
lib/doodle/inherit.rb
|
43
|
+
lib/doodle/json.rb
|
44
|
+
lib/doodle/marshal.rb
|
45
|
+
lib/doodle/normalized_array.rb
|
46
|
+
lib/doodle/normalized_hash.rb
|
47
|
+
lib/doodle/ordered-hash.rb
|
28
48
|
lib/doodle/rfc822.rb
|
49
|
+
lib/doodle/singleton.rb
|
50
|
+
lib/doodle/smoke-and-mirrors.rb
|
51
|
+
lib/doodle/to_hash.rb
|
29
52
|
lib/doodle/utils.rb
|
53
|
+
lib/doodle/validation.rb
|
30
54
|
lib/doodle/version.rb
|
31
55
|
lib/doodle/xml.rb
|
56
|
+
lib/doodle.rb
|
32
57
|
lib/molic_orderedhash.rb
|
33
58
|
log/debug.log
|
34
59
|
script/console
|
data/README.txt
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
* Homepage: http://doodle.rubyforge.org
|
4
4
|
* Github repo: http://github.com/seanohalpin/doodle/tree/master
|
5
|
+
* Lighthouse issue tracker: http://seanohalpin.lighthouseapp.com/projects/26673-doodle/overview
|
5
6
|
|
6
7
|
== DESCRIPTION:
|
7
8
|
|
@@ -16,7 +17,7 @@ Doodle has been tested with Ruby 1.8.6, 1.9.1 and JRuby 1.1. It has
|
|
16
17
|
not yet been tested with Rubinius.
|
17
18
|
|
18
19
|
Please feel free to post bug reports, feature requests, and any
|
19
|
-
comments or discussion topics to the doodle Google group:
|
20
|
+
comments or discussion topics to the doodle Google group:
|
20
21
|
http://groups.google.com/group/ruby-doodle
|
21
22
|
|
22
23
|
== FEATURES:
|
@@ -40,7 +41,7 @@ http://groups.google.com/group/ruby-doodle
|
|
40
41
|
require 'date'
|
41
42
|
require 'doodle'
|
42
43
|
|
43
|
-
class DateRange < Doodle
|
44
|
+
class DateRange < Doodle
|
44
45
|
has :start_date do
|
45
46
|
default { Date.today }
|
46
47
|
end
|
@@ -60,7 +61,7 @@ http://groups.google.com/group/ruby-doodle
|
|
60
61
|
require 'doodle'
|
61
62
|
require 'doodle/utils' # for try
|
62
63
|
|
63
|
-
class DateRange < Doodle
|
64
|
+
class DateRange < Doodle
|
64
65
|
has :start_date, :kind => Date do
|
65
66
|
default { Date.today }
|
66
67
|
from String do |s|
|
@@ -119,14 +120,14 @@ http://groups.google.com/group/ruby-doodle
|
|
119
120
|
|
120
121
|
p try {
|
121
122
|
dr = DateRange.from 'Hello World'
|
122
|
-
dr.start_date # =>
|
123
|
-
dr.end_date # =>
|
123
|
+
dr.start_date # =>
|
124
|
+
dr.end_date # =>
|
124
125
|
}
|
125
126
|
|
126
127
|
p try {
|
127
|
-
dr = DateRange '2008-01-01', '2007-12-31'
|
128
|
-
dr.start_date # =>
|
129
|
-
dr.end_date # =>
|
128
|
+
dr = DateRange '2008-01-01', '2007-12-31'
|
129
|
+
dr.start_date # =>
|
130
|
+
dr.end_date # =>
|
130
131
|
}
|
131
132
|
# >> #<Doodle::ConversionError: Cannot parse date: 'Hello World'>
|
132
133
|
# >> #<Doodle::ValidationError: DateRange must have end_date >= start_date>
|
data/lib/doodle.rb
CHANGED
@@ -1,1511 +1,58 @@
|
|
1
|
-
# doodle
|
2
|
-
#
|
1
|
+
# *doodle* is an eco-friendly metaprogramming framework that does not
|
2
|
+
# pollute core Ruby objects such as Object, Class and Module.
|
3
|
+
#
|
4
|
+
# While doodle itself is useful for defining classes, my main goal is to
|
5
|
+
# come up with a useful DSL notation for class definitions which can be
|
6
|
+
# reused in many contexts.
|
7
|
+
#
|
8
|
+
# Docs at http://doodle.rubyforge.org
|
9
|
+
#
|
10
|
+
# Requires Ruby 1.8.6 or higher
|
11
|
+
#
|
3
12
|
# Copyright (C) 2007-2009 by Sean O'Halpin
|
13
|
+
#
|
4
14
|
# 2007-11-24 first version
|
5
|
-
# 2008-04-18
|
15
|
+
# 2008-04-18 0.0.12
|
6
16
|
# 2008-05-07 0.1.6
|
7
17
|
# 2008-05-12 0.1.7
|
8
18
|
# 2009-02-26 0.2.0
|
9
|
-
#
|
19
|
+
# 2009-03-11 0.2.3
|
20
|
+
|
10
21
|
if RUBY_VERSION < '1.8.6'
|
11
22
|
raise Exception, "Sorry - doodle does not work with versions of Ruby below 1.8.6"
|
12
23
|
end
|
13
24
|
|
25
|
+
# set up load path
|
14
26
|
$:.unshift(File.dirname(__FILE__)) unless
|
15
27
|
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
16
28
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
require '
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
def parent
|
45
|
-
context[-1]
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# two doodles of the same class with the same attribute values are
|
50
|
-
# considered equal
|
51
|
-
module Equality
|
52
|
-
def eql?(o)
|
53
|
-
# p [:comparing, self.class, o.class, self.class == o.class]
|
54
|
-
# p [:values, self.doodle.values, o.doodle.values, self.doodle.values == o.doodle.values]
|
55
|
-
# p [:attributes, doodle.attributes.map { |k, a| [k, send(k).==(o.send(k))] }]
|
56
|
-
res = self.class == o.class &&
|
57
|
-
#self.doodle.values == o.doodle.values
|
58
|
-
# short circuit comparison
|
59
|
-
doodle.attributes.all? { |k, a| send(k).==(o.send(k)) }
|
60
|
-
# p [:res, res]
|
61
|
-
res
|
62
|
-
end
|
63
|
-
def ==(o)
|
64
|
-
eql?(o)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
# doodles are compared (sorted) on values
|
69
|
-
module Comparable
|
70
|
-
def <=>(o)
|
71
|
-
doodle.values <=> o.doodle.values
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
# debugging utilities
|
76
|
-
module Debug
|
77
|
-
class << self
|
78
|
-
# output result of block if ENV['DEBUG_DOODLE'] set
|
79
|
-
def d(&block)
|
80
|
-
p(block.call) if ENV['DEBUG_DOODLE']
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
# Place to hold ref to built-in classes that need special handling
|
86
|
-
module BuiltIns
|
87
|
-
BUILTINS = [String, Hash, Array]
|
88
|
-
end
|
89
|
-
|
90
|
-
# Set of utility functions to avoid monkeypatching base classes
|
91
|
-
module Utils
|
92
|
-
class << self
|
93
|
-
# Unnest arrays by one level of nesting, e.g. [1, [[2], 3]] => [1, [2], 3].
|
94
|
-
def flatten_first_level(enum)
|
95
|
-
enum.inject([]) {|arr, i|
|
96
|
-
if i.kind_of?(Array)
|
97
|
-
arr.push(*i)
|
98
|
-
else
|
99
|
-
arr.push(i)
|
100
|
-
end
|
101
|
-
}
|
102
|
-
end
|
103
|
-
# from facets/string/case.rb, line 80
|
104
|
-
def snake_case(camel_cased_word)
|
105
|
-
# if all caps, just downcase it
|
106
|
-
if camel_cased_word =~ /^[A-Z]+$/
|
107
|
-
camel_cased_word.downcase
|
108
|
-
else
|
109
|
-
camel_cased_word.to_s.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
|
110
|
-
end
|
111
|
-
end
|
112
|
-
# resolve a constant of the form Some::Class::Or::Module -
|
113
|
-
# doesn't work with constants defined in anonymous
|
114
|
-
# classes/modules
|
115
|
-
def const_resolve(constant)
|
116
|
-
constant.to_s.split(/::/).reject{|x| x.empty?}.inject(Object) { |prev, this| prev.const_get(this) }
|
117
|
-
end
|
118
|
-
# deep copy of object (unlike shallow copy dup or clone)
|
119
|
-
def deep_copy(obj)
|
120
|
-
Marshal.load(Marshal.dump(obj))
|
121
|
-
end
|
122
|
-
# normalize hash keys using method (e.g. :to_sym, :to_s)
|
123
|
-
# - updates target hash
|
124
|
-
# - optionally recurse into child hashes
|
125
|
-
def normalize_keys!(hash, recursive = false, method = :to_sym)
|
126
|
-
if hash.kind_of?(Hash)
|
127
|
-
hash.keys.each do |key|
|
128
|
-
normalized_key = key.respond_to?(method) ? key.send(method) : key
|
129
|
-
v = hash.delete(key)
|
130
|
-
if recursive
|
131
|
-
if v.kind_of?(Hash)
|
132
|
-
v = normalize_keys!(v, recursive, method)
|
133
|
-
elsif v.kind_of?(Array)
|
134
|
-
v = v.map{ |x| normalize_keys!(x, recursive, method) }
|
135
|
-
end
|
136
|
-
end
|
137
|
-
hash[normalized_key] = v
|
138
|
-
end
|
139
|
-
end
|
140
|
-
hash
|
141
|
-
end
|
142
|
-
# normalize hash keys using method (e.g. :to_sym, :to_s)
|
143
|
-
# - returns copy of hash
|
144
|
-
# - optionally recurse into child hashes
|
145
|
-
def normalize_keys(hash, recursive = false, method = :to_sym)
|
146
|
-
if recursive
|
147
|
-
h = deep_copy(hash)
|
148
|
-
else
|
149
|
-
h = hash.dup
|
150
|
-
end
|
151
|
-
normalize_keys!(h, recursive, method)
|
152
|
-
end
|
153
|
-
# convert keys to symbols
|
154
|
-
# - updates target hash in place
|
155
|
-
# - optionally recurse into child hashes
|
156
|
-
def symbolize_keys!(hash, recursive = false)
|
157
|
-
normalize_keys!(hash, recursive, :to_sym)
|
158
|
-
end
|
159
|
-
# convert keys to symbols
|
160
|
-
# - returns copy of hash
|
161
|
-
# - optionally recurse into child hashes
|
162
|
-
def symbolize_keys(hash, recursive = false)
|
163
|
-
normalize_keys(hash, recursive, :to_sym)
|
164
|
-
end
|
165
|
-
# convert keys to strings
|
166
|
-
# - updates target hash in place
|
167
|
-
# - optionally recurse into child hashes
|
168
|
-
def stringify_keys!(hash, recursive = false)
|
169
|
-
normalize_keys!(hash, recursive, :to_s)
|
170
|
-
end
|
171
|
-
# convert keys to strings
|
172
|
-
# - returns copy of hash
|
173
|
-
# - optionally recurse into child hashes
|
174
|
-
def stringify_keys(hash, recursive = false)
|
175
|
-
normalize_keys(hash, recursive, :to_s)
|
176
|
-
end
|
177
|
-
# simple (!) pluralization - if you want fancier, override this method
|
178
|
-
def pluralize(string)
|
179
|
-
s = string.to_s
|
180
|
-
if s =~ /s$/
|
181
|
-
s + 'es'
|
182
|
-
else
|
183
|
-
s + 's'
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
# caller
|
188
|
-
def doodle_caller
|
189
|
-
if $DEBUG
|
190
|
-
caller
|
191
|
-
else
|
192
|
-
[caller[-1]]
|
193
|
-
end
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
# error handling
|
199
|
-
@@raise_exception_on_error = true
|
200
|
-
def self.raise_exception_on_error
|
201
|
-
@@raise_exception_on_error
|
202
|
-
end
|
203
|
-
def self.raise_exception_on_error=(tf)
|
204
|
-
@@raise_exception_on_error = tf
|
205
|
-
end
|
206
|
-
|
207
|
-
# internal error raised when a default was expected but not found
|
208
|
-
class NoDefaultError < Exception
|
209
|
-
end
|
210
|
-
# raised when a validation rule returns false
|
211
|
-
class ValidationError < Exception
|
212
|
-
end
|
213
|
-
# raised when an unknown parameter is passed to initialize
|
214
|
-
class UnknownAttributeError < Exception
|
215
|
-
end
|
216
|
-
# raised when a conversion fails
|
217
|
-
class ConversionError < Exception
|
218
|
-
end
|
219
|
-
# raised when arg_order called with incorrect arguments
|
220
|
-
class InvalidOrderError < Exception
|
221
|
-
end
|
222
|
-
# raised when try to set a readonly attribute after initialization
|
223
|
-
class ReadOnlyError < Exception
|
224
|
-
end
|
225
|
-
|
226
|
-
# provides more direct access to the singleton class and a way to
|
227
|
-
# treat singletons, Modules and Classes equally in a meta context
|
228
|
-
module SelfClass
|
229
|
-
# return the 'singleton class' of an object, optionally executing
|
230
|
-
# a block argument in the (module/class) context of that object
|
231
|
-
def singleton_class(&block)
|
232
|
-
sc = class << self; self; end
|
233
|
-
sc.module_eval(&block) if block_given?
|
234
|
-
sc
|
235
|
-
end
|
236
|
-
# evaluate in class context of self, whether Class, Module or singleton
|
237
|
-
def sc_eval(*args, &block)
|
238
|
-
if self.kind_of?(Module)
|
239
|
-
klass = self
|
240
|
-
else
|
241
|
-
klass = self.singleton_class
|
242
|
-
end
|
243
|
-
klass.module_eval(*args, &block)
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
# = embrace
|
248
|
-
# the intent of embrace is to provide a way to create directives
|
249
|
-
# that affect all members of a class 'family' without having to
|
250
|
-
# modify Module, Class or Object - in some ways, it's similar to Ara
|
251
|
-
# Howard's mixable[http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/197296]
|
252
|
-
# though not as tidy :S
|
253
|
-
#
|
254
|
-
# this works down to third level <tt>class << self</tt> - in practice, this is
|
255
|
-
# perfectly good - it would be great to have a completely general
|
256
|
-
# solution but I'm doubt whether the payoff is worth the effort
|
257
|
-
|
258
|
-
module Embrace
|
259
|
-
# fake module inheritance chain
|
260
|
-
def embrace(other, &block)
|
261
|
-
# include in instance method chain
|
262
|
-
include other
|
263
|
-
sc = class << self; self; end
|
264
|
-
sc.module_eval {
|
265
|
-
# class method chain
|
266
|
-
include other
|
267
|
-
# singleton method chain
|
268
|
-
extend other
|
269
|
-
# ensure that subclasses are also embraced
|
270
|
-
define_method :inherited do |klass|
|
271
|
-
#p [:embrace, :inherited, klass]
|
272
|
-
klass.__send__(:embrace, other) # n.b. closure
|
273
|
-
klass.__send__(:include, Factory) # is there another way to do this? i.e. not in embrace
|
274
|
-
#super(klass) if defined?(super)
|
275
|
-
end
|
276
|
-
}
|
277
|
-
sc.module_eval(&block) if block_given?
|
278
|
-
end
|
279
|
-
end
|
280
|
-
|
281
|
-
# save a block for later execution
|
282
|
-
class DeferredBlock
|
283
|
-
attr_accessor :block
|
284
|
-
def initialize(arg_block = nil, &block)
|
285
|
-
arg_block = block if block_given?
|
286
|
-
@block = arg_block
|
287
|
-
end
|
288
|
-
def call(*a, &b)
|
289
|
-
block.call(*a, &b)
|
290
|
-
end
|
291
|
-
end
|
292
|
-
|
293
|
-
# A Validation represents a validation rule applied to the instance
|
294
|
-
# after initialization. Generated using the Doodle::BaseMethods#must directive.
|
295
|
-
class Validation
|
296
|
-
attr_accessor :message
|
297
|
-
attr_accessor :block
|
298
|
-
# create a new validation rule. This is typically a result of
|
299
|
-
# calling +must+ so the text should work following the word
|
300
|
-
# "must", e.g. "must not be nil", "must be >= 10", etc.
|
301
|
-
def initialize(message = 'not be nil', &block)
|
302
|
-
@message = message
|
303
|
-
@block = block_given? ? block : proc { |x| !self.nil? }
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
|
-
# place to stash bookkeeping info
|
308
|
-
class DoodleInfo
|
309
|
-
attr_accessor :this
|
310
|
-
attr_accessor :local_attributes
|
311
|
-
attr_accessor :local_validations
|
312
|
-
attr_accessor :local_conversions
|
313
|
-
attr_accessor :validation_on
|
314
|
-
attr_accessor :arg_order
|
315
|
-
attr_accessor :errors
|
316
|
-
attr_accessor :parent
|
317
|
-
|
318
|
-
def initialize(object)
|
319
|
-
@this = object
|
320
|
-
@local_attributes = Doodle::OrderedHash.new
|
321
|
-
@local_validations = []
|
322
|
-
@validation_on = true
|
323
|
-
@local_conversions = {}
|
324
|
-
@arg_order = []
|
325
|
-
@errors = []
|
326
|
-
#@parent = nil
|
327
|
-
@parent = Doodle.parent
|
328
|
-
end
|
329
|
-
# hide from inspect
|
330
|
-
m = instance_method(:inspect)
|
331
|
-
define_method :__inspect__ do
|
332
|
-
m.bind(self).call
|
333
|
-
end
|
334
|
-
def inspect
|
335
|
-
''
|
336
|
-
end
|
337
|
-
|
338
|
-
# handle errors either by collecting in :errors or raising an exception
|
339
|
-
def handle_error(name, *args)
|
340
|
-
# don't include duplicates (FIXME: hacky - shouldn't have duplicates in the first place)
|
341
|
-
if !errors.include?([name, *args])
|
342
|
-
errors << [name, *args]
|
343
|
-
end
|
344
|
-
if Doodle.raise_exception_on_error
|
345
|
-
raise(*args)
|
346
|
-
end
|
347
|
-
end
|
348
|
-
|
349
|
-
# provide an alternative inheritance chain that works for singleton
|
350
|
-
# classes as well as modules, classes and instances
|
351
|
-
def parents
|
352
|
-
anc = if @this.respond_to?(:ancestors)
|
353
|
-
if @this.ancestors.include?(@this)
|
354
|
-
@this.ancestors[1..-1]
|
355
|
-
else
|
356
|
-
# singletons have no doodle_parents (they're orphans)
|
357
|
-
[]
|
358
|
-
end
|
359
|
-
else
|
360
|
-
@this.class.ancestors
|
361
|
-
end
|
362
|
-
anc.select{|x| x.kind_of?(Class)}
|
363
|
-
end
|
364
|
-
|
365
|
-
# send message to all doodle_parents and collect results
|
366
|
-
def collect_inherited(message)
|
367
|
-
result = []
|
368
|
-
parents.each do |klass|
|
369
|
-
if klass.respond_to?(:doodle) && klass.doodle.respond_to?(message)
|
370
|
-
result.unshift(*klass.doodle.__send__(message))
|
371
|
-
else
|
372
|
-
break
|
373
|
-
end
|
374
|
-
end
|
375
|
-
result
|
376
|
-
end
|
377
|
-
|
378
|
-
def handle_inherited_hash(tf, method)
|
379
|
-
if tf
|
380
|
-
collect_inherited(method).inject(Doodle::OrderedHash.new){ |hash, item|
|
381
|
-
hash.merge(Doodle::OrderedHash[*item])
|
382
|
-
}.merge(@this.doodle.__send__(method))
|
383
|
-
else
|
384
|
-
@this.doodle.__send__(method)
|
385
|
-
end
|
386
|
-
end
|
387
|
-
|
388
|
-
# returns array of Attributes
|
389
|
-
# - if tf == true, returns all inherited attributes
|
390
|
-
# - if tf == false, returns only those attributes defined in the current object/class
|
391
|
-
def attributes(tf = true)
|
392
|
-
results = handle_inherited_hash(tf, :local_attributes)
|
393
|
-
# if an instance, include the singleton_class attributes
|
394
|
-
if !@this.kind_of?(Class) && @this.singleton_class.doodle.respond_to?(:attributes)
|
395
|
-
results = results.merge(@this.singleton_class.doodle.attributes)
|
396
|
-
end
|
397
|
-
results
|
398
|
-
end
|
399
|
-
|
400
|
-
# returns array of values
|
401
|
-
# - if tf == true, returns all inherited values (default)
|
402
|
-
# - if tf == false, returns only those values defined in current object
|
403
|
-
def values(tf = true)
|
404
|
-
attributes(tf).map{ |k, a| @this.send(k)}
|
405
|
-
end
|
406
|
-
|
407
|
-
# returns array of attribute names
|
408
|
-
# - if tf == true, returns all inherited attribute names (default)
|
409
|
-
# - if tf == false, returns only those attribute names defined in current object
|
410
|
-
def keys(tf = true)
|
411
|
-
attributes(tf).keys
|
412
|
-
end
|
413
|
-
|
414
|
-
# return class level attributes
|
415
|
-
def class_attributes
|
416
|
-
attrs = Doodle::OrderedHash.new
|
417
|
-
if @this.kind_of?(Class)
|
418
|
-
attrs = collect_inherited(:class_attributes).inject(Doodle::OrderedHash.new){ |hash, item|
|
419
|
-
hash.merge(Doodle::OrderedHash[*item])
|
420
|
-
}.merge(@this.singleton_class.doodle.respond_to?(:attributes) ? @this.singleton_class.doodle.attributes : { })
|
421
|
-
attrs
|
422
|
-
else
|
423
|
-
@this.class.doodle.class_attributes
|
424
|
-
end
|
425
|
-
end
|
426
|
-
|
427
|
-
def validations(tf = true)
|
428
|
-
if tf
|
429
|
-
# note: validations are handled differently to attributes and
|
430
|
-
# conversions because ~all~ validations apply (so are stored
|
431
|
-
# as an array), whereas attributes and conversions are keyed
|
432
|
-
# by name and kind respectively, so only the most recent
|
433
|
-
# applies
|
434
|
-
|
435
|
-
local_validations + collect_inherited(:local_validations)
|
436
|
-
else
|
437
|
-
local_validations
|
438
|
-
end
|
439
|
-
end
|
440
|
-
|
441
|
-
def lookup_attribute(name)
|
442
|
-
# (look at singleton attributes first)
|
443
|
-
# fixme[this smells like a hack to me]
|
444
|
-
if @this.class == Class
|
445
|
-
class_attributes[name]
|
446
|
-
else
|
447
|
-
attributes[name]
|
448
|
-
end
|
449
|
-
end
|
450
|
-
|
451
|
-
# returns hash of conversions
|
452
|
-
# - if tf == true, returns all inherited conversions
|
453
|
-
# - if tf == false, returns only those conversions defined in the current object/class
|
454
|
-
def conversions(tf = true)
|
455
|
-
handle_inherited_hash(tf, :local_conversions)
|
456
|
-
end
|
457
|
-
|
458
|
-
def initial_values(tf = true)
|
459
|
-
attributes(tf).select{|n, a| a.init_defined? }.inject({}) {|hash, (n, a)|
|
460
|
-
#p [:initial_values, a.name]
|
461
|
-
hash[n] = case a.init
|
462
|
-
when NilClass, TrueClass, FalseClass, Fixnum, Float, Bignum, Symbol
|
463
|
-
# uncloneable values
|
464
|
-
#p [:initial_values, :special, a.name, a.init]
|
465
|
-
a.init
|
466
|
-
when DeferredBlock
|
467
|
-
#p [:initial_values, self, DeferredBlock, a.name]
|
468
|
-
begin
|
469
|
-
@this.instance_eval(&a.init.block)
|
470
|
-
rescue Object => e
|
471
|
-
#p [:exception_in_deferred_block, e]
|
472
|
-
raise
|
473
|
-
end
|
474
|
-
else
|
475
|
-
#p [:initial_values, :clone, a.name]
|
476
|
-
begin
|
477
|
-
a.init.clone
|
478
|
-
rescue Exception => e
|
479
|
-
warn "tried to clone #{a.init.class} in :init option (#{e})"
|
480
|
-
#p [:initial_values, :exception, a.name, e]
|
481
|
-
a.init
|
482
|
-
end
|
483
|
-
end
|
484
|
-
hash
|
485
|
-
}
|
486
|
-
end
|
487
|
-
|
488
|
-
# turn off validation, execute block, then set validation to same
|
489
|
-
# state as it was before +defer_validation+ was called - can be nested
|
490
|
-
def defer_validation(&block)
|
491
|
-
old_validation = self.validation_on
|
492
|
-
self.validation_on = false
|
493
|
-
v = nil
|
494
|
-
begin
|
495
|
-
v = @this.instance_eval(&block)
|
496
|
-
ensure
|
497
|
-
self.validation_on = old_validation
|
498
|
-
end
|
499
|
-
@this.validate!(false)
|
500
|
-
v
|
501
|
-
end
|
502
|
-
|
503
|
-
# helper function to initialize from hash - this is safe to use
|
504
|
-
# after initialization (validate! is called if this method is
|
505
|
-
# called after initialization)
|
506
|
-
def initialize_from_hash(*args)
|
507
|
-
# p [:doodle_initialize_from_hash, :args, *args]
|
508
|
-
defer_validation do
|
509
|
-
# hash initializer
|
510
|
-
# separate into array of hashes of form [{:k1 => v1}, {:k2 => v2}] and positional args
|
511
|
-
key_values, args = args.partition{ |x| x.kind_of?(Hash)}
|
512
|
-
#DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :key_values, key_values, :args, args] }
|
513
|
-
#!p [self.class, :doodle_initialize_from_hash, :key_values, key_values, :args, args]
|
514
|
-
|
515
|
-
# set up initial values with ~clones~ of specified values (so not shared between instances)
|
516
|
-
#init_values = initial_values
|
517
|
-
#!p [:init_values, init_values]
|
518
|
-
|
519
|
-
# match up positional args with attribute names (from arg_order) using idiom to create hash from array of assocs
|
520
|
-
#arg_keywords = init_values.merge(Hash[*(Utils.flatten_first_level(self.class.arg_order[0...args.size].zip(args)))])
|
521
|
-
arg_keywords = Hash[*(Utils.flatten_first_level(self.class.arg_order[0...args.size].zip(args)))]
|
522
|
-
#!p [self.class, :doodle_initialize_from_hash, :arg_keywords, arg_keywords]
|
523
|
-
|
524
|
-
# merge all hash args into one
|
525
|
-
key_values = key_values.inject(arg_keywords) { |hash, item|
|
526
|
-
#!p [self.class, :doodle_initialize_from_hash, :merge, hash, item]
|
527
|
-
hash.merge(item)
|
528
|
-
}
|
529
|
-
#!p [self.class, :doodle_initialize_from_hash, :key_values2, key_values]
|
530
|
-
|
531
|
-
# convert keys to symbols (note not recursively - only first level == doodle keywords)
|
532
|
-
Doodle::Utils.symbolize_keys!(key_values)
|
533
|
-
#DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :key_values2, key_values, :args2, args] }
|
534
|
-
#!p [self.class, :doodle_initialize_from_hash, :key_values3, key_values]
|
535
|
-
|
536
|
-
# create attributes
|
537
|
-
key_values.keys.each do |key|
|
538
|
-
#DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :setting, key, key_values[key]] }
|
539
|
-
#p [self.class, :doodle_initialize_from_hash, :setting, key, key_values[key]]
|
540
|
-
if respond_to?(key)
|
541
|
-
__send__(key, key_values[key])
|
542
|
-
else
|
543
|
-
# raise error if not defined
|
544
|
-
__doodle__.handle_error key, Doodle::UnknownAttributeError, "unknown attribute '#{key}' => #{key_values[key].inspect} for #{self} #{doodle.attributes.map{ |k,v| k.inspect}.join(', ')}", Doodle::Utils.doodle_caller
|
545
|
-
end
|
546
|
-
end
|
547
|
-
# do init_values after user supplied values so init blocks can depend on user supplied values
|
548
|
-
#p [:getting_init_values, instance_variables]
|
549
|
-
__doodle__.initial_values.each do |key, value|
|
550
|
-
if !key_values.key?(key) && respond_to?(key)
|
551
|
-
#p [:initial_values, key, value]
|
552
|
-
__send__(key, value)
|
553
|
-
end
|
554
|
-
end
|
555
|
-
end
|
556
|
-
end
|
557
|
-
|
558
|
-
end
|
559
|
-
|
560
|
-
# what it says on the tin :) various hacks to hide @__doodle__ variable
|
561
|
-
module SmokeAndMirrors
|
562
|
-
# redefine instance_variables to ignore our private @__doodle__ variable
|
563
|
-
# (hack to fool yaml and anything else that queries instance_variables)
|
564
|
-
meth = Object.instance_method(:instance_variables)
|
565
|
-
define_method :instance_variables do
|
566
|
-
meth.bind(self).call.reject{ |x| x.to_s =~ /@__doodle__/}
|
567
|
-
end
|
568
|
-
# hide @__doodle__ from inspect
|
569
|
-
def inspect
|
570
|
-
super.gsub(/\s*@__doodle__=,/,'').gsub(/,?\s*@__doodle__=/,'')
|
571
|
-
end
|
572
|
-
# fix for pp
|
573
|
-
def pretty_print(q)
|
574
|
-
q.pp_object(self)
|
575
|
-
end
|
576
|
-
end
|
577
|
-
|
578
|
-
# implements the #doodle directive
|
579
|
-
class DataTypeHolder
|
580
|
-
attr_accessor :klass
|
581
|
-
def initialize(klass, &block)
|
582
|
-
@klass = klass
|
583
|
-
instance_eval(&block) if block_given?
|
584
|
-
end
|
585
|
-
def define(name, params, block, type_params, &type_block)
|
586
|
-
@klass.class_eval {
|
587
|
-
td = has(name, type_params.merge(params), &type_block)
|
588
|
-
td.instance_eval(&block) if block
|
589
|
-
td
|
590
|
-
}
|
591
|
-
end
|
592
|
-
def has(*args, &block)
|
593
|
-
@klass.class_eval { has(*args, &block) }
|
594
|
-
end
|
595
|
-
def must(*args, &block)
|
596
|
-
@klass.class_eval { must(*args, &block) }
|
597
|
-
end
|
598
|
-
def from(*args, &block)
|
599
|
-
@klass.class_eval { from(*args, &block) }
|
600
|
-
end
|
601
|
-
def arg_order(*args, &block)
|
602
|
-
@klass.class_eval { arg_order(*args, &block) }
|
603
|
-
end
|
604
|
-
def doc(*args, &block)
|
605
|
-
@klass.class_eval { doc(*args, &block) }
|
606
|
-
end
|
607
|
-
end
|
608
|
-
|
609
|
-
# the core module of Doodle - however, to get most facilities
|
610
|
-
# provided by Doodle without inheriting from Doodle, include
|
611
|
-
# Doodle::Core, not this module
|
612
|
-
module BaseMethods
|
613
|
-
include SelfClass
|
614
|
-
include SmokeAndMirrors
|
615
|
-
|
616
|
-
# NOTE: can't do either of these
|
617
|
-
|
618
|
-
# include Equality
|
619
|
-
# include Comparable
|
620
|
-
|
621
|
-
# def self.included(other)
|
622
|
-
# other.module_eval {
|
623
|
-
# include Equality
|
624
|
-
# include Comparable
|
625
|
-
# }
|
626
|
-
# end
|
627
|
-
|
628
|
-
# this is the only way to get at internal values. Note: this is
|
629
|
-
# initialized on the fly rather than in #initialize because
|
630
|
-
# classes and singletons don't call #initialize
|
631
|
-
def __doodle__
|
632
|
-
@__doodle__ ||= DoodleInfo.new(self)
|
633
|
-
end
|
634
|
-
protected :__doodle__
|
635
|
-
|
636
|
-
# set up global datatypes
|
637
|
-
def datatypes(*mods)
|
638
|
-
mods.each do |mod|
|
639
|
-
DataTypeHolder.class_eval { include mod }
|
640
|
-
end
|
641
|
-
end
|
642
|
-
|
643
|
-
# vector through this method to get to doodle info or enable global
|
644
|
-
# datatypes and provide an interface that allows you to add your own
|
645
|
-
# datatypes to this declaration
|
646
|
-
def doodle(*mods, &block)
|
647
|
-
if mods.size == 0 && !block_given?
|
648
|
-
__doodle__
|
649
|
-
else
|
650
|
-
dh = Doodle::DataTypeHolder.new(self)
|
651
|
-
mods.each do |mod|
|
652
|
-
dh.extend(mod)
|
653
|
-
end
|
654
|
-
dh.instance_eval(&block)
|
655
|
-
end
|
656
|
-
end
|
657
|
-
|
658
|
-
# helper for Marshal.dump
|
659
|
-
def marshal_dump
|
660
|
-
# note: perhaps should also dump singleton attribute definitions?
|
661
|
-
instance_variables.map{|x| [x, instance_variable_get(x)] }
|
662
|
-
end
|
663
|
-
# helper for Marshal.load
|
664
|
-
def marshal_load(data)
|
665
|
-
data.each do |name, value|
|
666
|
-
instance_variable_set(name, value)
|
667
|
-
end
|
668
|
-
end
|
669
|
-
|
670
|
-
# either get an attribute value (if no args given) or set it
|
671
|
-
# (using args and/or block)
|
672
|
-
# FIXME: move
|
673
|
-
def getter_setter(name, *args, &block)
|
674
|
-
#p [:getter_setter, name]
|
675
|
-
name = name.to_sym
|
676
|
-
if block_given? || args.size > 0
|
677
|
-
#!p [:getter_setter, :setter, name, *args]
|
678
|
-
_setter(name, *args, &block)
|
679
|
-
else
|
680
|
-
#!p [:getter_setter, :getter, name]
|
681
|
-
_getter(name)
|
682
|
-
end
|
683
|
-
end
|
684
|
-
private :getter_setter
|
685
|
-
|
686
|
-
# get an attribute by name - return default if not otherwise defined
|
687
|
-
# FIXME: init deferred blocks are not getting resolved in all cases
|
688
|
-
def _getter(name, &block)
|
689
|
-
begin
|
690
|
-
#p [:_getter, name]
|
691
|
-
ivar = "@#{name}"
|
692
|
-
if instance_variable_defined?(ivar)
|
693
|
-
#p [:_getter, :instance_variable_defined, name, ivar, instance_variable_get(ivar)]
|
694
|
-
instance_variable_get(ivar)
|
695
|
-
else
|
696
|
-
# handle default
|
697
|
-
# Note: use :init => value to cover cases where defaults don't work
|
698
|
-
# (e.g. arrays that disappear when you go out of scope)
|
699
|
-
att = __doodle__.lookup_attribute(name)
|
700
|
-
# special case for class/singleton :init
|
701
|
-
if att && att.optional?
|
702
|
-
optional_value = att.init_defined? ? att.init : att.default
|
703
|
-
#p [:optional_value, optional_value]
|
704
|
-
case optional_value
|
705
|
-
when DeferredBlock
|
706
|
-
#p [:deferred_block]
|
707
|
-
v = instance_eval(&optional_value.block)
|
708
|
-
when Proc
|
709
|
-
v = instance_eval(&optional_value)
|
710
|
-
else
|
711
|
-
v = optional_value
|
712
|
-
end
|
713
|
-
if att.init_defined?
|
714
|
-
_setter(name, v)
|
715
|
-
end
|
716
|
-
v
|
717
|
-
else
|
718
|
-
# This is an internal error (i.e. shouldn't happen)
|
719
|
-
__doodle__.handle_error name, NoDefaultError, "'#{name}' has no default defined", Doodle::Utils.doodle_caller
|
720
|
-
end
|
721
|
-
end
|
722
|
-
rescue Object => e
|
723
|
-
__doodle__.handle_error name, e, e.to_s, Doodle::Utils.doodle_caller
|
724
|
-
end
|
725
|
-
end
|
726
|
-
private :_getter
|
727
|
-
|
728
|
-
def after_update(params)
|
729
|
-
end
|
730
|
-
|
731
|
-
# set an instance variable by symbolic name and call after_update if changed
|
732
|
-
def ivar_set(name, *args)
|
733
|
-
ivar = "@#{name}"
|
734
|
-
if instance_variable_defined?(ivar)
|
735
|
-
old_value = instance_variable_get(ivar)
|
736
|
-
else
|
737
|
-
old_value = nil
|
738
|
-
end
|
739
|
-
instance_variable_set(ivar, *args)
|
740
|
-
new_value = instance_variable_get(ivar)
|
741
|
-
if new_value != old_value
|
742
|
-
#pp [Doodle, :after_update, { :instance => self, :name => name, :old_value => old_value, :new_value => new_value }]
|
743
|
-
after_update :instance => self, :name => name, :old_value => old_value, :new_value => new_value
|
744
|
-
end
|
745
|
-
end
|
746
|
-
private :ivar_set
|
747
|
-
|
748
|
-
# set an attribute by name - apply validation if defined
|
749
|
-
# FIXME: move
|
750
|
-
def _setter(name, *args, &block)
|
751
|
-
##DBG: Doodle::Debug.d { [:_setter, name, args] }
|
752
|
-
#p [:_setter, name, *args]
|
753
|
-
att = __doodle__.lookup_attribute(name)
|
754
|
-
if att && doodle.validation_on && att.readonly
|
755
|
-
raise Doodle::ReadOnlyError, "Trying to set a readonly attribute: #{att.name}", Doodle::Utils.doodle_caller
|
756
|
-
end
|
757
|
-
if block_given?
|
758
|
-
# if a class has been defined, let's assume it can take a
|
759
|
-
# block initializer (test that it's a Doodle or Proc)
|
760
|
-
if att.kind && !att.abstract && klass = att.kind.first
|
761
|
-
if [Doodle, Proc].any?{ |c| klass <= c }
|
762
|
-
# p [:_setter, '# 1 converting arg to value with kind ' + klass.to_s]
|
763
|
-
args = [klass.new(*args, &block)]
|
764
|
-
else
|
765
|
-
__doodle__.handle_error att.name, ArgumentError, "#{klass} #{att.name} does not take a block initializer", Doodle::Utils.doodle_caller
|
766
|
-
end
|
767
|
-
else
|
768
|
-
# this is used by init do ... block
|
769
|
-
args.unshift(DeferredBlock.new(block))
|
770
|
-
end
|
771
|
-
end
|
772
|
-
if att # = __doodle__.lookup_attribute(name)
|
773
|
-
if att.kind && !att.abstract && klass = att.kind.first
|
774
|
-
if !args.first.kind_of?(klass) && [Doodle].any?{ |c| klass <= c }
|
775
|
-
#p [:_setter, "#2 converting arg #{att.name} to value with kind #{klass.to_s}"]
|
776
|
-
#p [:_setter, args]
|
777
|
-
begin
|
778
|
-
args = [klass.new(*args, &block)]
|
779
|
-
rescue Object => e
|
780
|
-
__doodle__.handle_error att.name, e.class, e.to_s, Doodle::Utils.doodle_caller
|
781
|
-
end
|
782
|
-
end
|
783
|
-
end
|
784
|
-
#p [:_setter, :got_att1, name, ivar, *args]
|
785
|
-
v = ivar_set(name, att.validate(self, *args))
|
786
|
-
|
787
|
-
#p [:_setter, :got_att2, name, ivar, :value, v]
|
788
|
-
#v = instance_variable_set(ivar, *args)
|
789
|
-
else
|
790
|
-
#p [:_setter, :no_att, name, *args]
|
791
|
-
##DBG: Doodle::Debug.d { [:_setter, "no attribute"] }
|
792
|
-
v = ivar_set(name, *args)
|
793
|
-
end
|
794
|
-
validate!(false)
|
795
|
-
v
|
796
|
-
end
|
797
|
-
private :_setter
|
798
|
-
|
799
|
-
# if block passed, define a conversion from class
|
800
|
-
# if no args, apply conversion to arguments
|
801
|
-
def from(*args, &block)
|
802
|
-
#p [:from, self, args]
|
803
|
-
if block_given?
|
804
|
-
# set the rule for each arg given
|
805
|
-
args.each do |arg|
|
806
|
-
__doodle__.local_conversions[arg] = block
|
807
|
-
end
|
808
|
-
else
|
809
|
-
convert(self, *args)
|
810
|
-
end
|
811
|
-
end
|
812
|
-
|
813
|
-
# add a validation
|
814
|
-
def must(constraint = 'be valid', &block)
|
815
|
-
__doodle__.local_validations << Validation.new(constraint, &block)
|
816
|
-
end
|
817
|
-
|
818
|
-
# add a validation that attribute must be of class <= kind
|
819
|
-
def kind(*args, &block)
|
820
|
-
if args.size > 0
|
821
|
-
@kind = [args].flatten
|
822
|
-
# todo[figure out how to handle kind being specified twice?]
|
823
|
-
if @kind.size > 2
|
824
|
-
kind_text = "be a kind of #{ @kind[0..-2].map{ |x| x.to_s }.join(', ') } or #{@kind[-1].to_s}" # =>
|
825
|
-
else
|
826
|
-
kind_text = "be a kind of #{@kind.to_s}"
|
827
|
-
end
|
828
|
-
__doodle__.local_validations << (Validation.new(kind_text) { |x| @kind.any? { |klass| x.kind_of?(klass) } })
|
829
|
-
else
|
830
|
-
@kind ||= []
|
831
|
-
end
|
832
|
-
end
|
833
|
-
|
834
|
-
# convert a value according to conversion rules
|
835
|
-
# FIXME: move
|
836
|
-
def convert(owner, *args)
|
837
|
-
#pp( { :convert => 1, :owner => owner, :args => args, :conversions => __doodle__.conversions } )
|
838
|
-
begin
|
839
|
-
args = args.map do |value|
|
840
|
-
#!p [:convert, 2, value]
|
841
|
-
if (converter = __doodle__.conversions[value.class])
|
842
|
-
#p [:convert, 3, value, self, caller]
|
843
|
-
value = converter[value]
|
844
|
-
#!p [:convert, 4, value]
|
845
|
-
else
|
846
|
-
#!p [:convert, 5, value]
|
847
|
-
# try to find nearest ancestor
|
848
|
-
this_ancestors = value.class.ancestors
|
849
|
-
#!p [:convert, 6, this_ancestors]
|
850
|
-
matches = this_ancestors & __doodle__.conversions.keys
|
851
|
-
#!p [:convert, 7, matches]
|
852
|
-
indexed_matches = matches.map{ |x| this_ancestors.index(x)}
|
853
|
-
#!p [:convert, 8, indexed_matches]
|
854
|
-
if indexed_matches.size > 0
|
855
|
-
#!p [:convert, 9]
|
856
|
-
converter_class = this_ancestors[indexed_matches.min]
|
857
|
-
#!p [:convert, 10, converter_class]
|
858
|
-
if converter = __doodle__.conversions[converter_class]
|
859
|
-
#!p [:convert, 11, converter]
|
860
|
-
value = converter[value]
|
861
|
-
#!p [:convert, 12, value]
|
862
|
-
end
|
863
|
-
else
|
864
|
-
#!p [:convert, 13, :kind, kind, name, value]
|
865
|
-
mappable_kinds = kind.select{ |x| x <= Doodle::Core }
|
866
|
-
#!p [:convert, 13.1, :kind, kind, mappable_kinds]
|
867
|
-
if mappable_kinds.size > 0
|
868
|
-
mappable_kinds.each do |mappable_kind|
|
869
|
-
#!p [:convert, 14, :kind_is_a_doodle, value.class, mappable_kind, mappable_kind.doodle.conversions, args]
|
870
|
-
if converter = mappable_kind.doodle.conversions[value.class]
|
871
|
-
#!p [:convert, 15, value, mappable_kind, args]
|
872
|
-
value = converter[value]
|
873
|
-
break
|
874
|
-
else
|
875
|
-
#!p [:convert, 16, :no_conversion_for, value.class]
|
876
|
-
end
|
877
|
-
end
|
878
|
-
else
|
879
|
-
#!p [:convert, 17, :kind_has_no_conversions]
|
880
|
-
end
|
881
|
-
end
|
882
|
-
end
|
883
|
-
#!p [:convert, 18, value]
|
884
|
-
value
|
885
|
-
end
|
886
|
-
rescue Exception => e
|
887
|
-
owner.__doodle__.handle_error name, ConversionError, "#{e.message}", Doodle::Utils.doodle_caller
|
888
|
-
end
|
889
|
-
if args.size > 1
|
890
|
-
args
|
891
|
-
else
|
892
|
-
args.first
|
893
|
-
end
|
894
|
-
end
|
895
|
-
|
896
|
-
# validate that args meet rules defined with +must+
|
897
|
-
# fixme: move
|
898
|
-
def validate(owner, *args)
|
899
|
-
##DBG: Doodle::Debug.d { [:validate, self, :owner, owner, :args, args ] }
|
900
|
-
#p [:validate, 1, args]
|
901
|
-
begin
|
902
|
-
value = convert(owner, *args)
|
903
|
-
rescue Exception => e
|
904
|
-
owner.__doodle__.handle_error name, ConversionError, "#{owner.kind_of?(Class) ? owner : owner.class}.#{ name } - #{e.message}", Doodle::Utils.doodle_caller
|
905
|
-
end
|
906
|
-
#p [:validate, 2, args, :becomes, value]
|
907
|
-
__doodle__.validations.each do |v|
|
908
|
-
##DBG: Doodle::Debug.d { [:validate, self, v, args, value] }
|
909
|
-
if !v.block[value]
|
910
|
-
owner.__doodle__.handle_error name, ValidationError, "#{owner.kind_of?(Class) ? owner : owner.class}.#{ name } must #{ v.message } - got #{ value.class }(#{ value.inspect })", Doodle::Utils.doodle_caller
|
911
|
-
end
|
912
|
-
end
|
913
|
-
#p [:validate, 3, value]
|
914
|
-
value
|
915
|
-
end
|
916
|
-
|
917
|
-
# define a getter_setter
|
918
|
-
# fixme: move
|
919
|
-
def define_getter_setter(name, params = { }, &block)
|
920
|
-
# need to use string eval because passing block
|
921
|
-
sc_eval "def #{name}(*args, &block); getter_setter(:#{name}, *args, &block); end", __FILE__, __LINE__
|
922
|
-
sc_eval "def #{name}=(*args, &block); _setter(:#{name}, *args); end", __FILE__, __LINE__
|
923
|
-
|
924
|
-
# this is how it should be done (in 1.9)
|
925
|
-
# module_eval {
|
926
|
-
# define_method name do |*args, &block|
|
927
|
-
# getter_setter(name.to_sym, *args, &block)
|
928
|
-
# end
|
929
|
-
# define_method "#{name}=" do |*args, &block|
|
930
|
-
# _setter(name.to_sym, *args, &block)
|
931
|
-
# end
|
932
|
-
# }
|
933
|
-
end
|
934
|
-
private :define_getter_setter
|
935
|
-
|
936
|
-
# +doc+ add docs to doodle class or attribute
|
937
|
-
def doc(*args, &block)
|
938
|
-
if args.size > 0
|
939
|
-
@doc = *args
|
940
|
-
else
|
941
|
-
@doc
|
942
|
-
end
|
943
|
-
end
|
944
|
-
alias :doc= :doc
|
945
|
-
|
946
|
-
# +has+ is an extended +attr_accessor+
|
947
|
-
#
|
948
|
-
# simple usage - just like +attr_accessor+:
|
949
|
-
#
|
950
|
-
# class Event
|
951
|
-
# has :date
|
952
|
-
# end
|
953
|
-
#
|
954
|
-
# set default value:
|
955
|
-
#
|
956
|
-
# class Event
|
957
|
-
# has :date, :default => Date.today
|
958
|
-
# end
|
959
|
-
#
|
960
|
-
# set lazily evaluated default value:
|
961
|
-
#
|
962
|
-
# class Event
|
963
|
-
# has :date do
|
964
|
-
# default { Date.today }
|
965
|
-
# end
|
966
|
-
# end
|
967
|
-
#
|
968
|
-
def has(*args, &block)
|
969
|
-
#DBG: Doodle::Debug.d { [:has, self, self.class, args] }
|
970
|
-
|
971
|
-
params = DoodleAttribute.params_from_args(self, *args)
|
972
|
-
# get specialized attribute class or use default
|
973
|
-
attribute_class = params.delete(:using) || DoodleAttribute
|
974
|
-
|
975
|
-
# could this be handled in DoodleAttribute?
|
976
|
-
# define getter setter before setting up attribute
|
977
|
-
define_getter_setter params[:name], params, &block
|
978
|
-
#p [:attribute, attribute_class, params]
|
979
|
-
attr = __doodle__.local_attributes[params[:name]] = attribute_class.new(params, &block)
|
980
|
-
end
|
981
|
-
|
982
|
-
# define order for positional arguments
|
983
|
-
def arg_order(*args)
|
984
|
-
if args.size > 0
|
985
|
-
begin
|
986
|
-
args = args.uniq
|
987
|
-
args.each do |x|
|
988
|
-
__doodle__.handle_error :arg_order, ArgumentError, "#{x} not a Symbol", Doodle::Utils.doodle_caller if !(x.class <= Symbol)
|
989
|
-
__doodle__.handle_error :arg_order, NameError, "#{x} not an attribute name", Doodle::Utils.doodle_caller if !doodle.attributes.keys.include?(x)
|
990
|
-
end
|
991
|
-
__doodle__.arg_order = args
|
992
|
-
rescue Exception => e
|
993
|
-
__doodle__.handle_error :arg_order, InvalidOrderError, e.to_s, Doodle::Utils.doodle_caller
|
994
|
-
end
|
995
|
-
else
|
996
|
-
__doodle__.arg_order + (__doodle__.attributes.keys - __doodle__.arg_order)
|
997
|
-
end
|
998
|
-
end
|
999
|
-
|
1000
|
-
# return true if instance variable +name+ defined
|
1001
|
-
# fixme: move
|
1002
|
-
def ivar_defined?(name)
|
1003
|
-
instance_variable_defined?("@#{name}")
|
1004
|
-
end
|
1005
|
-
private :ivar_defined?
|
1006
|
-
|
1007
|
-
# return true if attribute has default defined and not yet been
|
1008
|
-
# assigned to (i.e. still has default value)
|
1009
|
-
def default?(name)
|
1010
|
-
doodle.attributes[name.to_sym].optional? && !ivar_defined?(name)
|
1011
|
-
end
|
1012
|
-
|
1013
|
-
# validate this object by applying all validations in sequence
|
1014
|
-
# - if all == true, validate all attributes, e.g. when loaded from YAML, else validate at object level only
|
1015
|
-
def validate!(all = true)
|
1016
|
-
##DBG: Doodle::Debug.d { [:validate!, all, caller] }
|
1017
|
-
if all
|
1018
|
-
__doodle__.errors.clear
|
1019
|
-
end
|
1020
|
-
if __doodle__.validation_on
|
1021
|
-
if self.class == Class
|
1022
|
-
attribs = __doodle__.class_attributes
|
1023
|
-
##DBG: Doodle::Debug.d { [:validate!, "using class_attributes", class_attributes] }
|
1024
|
-
else
|
1025
|
-
attribs = __doodle__.attributes
|
1026
|
-
##DBG: Doodle::Debug.d { [:validate!, "using instance_attributes", doodle.attributes] }
|
1027
|
-
end
|
1028
|
-
attribs.each do |name, att|
|
1029
|
-
ivar_name = "@#{att.name}"
|
1030
|
-
if instance_variable_defined?(ivar_name)
|
1031
|
-
# if all == true, reset values so conversions and
|
1032
|
-
# validations are applied to raw instance variables
|
1033
|
-
# e.g. when loaded from YAML
|
1034
|
-
if all && !att.readonly
|
1035
|
-
##DBG: Doodle::Debug.d { [:validate!, :sending, att.name, instance_variable_get(ivar_name) ] }
|
1036
|
-
__send__("#{att.name}=", instance_variable_get(ivar_name))
|
1037
|
-
end
|
1038
|
-
elsif att.optional? # treat default/init as special case
|
1039
|
-
##DBG: Doodle::Debug.d { [:validate!, :optional, name ]}
|
1040
|
-
next
|
1041
|
-
elsif self.class != Class
|
1042
|
-
__doodle__.handle_error name, Doodle::ValidationError, "#{self.kind_of?(Class) ? self : self.class } missing required attribute '#{name}'", Doodle::Utils.doodle_caller
|
1043
|
-
end
|
1044
|
-
end
|
1045
|
-
|
1046
|
-
# now apply instance level validations
|
1047
|
-
|
1048
|
-
##DBG: Doodle::Debug.d { [:validate!, "validations", doodle_validations ]}
|
1049
|
-
__doodle__.validations.each do |v|
|
1050
|
-
##DBG: Doodle::Debug.d { [:validate!, self, v ] }
|
1051
|
-
begin
|
1052
|
-
if !instance_eval(&v.block)
|
1053
|
-
__doodle__.handle_error self, ValidationError, "#{ self.class } must #{ v.message }", Doodle::Utils.doodle_caller
|
1054
|
-
end
|
1055
|
-
rescue Exception => e
|
1056
|
-
__doodle__.handle_error self, ValidationError, e.to_s, Doodle::Utils.doodle_caller
|
1057
|
-
end
|
1058
|
-
end
|
1059
|
-
end
|
1060
|
-
# if OK, then return self
|
1061
|
-
self
|
1062
|
-
end
|
1063
|
-
|
1064
|
-
# object can be initialized from a mixture of positional arguments,
|
1065
|
-
# hash of keyword value pairs and a block which is instance_eval'd
|
1066
|
-
def initialize(*args, &block)
|
1067
|
-
built_in = Doodle::BuiltIns::BUILTINS.select{ |x| self.kind_of?(x) }.first
|
1068
|
-
if built_in
|
1069
|
-
super
|
1070
|
-
end
|
1071
|
-
__doodle__.validation_on = true
|
1072
|
-
#p [:doodle_parent, Doodle.parent, caller[-1]]
|
1073
|
-
Doodle.context.push(self)
|
1074
|
-
__doodle__.defer_validation do
|
1075
|
-
doodle.initialize_from_hash(*args)
|
1076
|
-
instance_eval(&block) if block_given?
|
1077
|
-
end
|
1078
|
-
Doodle.context.pop
|
1079
|
-
#p [:doodle, __doodle__.__inspect__]
|
1080
|
-
#p [:doodle, __doodle__.attributes]
|
1081
|
-
#p [:doodle_parent, __doodle__.parent]
|
1082
|
-
end
|
1083
|
-
|
1084
|
-
# create 'pure' hash of scalars only from attributes - hacky but works (kinda)
|
1085
|
-
def to_hash
|
1086
|
-
Doodle::Utils.symbolize_keys!(YAML::load(to_yaml.gsub(/!ruby\/object:.*$/, '')) || { }, true)
|
1087
|
-
#begin
|
1088
|
-
# YAML::load(to_yaml.gsub(/!ruby\/object:.*$/, '')) || { }
|
1089
|
-
#rescue Object => e
|
1090
|
-
# doodle.attributes.inject({}) {|hash, (name, attribute)| hash[name] = send(name); hash}
|
1091
|
-
#end
|
1092
|
-
end
|
1093
|
-
def to_string_hash
|
1094
|
-
Doodle::Utils.stringify_keys!(YAML::load(to_yaml.gsub(/!ruby\/object:.*$/, '')) || { }, true)
|
1095
|
-
end
|
1096
|
-
|
1097
|
-
end
|
1098
|
-
|
1099
|
-
# A factory function is a function that has the same name as
|
1100
|
-
# a class which acts just like class.new. For example:
|
1101
|
-
# Cat(:name => 'Ren')
|
1102
|
-
# is the same as:
|
1103
|
-
# Cat.new(:name => 'Ren')
|
1104
|
-
# As the notion of a factory function is somewhat contentious [xref
|
1105
|
-
# ruby-talk], you need to explicitly ask for them by including Factory
|
1106
|
-
# in your base class:
|
1107
|
-
# class Animal < Doodle
|
1108
|
-
# include Factory
|
1109
|
-
# end
|
1110
|
-
# class Dog < Animal
|
1111
|
-
# end
|
1112
|
-
# stimpy = Dog(:name => 'Stimpy')
|
1113
|
-
# etc.
|
1114
|
-
module Utils
|
1115
|
-
# normalize a name to contain only legal characters for a Ruby
|
1116
|
-
# constant
|
1117
|
-
def self.normalize_const(const)
|
1118
|
-
const.to_s.gsub(/[^A-Za-z_0-9]/, '')
|
1119
|
-
end
|
1120
|
-
|
1121
|
-
# lookup a constant along the module nesting path
|
1122
|
-
def const_lookup(const, context = self)
|
1123
|
-
#p [:const_lookup, const, context]
|
1124
|
-
const = Utils.normalize_const(const)
|
1125
|
-
result = nil
|
1126
|
-
if !context.kind_of?(Module)
|
1127
|
-
context = context.class
|
1128
|
-
end
|
1129
|
-
klasses = context.to_s.split(/::/)
|
1130
|
-
#p klasses
|
1131
|
-
|
1132
|
-
path = []
|
1133
|
-
0.upto(klasses.size - 1) do |i|
|
1134
|
-
path << Doodle::Utils.const_resolve(klasses[0..i].join('::'))
|
1135
|
-
end
|
1136
|
-
path = (path.reverse + context.ancestors).flatten
|
1137
|
-
#p [:const, context, path]
|
1138
|
-
path.each do |ctx|
|
1139
|
-
#p [:checking, ctx]
|
1140
|
-
if ctx.const_defined?(const)
|
1141
|
-
result = ctx.const_get(const)
|
1142
|
-
break
|
1143
|
-
end
|
1144
|
-
end
|
1145
|
-
raise NameError, "Uninitialized constant #{const} in context #{context}" if result.nil?
|
1146
|
-
result
|
1147
|
-
end
|
1148
|
-
module_function :const_lookup
|
1149
|
-
end
|
1150
|
-
|
1151
|
-
module Factory
|
1152
|
-
RX_IDENTIFIER = /^[A-Za-z_][A-Za-z_0-9]+\??$/
|
1153
|
-
#class << self
|
1154
|
-
# create a factory function in appropriate module for the specified class
|
1155
|
-
def self.factory(konst)
|
1156
|
-
#p [:factory, :ancestors, konst, konst.ancestors]
|
1157
|
-
#p [:factory, :lookup, Module.nesting]
|
1158
|
-
name = konst.to_s
|
1159
|
-
#p [:factory, :name, name]
|
1160
|
-
anon_class = false
|
1161
|
-
if name =~ /#<Class:0x[a-fA-F0-9]+>::/
|
1162
|
-
#p [:factory_anon_class, name]
|
1163
|
-
anon_class = true
|
1164
|
-
end
|
1165
|
-
names = name.split(/::/)
|
1166
|
-
name = names.pop
|
1167
|
-
# TODO: the code below is almost the same - refactor
|
1168
|
-
#p [:factory, :names, names, name]
|
1169
|
-
if names.empty? && !anon_class
|
1170
|
-
#p [:factory, :top_level_class]
|
1171
|
-
# top level class - should be available to all
|
1172
|
-
parent_class = Object
|
1173
|
-
method_defined = begin
|
1174
|
-
method(name)
|
1175
|
-
true
|
1176
|
-
rescue Object
|
1177
|
-
false
|
1178
|
-
end
|
1179
|
-
|
1180
|
-
if name =~ Factory::RX_IDENTIFIER && !method_defined && !parent_class.respond_to?(name) && !eval("respond_to?(:#{name})", TOPLEVEL_BINDING)
|
1181
|
-
eval("def #{ name }(*args, &block); ::#{name}.new(*args, &block); end", ::TOPLEVEL_BINDING, __FILE__, __LINE__)
|
1182
|
-
end
|
1183
|
-
else
|
1184
|
-
#p [:factory, :other_level_class]
|
1185
|
-
parent_class = Object
|
1186
|
-
if !anon_class
|
1187
|
-
parent_class = names.inject(parent_class) {|c, n| c.const_get(n)}
|
1188
|
-
#p [:factory, :parent_class, parent_class]
|
1189
|
-
if name =~ Factory::RX_IDENTIFIER && !parent_class.respond_to?(name)
|
1190
|
-
parent_class.module_eval("def self.#{name}(*args, &block); #{name}.new(*args, &block); end", __FILE__, __LINE__)
|
1191
|
-
end
|
1192
|
-
else
|
1193
|
-
# NOTE: ruby 1.9.1 specific
|
1194
|
-
parent_class_name = names.join('::')
|
1195
|
-
#p [:factory, :parent_class_name, parent_class_name]
|
1196
|
-
#p [:parent_class_name, parent_class_name]
|
1197
|
-
# FIXME: this is truly horrible...
|
1198
|
-
hex_object_id = parent_class_name.match(/:(0x[a-zA-Z0-9]+)/)[1]
|
1199
|
-
oid = hex_object_id.to_i(16) >> 1
|
1200
|
-
# p [:object_id, oid, hex_object_id, hex_object_id.to_i(16) >> 1]
|
1201
|
-
parent_class = ObjectSpace._id2ref(oid)
|
1202
|
-
|
1203
|
-
#p [:parent_object_id, parent_class.object_id, names, parent_class, parent_class_name, parent_class.name]
|
1204
|
-
# p [:names, :oid, "%x" % (oid << 1), :konst, konst, :pc, parent_class, :names, names, :self, self]
|
1205
|
-
if name =~ Factory::RX_IDENTIFIER && !parent_class.respond_to?(name)
|
1206
|
-
#p [:context, context]
|
1207
|
-
parent_class.module_eval("def #{name}(*args, &block); #{name}.new(*args, &block); end", __FILE__, __LINE__)
|
1208
|
-
end
|
1209
|
-
end
|
1210
|
-
# TODO: check how many times this is being called
|
1211
|
-
end
|
1212
|
-
end
|
1213
|
-
|
1214
|
-
# inherit the factory function capability
|
1215
|
-
def self.included(other)
|
1216
|
-
#p [:included, other]
|
1217
|
-
super
|
1218
|
-
# make +factory+ method available
|
1219
|
-
factory other
|
1220
|
-
end
|
1221
|
-
#end
|
1222
|
-
end
|
1223
|
-
|
1224
|
-
# Include Doodle::Core if you want to derive from another class
|
1225
|
-
# but still get Doodle goodness in your class (including Factory
|
1226
|
-
# methods).
|
1227
|
-
module Core
|
1228
|
-
def self.included(other)
|
1229
|
-
super
|
1230
|
-
other.module_eval {
|
1231
|
-
# FIXME: this is getting a bit arbitrary
|
1232
|
-
include Doodle::Equality
|
1233
|
-
include Doodle::Comparable
|
1234
|
-
extend Embrace
|
1235
|
-
embrace BaseMethods
|
1236
|
-
include Factory
|
1237
|
-
}
|
1238
|
-
end
|
1239
|
-
end
|
1240
|
-
|
1241
|
-
include Core
|
1242
|
-
end
|
1243
|
-
|
1244
|
-
class Doodle
|
1245
|
-
# Attribute is itself a Doodle object that is created by #has and
|
1246
|
-
# added to the #attributes collection in an object's DoodleInfo
|
1247
|
-
#
|
1248
|
-
# It is used to provide a context for defining #must and #from rules
|
1249
|
-
#
|
1250
|
-
class DoodleAttribute < Doodle
|
1251
|
-
# note: using extend with a module causes an infinite loop in 1.9
|
1252
|
-
# hence the inline
|
1253
|
-
|
1254
|
-
class << self
|
1255
|
-
# rewrite rules for the argument list to #has
|
1256
|
-
def params_from_args(owner, *args)
|
1257
|
-
key_values, positional_args = args.partition{ |x| x.kind_of?(Hash)}
|
1258
|
-
params = { }
|
1259
|
-
if positional_args.size > 0
|
1260
|
-
name = positional_args.shift
|
1261
|
-
case name
|
1262
|
-
# has Person --> has :person, :kind => Person
|
1263
|
-
when Class
|
1264
|
-
params[:name] = Utils.snake_case(name.to_s.split(/::/).last)
|
1265
|
-
params[:kind] = name
|
1266
|
-
else
|
1267
|
-
params[:name] = name.to_s.to_sym
|
1268
|
-
end
|
1269
|
-
end
|
1270
|
-
params = key_values.inject(params){ |acc, item| acc.merge(item)}
|
1271
|
-
#DBG: Doodle::Debug.d { [:has, self, self.class, params] }
|
1272
|
-
if !params.key?(:name)
|
1273
|
-
__doodle__.handle_error name, ArgumentError, "#{self.class} must have a name", Doodle::Utils.doodle_caller
|
1274
|
-
params[:name] = :__ERROR_missing_name__
|
1275
|
-
else
|
1276
|
-
# ensure that :name is a symbol
|
1277
|
-
params[:name] = params[:name].to_sym
|
1278
|
-
end
|
1279
|
-
name = params[:name]
|
1280
|
-
__doodle__.handle_error name, ArgumentError, "#{self.class} has too many arguments", Doodle::Utils.doodle_caller if positional_args.size > 0
|
1281
|
-
|
1282
|
-
if collector = params.delete(:collect)
|
1283
|
-
if !params.key?(:using)
|
1284
|
-
if params.key?(:key)
|
1285
|
-
params[:using] = KeyedAttribute
|
1286
|
-
else
|
1287
|
-
params[:using] = AppendableAttribute
|
1288
|
-
end
|
1289
|
-
end
|
1290
|
-
# this in generic CollectorAttribute class
|
1291
|
-
# collector from(Hash)
|
1292
|
-
if collector.kind_of?(Hash)
|
1293
|
-
collector_name, collector_class = collector.to_a[0]
|
1294
|
-
else
|
1295
|
-
# if Capitalized word given, treat as classname
|
1296
|
-
# and create collector for specific class
|
1297
|
-
collector_class = collector.to_s
|
1298
|
-
#p [:collector_klass, collector_klass]
|
1299
|
-
collector_name = Utils.snake_case(collector_class.split(/::/).last)
|
1300
|
-
#p [:collector_name, collector_class, collector_name]
|
1301
|
-
# FIXME: sanitize class name (make this a Utils function)
|
1302
|
-
collector_class = collector_class.gsub(/#<Class:0x[a-fA-F0-9]+>::/, '')
|
1303
|
-
if collector_class !~ /^[A-Z]/
|
1304
|
-
collector_class = nil
|
1305
|
-
end
|
1306
|
-
#!p [:collector_klass, collector_klass, params[:init]]
|
1307
|
-
end
|
1308
|
-
params[:collector_class] = collector_class
|
1309
|
-
params[:collector_name] = collector_name
|
1310
|
-
end
|
1311
|
-
params[:doodle_owner] = owner
|
1312
|
-
#p [:params, owner, params]
|
1313
|
-
params
|
1314
|
-
end
|
1315
|
-
end
|
1316
|
-
|
1317
|
-
# must define these methods before using them in #has below
|
1318
|
-
|
1319
|
-
# hack: bump off +validate!+ for Attributes - maybe better way of doing
|
1320
|
-
# this however, without this, tries to validate Attribute to :kind
|
1321
|
-
# specified, e.g. if you have
|
1322
|
-
#
|
1323
|
-
# has :date, :kind => Date
|
1324
|
-
#
|
1325
|
-
# it will fail because Attribute is not a kind of Date -
|
1326
|
-
# obviously, I have to think about this some more :S
|
1327
|
-
#
|
1328
|
-
# at least, I could hand roll a custom validate! method for Attribute
|
1329
|
-
#
|
1330
|
-
def validate!(all = true)
|
1331
|
-
end
|
1332
|
-
|
1333
|
-
# has default been defined?
|
1334
|
-
def default_defined?
|
1335
|
-
ivar_defined?(:default)
|
1336
|
-
end
|
1337
|
-
|
1338
|
-
# has default been defined?
|
1339
|
-
def init_defined?
|
1340
|
-
ivar_defined?(:init)
|
1341
|
-
end
|
1342
|
-
|
1343
|
-
# is this attribute optional? true if it has a default defined for it
|
1344
|
-
def optional?
|
1345
|
-
default_defined? or init_defined?
|
1346
|
-
end
|
1347
|
-
|
1348
|
-
# an attribute is required if it has no default or initial value defined for it
|
1349
|
-
def required?
|
1350
|
-
# d { [:default?, self.class, self.name, instance_variable_defined?("@default"), @default] }
|
1351
|
-
!optional?
|
1352
|
-
end
|
1353
|
-
|
1354
|
-
# special case - not an attribute
|
1355
|
-
define_getter_setter :doodle_owner
|
1356
|
-
|
1357
|
-
# temporarily fake existence of abstract attribute - later has
|
1358
|
-
# :abstract overrides this
|
1359
|
-
def abstract
|
1360
|
-
@abstract = false
|
1361
|
-
end
|
1362
|
-
def readonly
|
1363
|
-
false
|
1364
|
-
end
|
1365
|
-
|
1366
|
-
# name of attribute
|
1367
|
-
has :name, :kind => Symbol do
|
1368
|
-
from String do |s|
|
1369
|
-
s.to_sym
|
1370
|
-
end
|
1371
|
-
end
|
1372
|
-
|
1373
|
-
# default value (can be a block)
|
1374
|
-
has :default, :default => nil
|
1375
|
-
|
1376
|
-
# initial value
|
1377
|
-
has :init, :default => nil
|
1378
|
-
|
1379
|
-
# documentation
|
1380
|
-
has :doc, :default => ""
|
1381
|
-
|
1382
|
-
# don't try to initialize from this class
|
1383
|
-
remove_method(:abstract) # because we faked it earlier - remove to avoid redefinition warning
|
1384
|
-
has :abstract, :default => false
|
1385
|
-
remove_method(:readonly) # because we faked it earlier - remove to avoid redefinition warning
|
1386
|
-
has :readonly, :default => false
|
1387
|
-
end
|
1388
|
-
|
1389
|
-
# base class for attribute collector classes
|
1390
|
-
class AttributeCollector < DoodleAttribute
|
1391
|
-
has :collector_class
|
1392
|
-
has :collector_name
|
1393
|
-
|
1394
|
-
def resolve_collector_class
|
1395
|
-
if !collector_class.kind_of?(Class)
|
1396
|
-
self.collector_class = Doodle::Utils.const_resolve(collector_class)
|
1397
|
-
end
|
1398
|
-
end
|
1399
|
-
def resolve_value(value)
|
1400
|
-
if value.kind_of?(collector_class)
|
1401
|
-
#p [:resolve_value, :value, value]
|
1402
|
-
value
|
1403
|
-
elsif collector_class.__doodle__.conversions.key?(value.class)
|
1404
|
-
#p [:resolve_value, :collector_class_from, value]
|
1405
|
-
collector_class.from(value)
|
1406
|
-
else
|
1407
|
-
#p [:resolve_value, :collector_class_new, value]
|
1408
|
-
collector_class.new(value)
|
1409
|
-
end
|
1410
|
-
end
|
1411
|
-
def initialize(*args, &block)
|
1412
|
-
super
|
1413
|
-
define_collection
|
1414
|
-
from Hash do |hash|
|
1415
|
-
resolve_collector_class
|
1416
|
-
hash.inject(self.init.clone) do |h, (key, value)|
|
1417
|
-
h[key] = resolve_value(value)
|
1418
|
-
h
|
1419
|
-
end
|
1420
|
-
end
|
1421
|
-
from Enumerable do |enum|
|
1422
|
-
#p [:enum, Enumerable]
|
1423
|
-
resolve_collector_class
|
1424
|
-
# this is not very elegant but String is a classified as an
|
1425
|
-
# Enumerable in 1.8.x (but behaves differently)
|
1426
|
-
if enum.kind_of?(String) && self.init.kind_of?(String)
|
1427
|
-
post_process( resolve_value(enum) )
|
1428
|
-
else
|
1429
|
-
post_process( enum.map{ |value| resolve_value(value) } )
|
1430
|
-
end
|
1431
|
-
end
|
1432
|
-
end
|
1433
|
-
def post_process(results)
|
1434
|
-
#p [:post_process, results]
|
1435
|
-
self.init.clone.replace(results)
|
1436
|
-
end
|
1437
|
-
end
|
1438
|
-
|
1439
|
-
# define collector methods for array-like attribute collectors
|
1440
|
-
class AppendableAttribute < AttributeCollector
|
1441
|
-
# has :init, :init => DoodleArray.new
|
1442
|
-
has :init, :init => []
|
1443
|
-
|
1444
|
-
# define a collector for appendable collections
|
1445
|
-
# - collection should provide a :<< method
|
1446
|
-
def define_collection
|
1447
|
-
# FIXME: don't use eval in 1.9+
|
1448
|
-
if collector_class.nil?
|
1449
|
-
doodle_owner.sc_eval("def #{collector_name}(*args, &block)
|
1450
|
-
junk = #{name} if !#{name} # force initialization for classes
|
1451
|
-
args.unshift(block) if block_given?
|
1452
|
-
#{name}.<<(*args);
|
1453
|
-
end", __FILE__, __LINE__)
|
1454
|
-
else
|
1455
|
-
doodle_owner.sc_eval("def #{collector_name}(*args, &block)
|
1456
|
-
junk = #{name} if !#{name} # force initialization for classes
|
1457
|
-
if args.size > 0 and args.all?{|x| x.kind_of?(#{collector_class})}
|
1458
|
-
#{name}.<<(*args)
|
1459
|
-
else
|
1460
|
-
#{name} << #{collector_class}.new(*args, &block)
|
1461
|
-
end
|
1462
|
-
end", __FILE__, __LINE__)
|
1463
|
-
end
|
1464
|
-
end
|
1465
|
-
|
1466
|
-
end
|
1467
|
-
|
1468
|
-
# define collector methods for hash-like attribute collectors
|
1469
|
-
class KeyedAttribute < AttributeCollector
|
1470
|
-
# has :init, :init => DoodleHash.new
|
1471
|
-
has :init, :init => { }
|
1472
|
-
has :key
|
1473
|
-
|
1474
|
-
def post_process(results)
|
1475
|
-
results.inject(self.init.clone) do |h, result|
|
1476
|
-
h[result.send(key)] = result
|
1477
|
-
h
|
1478
|
-
end
|
1479
|
-
end
|
1480
|
-
|
1481
|
-
# define a collector for keyed collections
|
1482
|
-
# - collection should provide a :[] method
|
1483
|
-
def define_collection
|
1484
|
-
# need to use string eval because passing block
|
1485
|
-
# FIXME: don't use eval in 1.9+
|
1486
|
-
if collector_class.nil?
|
1487
|
-
doodle_owner.sc_eval("def #{collector_name}(*args, &block)
|
1488
|
-
junk = #{name} if !#{name} # force initialization for classes
|
1489
|
-
args.each do |arg|
|
1490
|
-
#{name}[arg.send(:#{key})] = arg
|
1491
|
-
end
|
1492
|
-
end", __FILE__, __LINE__)
|
1493
|
-
else
|
1494
|
-
doodle_owner.sc_eval("def #{collector_name}(*args, &block)
|
1495
|
-
junk = #{name} if !#{name} # force initialization for classes
|
1496
|
-
if args.size > 0 and args.all?{|x| x.kind_of?(#{collector_class})}
|
1497
|
-
args.each do |arg|
|
1498
|
-
#{name}[arg.send(:#{key})] = arg
|
1499
|
-
end
|
1500
|
-
else
|
1501
|
-
obj = #{collector_class}.new(*args, &block)
|
1502
|
-
#{name}[obj.send(:#{key})] = obj
|
1503
|
-
end
|
1504
|
-
end", __FILE__, __LINE__)
|
1505
|
-
end
|
1506
|
-
end
|
1507
|
-
end
|
1508
|
-
end
|
29
|
+
# utils
|
30
|
+
require 'doodle/debug'
|
31
|
+
require 'doodle/ordered-hash'
|
32
|
+
require 'doodle/utils'
|
33
|
+
# doodle proper
|
34
|
+
require 'doodle/equality'
|
35
|
+
require 'doodle/comparable'
|
36
|
+
require 'doodle/exceptions'
|
37
|
+
require 'doodle/singleton'
|
38
|
+
require 'doodle/conversion'
|
39
|
+
require 'doodle/validation'
|
40
|
+
require 'doodle/deferred'
|
41
|
+
require 'doodle/info'
|
42
|
+
require 'doodle/smoke-and-mirrors'
|
43
|
+
require 'doodle/datatype-holder'
|
44
|
+
require 'doodle/to_hash'
|
45
|
+
require 'doodle/getter-setter'
|
46
|
+
require 'doodle/marshal'
|
47
|
+
require 'doodle/factory'
|
48
|
+
require 'doodle/inherit'
|
49
|
+
# now start assembling them together
|
50
|
+
require 'doodle/base'
|
51
|
+
require 'doodle/core'
|
52
|
+
require 'doodle/attribute'
|
53
|
+
require 'doodle/normalized_array'
|
54
|
+
require 'doodle/normalized_hash'
|
55
|
+
require 'doodle/collector'
|
1509
56
|
|
1510
57
|
############################################################
|
1511
58
|
# and we're bootstrapped! :)
|