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.
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
@@ -0,0 +1,122 @@
1
+ class Doodle
2
+ # A Validation represents a validation rule applied to the instance
3
+ # after initialization. Generated using the Doodle::BaseMethods#must directive.
4
+ class Validation
5
+ attr_accessor :message
6
+ attr_accessor :block
7
+ # create a new validation rule. This is typically a result of
8
+ # calling +must+ so the text should work following the word
9
+ # "must", e.g. "must not be nil", "must be >= 10", etc.
10
+ def initialize(message = 'not be nil', &block)
11
+ @message = message
12
+ @block = block_given? ? block : proc { |x| !self.nil? }
13
+ end
14
+ end
15
+
16
+ module ValidationHelper
17
+ # add a validation
18
+ def must(constraint = 'be valid', &block)
19
+ __doodle__.local_validations << Validation.new(constraint, &block)
20
+ end
21
+
22
+ # add a validation that attribute must be of class <= kind
23
+ def kind(*args, &block)
24
+ if args.size > 0
25
+ @kind = [args].flatten
26
+ # todo[figure out how to handle kind being specified twice?]
27
+ if @kind.size > 2
28
+ kind_text = "be a kind of #{ @kind[0..-2].map{ |x| x.to_s }.join(', ') } or #{@kind[-1].to_s}" # =>
29
+ else
30
+ kind_text = "be a kind of #{@kind.to_s}"
31
+ end
32
+ __doodle__.local_validations << (Validation.new(kind_text) { |x| @kind.any? { |klass| x.kind_of?(klass) } })
33
+ else
34
+ @kind ||= []
35
+ end
36
+ end
37
+
38
+ # validate that individual attribute args meet rules defined with +must+
39
+ # fixme: move
40
+ def validate(owner, *args)
41
+ ##DBG: Doodle::Debug.d { [:validate, self, :owner, owner, :args, args ] }
42
+ #p [:validate, 1, args]
43
+ begin
44
+ value = convert(owner, *args)
45
+ rescue Exception => e
46
+ owner.__doodle__.handle_error name, ConversionError, "#{owner.kind_of?(Class) ? owner : owner.class}.#{ name } - #{e.message}", Doodle::Utils.doodle_caller
47
+ end
48
+ #
49
+ # Note to self: these validations are not affected by
50
+ # doodle.validation_on because they are for ~individual
51
+ # attributes~ - validation_on is for the ~object as a whole~ -
52
+ # so don't futz with this again :)
53
+ #
54
+ # p [:validate, 2, args, :becomes, value]
55
+ __doodle__.validations.each do |v|
56
+ ##DBG: Doodle::Debug.d { [:validate, self, v, args, value] }
57
+ if !v.block[value]
58
+ 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
59
+ end
60
+ end
61
+ #p [:validate, 3, value]
62
+ value
63
+ end
64
+
65
+ # validate this object by applying all validations in sequence
66
+ #
67
+ # - if all == true, validate all attributes, e.g. when loaded from
68
+ # YAML, else validate at object level only
69
+ #
70
+ def validate!(all = true)
71
+ ##DBG: Doodle::Debug.d { [:validate!, all, caller] }
72
+ if all
73
+ __doodle__.errors.clear
74
+ end
75
+
76
+ # first check that individual attributes are valid
77
+
78
+ if __doodle__.validation_on
79
+ if self.class == Class
80
+ attribs = __doodle__.class_attributes
81
+ ##DBG: Doodle::Debug.d { [:validate!, "using class_attributes", class_attributes] }
82
+ else
83
+ attribs = __doodle__.attributes
84
+ ##DBG: Doodle::Debug.d { [:validate!, "using instance_attributes", doodle.attributes] }
85
+ end
86
+ attribs.each do |name, att|
87
+ if ivar_defined?(name)
88
+ # if all == true, reset values so conversions and
89
+ # validations are applied to raw instance variables
90
+ # e.g. when loaded from YAML
91
+ if all && !att.readonly
92
+ ##DBG: Doodle::Debug.d { [:validate!, :sending, att.name, instance_variable_get(ivar_name) ] }
93
+ __send__("#{name}=", ivar_get(name))
94
+ end
95
+ elsif att.optional? # treat default/init as special case
96
+ ##DBG: Doodle::Debug.d { [:validate!, :optional, name ]}
97
+ next
98
+ elsif self.class != Class
99
+ __doodle__.handle_error name, Doodle::ValidationError, "#{self.kind_of?(Class) ? self : self.class } missing required attribute '#{name}'", Doodle::Utils.doodle_caller
100
+ end
101
+ end
102
+
103
+ # now apply whole object level validations
104
+
105
+ ##DBG: Doodle::Debug.d { [:validate!, "validations", doodle_validations ]}
106
+ __doodle__.validations.each do |v|
107
+ ##DBG: Doodle::Debug.d { [:validate!, self, v ] }
108
+ begin
109
+ if !instance_eval(&v.block)
110
+ __doodle__.handle_error self, ValidationError, "#{ self.class } must #{ v.message }", Doodle::Utils.doodle_caller
111
+ end
112
+ rescue Exception => e
113
+ __doodle__.handle_error self, ValidationError, e.to_s, Doodle::Utils.doodle_caller
114
+ end
115
+ end
116
+ end
117
+ # if OK, then return self
118
+ self
119
+ end
120
+
121
+ end
122
+ end
@@ -2,7 +2,7 @@ class Doodle #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
4
  MINOR = 2
