doodle 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/History.txt +24 -0
  2. data/Manifest.txt +26 -1
  3. data/README.txt +9 -8
  4. data/lib/doodle.rb +43 -1496
  5. data/lib/doodle/app.rb +6 -0
  6. data/lib/doodle/attribute.rb +165 -0
  7. data/lib/doodle/base.rb +180 -0
  8. data/lib/doodle/collector-1.9.rb +72 -0
  9. data/lib/doodle/collector.rb +191 -0
  10. data/lib/doodle/comparable.rb +8 -0
  11. data/lib/doodle/conversion.rb +80 -0
  12. data/lib/doodle/core.rb +42 -0
  13. data/lib/doodle/datatype-holder.rb +39 -0
  14. data/lib/doodle/debug.rb +20 -0
  15. data/lib/doodle/deferred.rb +13 -0
  16. data/lib/doodle/equality.rb +21 -0
  17. data/lib/doodle/exceptions.rb +29 -0
  18. data/lib/doodle/factory.rb +91 -0
  19. data/lib/doodle/getter-setter.rb +154 -0
  20. data/lib/doodle/info.rb +298 -0
  21. data/lib/doodle/inherit.rb +40 -0
  22. data/lib/doodle/json.rb +38 -0
  23. data/lib/doodle/marshal.rb +16 -0
  24. data/lib/doodle/normalized_array.rb +512 -0
  25. data/lib/doodle/normalized_hash.rb +356 -0
  26. data/lib/doodle/ordered-hash.rb +8 -0
  27. data/lib/doodle/singleton.rb +23 -0
  28. data/lib/doodle/smoke-and-mirrors.rb +23 -0
  29. data/lib/doodle/to_hash.rb +17 -0
  30. data/lib/doodle/utils.rb +173 -11
  31. data/lib/doodle/validation.rb +122 -0
  32. data/lib/doodle/version.rb +1 -1
  33. data/lib/molic_orderedhash.rb +24 -10
  34. data/spec/assigned_spec.rb +45 -0
  35. data/spec/attributes_spec.rb +7 -7
  36. data/spec/collector_spec.rb +100 -13
  37. data/spec/doodle_context_spec.rb +5 -5
  38. data/spec/from_spec.rb +43 -3
  39. data/spec/json_spec.rb +232 -0
  40. data/spec/member_init_spec.rb +11 -11
  41. data/spec/modules_spec.rb +4 -4
  42. data/spec/multi_collector_spec.rb +91 -0
  43. data/spec/must_spec.rb +32 -0
  44. data/spec/spec_helper.rb +14 -4
  45. data/spec/specialized_attribute_class_spec.rb +2 -2
  46. data/spec/typed_collector_spec.rb +57 -0
  47. data/spec/xml_spec.rb +8 -8
  48. metadata +33 -3
@@ -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:
@@ -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>
@@ -1,1511 +1,58 @@
1
- # doodle
2
- # -*- mode: ruby; ruby-indent-level: 2; tab-width: 2 -*- vim: sw=2 ts=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 latest release 0.0.12
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
- # require Ruby 1.8.6 or higher
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
- if RUBY_VERSION < '1.9.0'
18
- require 'molic_orderedhash' # TODO: replace this with own (required functions only) version
19
- else
20
- # 1.9+ hashes are ordered by default
21
- class Doodle
22
- OrderedHash = ::Hash
23
- end
24
- end
25
-
26
- require 'yaml'
27
-
28
- # *doodle* is my attempt at an eco-friendly metaprogramming framework that does not
29
- # have pollute core Ruby objects such as Object, Class and Module.
30
- #
31
- # While doodle itself is useful for defining classes, my main goal is to
32
- # come up with a useful DSL notation for class definitions which can be
33
- # reused in many contexts.
34
- #
35
- # Docs at http://doodle.rubyforge.org
36
- #
37
- class Doodle
38
- class << self
39
- # provide somewhere to hold thread-specific context information
40
- # (I'm claiming the :doodle_xxx namespace)
41
- def context
42
- Thread.current[:doodle_context] ||= []
43
- end
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! :)