mongoid-tree 0.7.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -2
- data/LICENSE +1 -1
- data/README.md +215 -0
- data/Rakefile +2 -8
- data/lib/mongoid/tree.rb +164 -45
- data/lib/mongoid/tree/ordering.rb +30 -0
- data/lib/mongoid/tree/traversal.rb +34 -36
- data/spec/mongoid/tree/traversal_spec.rb +50 -36
- data/spec/mongoid/tree_spec.rb +23 -13
- data/spec/spec_helper.rb +2 -5
- data/spec/support/models/node.rb +8 -0
- metadata +43 -33
- data/README.rdoc +0 -192
@@ -44,6 +44,8 @@ module Mongoid
|
|
44
44
|
|
45
45
|
##
|
46
46
|
# Returns a chainable criteria for this document's ancestors
|
47
|
+
#
|
48
|
+
# @return [Mongoid::Criteria] Mongoid criteria to retrieve the documents ancestors
|
47
49
|
def ancestors
|
48
50
|
base_class.unscoped.where(:_id.in => parent_ids)
|
49
51
|
end
|
@@ -51,6 +53,8 @@ module Mongoid
|
|
51
53
|
##
|
52
54
|
# Returns siblings below the current document.
|
53
55
|
# Siblings with a position greater than this documents's position.
|
56
|
+
#
|
57
|
+
# @return [Mongoid::Criteria] Mongoid criteria to retrieve the documents lower_siblings
|
54
58
|
def lower_siblings
|
55
59
|
self.siblings.where(:position.gt => self.position)
|
56
60
|
end
|
@@ -58,36 +62,48 @@ module Mongoid
|
|
58
62
|
##
|
59
63
|
# Returns siblings above the current document.
|
60
64
|
# Siblings with a position lower than this documents's position.
|
65
|
+
#
|
66
|
+
# @return [Mongoid::Criteria] Mongoid criteria to retrieve the documents higher_siblings
|
61
67
|
def higher_siblings
|
62
68
|
self.siblings.where(:position.lt => self.position)
|
63
69
|
end
|
64
70
|
|
65
71
|
##
|
66
72
|
# Returns the lowest sibling (could be self)
|
73
|
+
#
|
74
|
+
# @return [Mongoid::Document] The lowest sibling
|
67
75
|
def last_sibling_in_list
|
68
76
|
siblings_and_self.last
|
69
77
|
end
|
70
78
|
|
71
79
|
##
|
72
80
|
# Returns the highest sibling (could be self)
|
81
|
+
#
|
82
|
+
# @return [Mongoid::Document] The highest sibling
|
73
83
|
def first_sibling_in_list
|
74
84
|
siblings_and_self.first
|
75
85
|
end
|
76
86
|
|
77
87
|
##
|
78
88
|
# Is this the highest sibling?
|
89
|
+
#
|
90
|
+
# @return [Boolean] Whether the document is the highest sibling
|
79
91
|
def at_top?
|
80
92
|
higher_siblings.empty?
|
81
93
|
end
|
82
94
|
|
83
95
|
##
|
84
96
|
# Is this the lowest sibling?
|
97
|
+
#
|
98
|
+
# @return [Boolean] Whether the document is the lowest sibling
|
85
99
|
def at_bottom?
|
86
100
|
lower_siblings.empty?
|
87
101
|
end
|
88
102
|
|
89
103
|
##
|
90
104
|
# Move this node above all its siblings
|
105
|
+
#
|
106
|
+
# @return [undefined]
|
91
107
|
def move_to_top
|
92
108
|
return true if at_top?
|
93
109
|
move_above(first_sibling_in_list)
|
@@ -95,6 +111,8 @@ module Mongoid
|
|
95
111
|
|
96
112
|
##
|
97
113
|
# Move this node below all its siblings
|
114
|
+
#
|
115
|
+
# @return [undefined]
|
98
116
|
def move_to_bottom
|
99
117
|
return true if at_bottom?
|
100
118
|
move_below(last_sibling_in_list)
|
@@ -102,6 +120,8 @@ module Mongoid
|
|
102
120
|
|
103
121
|
##
|
104
122
|
# Move this node one position up
|
123
|
+
#
|
124
|
+
# @return [undefined]
|
105
125
|
def move_up
|
106
126
|
return if at_top?
|
107
127
|
siblings.where(:position => self.position - 1).first.inc(:position, 1)
|
@@ -110,6 +130,8 @@ module Mongoid
|
|
110
130
|
|
111
131
|
##
|
112
132
|
# Move this node one position down
|
133
|
+
#
|
134
|
+
# @return [undefined]
|
113
135
|
def move_down
|
114
136
|
return if at_bottom?
|
115
137
|
siblings.where(:position => self.position + 1).first.inc(:position, -1)
|
@@ -120,6 +142,10 @@ module Mongoid
|
|
120
142
|
# Move this node above the specified node
|
121
143
|
#
|
122
144
|
# This method changes the node's parent if nescessary.
|
145
|
+
#
|
146
|
+
# @param [Mongoid::Tree] other document to move this document above
|
147
|
+
#
|
148
|
+
# @return [undefined]
|
123
149
|
def move_above(other)
|
124
150
|
unless sibling_of?(other)
|
125
151
|
self.parent_id = other.parent_id
|
@@ -144,6 +170,10 @@ module Mongoid
|
|
144
170
|
# Move this node below the specified node
|
145
171
|
#
|
146
172
|
# This method changes the node's parent if nescessary.
|
173
|
+
#
|
174
|
+
# @param [Mongoid::Tree] other document to move this document below
|
175
|
+
#
|
176
|
+
# @return [undefined]
|
147
177
|
def move_below(other)
|
148
178
|
unless sibling_of?(other)
|
149
179
|
self.parent_id = other.parent_id
|
@@ -1,4 +1,4 @@
|
|
1
|
-
module Mongoid
|
1
|
+
module Mongoid
|
2
2
|
module Tree
|
3
3
|
##
|
4
4
|
# = Mongoid::Tree::Traversal
|
@@ -48,37 +48,34 @@ module Mongoid # :nodoc:
|
|
48
48
|
module Traversal
|
49
49
|
extend ActiveSupport::Concern
|
50
50
|
|
51
|
-
##
|
52
|
-
# :singleton-method: traverse
|
53
|
-
# Traverses the entire tree, one root at a time, using the given traversal
|
54
|
-
# method (Default is :depth_first).
|
55
|
-
#
|
56
|
-
# See Mongoid::Tree::Traversal for available traversal methods.
|
57
|
-
#
|
58
|
-
# Example:
|
59
|
-
#
|
60
|
-
# # Say we have the following tree, and want to print its hierarchy:
|
61
|
-
# # root_1
|
62
|
-
# # child_1_a
|
63
|
-
# # root_2
|
64
|
-
# # child_2_a
|
65
|
-
# # child_2_a_1
|
66
|
-
#
|
67
|
-
# Node.traverse(:depth_first) do |node|
|
68
|
-
# indentation = ' ' * node.depth
|
69
|
-
#
|
70
|
-
# puts "#{indentation}#{node.name}"
|
71
|
-
# end
|
72
|
-
#
|
73
51
|
|
74
52
|
##
|
75
|
-
#
|
76
|
-
#
|
77
|
-
module ClassMethods
|
53
|
+
# This module implements class methods that will be available
|
54
|
+
# on the document that includes Mongoid::Tree::Traversal
|
55
|
+
module ClassMethods
|
56
|
+
##
|
57
|
+
# Traverses the entire tree, one root at a time, using the given traversal
|
58
|
+
# method (Default is :depth_first).
|
59
|
+
#
|
60
|
+
# See Mongoid::Tree::Traversal for available traversal methods.
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
#
|
64
|
+
# # Say we have the following tree, and want to print its hierarchy:
|
65
|
+
# # root_1
|
66
|
+
# # child_1_a
|
67
|
+
# # root_2
|
68
|
+
# # child_2_a
|
69
|
+
# # child_2_a_1
|
70
|
+
#
|
71
|
+
# Node.traverse(:depth_first) do |node|
|
72
|
+
# indentation = ' ' * node.depth
|
73
|
+
#
|
74
|
+
# puts "#{indentation}#{node.name}"
|
75
|
+
# end
|
76
|
+
#
|
78
77
|
def traverse(type = :depth_first, &block)
|
79
|
-
|
80
|
-
roots.each { |root| res.concat root.traverse(type, &block) }
|
81
|
-
res
|
78
|
+
roots.collect { |root| root.traverse(type, &block) }.flatten
|
82
79
|
end
|
83
80
|
end
|
84
81
|
|
@@ -88,7 +85,7 @@ module Mongoid # :nodoc:
|
|
88
85
|
#
|
89
86
|
# See Mongoid::Tree::Traversal for available traversal methods.
|
90
87
|
#
|
91
|
-
#
|
88
|
+
# @example
|
92
89
|
#
|
93
90
|
# results = []
|
94
91
|
# root.traverse(:depth_first) do |node|
|
@@ -96,28 +93,29 @@ module Mongoid # :nodoc:
|
|
96
93
|
# end
|
97
94
|
#
|
98
95
|
# root.traverse(:depth_first).map(&:name)
|
96
|
+
# root.traverse(:depth_first, &:name)
|
99
97
|
#
|
100
98
|
def traverse(type = :depth_first, &block)
|
101
|
-
|
102
|
-
block ||= lambda { |node| res << node }
|
99
|
+
block ||= lambda { |node| node }
|
103
100
|
send("#{type}_traversal", &block)
|
104
|
-
res
|
105
101
|
end
|
106
102
|
|
107
103
|
private
|
108
104
|
|
109
105
|
def depth_first_traversal(&block)
|
110
|
-
block.call(self)
|
111
|
-
|
106
|
+
result = [block.call(self)] + self.children.collect { |c| c.send(:depth_first_traversal, &block) }
|
107
|
+
result.flatten
|
112
108
|
end
|
113
109
|
|
114
110
|
def breadth_first_traversal(&block)
|
111
|
+
result = []
|
115
112
|
queue = [self]
|
116
113
|
while queue.any? do
|
117
114
|
node = queue.shift
|
118
|
-
block.call(node)
|
115
|
+
result << block.call(node)
|
119
116
|
queue += node.children
|
120
117
|
end
|
118
|
+
result
|
121
119
|
end
|
122
120
|
end
|
123
121
|
end
|
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Mongoid::Tree::Traversal do
|
4
4
|
|
5
|
-
subject {
|
5
|
+
subject { OrderedNode }
|
6
6
|
|
7
7
|
describe '#traverse' do
|
8
8
|
|
@@ -22,53 +22,59 @@ describe Mongoid::Tree::Traversal do
|
|
22
22
|
subject.should_receive(:depth_first_traversal)
|
23
23
|
subject.traverse {}
|
24
24
|
end
|
25
|
-
|
26
25
|
end
|
27
26
|
|
28
27
|
describe 'depth first traversal' do
|
29
28
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
-
|
35
|
-
|
36
|
-
-
|
37
|
-
|
38
|
-
|
39
|
-
ENDTREE
|
40
|
-
|
41
|
-
result = []
|
42
|
-
node(:node1).traverse(:depth_first) { |node| result << node }
|
43
|
-
result.collect { |n| n.name.to_sym }.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
44
|
-
end
|
45
|
-
|
46
|
-
it "should traverse correctly on merged trees" do
|
47
|
-
|
48
|
-
setup_tree <<-ENDTREE
|
49
|
-
- node4:
|
50
|
-
- node5
|
51
|
-
- node6:
|
29
|
+
describe 'with unmodified tree' do
|
30
|
+
before do
|
31
|
+
setup_tree <<-ENDTREE
|
32
|
+
node1:
|
33
|
+
- node2:
|
34
|
+
- node3
|
35
|
+
- node4:
|
36
|
+
- node5
|
37
|
+
- node6
|
52
38
|
- node7
|
39
|
+
ENDTREE
|
40
|
+
end
|
53
41
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
42
|
+
it "should traverse correctly" do
|
43
|
+
result = []
|
44
|
+
node(:node1).traverse(:depth_first) { |node| result << node }
|
45
|
+
result.collect { |n| n.name.to_sym }.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
46
|
+
end
|
58
47
|
|
48
|
+
it "should return and array containing the results of the block for each node" do
|
49
|
+
result = node(:node1).traverse(:depth_first) { |n| n.name.to_sym }
|
50
|
+
result.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
51
|
+
end
|
52
|
+
end
|
59
53
|
|
60
|
-
|
54
|
+
describe 'with merged trees' do
|
55
|
+
before do
|
56
|
+
setup_tree <<-ENDTREE
|
57
|
+
- node4:
|
58
|
+
- node5
|
59
|
+
- node6:
|
60
|
+
- node7
|
61
61
|
|
62
|
+
- node1:
|
63
|
+
- node2:
|
64
|
+
- node3
|
65
|
+
ENDTREE
|
62
66
|
|
63
|
-
|
64
|
-
|
65
|
-
|
67
|
+
node(:node1).children << node(:node4)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should traverse correctly" do
|
71
|
+
result = node(:node1).traverse(:depth_first) { |n| n.name.to_sym }
|
72
|
+
result.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
73
|
+
end
|
66
74
|
end
|
67
75
|
|
68
76
|
describe 'with reordered nodes' do
|
69
77
|
|
70
|
-
subject { OrderedNode }
|
71
|
-
|
72
78
|
before do
|
73
79
|
setup_tree <<-ENDTREE
|
74
80
|
node1:
|
@@ -79,6 +85,7 @@ describe Mongoid::Tree::Traversal do
|
|
79
85
|
- node5
|
80
86
|
- node7
|
81
87
|
ENDTREE
|
88
|
+
|
82
89
|
node(:node5).move_above(node(:node6))
|
83
90
|
end
|
84
91
|
|
@@ -99,8 +106,8 @@ describe Mongoid::Tree::Traversal do
|
|
99
106
|
|
100
107
|
describe 'breadth first traversal' do
|
101
108
|
|
102
|
-
|
103
|
-
|
109
|
+
before do
|
110
|
+
setup_tree <<-ENDTREE
|
104
111
|
node1:
|
105
112
|
- node2:
|
106
113
|
- node5
|
@@ -109,12 +116,19 @@ describe Mongoid::Tree::Traversal do
|
|
109
116
|
- node7
|
110
117
|
- node4
|
111
118
|
ENDTREE
|
119
|
+
end
|
112
120
|
|
121
|
+
it "should traverse correctly" do
|
113
122
|
result = []
|
114
123
|
node(:node1).traverse(:breadth_first) { |n| result << n }
|
115
124
|
result.collect { |n| n.name.to_sym }.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
116
125
|
end
|
117
126
|
|
127
|
+
it "should return and array containing the results of the block for each node" do
|
128
|
+
result = node(:node1).traverse(:breadth_first) { |n| n.name.to_sym }
|
129
|
+
result.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
130
|
+
end
|
131
|
+
|
118
132
|
end
|
119
133
|
|
120
134
|
describe '.traverse' do
|
data/spec/mongoid/tree_spec.rb
CHANGED
@@ -7,16 +7,16 @@ describe Mongoid::Tree do
|
|
7
7
|
it "should reference many children as inverse of parent with index" do
|
8
8
|
a = Node.reflect_on_association(:children)
|
9
9
|
a.should be
|
10
|
-
a.macro.should eql(:
|
10
|
+
a.macro.should eql(:has_many)
|
11
11
|
a.class_name.should eql('Node')
|
12
12
|
a.foreign_key.should eql('parent_id')
|
13
|
-
Node.index_options.should have_key('parent_id')
|
13
|
+
Node.index_options.should have_key('parent_id' => 1)
|
14
14
|
end
|
15
15
|
|
16
16
|
it "should be referenced in one parent as inverse of children" do
|
17
17
|
a = Node.reflect_on_association(:parent)
|
18
18
|
a.should be
|
19
|
-
a.macro.should eql(:
|
19
|
+
a.macro.should eql(:belongs_to)
|
20
20
|
a.class_name.should eql('Node')
|
21
21
|
a.inverse_of.should eql(:children)
|
22
22
|
end
|
@@ -26,7 +26,7 @@ describe Mongoid::Tree do
|
|
26
26
|
f.should be
|
27
27
|
f.options[:type].should eql(Array)
|
28
28
|
f.options[:default].should eql([])
|
29
|
-
Node.index_options.should have_key(:parent_ids)
|
29
|
+
Node.index_options.should have_key(:parent_ids => 1)
|
30
30
|
end
|
31
31
|
|
32
32
|
describe 'when new' do
|
@@ -45,7 +45,7 @@ describe Mongoid::Tree do
|
|
45
45
|
it "should save its unsaved children" do
|
46
46
|
root = Node.new(:name => 'root'); child = Node.new(:name => 'child')
|
47
47
|
root.children << child
|
48
|
-
child.should_receive(:save)
|
48
|
+
child.should_receive(:save)
|
49
49
|
root.save
|
50
50
|
end
|
51
51
|
end
|
@@ -66,15 +66,15 @@ describe Mongoid::Tree do
|
|
66
66
|
it "should set the child's parent_id when added to parent's children" do
|
67
67
|
root = Node.create; child = Node.create
|
68
68
|
root.children << child
|
69
|
-
child.parent.should
|
70
|
-
child.parent_id.should
|
69
|
+
child.parent.should eq(root)
|
70
|
+
child.parent_id.should eq(root.id)
|
71
71
|
end
|
72
72
|
|
73
73
|
it "should set the child's parent_id parent is set on child" do
|
74
74
|
root = Node.create; child = Node.create
|
75
75
|
child.parent = root
|
76
|
-
child.parent.should
|
77
|
-
child.parent_id.should
|
76
|
+
child.parent.should eq(root)
|
77
|
+
child.parent_id.should eq(root.id)
|
78
78
|
end
|
79
79
|
|
80
80
|
it "should rebuild its parent_ids" do
|
@@ -245,9 +245,9 @@ describe Mongoid::Tree do
|
|
245
245
|
|
246
246
|
describe '#depth' do
|
247
247
|
it "should return the depth of this document" do
|
248
|
-
node(:root).depth.should
|
249
|
-
node(:child).depth.should
|
250
|
-
node(:subchild).depth.should
|
248
|
+
node(:root).depth.should eq(0)
|
249
|
+
node(:child).depth.should eq(1)
|
250
|
+
node(:subchild).depth.should eq(2)
|
251
251
|
end
|
252
252
|
end
|
253
253
|
|
@@ -317,7 +317,7 @@ describe Mongoid::Tree do
|
|
317
317
|
end
|
318
318
|
|
319
319
|
it "#siblings_and_self should return the documents siblings and itself" do
|
320
|
-
node(:child).siblings_and_self.
|
320
|
+
node(:child).siblings_and_self.should be_kind_of(Mongoid::Criteria)
|
321
321
|
node(:child).siblings_and_self.to_a.should == [node(:child), node(:other_child)]
|
322
322
|
end
|
323
323
|
|
@@ -378,5 +378,15 @@ describe Mongoid::Tree do
|
|
378
378
|
|
379
379
|
end
|
380
380
|
|
381
|
+
describe 'cascading to embedded documents' do
|
382
|
+
|
383
|
+
it 'should not raise a NoMethodError' do
|
384
|
+
node = NodeWithEmbeddedDocument.new
|
385
|
+
document = node.build_embedded_document
|
386
|
+
expect { node.save }.to_not raise_error NoMethodError
|
387
|
+
end
|
388
|
+
|
389
|
+
end
|
390
|
+
|
381
391
|
end
|
382
392
|
end
|