5
- TINY = 2
5
+ TINY = 3
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -1,14 +1,14 @@
1
- # AUTHOR
2
- # jan molic /mig/at/1984/dot/cz/
3
- #
4
- # DESCRIPTION
5
- # Hash with preserved order and some array-like extensions
6
- # Public domain.
7
- #
8
- # THANKS
9
- # Andrew Johnson for his suggestions and fixes of Hash[],
10
- # merge, to_a, inspect and shift
11
1
  class Doodle
2
+ # AUTHOR
3
+ # jan molic /mig/at/1984/dot/cz/
4
+ #
5
+ # DESCRIPTION
6
+ # Hash with preserved order and some array-like extensions
7
+ # Public domain.
8
+ #
9
+ # THANKS
10
+ # Andrew Johnson for his suggestions and fixes of Hash[],
11
+ # merge, to_a, inspect and shift
12
12
  class OrderedHash < ::Hash
13
13
  attr_accessor :order
14
14
 
@@ -29,6 +29,16 @@ class Doodle
29
29
  super
30
30
  @order = []
31
31
  end
32
+ def clone
33
+ c = super
34
+ c.order = order.clone
35
+ c
36
+ end
37
+ def dup
38
+ c = super
39
+ c.order = order.dup
40
+ c
41
+ end
32
42
  def store_only(a,b)
33
43
  store a,b
34
44
  end
@@ -39,9 +49,13 @@ class Doodle
39
49
  end
40
50
  alias []= store
41
51
  def ==(hsh2)
52
+ return false if !hsh2.respond_to?(:order)
42
53
  return false if @order != hsh2.order
43
54
  super hsh2
44
55
  end
56
+ # def eql?(hsh2)
57
+ # super hsh2
58
+ # end
45
59
  def clear
46
60
  @order = []
47
61
  super
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe Doodle, 'assigned? with default' do
4
+ temporary_constant :Foo do
5
+
6
+ before :each do
7
+ class Foo < Doodle
8
+ has :name, :default => nil
9
+ end
10
+ end
11
+
12
+ it 'should return false if attribute not assigned' do
13
+ foo = Foo.new
14
+ foo.assigned?(:name).should_be false
15
+ end
16
+
17
+ it 'should return true if attribute assigned' do
18
+ foo = Foo.new('foo')
19
+ foo.assigned?(:name).should_be true
20
+ end
21
+
22
+ end
23
+ end
24
+
25
+ describe Doodle, 'assigned? with init' do
26
+ temporary_constant :Foo do
27
+
28
+ before :each do
29
+ class Foo < Doodle
30
+ has :name, :init => ""
31
+ end
32
+ end
33
+
34
+ it 'should return true if attribute has init even when not specifically assigned' do
35
+ foo = Foo.new
36
+ foo.assigned?(:name).should_be true
37
+ end
38
+
39
+ it 'should return true if attribute has init and has been assigned' do
40
+ foo = Foo.new('foo')
41
+ foo.assigned?(:name).should_be true
42
+ end
43
+
44
+ end
45
+ end
@@ -16,7 +16,7 @@ describe Doodle::DoodleAttribute, 'basics' do
16
16
  has :cvar2
17
17
  end
18
18
  end
19
-
19
+
20
20
  @foo = Foo.new
21
21
  class << @foo
22
22
  has :svar1
@@ -31,7 +31,7 @@ describe Doodle::DoodleAttribute, 'basics' do
31
31
  @bar = nil
32
32
  @foo = nil
33
33
  end
34
-
34
+
35
35
  it 'should have attribute :ivar1 with default defined' do
36
36
  @foo.doodle.attributes[:ivar1].default.should == 'Hello'
37
37
  end
@@ -68,7 +68,7 @@ describe Doodle::DoodleAttribute, 'basics' do
68
68
  # expected_doodle.parents = RUBY_VERSION <= "1.8.6" ? [Foo, Object] : [Foo, Object, BasicObject]
69
69
  # Bar.doodle.parents.should == expected_doodle.parents
70
70
  # end
71
-
71
+
72
72
  it "should have Bar's singleton doodle.parents in reverse order of definition" do
