locomotive-mongoid-tree 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +2 -0
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +192 -0
- data/Rakefile +31 -0
- data/lib/mongoid/tree.rb +318 -0
- data/lib/mongoid/tree/ordering.rb +195 -0
- data/lib/mongoid/tree/traversal.rb +119 -0
- data/spec/mongoid/tree/ordering_spec.rb +328 -0
- data/spec/mongoid/tree/traversal_spec.rb +175 -0
- data/spec/mongoid/tree_spec.rb +382 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/macros/tree_macros.rb +44 -0
- data/spec/support/models/node.rb +28 -0
- metadata +125 -0
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mongoid::Tree::Traversal do
|
4
|
+
|
5
|
+
subject { Node }
|
6
|
+
|
7
|
+
describe '#traverse' do
|
8
|
+
|
9
|
+
subject { Node.new }
|
10
|
+
|
11
|
+
it "should require a block" do
|
12
|
+
expect { subject.traverse }.to raise_error(/No block given/)
|
13
|
+
end
|
14
|
+
|
15
|
+
[:depth_first, :breadth_first].each do |method|
|
16
|
+
it "should support #{method} traversal" do
|
17
|
+
expect { subject.traverse(method) {} }.to_not raise_error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should complain about unsupported traversal methods" do
|
22
|
+
expect { subject.traverse('non_existing') {} }.to raise_error
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should default to depth_first traversal" do
|
26
|
+
subject.should_receive(:depth_first_traversal)
|
27
|
+
subject.traverse {}
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'depth first traversal' do
|
33
|
+
|
34
|
+
it "should traverse correctly" do
|
35
|
+
setup_tree <<-ENDTREE
|
36
|
+
node1:
|
37
|
+
- node2:
|
38
|
+
- node3
|
39
|
+
- node4:
|
40
|
+
- node5
|
41
|
+
- node6
|
42
|
+
- node7
|
43
|
+
ENDTREE
|
44
|
+
|
45
|
+
result = []
|
46
|
+
node(:node1).traverse(:depth_first) { |node| result << node }
|
47
|
+
result.collect { |n| n.name.to_sym }.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should traverse correctly on merged trees" do
|
51
|
+
|
52
|
+
setup_tree <<-ENDTREE
|
53
|
+
- node4:
|
54
|
+
- node5
|
55
|
+
- node6:
|
56
|
+
- node7
|
57
|
+
|
58
|
+
- node1:
|
59
|
+
- node2:
|
60
|
+
- node3
|
61
|
+
ENDTREE
|
62
|
+
|
63
|
+
|
64
|
+
node(:node1).children << node(:node4)
|
65
|
+
|
66
|
+
|
67
|
+
result = []
|
68
|
+
node(:node1).traverse(:depth_first) { |node| result << node }
|
69
|
+
result.collect { |n| n.name.to_sym }.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'with reordered nodes' do
|
73
|
+
|
74
|
+
subject { OrderedNode }
|
75
|
+
|
76
|
+
before do
|
77
|
+
setup_tree <<-ENDTREE
|
78
|
+
node1:
|
79
|
+
- node2:
|
80
|
+
- node3
|
81
|
+
- node4:
|
82
|
+
- node6
|
83
|
+
- node5
|
84
|
+
- node7
|
85
|
+
ENDTREE
|
86
|
+
node(:node5).move_above(node(:node6))
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should return the nodes in the correct order' do
|
90
|
+
result = []
|
91
|
+
node(:node1).traverse(:depth_first) { |node| result << node }
|
92
|
+
result.collect { |n| n.name.to_sym }.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
describe 'breadth first traversal' do
|
100
|
+
|
101
|
+
it "should traverse correctly" do
|
102
|
+
tree = setup_tree <<-ENDTREE
|
103
|
+
node1:
|
104
|
+
- node2:
|
105
|
+
- node5
|
106
|
+
- node3:
|
107
|
+
- node6
|
108
|
+
- node7
|
109
|
+
- node4
|
110
|
+
ENDTREE
|
111
|
+
|
112
|
+
result = []
|
113
|
+
node(:node1).traverse(:breadth_first) { |n| result << n }
|
114
|
+
result.collect { |n| n.name.to_sym }.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
describe '.traverse' do
|
120
|
+
|
121
|
+
describe 'when not given a block' do
|
122
|
+
|
123
|
+
it 'raises an error' do
|
124
|
+
expect {Node.traverse}.to raise_error ArgumentError, 'No block given'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
before :each do
|
129
|
+
setup_tree <<-ENDTREE
|
130
|
+
- root1
|
131
|
+
- root2
|
132
|
+
ENDTREE
|
133
|
+
|
134
|
+
@block = Proc.new {}
|
135
|
+
@root1 = node(:root1)
|
136
|
+
@root2 = node(:root2)
|
137
|
+
|
138
|
+
Node.stub(:roots).and_return [@root1, @root2]
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'grabs each root' do
|
142
|
+
Node.should_receive(:roots).and_return []
|
143
|
+
|
144
|
+
Node.traverse &@block
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'defaults the "type" arg to :depth_first' do
|
148
|
+
@root1.should_receive(:traverse).with(:depth_first)
|
149
|
+
@root2.should_receive(:traverse).with(:depth_first)
|
150
|
+
|
151
|
+
Node.traverse &@block
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'traverses each root' do
|
155
|
+
@root1.should_receive(:traverse)
|
156
|
+
@root2.should_receive(:traverse)
|
157
|
+
|
158
|
+
Node.traverse &@block
|
159
|
+
end
|
160
|
+
|
161
|
+
describe 'when the "type" arg is :breadth_first' do
|
162
|
+
|
163
|
+
it 'traverses breadth-first' do
|
164
|
+
@root1.should_receive(:traverse).with(:breadth_first)
|
165
|
+
@root2.should_receive(:traverse).with(:breadth_first)
|
166
|
+
|
167
|
+
Node.traverse :breadth_first, &@block
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'returns nil' do
|
172
|
+
Node.traverse(&@block).should be nil
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,382 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mongoid::Tree do
|
4
|
+
|
5
|
+
subject { Node }
|
6
|
+
|
7
|
+
it "should reference many children as inverse of parent with index" do
|
8
|
+
a = Node.reflect_on_association(:children)
|
9
|
+
a.should be
|
10
|
+
a.macro.should eql(:references_many)
|
11
|
+
a.class_name.should eql('Node')
|
12
|
+
a.foreign_key.should eql('parent_id')
|
13
|
+
Node.index_options.should have_key('parent_id')
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should be referenced in one parent as inverse of children" do
|
17
|
+
a = Node.reflect_on_association(:parent)
|
18
|
+
a.should be
|
19
|
+
a.macro.should eql(:referenced_in)
|
20
|
+
a.class_name.should eql('Node')
|
21
|
+
a.inverse_of.should eql(:children)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should store parent_ids as Array with [] as default with index" do
|
25
|
+
f = Node.fields['parent_ids']
|
26
|
+
f.should be
|
27
|
+
f.options[:type].should eql(Array)
|
28
|
+
f.options[:default].should eql([])
|
29
|
+
Node.index_options.should have_key(:parent_ids)
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'when new' do
|
33
|
+
it "should not require a saved parent when adding children" do
|
34
|
+
root = Node.new(:name => 'root'); child = Node.new(:name => 'child')
|
35
|
+
expect { root.children << child; root.save! }.to_not raise_error(Mongoid::Errors::DocumentNotFound)
|
36
|
+
child.should be_persisted
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should not be saved when parent is not saved" do
|
40
|
+
root = Node.new(:name => 'root'); child = Node.new(:name => 'child')
|
41
|
+
child.should_not_receive(:save)
|
42
|
+
root.children << child
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should save its unsaved children" do
|
46
|
+
root = Node.new(:name => 'root'); child = Node.new(:name => 'child')
|
47
|
+
root.children << child
|
48
|
+
child.should_receive(:save).at_most(2).times
|
49
|
+
root.save
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'when saved' do
|
54
|
+
|
55
|
+
before(:each) do
|
56
|
+
setup_tree <<-ENDTREE
|
57
|
+
- root:
|
58
|
+
- child:
|
59
|
+
- subchild:
|
60
|
+
- subsubchild
|
61
|
+
- other_root:
|
62
|
+
- other_child
|
63
|
+
ENDTREE
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should set the child's parent_id when added to parent's children" do
|
67
|
+
root = Node.create; child = Node.create
|
68
|
+
root.children << child
|
69
|
+
child.parent.should == root
|
70
|
+
child.parent_id.should == root.id
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should set the child's parent_id parent is set on child" do
|
74
|
+
root = Node.create; child = Node.create
|
75
|
+
child.parent = root
|
76
|
+
child.parent.should == root
|
77
|
+
child.parent_id.should == root.id
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should rebuild its parent_ids" do
|
81
|
+
root = Node.create; child = Node.create
|
82
|
+
root.children << child
|
83
|
+
child.parent_ids.should == [root.id]
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should rebuild its children's parent_ids when its own parent_ids changed" do
|
87
|
+
other_root = node(:other_root); child = node(:child); subchild = node(:subchild);
|
88
|
+
other_root.children << child
|
89
|
+
subchild.reload # To get the updated version
|
90
|
+
subchild.parent_ids.should == [other_root.id, child.id]
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should correctly rebuild its descendants' parent_ids when moved into an other subtree" do
|
94
|
+
subchild = node(:subchild); subsubchild = node(:subsubchild); other_child = node(:other_child)
|
95
|
+
other_child.children << subchild
|
96
|
+
subsubchild.reload
|
97
|
+
subsubchild.parent_ids.should == [node(:other_root).id, other_child.id, subchild.id]
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should rebuild its children's parent_ids when its own parent_id is removed" do
|
101
|
+
c = node(:child)
|
102
|
+
c.parent_id = nil
|
103
|
+
c.save
|
104
|
+
node(:subchild).parent_ids.should == [node(:child).id]
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should not rebuild its children's parent_ids when it's not required" do
|
108
|
+
root = node(:root)
|
109
|
+
root.should_not_receive(:rearrange_children)
|
110
|
+
root.save
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should prevent cycles" do
|
114
|
+
child = node(:child)
|
115
|
+
child.parent = node(:subchild)
|
116
|
+
child.should_not be_valid
|
117
|
+
child.errors[:parent_id].should_not be_nil
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should save its children when added" do
|
121
|
+
new_child = Node.new(:name => 'new_child')
|
122
|
+
node(:root).children << new_child
|
123
|
+
new_child.should be_persisted
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe 'when subclassed' do
|
128
|
+
|
129
|
+
before(:each) do
|
130
|
+
setup_tree <<-ENDTREE
|
131
|
+
- root:
|
132
|
+
- child:
|
133
|
+
- subchild
|
134
|
+
- other_child
|
135
|
+
- other_root
|
136
|
+
ENDTREE
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should allow to store any subclass within the tree" do
|
140
|
+
subclassed = SubclassedNode.create!(:name => 'subclassed_subchild')
|
141
|
+
node(:child).children << subclassed
|
142
|
+
subclassed.root.should == node(:root)
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
describe 'destroy strategies' do
|
148
|
+
|
149
|
+
before(:each) do
|
150
|
+
setup_tree <<-ENDTREE
|
151
|
+
- root:
|
152
|
+
- child:
|
153
|
+
- subchild
|
154
|
+
- other_child
|
155
|
+
- other_root
|
156
|
+
ENDTREE
|
157
|
+
end
|
158
|
+
|
159
|
+
describe ':nullify_children' do
|
160
|
+
it "should set its children's parent_id to null" do
|
161
|
+
node(:root).nullify_children
|
162
|
+
node(:child).should be_root
|
163
|
+
node(:subchild).reload.should_not be_descendant_of node(:root)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe ':move_children_to_parent' do
|
168
|
+
it "should set its childen's parent_id to the documents parent_id" do
|
169
|
+
node(:child).move_children_to_parent
|
170
|
+
node(:child).should be_leaf
|
171
|
+
node(:root).children.to_a.should =~ [node(:child), node(:other_child), node(:subchild)]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe ':destroy_children' do
|
176
|
+
it "should destroy all children" do
|
177
|
+
root = node(:root)
|
178
|
+
root.children.should_receive(:destroy_all)
|
179
|
+
root.destroy_children
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe ':delete_descendants' do
|
184
|
+
it "should delete all descendants" do
|
185
|
+
root = node(:root)
|
186
|
+
Node.should_receive(:delete_all).with(:conditions => { :parent_ids => root.id })
|
187
|
+
root.delete_descendants
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
describe 'utility methods' do
|
194
|
+
|
195
|
+
before(:each) do
|
196
|
+
setup_tree <<-ENDTREE
|
197
|
+
- root:
|
198
|
+
- child:
|
199
|
+
- subchild
|
200
|
+
- other_child
|
201
|
+
- other_root
|
202
|
+
ENDTREE
|
203
|
+
end
|
204
|
+
|
205
|
+
describe '.root' do
|
206
|
+
it "should return the first root document" do
|
207
|
+
Node.root.should == node(:root)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe '.roots' do
|
212
|
+
it "should return all root documents" do
|
213
|
+
Node.roots.to_a.should == [node(:root), node(:other_root)]
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
describe '.leaves' do
|
218
|
+
it "should return all leaf documents" do
|
219
|
+
Node.leaves.to_a.should =~ [node(:subchild), node(:other_child), node(:other_root)]
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe '#root?' do
|
224
|
+
it "should return true for root documents" do
|
225
|
+
node(:root).should be_root
|
226
|
+
end
|
227
|
+
|
228
|
+
it "should return false for non-root documents" do
|
229
|
+
node(:child).should_not be_root
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
describe '#leaf?' do
|
234
|
+
it "should return true for leaf documents" do
|
235
|
+
node(:subchild).should be_leaf
|
236
|
+
node(:other_child).should be_leaf
|
237
|
+
Node.new.should be_leaf
|
238
|
+
end
|
239
|
+
|
240
|
+
it "should return false for non-leaf documents" do
|
241
|
+
node(:child).should_not be_leaf
|
242
|
+
node(:root).should_not be_leaf
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
describe '#depth' do
|
247
|
+
it "should return the depth of this document" do
|
248
|
+
node(:root).depth.should == 0
|
249
|
+
node(:child).depth.should == 1
|
250
|
+
node(:subchild).depth.should == 2
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
describe '#root' do
|
255
|
+
it "should return the root for this document" do
|
256
|
+
node(:subchild).root.should == node(:root)
|
257
|
+
end
|
258
|
+
|
259
|
+
it "should return itself when there is no root" do
|
260
|
+
new_node = Node.new
|
261
|
+
new_node.root.should be(new_node)
|
262
|
+
end
|
263
|
+
|
264
|
+
it "should return it root when it's not saved yet" do
|
265
|
+
root = Node.new(:name => 'root')
|
266
|
+
new_node = Node.new(:name => 'child')
|
267
|
+
new_node.parent = root
|
268
|
+
new_node.root.should be(root)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
describe 'ancestors' do
|
273
|
+
it "#ancestors should return the documents ancestors" do
|
274
|
+
node(:subchild).ancestors.to_a.should == [node(:root), node(:child)]
|
275
|
+
end
|
276
|
+
|
277
|
+
it "#ancestors_and_self should return the documents ancestors and itself" do
|
278
|
+
node(:subchild).ancestors_and_self.to_a.should == [node(:root), node(:child), node(:subchild)]
|
279
|
+
end
|
280
|
+
|
281
|
+
describe '#ancestor_of?' do
|
282
|
+
it "should return true for ancestors" do
|
283
|
+
node(:child).should be_ancestor_of(node(:subchild))
|
284
|
+
end
|
285
|
+
|
286
|
+
it "should return false for non-ancestors" do
|
287
|
+
node(:other_child).should_not be_ancestor_of(node(:subchild))
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
describe 'descendants' do
|
293
|
+
it "#descendants should return the documents descendants" do
|
294
|
+
node(:root).descendants.to_a.should =~ [node(:child), node(:other_child), node(:subchild)]
|
295
|
+
end
|
296
|
+
|
297
|
+
it "#descendants_and_self should return the documents descendants and itself" do
|
298
|
+
node(:root).descendants_and_self.to_a.should =~ [node(:root), node(:child), node(:other_child), node(:subchild)]
|
299
|
+
end
|
300
|
+
|
301
|
+
describe '#descendant_of?' do
|
302
|
+
it "should return true for descendants" do
|
303
|
+
subchild = node(:subchild)
|
304
|
+
subchild.should be_descendant_of(node(:child))
|
305
|
+
subchild.should be_descendant_of(node(:root))
|
306
|
+
end
|
307
|
+
|
308
|
+
it "should return false for non-descendants" do
|
309
|
+
node(:subchild).should_not be_descendant_of(node(:other_child))
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe 'siblings' do
|
315
|
+
it "#siblings should return the documents siblings" do
|
316
|
+
node(:child).siblings.to_a.should == [node(:other_child)]
|
317
|
+
end
|
318
|
+
|
319
|
+
it "#siblings_and_self should return the documents siblings and itself" do
|
320
|
+
node(:child).siblings_and_self.is_a?(Mongoid::Criteria).should == true
|
321
|
+
node(:child).siblings_and_self.to_a.should == [node(:child), node(:other_child)]
|
322
|
+
end
|
323
|
+
|
324
|
+
describe '#sibling_of?' do
|
325
|
+
it "should return true for siblings" do
|
326
|
+
node(:child).should be_sibling_of(node(:other_child))
|
327
|
+
end
|
328
|
+
|
329
|
+
it "should return false for non-siblings" do
|
330
|
+
node(:root).should_not be_sibling_of(node(:other_child))
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
describe '#leaves' do
|
336
|
+
it "should return this documents leaves" do
|
337
|
+
node(:root).leaves.to_a.should =~ [node(:other_child), node(:subchild)]
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
end
|
342
|
+
|
343
|
+
describe 'callbacks' do
|
344
|
+
|
345
|
+
after(:each) do
|
346
|
+
Node.reset_callbacks(:rearrange)
|
347
|
+
end
|
348
|
+
|
349
|
+
it "should provide a before_rearrange callback" do
|
350
|
+
Node.should respond_to :before_rearrange
|
351
|
+
end
|
352
|
+
|
353
|
+
it "should provida an after_rearrange callback" do
|
354
|
+
Node.should respond_to :after_rearrange
|
355
|
+
end
|
356
|
+
|
357
|
+
describe 'before rearrange callback' do
|
358
|
+
|
359
|
+
it "should be called before the document is rearranged" do
|
360
|
+
Node.before_rearrange :callback
|
361
|
+
node = Node.new
|
362
|
+
node.should_receive(:callback).ordered
|
363
|
+
node.should_receive(:rearrange).ordered
|
364
|
+
node.save
|
365
|
+
end
|
366
|
+
|
367
|
+
end
|
368
|
+
|
369
|
+
describe 'after rearrange callback' do
|
370
|
+
|
371
|
+
it "should be called after the document is rearranged" do
|
372
|
+
Node.after_rearrange :callback
|
373
|
+
node = Node.new
|
374
|
+
node.should_receive(:rearrange).ordered
|
375
|
+
node.should_receive(:callback).ordered
|
376
|
+
node.save
|
377
|
+
end
|
378
|
+
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|
382
|
+
end
|