locomotive-mongoid-tree 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.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
|