73
73
  @bar.singleton_class.doodle.parents.should == []
74
74
  end
@@ -84,20 +84,20 @@ describe Doodle::DoodleAttribute, 'basics' do
84
84
  it 'should have inherited class_attributes in order of definition' do
85
85
  @bar.doodle.class_attributes.keys.should == [:cvar1, :cvar2]
86
86
  end
87
-
87
+
88
88
  it 'should have local class attributes in order of definition' do
89
89
  Bar.singleton_class.doodle.attributes(false).keys.should == [:cvar2]
90
90
  end
91
91
 
92
92
  # bit iffy this test - testing implementation, not interface
93
93
  it 'should not inherit singleton doodle.local_attributes' do
94
- @bar.singleton_class.class_eval { doodle.collect_inherited(:local_attributes).map { |x| x[0]} }.should == []
94
+ @bar.singleton_class.class_eval { doodle.send(:collect_inherited, :local_attributes).map { |x| x[0]} }.should == []
95
95
  end
96
96
 
97
97
  it 'should not inherit singleton attributes#1' do
98
98
  @bar.singleton_class.doodle.attributes.map { |x| x[0]} .should == [:svar2]
99
99
  end
100
-
100
+
101
101
  it 'should not inherit singleton attributes#2' do
102
102
  @bar.singleton_class.doodle.attributes.keys.should == [:svar2]
103
103
  end
@@ -128,7 +128,7 @@ describe Doodle::DoodleAttribute, 'attribute order' do
128
128
  has :c
129
129
  end
130
130
  end
131
-
131
+
132
132
  # it 'should keep order of inherited attributes' do
133
133
  # expected_doodle.parents = RUBY_VERSION <= "1.8.6" ? [B, A, Doodle, Object] : [B, A, Doodle, Object, BasicObject]
134
134
  # C.doodle.parents.should == expected_doodle.parents
@@ -113,12 +113,12 @@ describe Doodle, "typed collector with specified collector name" do
113
113
  end
114
114
  it "should collect items into attribute :list" do
115
115
  event = nil
116
- proc {
116
+ no_error {
117
117
  event = Event do
118
118
  place "Stage 1"
119
119
  place "Stage 2"
120
120
  end
121
- }.should_not raise_error
121
+ }
122
122
  event.locations.map{|loc| loc.name}.should_be ["Stage 1", "Stage 2"]
123
123
  event.locations.map{|loc| loc.class}.should_be [::Location, ::Location]
124
124
  end
@@ -150,9 +150,9 @@ describe Doodle, "typed collector with specified collector name initialized from
150
150
  :locations =>
151
151
  [ { :name => 'Backstage' } ] } ] }, { :name => "Stage 2" } ] }
152
152
  # note: wierd formatting above simply to pass coverage
153
- proc {
153
+ no_error {
154
154
  event = Event(data)
155
- }.should_not raise_error
155
+ }
156
156
  event.locations.map{|loc| loc.name}.should_be ["Stage 1", "Stage 2"]
157
157
  event.locations.map{|loc| loc.class}.should_be [::Location, ::Location]
158
158
  event.locations[0].events[0].kind_of?(Event).should_be true
@@ -180,7 +180,8 @@ describe Doodle, "Simple keyed collector" do
180
180
  end
181
181
 
182
182
  it "should collect items into attribute :list" do
183
- @foo.list.should_be( { 5 => "World" } )
183
+ # @foo.list.should_be( Doodle::OrderedHash[5, "World"] )
184
+ @foo.list.to_a.flatten.should_be( [5, "World"] )
184
185
  end
185
186
 
186
187
  end
@@ -208,7 +209,7 @@ describe Doodle, "Simple keyed collector #2" do
208
209
  item "Hello"
209
210
  item "World"
210
211
  end
211
- foo.list.to_a.map{ |k, v| [k, v.class, v.name] }.should_be( [["Hello", Item, "Hello"], ["World", Item, "World"]] )
212
+ foo.list.to_a.sort.map{ |k, v| [k, v.class, v.name] }.should_be( [["Hello", Item, "Hello"], ["World", Item, "World"]] )
212
213
  end
213
214
 
214
215
  it "should collect keyword argument enumerable into attribute :list" do
@@ -218,7 +219,7 @@ describe Doodle, "Simple keyed collector #2" do
218
219
  { :name => "World" }
219
220
  ]
220
221
  )
221
- foo.list.to_a.map{ |k, v| [k, v.class, v.name] }.should_be( [["Hello", Item, "Hello"], ["World", Item, "World"]] )
222
+ foo.list.to_a.sort.map{ |k, v| [k, v.class, v.name] }.should_be( [["Hello", Item, "Hello"], ["World", Item, "World"]] )
222
223
  end
223
224
 
224
225
  it "should collect positional argument enumerable into attribute :list" do
@@ -227,7 +228,7 @@ describe Doodle, "Simple keyed collector #2" do
227
228
  { :name => "World" }
228
229
  ]
229
230
  )
230
- foo.list.to_a.map{ |k, v| [k, v.class, v.name] }.should_be( [["Hello", Item, "Hello"], ["World", Item, "World"]] )
231
+ foo.list.to_a.sort.map{ |k, v| [k, v.class, v.name] }.should_be( [["Hello", Item, "Hello"], ["World", Item, "World"]] )
231
232
  end
232
233
 
233
234
  it "should collect named argument hash into attribute :list" do
@@ -236,7 +237,7 @@ describe Doodle, "Simple keyed collector #2" do
236
237
  "World" => { :name => "World" }
237
238
  }
238
239
  )
239
- foo.list.to_a.map{ |k, v| [k, v.class, v.name] }.should_be( [["Hello", Item, "Hello"], ["World", Item, "World"]] )
240
+ foo.list.to_a.sort.map{ |k, v| [k, v.class, v.name] }.should_be( [["Hello", Item, "Hello"], ["World", Item, "World"]] )
240
241
  end
241
242
 
242
243
  end
@@ -255,12 +256,12 @@ describe Doodle, 'using String as collector' do
255
256
  end
256
257
 
257
258
  it 'should not raise an exception' do
258
- proc {
259
+ no_error {
259
260
  text = Text do
260
261
  line "line 1"
261
262
  line "line 2"
262
263
  end
263
- }.should_not raise_error
264
+ }
264
265
  end
265
266
 
266
267
  it 'should concatenate strings' do
@@ -291,11 +292,11 @@ describe Doodle, 'collecting text values into non-String collector' do
291
292
  end
292
293
 
293
294
  it 'should not raise an exception' do
294
- proc {
295
+ no_error {
295
296
  signed_by = SignedBy do
296
297
  signature "Sean"
297
298
  end
298
- }.should_not raise_error
299
+ }
299
300
  end
300
301
 
301
302
  it 'should convert String values to instances of collector class' do
@@ -308,3 +309,89 @@ describe Doodle, 'collecting text values into non-String collector' do
308
309
  end
309
310
  end
310
311
  end
312
+
313
+ describe Doodle, ':collect' do
314
+ temporary_constants :ItemList, :Item do
315
+
316
+ before :each do
317
+ class ::Item < Doodle
318
+ has :title, :kind => String
319
+ end
320
+ class ItemList < Doodle
321
+ has :items, :collect => Item
322
+ # TODO: add warning/exception if collection name same as item name
323
+ end
324
+ end
325
+
326
+ it 'should allow adding items of specified type' do
327
+ no_error {
328
+ list = ItemList do
329
+ item Item("one")
330
+ item Item("two")
331
+ end
332
+ }
333
+ end
334
+
335
+ it 'should allow adding items of specified type via implicit type constructor' do
336
+ no_error {
337
+ list = ItemList do
338
+ item "one"
339
+ item "two"
340
+ end
341
+ }
342
+ end
343
+
344
+ it 'should restrict collected items to specified type' do
345
+ expect_error(Doodle::ValidationError) {
346
+ list = ItemList do
347
+ item Date.new
348
+ end
349
+ }
350
+ end
351
+
352
+ end
353
+ end
354
+
355
+ describe Doodle, ':collect' do
356
+ temporary_constants :Canvas, :Shape, :Circle, :Square do
357
+ before :each do
358
+ class ::Shape < Doodle
359
+ has :x
360
+ has :y
361
+ end
362
+ class ::Circle < Shape
363
+ has :radius
364
+ end
365
+ class ::Square < Shape
366
+ has :size
367
+ end
368
+ end
369
+
370
+ it 'should accept an array of types' do
371
+ class ::Canvas < Doodle
372
+ has :shapes, :collect => [Circle, Square]
373
+ end
374
+ canvas = Canvas do
375
+ circle 10,10,5
376
+ square 20,30,40
377
+ end
378
+ canvas.shapes.size.should_be 2
379
+ canvas.shapes[0].kind_of?(Circle).should_be true
380
+ canvas.shapes[1].should_be Square(20, 30, 40)
381
+ end
382
+
383
+ it 'should accept a hash of types' do
384
+ class ::Canvas < Doodle
385
+ has :shapes, :collect => { :circle => Circle, :square => Square }
386
+ end
387
+ canvas = Canvas do
388
+ circle 10,10,5
389
+ square 20,30,40
390
+ end
391
+ canvas.shapes.size.should_be 2
392
+ canvas.shapes[0].kind_of?(Circle).should_be true
393
+ canvas.shapes[1].kind_of?(Square).should_be true
394
+ end
395
+ end
396
+ end
397
+