├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .travis.yml
├── Gemfile
├── Guardfile
├── LICENSE
├── README.md
├── Rakefile
├── lib
└── mongoid
│ ├── tree.rb
│ └── tree
│ ├── counter_caching.rb
│ ├── ordering.rb
│ └── traversal.rb
├── mongoid-tree.gemspec
└── spec
├── mongoid
├── tree
│ ├── counter_caching_spec.rb
│ ├── ordering_spec.rb
│ └── traversal_spec.rb
└── tree_spec.rb
├── spec_helper.rb
└── support
├── logger.rb
├── macros
└── tree_macros.rb
└── models
└── node.rb
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on: [push]
3 | jobs:
4 | build:
5 | strategy:
6 | fail-fast: false
7 | matrix:
8 | mongoid: [4, 5, 6, 7, 8, 9 HEAD]
9 | ruby: [3.2]
10 | include:
11 | - mongoid: 9
12 | ruby: 3.1
13 | - mongoid: 9
14 | ruby: 3.2
15 | - mongoid: 9
16 | ruby: 3.3
17 |
18 | runs-on: ubuntu-latest
19 | steps:
20 | - id: mongodb
21 | name: Start MongoDB
22 | uses: mongodb-labs/drivers-evergreen-tools@master
23 | - uses: actions/checkout@v4
24 | - uses: ruby/setup-ruby@v1
25 | with:
26 | ruby-version: ${{ matrix.ruby }}
27 | bundler-cache: true
28 | - name: RSpec
29 | run: bundle exec rake
30 | env:
31 | MONGODB_URI: "${{ steps.mongodb.outputs.cluster-uri }}"
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | doc
3 | *.gem
4 | Gemfile.lock
5 | .yardoc/
6 | tmp/
7 | log
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | before_install:
2 | - gem update --system
3 | - gem --version
4 | language: ruby
5 | rvm:
6 | - 2.2.5
7 | - 2.3.1
8 | - jruby-9.1.13.0
9 | services:
10 | - mongodb
11 | matrix:
12 | allow_failures:
13 | - rvm: ruby-head
14 | include:
15 | - rvm: 2.3.1
16 | env: MONGOID_VERSION=4
17 | - rvm: 2.3.1
18 | env: MONGOID_VERSION=5
19 | - rvm: 2.3.1
20 | env: MONGOID_VERSION=6
21 | - rvm: 2.3.1
22 | env: MONGOID_VERSION=7
23 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
5 | case version = ENV['MONGOID_VERSION'] || '~> 7.0'
6 | when 'HEAD' then gem 'mongoid', github: 'mongodb/mongoid'
7 | when /9/ then gem 'mongoid', '~> 9.0'
8 | when /8/ then gem 'mongoid', '~> 8.0'
9 | when /7/ then gem 'mongoid', '~> 7.0'
10 | when /6/ then gem 'mongoid', '~> 6.0'
11 | when /5/ then gem 'mongoid', '~> 5.0'
12 | when /4/ then gem 'mongoid', '~> 4.0'
13 | else gem 'mongoid', version
14 | end
15 |
16 | unless ENV['CI']
17 | gem 'guard-rspec', '>= 0.6.0'
18 | gem 'ruby_gntp', '>= 0.3.4'
19 | gem 'rb-fsevent' if RUBY_PLATFORM =~ /darwin/
20 | end
21 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | guard 'rspec' do
2 | watch(%r{^spec/.+_spec\.rb$})
3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4 | watch(%r{^spec/support/.+\.rb$}) { "spec" }
5 | watch('spec/spec_helper.rb') { "spec" }
6 | end
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010-2013 Benedikt Deicke
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mongoid-tree [](https://github.com/benedikt/mongoid-tree)
2 |
3 | A tree structure for Mongoid documents using the materialized path pattern
4 |
5 | ## Requirements
6 |
7 | * mongoid (>= 4.0, < 10.0)
8 |
9 | For a mongoid 3.x compatible version, please use mongoid-tree 1.0.x,
10 | for a mongoid 2.x compatible version, please use mongoid-tree 0.7.x.
11 |
12 |
13 | ## Install
14 |
15 | To install mongoid_tree, simply add it to your Gemfile:
16 |
17 | gem 'mongoid-tree', require: 'mongoid/tree'
18 |
19 | In order to get the latest development version of mongoid-tree:
20 |
21 | gem 'mongoid-tree', git: 'git://github.com/benedikt/mongoid-tree', branch: :main
22 |
23 | You might want to add `require: nil` option and explicitly `require 'mongoid/tree'` where needed and finally run
24 |
25 | bundle install
26 |
27 | ### Upgrade from mongoid-tree 1.x
28 |
29 | To fix issues with the ordering of ancestors, mongoid-tree 2.0 introduces a new `depth` field to the documents that include the `Mongoid::Tree` module. In case your project uses its own `depth` field, you can now rely on mongoid-tree to handle this.
30 |
31 | ## Usage
32 |
33 | Read the API documentation at https://www.rubydoc.info/github/benedikt/mongoid-tree and take a look at the `Mongoid::Tree` module
34 |
35 | ```ruby
36 | class Node
37 | include Mongoid::Document
38 | include Mongoid::Tree
39 | end
40 | ```
41 |
42 | ### Utility methods
43 |
44 | There are several utility methods that help getting to other related documents in the tree:
45 |
46 | ```ruby
47 | Node.root
48 | Node.roots
49 | Node.leaves
50 |
51 | node.root
52 | node.parent
53 | node.children
54 | node.ancestors
55 | node.ancestors_and_self
56 | node.descendants
57 | node.descendants_and_self
58 | node.siblings
59 | node.siblings_and_self
60 | node.leaves
61 | ```
62 |
63 | In addition it's possible to check certain aspects of the document's position in the tree:
64 |
65 | ```ruby
66 | node.root?
67 | node.leaf?
68 | node.depth
69 | node.ancestor_of?(other)
70 | node.descendant_of?(other)
71 | node.sibling_of?(other)
72 | ```
73 |
74 | See `Mongoid::Tree` for more information on these methods.
75 |
76 |
77 | ### Ordering
78 |
79 | `Mongoid::Tree` doesn't order children by default. To enable ordering of tree nodes include the `Mongoid::Tree::Ordering` module. This will add a `position` field to your document and provide additional utility methods:
80 |
81 | ```ruby
82 | node.lower_siblings
83 | node.higher_siblings
84 | node.first_sibling_in_list
85 | node.last_sibling_in_list
86 |
87 | node.move_up
88 | node.move_down
89 | node.move_to_top
90 | node.move_to_bottom
91 | node.move_above(other)
92 | node.move_below(other)
93 |
94 | node.at_top?
95 | node.at_bottom?
96 | ```
97 |
98 | Example:
99 |
100 | ```ruby
101 | class Node
102 | include Mongoid::Document
103 | include Mongoid::Tree
104 | include Mongoid::Tree::Ordering
105 | end
106 | ```
107 |
108 | See `Mongoid::Tree::Ordering` for more information on these methods.
109 |
110 | ### Traversal
111 |
112 | It's possible to traverse the tree using different traversal methods using the `Mongoid::Tree::Traversal` module.
113 |
114 | Example:
115 |
116 | ```ruby
117 | class Node
118 | include Mongoid::Document
119 | include Mongoid::Tree
120 | include Mongoid::Tree::Traversal
121 | end
122 |
123 | node.traverse(:breadth_first) do |n|
124 | # Do something with Node n
125 | end
126 | ```
127 |
128 | ### Destroying
129 |
130 | `Mongoid::Tree` does not handle destroying of nodes by default. However it provides several strategies that help you to deal with children of deleted documents. You can simply add them as `before_destroy` callbacks.
131 |
132 | Available strategies are:
133 |
134 | * `:nullify_children` -- Sets the children's parent_id to null
135 | * `:move_children_to_parent` -- Moves the children to the current document's parent
136 | * `:destroy_children` -- Destroys all children by calling their `#destroy` method (invokes callbacks)
137 | * `:delete_descendants` -- Deletes all descendants using a database query (doesn't invoke callbacks)
138 |
139 | Example:
140 |
141 | ```ruby
142 | class Node
143 | include Mongoid::Document
144 | include Mongoid::Tree
145 |
146 | before_destroy :nullify_children
147 | end
148 | ```
149 |
150 |
151 | ### Callbacks
152 |
153 | There are two callbacks that are called before and after the rearranging process. This enables you to do additional computations after the documents position in the tree is updated. See `Mongoid::Tree` for details.
154 |
155 | Example:
156 |
157 | ```ruby
158 | class Page
159 | include Mongoid::Document
160 | include Mongoid::Tree
161 |
162 | after_rearrange :rebuild_path
163 |
164 | field :slug
165 | field :path
166 |
167 | private
168 |
169 | def rebuild_path
170 | self.path = self.ancestors_and_self.collect(&:slug).join('/')
171 | end
172 | end
173 | ```
174 |
175 | ### Validations
176 |
177 | `Mongoid::Tree` currently does not validate the document's children or parent associations by default. To explicitly enable validation for children and parent documents it's required to add a `validates_associated` validation.
178 |
179 | Example:
180 |
181 | ```ruby
182 | class Node
183 | include Mongoid::Document
184 | include Mongoid::Tree
185 |
186 | validates_associated :parent, :children
187 | end
188 | ```
189 |
190 | ## Build Status
191 |
192 | mongoid-tree is on [GitHub Actions](https://github.com/benedikt/mongoid-tree/actions) running the specs on Ruby 3.1-3.3 and Mongoid 4.x-9.x.
193 |
194 | ## Known issues
195 |
196 | See [https://github.com/benedikt/mongoid-tree/issues](https://github.com/benedikt/mongoid-tree/issues)
197 |
198 |
199 | ## Repository
200 |
201 | See [https://github.com/benedikt/mongoid-tree](https://github.com/benedikt/mongoid-tree) and feel free to fork it!
202 |
203 |
204 | ## Contributors
205 |
206 | See a list of all contributors at [https://github.com/benedikt/mongoid-tree/contributors](https://github.com/benedikt/mongoid-tree/contributors). Thanks a lot everyone!
207 |
208 |
209 | ## Copyright
210 |
211 | Copyright (c) 2010-2024 Benedikt Deicke. See LICENSE for details.
212 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 | require 'rspec/core/rake_task'
3 | require 'yard'
4 |
5 | RSpec::Core::RakeTask.new(:spec)
6 |
7 | task :default => :spec
8 |
9 | YARD::Rake::YardocTask.new(:doc)
10 |
11 | desc "Open an irb session"
12 | task :console do
13 | sh "irb -rubygems -I lib -r ./spec/spec_helper.rb"
14 | end
15 |
--------------------------------------------------------------------------------
/lib/mongoid/tree.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 |
3 | module Mongoid
4 | ##
5 | # = Mongoid::Tree
6 | #
7 | # This module extends any Mongoid document with tree functionality.
8 | #
9 | # == Usage
10 | #
11 | # Simply include the module in any Mongoid document:
12 | #
13 | # class Node
14 | # include Mongoid::Document
15 | # include Mongoid::Tree
16 | # end
17 | #
18 | # === Using the tree structure
19 | #
20 | # Each document references many children. You can access them using the #children method.
21 | #
22 | # node = Node.create
23 | # node.children.create
24 | # node.children.count # => 1
25 | #
26 | # Every document references one parent (unless it's a root document).
27 | #
28 | # node = Node.create
29 | # node.parent # => nil
30 | # node.children.create
31 | # node.children.first.parent # => node
32 | #
33 | # === Destroying
34 | #
35 | # Mongoid::Tree does not handle destroying of nodes by default. However it provides
36 | # several strategies that help you to deal with children of deleted documents. You can
37 | # simply add them as before_destroy callbacks.
38 | #
39 | # Available strategies are:
40 | #
41 | # * :nullify_children -- Sets the children's parent_id to null
42 | # * :move_children_to_parent -- Moves the children to the current document's parent
43 | # * :destroy_children -- Destroys all children by calling their #destroy method (invokes callbacks)
44 | # * :delete_descendants -- Deletes all descendants using a database query (doesn't invoke callbacks)
45 | #
46 | # Example:
47 | #
48 | # class Node
49 | # include Mongoid::Document
50 | # include Mongoid::Tree
51 | #
52 | # before_destroy :nullify_children
53 | # end
54 | #
55 | # === Callbacks
56 | #
57 | # Mongoid::Tree offers callbacks for its rearranging process. This enables you to
58 | # rebuild certain fields when the document was moved in the tree. Rearranging happens
59 | # before the document is validated. This gives you a chance to validate your additional
60 | # changes done in your callbacks. See ActiveModel::Callbacks and ActiveSupport::Callbacks
61 | # for further details on callbacks.
62 | #
63 | # Example:
64 | #
65 | # class Page
66 | # include Mongoid::Document
67 | # include Mongoid::Tree
68 | #
69 | # after_rearrange :rebuild_path
70 | #
71 | # field :slug
72 | # field :path
73 | #
74 | # private
75 | #
76 | # def rebuild_path
77 | # self.path = self.ancestors_and_self.collect(&:slug).join('/')
78 | # end
79 | # end
80 | #
81 | module Tree
82 | extend ActiveSupport::Concern
83 |
84 | autoload :Ordering, 'mongoid/tree/ordering'
85 | autoload :Traversal, 'mongoid/tree/traversal'
86 | autoload :CounterCaching, 'mongoid/tree/counter_caching'
87 |
88 | included do
89 | has_many :children, :class_name => self.name, :foreign_key => :parent_id, :inverse_of => :parent, :validate => false
90 |
91 | options = {
92 | :class_name => self.name,
93 | :inverse_of => :children,
94 | :index => true,
95 | :validate => false,
96 | :optional => true
97 | }
98 |
99 | options.delete(:optional) if Gem::Version.new(Mongoid::VERSION) < Gem::Version.new('6.0.0.beta')
100 |
101 | belongs_to :parent, options
102 |
103 | field :parent_ids, :type => Array, :default => []
104 | index :parent_ids => 1
105 |
106 | field :depth, :type => Integer
107 | index :depth => 1
108 |
109 | set_callback :save, :after, :rearrange_children, :if => :rearrange_children?
110 | set_callback :validation, :before do
111 | run_callbacks(:rearrange) { rearrange }
112 | end
113 |
114 | validate :position_in_tree
115 |
116 | define_model_callbacks :rearrange, :only => [:before, :after]
117 |
118 | class_eval "def base_class; ::#{self.name}; end"
119 | end
120 |
121 | ##
122 | # This module implements class methods that will be available
123 | # on the document that includes Mongoid::Tree
124 | module ClassMethods
125 |
126 | ##
127 | # Returns the first root document
128 | #
129 | # @example
130 | # Node.root
131 | #
132 | # @return [Mongoid::Document] The first root document
133 | def root
134 | roots.first
135 | end
136 |
137 | ##
138 | # Returns all root documents
139 | #
140 | # @example
141 | # Node.roots
142 | #
143 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve all root documents
144 | def roots
145 | where(:parent_id => nil)
146 | end
147 |
148 | ##
149 | # Returns all leaves (be careful, currently involves two queries)
150 | #
151 | # @example
152 | # Node.leaves
153 | #
154 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve all leave nodes
155 | def leaves
156 | where(:_id.nin => only(:parent_id).collect(&:parent_id))
157 | end
158 |
159 | end
160 |
161 | ##
162 | # @!method before_rearrange
163 | # @!scope class
164 | #
165 | # Sets a callback that is called before the document is rearranged
166 | #
167 | # @example
168 | # class Node
169 | # include Mongoid::Document
170 | # include Mongoid::Tree
171 | #
172 | # before_rearrage :do_something
173 | #
174 | # private
175 | #
176 | # def do_something
177 | # # ...
178 | # end
179 | # end
180 | #
181 | # @note Generated by ActiveSupport
182 | #
183 | # @return [undefined]
184 |
185 | ##
186 | # @!method after_rearrange
187 | # @!scope class
188 | #
189 | # Sets a callback that is called after the document is rearranged
190 | #
191 | # @example
192 | # class Node
193 | # include Mongoid::Document
194 | # include Mongoid::Tree
195 | #
196 | # after_rearrange :do_something
197 | #
198 | # private
199 | #
200 | # def do_something
201 | # # ...
202 | # end
203 | # end
204 | #
205 | # @note Generated by ActiveSupport
206 | #
207 | # @return [undefined]
208 |
209 | ##
210 | # @!method children
211 | # Returns a list of the document's children. It's a references_many association.
212 | #
213 | # @note Generated by Mongoid
214 | #
215 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's children
216 |
217 | ##
218 | # @!method parent
219 | # Returns the document's parent (unless it's a root document). It's a referenced_in association.
220 | #
221 | # @note Generated by Mongoid
222 | #
223 | # @return [Mongoid::Document] The document's parent document
224 |
225 | ##
226 | # @!method parent=(document)
227 | # Sets this documents parent document.
228 | #
229 | # @note Generated by Mongoid
230 | #
231 | # @param [Mongoid::Tree] document
232 |
233 | ##
234 | # @!method parent_ids
235 | # Returns a list of the document's parent_ids, starting with the root node.
236 | #
237 | # @note Generated by Mongoid
238 | #
239 | # @return [Array] The ids of the document's ancestors
240 |
241 | ##
242 | # Returns the depth of this document (number of ancestors)
243 | #
244 | # @example
245 | # Node.root.depth # => 0
246 | # Node.root.children.first.depth # => 1
247 | #
248 | # @return [Fixnum] Depth of this document
249 | def depth
250 | super || parent_ids.count
251 | end
252 |
253 | ##
254 | # Is this document a root node (has no parent)?
255 | #
256 | # @return [Boolean] Whether the document is a root node
257 | def root?
258 | parent_id.nil?
259 | end
260 |
261 | ##
262 | # Is this document a leaf node (has no children)?
263 | #
264 | # @return [Boolean] Whether the document is a leaf node
265 | def leaf?
266 | children.empty?
267 | end
268 |
269 | ##
270 | # Returns this document's root node. Returns `self` if the
271 | # current document is a root node
272 | #
273 | # @example
274 | # node = Node.find(...)
275 | # node.root
276 | #
277 | # @return [Mongoid::Document] The documents root node
278 | def root
279 | if parent_ids.present?
280 | base_class.find(parent_ids.first)
281 | else
282 | self.root? ? self : self.parent.root
283 | end
284 | end
285 |
286 | ##
287 | # Returns a chainable criteria for this document's ancestors
288 | #
289 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve the documents ancestors
290 | def ancestors
291 | base_class.where(:_id.in => parent_ids).order(:depth => :asc)
292 | end
293 |
294 | ##
295 | # Returns an array of this document's ancestors and itself
296 | #
297 | # @return [Array] Array of the document's ancestors and itself
298 | def ancestors_and_self
299 | ancestors + [self]
300 | end
301 |
302 | ##
303 | # Is this document an ancestor of the other document?
304 | #
305 | # @param [Mongoid::Tree] other document to check against
306 | #
307 | # @return [Boolean] The document is an ancestor of the other document
308 | def ancestor_of?(other)
309 | other.parent_ids.include?(self.id)
310 | end
311 |
312 | ##
313 | # Returns a chainable criteria for this document's descendants
314 | #
315 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's descendants
316 | def descendants
317 | base_class.where(:parent_ids => self.id)
318 | end
319 |
320 | ##
321 | # Returns and array of this document and it's descendants
322 | #
323 | # @return [Array] Array of the document itself and it's descendants
324 | def descendants_and_self
325 | [self] + descendants
326 | end
327 |
328 | ##
329 | # Is this document a descendant of the other document?
330 | #
331 | # @param [Mongoid::Tree] other document to check against
332 | #
333 | # @return [Boolean] The document is a descendant of the other document
334 | def descendant_of?(other)
335 | self.parent_ids.include?(other.id)
336 | end
337 |
338 | ##
339 | # Returns this document's siblings
340 | #
341 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's siblings
342 | def siblings
343 | siblings_and_self.excludes(:id => self.id)
344 | end
345 |
346 | ##
347 | # Returns this document's siblings and itself
348 | #
349 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's siblings and itself
350 | def siblings_and_self
351 | base_class.where(:parent_id => self.parent_id)
352 | end
353 |
354 | ##
355 | # Is this document a sibling of the other document?
356 | #
357 | # @param [Mongoid::Tree] other document to check against
358 | #
359 | # @return [Boolean] The document is a sibling of the other document
360 | def sibling_of?(other)
361 | self.parent_id == other.parent_id
362 | end
363 |
364 | ##
365 | # Returns all leaves of this document (be careful, currently involves two queries)
366 | #
367 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's leaves
368 | def leaves
369 | base_class.where(:_id.nin => base_class.only(:parent_id).collect(&:parent_id)).and(:parent_ids => self.id)
370 | end
371 |
372 | ##
373 | # Forces rearranging of all children after next save
374 | #
375 | # @return [undefined]
376 | def rearrange_children!
377 | @rearrange_children = true
378 | end
379 |
380 | ##
381 | # Will the children be rearranged after next save?
382 | #
383 | # @return [Boolean] Whether the children will be rearranged
384 | def rearrange_children?
385 | !!@rearrange_children
386 | end
387 |
388 | ##
389 | # Nullifies all children's parent_id
390 | #
391 | # @return [undefined]
392 | def nullify_children
393 | children.each do |c|
394 | c.parent = c.parent_id = nil
395 | c.save
396 | end
397 | end
398 |
399 | ##
400 | # Moves all children to this document's parent
401 | #
402 | # @return [undefined]
403 | def move_children_to_parent
404 | children.each do |c|
405 | c.parent = self.parent
406 | c.save
407 | end
408 | end
409 |
410 | ##
411 | # Deletes all descendants using the database (doesn't invoke callbacks)
412 | #
413 | # @return [undefined]
414 | def delete_descendants
415 | base_class.delete_all(:conditions => { :parent_ids => self.id })
416 | end
417 |
418 | ##
419 | # Destroys all children by calling their #destroy method (does invoke callbacks)
420 | #
421 | # @return [undefined]
422 | def destroy_children
423 | children.destroy_all
424 | end
425 |
426 | private
427 |
428 | ##
429 | # Updates the parent_ids and marks the children for
430 | # rearrangement when the parent_ids changed
431 | #
432 | # @private
433 | # @return [undefined]
434 | def rearrange
435 | if parent.present?
436 | self.parent_ids = parent.parent_ids + [self.parent_id]
437 | else
438 | self.parent_ids = []
439 | end
440 |
441 | self.depth = parent_ids.size
442 |
443 | rearrange_children! if self.parent_ids_changed?
444 | end
445 |
446 | def rearrange_children
447 | @rearrange_children = false
448 | self.children.each { |c| c.save }
449 | end
450 |
451 | def position_in_tree
452 | errors.add(:parent_id, :invalid) if self.parent_ids.include?(self.id)
453 | end
454 | end
455 | end
456 |
--------------------------------------------------------------------------------
/lib/mongoid/tree/counter_caching.rb:
--------------------------------------------------------------------------------
1 | module Mongoid
2 | module Tree
3 | ##
4 | # = Mongoid::Tree::CounterCaching
5 | #
6 | # Mongoid::Tree doesn't use a counter cache for the children by default.
7 | # To enable counter caching for each node's children, include
8 | # both Mongoid::Tree and Mongoid::Tree::CounterCaching into your document.
9 | module CounterCaching
10 | extend ActiveSupport::Concern
11 |
12 | included do
13 | field :children_count, :type => Integer, :default => 0
14 |
15 | metadata = relations['parent']
16 | metadata.options[:counter_cache] = true
17 |
18 | if respond_to?(:add_counter_cache_callbacks)
19 | add_counter_cache_callbacks(metadata)
20 | else
21 | metadata.send(:define_counter_cache_callbacks!)
22 | end
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/mongoid/tree/ordering.rb:
--------------------------------------------------------------------------------
1 | module Mongoid
2 | module Tree
3 | ##
4 | # = Mongoid::Tree::Ordering
5 | #
6 | # Mongoid::Tree doesn't order the tree by default. To enable ordering of children
7 | # include both Mongoid::Tree and Mongoid::Tree::Ordering into your document.
8 | #
9 | # == Utility methods
10 | #
11 | # This module adds methods to get related siblings depending on their position:
12 | #
13 | # node.lower_siblings
14 | # node.higher_siblings
15 | # node.first_sibling_in_list
16 | # node.last_sibling_in_list
17 | #
18 | # There are several methods to move nodes around in the list:
19 | #
20 | # node.move_up
21 | # node.move_down
22 | # node.move_to_top
23 | # node.move_to_bottom
24 | # node.move_above(other)
25 | # node.move_below(other)
26 | #
27 | # Additionally there are some methods to check aspects of the document
28 | # in the list of children:
29 | #
30 | # node.at_top?
31 | # node.at_bottom?
32 | module Ordering
33 | extend ActiveSupport::Concern
34 |
35 | included do
36 | field :position, :type => Integer
37 |
38 | default_scope ->{ asc(:position) }
39 |
40 | before_save :assign_default_position, :if => :assign_default_position?
41 | before_save :reposition_former_siblings, :if => :sibling_reposition_required?
42 | after_destroy :move_lower_siblings_up
43 | end
44 |
45 | ##
46 | # Returns a chainable criteria for this document's ancestors
47 | #
48 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's ancestors
49 | def ancestors
50 | base_class.unscoped { super }
51 | end
52 |
53 | ##
54 | # Returns siblings below the current document.
55 | # Siblings with a position greater than this document's position.
56 | #
57 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's lower siblings
58 | def lower_siblings
59 | self.siblings.where(:position.gt => self.position)
60 | end
61 |
62 | ##
63 | # Returns siblings above the current document.
64 | # Siblings with a position lower than this document's position.
65 | #
66 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's higher siblings
67 | def higher_siblings
68 | self.siblings.where(:position.lt => self.position)
69 | end
70 |
71 | ##
72 | # Returns siblings between the current document and the other document
73 | # Siblings with a position between this document's position and the other document's position.
74 | #
75 | # @return [Mongoid::Criteria] Mongoid criteria to retrieve the documents between this and the other document
76 | def siblings_between(other)
77 | range = [self.position, other.position].sort
78 | self.siblings.where(:position.gt => range.first, :position.lt => range.last)
79 | end
80 |
81 | ##
82 | # Returns the lowest sibling (could be self)
83 | #
84 | # @return [Mongoid::Document] The lowest sibling
85 | def last_sibling_in_list
86 | siblings_and_self.last
87 | end
88 |
89 | ##
90 | # Returns the highest sibling (could be self)
91 | #
92 | # @return [Mongoid::Document] The highest sibling
93 | def first_sibling_in_list
94 | siblings_and_self.first
95 | end
96 |
97 | ##
98 | # Is this the highest sibling?
99 | #
100 | # @return [Boolean] Whether the document is the highest sibling
101 | def at_top?
102 | higher_siblings.empty?
103 | end
104 |
105 | ##
106 | # Is this the lowest sibling?
107 | #
108 | # @return [Boolean] Whether the document is the lowest sibling
109 | def at_bottom?
110 | lower_siblings.empty?
111 | end
112 |
113 | ##
114 | # Move this node above all its siblings
115 | #
116 | # @return [undefined]
117 | def move_to_top
118 | return true if at_top?
119 | move_above(first_sibling_in_list)
120 | end
121 |
122 | ##
123 | # Move this node below all its siblings
124 | #
125 | # @return [undefined]
126 | def move_to_bottom
127 | return true if at_bottom?
128 | move_below(last_sibling_in_list)
129 | end
130 |
131 | ##
132 | # Move this node one position up
133 | #
134 | # @return [undefined]
135 | def move_up
136 | switch_with_sibling_at_offset(-1) unless at_top?
137 | end
138 |
139 | ##
140 | # Move this node one position down
141 | #
142 | # @return [undefined]
143 | def move_down
144 | switch_with_sibling_at_offset(1) unless at_bottom?
145 | end
146 |
147 | ##
148 | # Move this node above the specified node
149 | #
150 | # This method changes the node's parent if nescessary.
151 | #
152 | # @param [Mongoid::Tree] other document to move this document above
153 | #
154 | # @return [undefined]
155 | def move_above(other)
156 | ensure_to_be_sibling_of(other)
157 |
158 | if position > other.position
159 | new_position = other.position
160 | self.siblings_between(other).inc(:position => 1)
161 | other.inc(:position => 1)
162 | else
163 | new_position = other.position - 1
164 | self.siblings_between(other).inc(:position => -1)
165 | end
166 |
167 | self.position = new_position
168 | save!
169 | end
170 |
171 | ##
172 | # Move this node below the specified node
173 | #
174 | # This method changes the node's parent if nescessary.
175 | #
176 | # @param [Mongoid::Tree] other document to move this document below
177 | #
178 | # @return [undefined]
179 | def move_below(other)
180 | ensure_to_be_sibling_of(other)
181 |
182 | if position > other.position
183 | new_position = other.position + 1
184 | self.siblings_between(other).inc(:position => 1)
185 | else
186 | new_position = other.position
187 | self.siblings_between(other).inc(:position => -1)
188 | other.inc(:position => -1)
189 | end
190 |
191 | self.position = new_position
192 | save!
193 | end
194 |
195 | private
196 |
197 | def switch_with_sibling_at_offset(offset)
198 | siblings.where(:position => self.position + offset).first.inc(:position => -offset)
199 | inc(:position => offset)
200 | end
201 |
202 | def ensure_to_be_sibling_of(other)
203 | return if sibling_of?(other)
204 | self.parent_id = other.parent_id
205 | save!
206 | end
207 |
208 | def move_lower_siblings_up
209 | lower_siblings.inc(:position => -1)
210 | end
211 |
212 | def reposition_former_siblings
213 | former_siblings = base_class.where(:parent_id => attribute_was('parent_id')).
214 | and(:position.gt => (attribute_was('position') || 0)).
215 | excludes(:id => self.id)
216 | former_siblings.inc(:position => -1)
217 | end
218 |
219 | def sibling_reposition_required?
220 | parent_id_changed? && persisted?
221 | end
222 |
223 | def assign_default_position
224 | self.position = if self.siblings.where(:position.ne => nil).any?
225 | self.last_sibling_in_list.position + 1
226 | else
227 | 0
228 | end
229 | end
230 |
231 | def assign_default_position?
232 | self.position.nil? || self.parent_id_changed?
233 | end
234 | end
235 | end
236 | end
237 |
--------------------------------------------------------------------------------
/lib/mongoid/tree/traversal.rb:
--------------------------------------------------------------------------------
1 | module Mongoid
2 | module Tree
3 | ##
4 | # = Mongoid::Tree::Traversal
5 | #
6 | # Mongoid::Tree::Traversal provides a #traverse method to walk through the tree.
7 | # It supports these traversal methods:
8 | #
9 | # * depth_first
10 | # * breadth_first
11 | #
12 | # == Depth First Traversal
13 | #
14 | # See http://en.wikipedia.org/wiki/Depth-first_search for a proper description.
15 | #
16 | # Given a tree like:
17 | #
18 | # node1:
19 | # - node2:
20 | # - node3
21 | # - node4:
22 | # - node5
23 | # - node6
24 | # - node7
25 | #
26 | # Traversing the tree using depth first traversal would visit each node in this order:
27 | #
28 | # node1, node2, node3, node4, node5, node6, node7
29 | #
30 | # == Breadth First Traversal
31 | #
32 | # See http://en.wikipedia.org/wiki/Breadth-first_search for a proper description.
33 | #
34 | # Given a tree like:
35 | #
36 | # node1:
37 | # - node2:
38 | # - node5
39 | # - node3:
40 | # - node6
41 | # - node7
42 | # - node4
43 | #
44 | # Traversing the tree using breadth first traversal would visit each node in this order:
45 | #
46 | # node1, node2, node3, node4, node5, node6, node7
47 | #
48 | module Traversal
49 | extend ActiveSupport::Concern
50 |
51 |
52 | ##
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 | #
77 | def traverse(type = :depth_first, &block)
78 | roots.collect { |root| root.traverse(type, &block) }.flatten
79 | end
80 | end
81 |
82 | ##
83 | # Traverses the tree using the given traversal method (Default is :depth_first)
84 | # and passes each document node to the block.
85 | #
86 | # See Mongoid::Tree::Traversal for available traversal methods.
87 | #
88 | # @example
89 | #
90 | # results = []
91 | # root.traverse(:depth_first) do |node|
92 | # results << node
93 | # end
94 | #
95 | # root.traverse(:depth_first).map(&:name)
96 | # root.traverse(:depth_first, &:name)
97 | #
98 | def traverse(type = :depth_first, &block)
99 | block ||= lambda { |node| node }
100 | send("#{type}_traversal", &block)
101 | end
102 |
103 | private
104 |
105 | def depth_first_traversal(&block)
106 | result = [block.call(self)] + self.children.collect { |c| c.send(:depth_first_traversal, &block) }
107 | result.flatten
108 | end
109 |
110 | def breadth_first_traversal(&block)
111 | result = []
112 | queue = [self]
113 | while queue.any? do
114 | node = queue.shift
115 | result << block.call(node)
116 | queue += node.children
117 | end
118 | result
119 | end
120 | end
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/mongoid-tree.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |s|
2 | s.name = 'mongoid-tree'
3 | s.version = '2.3.0'
4 | s.platform = Gem::Platform::RUBY
5 | s.authors = ['Benedikt Deicke']
6 | s.email = ['benedikt@synatic.net']
7 | s.homepage = 'https://github.com/benedikt/mongoid-tree'
8 | s.summary = 'A tree structure for Mongoid documents'
9 | s.description = 'A tree structure for Mongoid documents using the materialized path pattern'
10 |
11 | s.license = 'MIT'
12 |
13 | s.files = Dir.glob('{lib,spec}/**/*') + %w(LICENSE README.md Rakefile Gemfile)
14 |
15 | s.add_runtime_dependency('mongoid', ['>= 4.0', '< 10'])
16 | s.add_development_dependency('mongoid-compatibility')
17 | s.add_development_dependency('rake', ['>= 0.9.2'])
18 | s.add_development_dependency('rspec', ['~> 3.0'])
19 | s.add_development_dependency('yard', ['>= 0.9.20'])
20 | end
21 |
--------------------------------------------------------------------------------
/spec/mongoid/tree/counter_caching_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Mongoid::Tree::CounterCaching do
4 |
5 | subject { CounterCachedNode }
6 |
7 | before do
8 | setup_tree <<-ENDTREE
9 | node1:
10 | - node2:
11 | - node3
12 | - node4:
13 | - node5
14 | - node6
15 | - node7
16 | ENDTREE
17 | end
18 |
19 | context 'when a child gets created' do
20 | it 'should calculate the counter cache' do
21 | expect(node(:node1).children_count).to eq(3)
22 | end
23 | end
24 |
25 | context 'when a child gets destroyed' do
26 | it 'should update the counter cache' do
27 | node(:node4).destroy
28 | expect(node(:node1).children_count).to eq(2)
29 | end
30 | end
31 |
32 | context 'when a child gets moved' do
33 | it 'should update the counter cache' do
34 | node(:node6).update(parent: node(:node1))
35 | expect(node(:node4).children_count).to eq(1)
36 | expect(node(:node1).children_count).to eq(4)
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/mongoid/tree/ordering_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Mongoid::Tree::Ordering do
4 |
5 | subject { OrderedNode }
6 |
7 | it "should store position as an Integer with a default of nil" do
8 | f = OrderedNode.fields['position']
9 | expect(f).not_to be_nil
10 | expect(f.options[:type]).to eq(Integer)
11 | expect(f.options[:default]).not_to be
12 | end
13 |
14 | describe 'when saved' do
15 | before(:each) do
16 | setup_tree <<-ENDTREE
17 | - root:
18 | - child:
19 | - subchild:
20 | - subsubchild
21 | - other_root:
22 | - other_child
23 | - another_child
24 | ENDTREE
25 | end
26 |
27 | it "should assign a default position of 0 to each node without a sibling" do
28 | expect(node(:child).position).to eq(0)
29 | expect(node(:subchild).position).to eq(0)
30 | expect(node(:subsubchild).position).to eq(0)
31 | end
32 |
33 | it "should place siblings at the end of the list by default" do
34 | expect(node(:root).position).to eq(0)
35 | expect(node(:other_root).position).to eq(1)
36 | expect(node(:other_child).position).to eq(0)
37 | expect(node(:another_child).position).to eq(1)
38 | end
39 |
40 | it "should move a node to the end of a list when it is moved to a new parent" do
41 | other_root = node(:other_root)
42 | child = node(:child)
43 | expect(child.position).to eq(0)
44 | other_root.children << child
45 | child.reload
46 | expect(child.position).to eq(2)
47 | end
48 |
49 | it "should correctly reposition siblings when one of them is removed" do
50 | node(:other_child).destroy
51 | expect(node(:another_child).position).to eq(0)
52 | end
53 |
54 | it "should correctly reposition siblings when one of them is added to another parent" do
55 | node(:root).children << node(:other_child)
56 | expect(node(:another_child).position).to eq(0)
57 | end
58 |
59 | it "should correctly reposition siblings when the parent is changed" do
60 | other_child = node(:other_child)
61 | other_child.parent = node(:root)
62 | other_child.save!
63 | expect(node(:another_child).position).to eq(0)
64 | end
65 |
66 | it "should not reposition siblings when it's not yet saved" do
67 | new_node = OrderedNode.new(:name => 'new')
68 | new_node.parent = node(:root)
69 | expect(new_node).not_to receive(:reposition_former_siblings)
70 | new_node.save
71 | end
72 | end
73 |
74 | describe 'destroy strategies' do
75 | before(:each) do
76 | setup_tree <<-ENDTREE
77 | - root:
78 | - child:
79 | - subchild
80 | - other_child
81 | - other_root
82 | ENDTREE
83 | end
84 |
85 | describe ':move_children_to_parent' do
86 | it "should set its childen's parent_id to the documents parent_id" do
87 | node(:child).move_children_to_parent
88 | expect(node(:child)).to be_leaf
89 | expect(node(:root).children.to_a).to eq([node(:child), node(:other_child), node(:subchild)])
90 | end
91 | end
92 | end
93 |
94 | describe 'utility methods' do
95 | before(:each) do
96 | setup_tree <<-ENDTREE
97 | - first_root:
98 | - first_child_of_first_root
99 | - second_child_of_first_root
100 | - second_root
101 | - third_root
102 | ENDTREE
103 | end
104 |
105 | describe '#lower_siblings' do
106 | it "should return a collection of siblings lower on the list" do
107 | node(:second_child_of_first_root).reload
108 | expect(node(:first_root).lower_siblings.to_a).to eq([node(:second_root), node(:third_root)])
109 | expect(node(:second_root).lower_siblings.to_a).to eq([node(:third_root)])
110 | expect(node(:third_root).lower_siblings.to_a).to eq([])
111 | expect(node(:first_child_of_first_root).lower_siblings.to_a).to eq([node(:second_child_of_first_root)])
112 | expect(node(:second_child_of_first_root).lower_siblings.to_a).to eq([])
113 | end
114 | end
115 |
116 | describe '#higher_siblings' do
117 | it "should return a collection of siblings lower on the list" do
118 | expect(node(:first_root).higher_siblings.to_a).to eq([])
119 | expect(node(:second_root).higher_siblings.to_a).to eq([node(:first_root)])
120 | expect(node(:third_root).higher_siblings.to_a).to eq([node(:first_root), node(:second_root)])
121 | expect(node(:first_child_of_first_root).higher_siblings.to_a).to eq([])
122 | expect(node(:second_child_of_first_root).higher_siblings.to_a).to eq([node(:first_child_of_first_root)])
123 | end
124 | end
125 |
126 | describe '#at_top?' do
127 | it "should return true when the node is first in the list" do
128 | expect(node(:first_root)).to be_at_top
129 | expect(node(:first_child_of_first_root)).to be_at_top
130 | end
131 |
132 | it "should return false when the node is not first in the list" do
133 | expect(node(:second_root)).not_to be_at_top
134 | expect(node(:third_root)).not_to be_at_top
135 | expect(node(:second_child_of_first_root)).not_to be_at_top
136 | end
137 | end
138 |
139 | describe '#at_bottom?' do
140 | it "should return true when the node is last in the list" do
141 | expect(node(:third_root)).to be_at_bottom
142 | expect(node(:second_child_of_first_root)).to be_at_bottom
143 | end
144 |
145 | it "should return false when the node is not last in the list" do
146 | expect(node(:first_root)).not_to be_at_bottom
147 | expect(node(:second_root)).not_to be_at_bottom
148 | expect(node(:first_child_of_first_root)).not_to be_at_bottom
149 | end
150 | end
151 |
152 | describe '#last_sibling_in_list' do
153 | it "should return the last sibling in the list containing the current sibling" do
154 | expect(node(:first_root).last_sibling_in_list).to eq(node(:third_root))
155 | expect(node(:second_root).last_sibling_in_list).to eq(node(:third_root))
156 | expect(node(:third_root).last_sibling_in_list).to eq(node(:third_root))
157 | end
158 | end
159 |
160 | describe '#first_sibling_in_list' do
161 | it "should return the first sibling in the list containing the current sibling" do
162 | expect(node(:first_root).first_sibling_in_list).to eq(node(:first_root))
163 | expect(node(:second_root).first_sibling_in_list).to eq(node(:first_root))
164 | expect(node(:third_root).first_sibling_in_list).to eq(node(:first_root))
165 | end
166 | end
167 |
168 | describe '#ancestors' do
169 | it "should be returned in the correct order" do
170 | setup_tree <<-ENDTREE
171 | - root:
172 | - level_1_a
173 | - level_1_b:
174 | - level_2_a:
175 | - leaf
176 | ENDTREE
177 |
178 | expect(node(:leaf).ancestors.to_a).to eq([node(:root), node(:level_1_b), node(:level_2_a)])
179 | end
180 |
181 | it "should return the ancestors in correct order even after rearranging" do
182 | setup_tree <<-ENDTREE
183 | - root:
184 | - child:
185 | - subchild
186 | ENDTREE
187 |
188 | child = node(:child); child.parent = nil; child.save!
189 | root = node(:root); root.parent = node(:child); root.save!
190 | subchild = node(:subchild); subchild.parent = root; subchild.save!
191 |
192 | expect(subchild.ancestors.to_a).to eq([child, root])
193 | end
194 | end
195 | end
196 |
197 | describe 'moving nodes around' do
198 | before(:each) do
199 | setup_tree <<-ENDTREE
200 | - first_root:
201 | - first_child_of_first_root
202 | - second_child_of_first_root
203 | - second_root:
204 | - first_child_of_second_root
205 | - third_root:
206 | - first
207 | - second
208 | - third
209 | ENDTREE
210 | end
211 |
212 | describe '#move_below' do
213 | it 'should fix positions within the current list when moving an sibling away from its current parent' do
214 | node_to_move = node(:first_child_of_first_root)
215 | node_to_move.move_below(node(:first_child_of_second_root))
216 | expect(node(:second_child_of_first_root).position).to eq(0)
217 | end
218 |
219 | it 'should work when moving to a different parent' do
220 | node_to_move = node(:first_child_of_first_root)
221 | new_parent = node(:second_root)
222 | node_to_move.move_below(node(:first_child_of_second_root))
223 | node_to_move.reload
224 | expect(node_to_move).to be_at_bottom
225 | expect(node(:first_child_of_second_root)).to be_at_top
226 | end
227 |
228 | it 'should be able to move the first node below the second node' do
229 | first_node = node(:first_root)
230 | second_node = node(:second_root)
231 | first_node.move_below(second_node)
232 | first_node.reload
233 | second_node.reload
234 | expect(second_node).to be_at_top
235 | expect(first_node.higher_siblings.to_a).to eq([second_node])
236 | end
237 |
238 | it 'should be able to move the last node below the first node' do
239 | first_node = node(:first_root)
240 | last_node = node(:third_root)
241 | last_node.move_below(first_node)
242 | first_node.reload
243 | last_node.reload
244 | expect(last_node).not_to be_at_bottom
245 | expect(node(:second_root)).to be_at_bottom
246 | expect(last_node.higher_siblings.to_a).to eq([first_node])
247 | end
248 | end
249 |
250 | describe '#move_above' do
251 | it 'should fix positions within the current list when moving an sibling away from its current parent' do
252 | node_to_move = node(:first_child_of_first_root)
253 | node_to_move.move_above(node(:first_child_of_second_root))
254 | expect(node(:second_child_of_first_root).position).to eq(0)
255 | end
256 |
257 | it 'should work when moving to a different parent' do
258 | node_to_move = node(:first_child_of_first_root)
259 | new_parent = node(:second_root)
260 | node_to_move.move_above(node(:first_child_of_second_root))
261 | node_to_move.reload
262 | expect(node_to_move).to be_at_top
263 | expect(node(:first_child_of_second_root)).to be_at_bottom
264 | end
265 |
266 | it 'should be able to move the last node above the second node' do
267 | last_node = node(:third_root)
268 | second_node = node(:second_root)
269 | last_node.move_above(second_node)
270 | last_node.reload
271 | second_node.reload
272 | expect(second_node).to be_at_bottom
273 | expect(last_node.higher_siblings.to_a).to eq([node(:first_root)])
274 | end
275 |
276 | it 'should be able to move the first node above the last node' do
277 | first_node = node(:first_root)
278 | last_node = node(:third_root)
279 | first_node.move_above(last_node)
280 | first_node.reload
281 | last_node.reload
282 | expect(node(:second_root)).to be_at_top
283 | expect(first_node.higher_siblings.to_a).to eq([node(:second_root)])
284 | end
285 | end
286 |
287 | describe "#move_to_top" do
288 | it "should return true when attempting to move the first sibling" do
289 | expect(node(:first_root).move_to_top).to eq(true)
290 | expect(node(:first_child_of_first_root).move_to_top).to eq(true)
291 | end
292 |
293 | it "should be able to move the last sibling to the top" do
294 | first_node = node(:first_root)
295 | last_node = node(:third_root)
296 | last_node.move_to_top
297 | first_node.reload
298 | expect(last_node).to be_at_top
299 | expect(first_node).not_to be_at_top
300 | expect(first_node.higher_siblings.to_a).to eq([last_node])
301 | expect(last_node.lower_siblings.to_a).to eq([first_node, node(:second_root)])
302 | end
303 | end
304 |
305 | describe "#move_to_bottom" do
306 | it "should return true when attempting to move the last sibling" do
307 | expect(node(:third_root).move_to_bottom).to eq(true)
308 | expect(node(:second_child_of_first_root).move_to_bottom).to eq(true)
309 | end
310 |
311 | it "should be able to move the first sibling to the bottom" do
312 | first_node = node(:first_root)
313 | middle_node = node(:second_root)
314 | last_node = node(:third_root)
315 | first_node.move_to_bottom
316 | middle_node.reload
317 | last_node.reload
318 | expect(first_node).not_to be_at_top
319 | expect(first_node).to be_at_bottom
320 | expect(last_node).not_to be_at_bottom
321 | expect(last_node).not_to be_at_top
322 | expect(middle_node).to be_at_top
323 | expect(first_node.lower_siblings.to_a).to eq([])
324 | expect(last_node.higher_siblings.to_a).to eq([middle_node])
325 | end
326 | end
327 |
328 | describe "#move_up" do
329 | it "should correctly move nodes up" do
330 | node(:third).move_up
331 | expect(node(:third_root).children).to eq([node(:first), node(:third), node(:second)])
332 | end
333 | end
334 |
335 | describe "#move_down" do
336 | it "should correctly move nodes down" do
337 | node(:first).move_down
338 | expect(node(:third_root).children).to eq([node(:second), node(:first), node(:third)])
339 | end
340 | end
341 | end # moving nodes around
342 | end # Mongoid::Tree::Ordering
343 |
--------------------------------------------------------------------------------
/spec/mongoid/tree/traversal_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Mongoid::Tree::Traversal do
4 |
5 | subject { OrderedNode }
6 |
7 | describe '#traverse' do
8 |
9 | subject { Node.new }
10 |
11 | [:depth_first, :breadth_first].each do |method|
12 | it "should support #{method} traversal" do
13 | expect { subject.traverse(method) {} }.to_not raise_error
14 | end
15 | end
16 |
17 | it "should complain about unsupported traversal methods" do
18 | expect { subject.traverse('non_existing') {} }.to raise_error NoMethodError
19 | end
20 |
21 | it "should default to depth_first traversal" do
22 | expect(subject).to receive(:depth_first_traversal)
23 | subject.traverse {}
24 | end
25 | end
26 |
27 | describe 'depth first traversal' do
28 |
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
38 | - node7
39 | ENDTREE
40 | end
41 |
42 | it "should traverse correctly" do
43 | result = []
44 | node(:node1).traverse(:depth_first) { |node| result << node }
45 | expect(result.collect { |n| n.name.to_sym }).to eq([:node1, :node2, :node3, :node4, :node5, :node6, :node7])
46 | end
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 | expect(result).to eq([:node1, :node2, :node3, :node4, :node5, :node6, :node7])
51 | end
52 | end
53 |
54 | describe 'with merged trees' do
55 | before do
56 | setup_tree <<-ENDTREE
57 | - node4:
58 | - node5
59 | - node6:
60 | - node7
61 |
62 | - node1:
63 | - node2:
64 | - node3
65 | ENDTREE
66 |
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 | expect(result).to eq([:node1, :node2, :node3, :node4, :node5, :node6, :node7])
73 | end
74 | end
75 |
76 | describe 'with reordered nodes' do
77 |
78 | before do
79 | setup_tree <<-ENDTREE
80 | node1:
81 | - node2:
82 | - node3
83 | - node4:
84 | - node6
85 | - node5
86 | - node7
87 | ENDTREE
88 |
89 | node(:node5).move_above(node(:node6))
90 | end
91 |
92 | it 'should iterate through the nodes in the correct order' do
93 | result = []
94 | node(:node1).traverse(:depth_first) { |node| result << node }
95 | expect(result.collect { |n| n.name.to_sym }).to eq([:node1, :node2, :node3, :node4, :node5, :node6, :node7])
96 | end
97 |
98 | it 'should return the nodes in the correct order' do
99 | result = node(:node1).traverse(:depth_first)
100 | expect(result.collect { |n| n.name.to_sym }).to eq([:node1, :node2, :node3, :node4, :node5, :node6, :node7])
101 | end
102 |
103 | end
104 |
105 | end
106 |
107 | describe 'breadth first traversal' do
108 |
109 | before do
110 | setup_tree <<-ENDTREE
111 | node1:
112 | - node2:
113 | - node5
114 | - node3:
115 | - node6
116 | - node7
117 | - node4
118 | ENDTREE
119 | end
120 |
121 | it "should traverse correctly" do
122 | result = []
123 | node(:node1).traverse(:breadth_first) { |n| result << n }
124 | expect(result.collect { |n| n.name.to_sym }).to eq([:node1, :node2, :node3, :node4, :node5, :node6, :node7])
125 | end
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 | expect(result).to eq([:node1, :node2, :node3, :node4, :node5, :node6, :node7])
130 | end
131 |
132 | end
133 |
134 | describe '.traverse' do
135 | before :each do
136 | setup_tree <<-ENDTREE
137 | - root1
138 | - root2
139 | ENDTREE
140 |
141 | @root1 = node(:root1)
142 | @root2 = node(:root2)
143 |
144 | allow(Node).to receive(:roots).and_return [@root1, @root2]
145 | end
146 |
147 | it 'should grab each root' do
148 | expect(Node).to receive(:roots).and_return []
149 |
150 | expect(Node.traverse).to eq([])
151 | end
152 |
153 | it 'should default the "type" arg to :depth_first' do
154 | expect(@root1).to receive(:traverse).with(:depth_first).and_return([])
155 | expect(@root2).to receive(:traverse).with(:depth_first).and_return([])
156 |
157 | expect(Node.traverse).to eq([])
158 | end
159 |
160 | it 'should traverse each root' do
161 | expect(@root1).to receive(:traverse).and_return([1, 2])
162 | expect(@root2).to receive(:traverse).and_return([3, 4])
163 |
164 | expect(Node.traverse).to eq([1, 2, 3, 4])
165 | end
166 |
167 | describe 'when the "type" arg is :breadth_first' do
168 |
169 | it 'should traverse breadth-first' do
170 | expect(@root1).to receive(:traverse).with(:breadth_first).and_return([])
171 | expect(@root2).to receive(:traverse).with(:breadth_first).and_return([])
172 |
173 | Node.traverse :breadth_first
174 | end
175 | end
176 | end
177 | end
178 |
--------------------------------------------------------------------------------
/spec/mongoid/tree_spec.rb:
--------------------------------------------------------------------------------
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 | expect(a).to be
10 | if Mongoid::Compatibility::Version.mongoid7?
11 | expect(a).to be_kind_of(Mongoid::Association::Referenced::HasMany)
12 | else
13 | expect(a.macro).to eq(:has_many)
14 | end
15 | expect(a.class_name).to eq('Node')
16 | expect(a.foreign_key).to eq('parent_id')
17 | expect(Node.index_specification(:parent_id => 1)).to be
18 | end
19 |
20 | it "should be referenced in one parent as inverse of children" do
21 | a = Node.reflect_on_association(:parent)
22 | expect(a).to be
23 | if Mongoid::Compatibility::Version.mongoid7?
24 | expect(a).to be_kind_of(Mongoid::Association::Referenced::BelongsTo)
25 | else
26 | expect(a.macro).to eq(:belongs_to)
27 | end
28 | expect(a.class_name).to eq('Node')
29 | expect(a.inverse_of).to eq(:children)
30 | end
31 |
32 | it "should store parent_ids as Array with [] as default with index" do
33 | f = Node.fields['parent_ids']
34 | expect(f).to be
35 | expect(f.options[:type]).to eq(Array)
36 | expect(f.options[:default]).to eq([])
37 | expect(Node.index_specification(:parent_ids => 1)).to be
38 | end
39 |
40 | it "should store the depth as Integer with index" do
41 | f = Node.fields['depth']
42 | expect(f).to be
43 | expect(f.options[:type]).to eq(Integer)
44 | expect(Node.index_specification(:depth => 1)).to be
45 | end
46 |
47 | describe 'when new' do
48 | it "should not require a saved parent when adding children" do
49 | root = Node.new(:name => 'root'); child = Node.new(:name => 'child')
50 | expect { root.children << child; root.save! }.to_not raise_error
51 | expect(child).to be_persisted
52 | end
53 |
54 | it "should not be saved when parent is not saved" do
55 | root = Node.new(:name => 'root'); child = Node.new(:name => 'child')
56 | expect(child).not_to receive(:save)
57 | root.children << child
58 | end
59 |
60 | it "should save its unsaved children" do
61 | root = Node.new(:name => 'root'); child = Node.new(:name => 'child')
62 | root.children << child
63 | expect(child).to receive(:save)
64 | root.save
65 | end
66 | end
67 |
68 | describe 'when saved' do
69 |
70 | before(:each) do
71 | setup_tree <<-ENDTREE
72 | - root:
73 | - child:
74 | - subchild:
75 | - subsubchild
76 | - other_root:
77 | - other_child
78 | ENDTREE
79 | end
80 |
81 | it "should set the child's parent_id when added to parent's children" do
82 | root = Node.create; child = Node.create
83 | root.children << child
84 | expect(child.parent).to eq(root)
85 | expect(child.parent_id).to eq(root.id)
86 | end
87 |
88 | it "should set the child's parent_id parent is set on child" do
89 | root = Node.create; child = Node.create
90 | child.parent = root
91 | expect(child.parent).to eq(root)
92 | expect(child.parent_id).to eq(root.id)
93 | end
94 |
95 | it "should rebuild its parent_ids" do
96 | root = Node.create; child = Node.create
97 | root.children << child
98 | expect(child.parent_ids).to eq([root.id])
99 | end
100 |
101 | it "should rebuild its children's parent_ids when its own parent_ids changed" do
102 | other_root = node(:other_root); child = node(:child); subchild = node(:subchild);
103 | other_root.children << child
104 | subchild.reload # To get the updated version
105 | expect(subchild.parent_ids).to eq([other_root.id, child.id])
106 | end
107 |
108 | it "should correctly rebuild its descendants' parent_ids when moved into an other subtree" do
109 | subchild = node(:subchild); subsubchild = node(:subsubchild); other_child = node(:other_child)
110 | other_child.children << subchild
111 | subsubchild.reload
112 | expect(subsubchild.parent_ids).to eq([node(:other_root).id, other_child.id, subchild.id])
113 | end
114 |
115 | it "should rebuild its children's parent_ids when its own parent_id is removed" do
116 | c = node(:child)
117 | c.parent_id = nil
118 | c.save
119 | expect(node(:subchild).parent_ids).to eq([node(:child).id])
120 | end
121 |
122 | it "should not rebuild its children's parent_ids when it's not required" do
123 | root = node(:root)
124 | expect(root).not_to receive(:rearrange_children)
125 | root.save
126 | end
127 |
128 | it "should prevent cycles" do
129 | child = node(:child)
130 | child.parent = node(:subchild)
131 | expect(child).not_to be_valid
132 | expect(child.errors[:parent_id]).not_to be_nil
133 | end
134 |
135 | it "should save its children when added" do
136 | new_child = Node.new(:name => 'new_child')
137 | node(:root).children << new_child
138 | expect(new_child).to be_persisted
139 | end
140 | end
141 |
142 | describe 'when subclassed' do
143 |
144 | before(:each) do
145 | setup_tree <<-ENDTREE
146 | - root:
147 | - child:
148 | - subchild
149 | - other_child
150 | - other_root
151 | ENDTREE
152 | end
153 |
154 | it "should allow to store any subclass within the tree" do
155 | subclassed = SubclassedNode.create!(:name => 'subclassed_subchild')
156 | node(:child).children << subclassed
157 | expect(subclassed.root).to eq(node(:root))
158 | end
159 |
160 | end
161 |
162 | describe 'destroy strategies' do
163 |
164 | before(:each) do
165 | setup_tree <<-ENDTREE
166 | - root:
167 | - child:
168 | - subchild
169 | - other_child
170 | - other_root
171 | ENDTREE
172 | end
173 |
174 | describe ':nullify_children' do
175 | it "should set its children's parent_id to null" do
176 | node(:root).nullify_children
177 | expect(node(:child)).to be_root
178 | expect(node(:subchild).reload).not_to be_descendant_of node(:root)
179 | end
180 | end
181 |
182 | describe ':move_children_to_parent' do
183 | it "should set its childen's parent_id to the documents parent_id" do
184 | node(:child).move_children_to_parent
185 | expect(node(:child)).to be_leaf
186 | expect(node(:root).children.to_a).to match_array([node(:child), node(:other_child), node(:subchild)])
187 | end
188 |
189 | it "should be able to handle a missing parent" do
190 | node(:root).delete
191 | expect { node(:child).move_children_to_parent }.to_not raise_error
192 | end
193 | end
194 |
195 | describe ':destroy_children' do
196 | it "should destroy all children" do
197 | root = node(:root)
198 | expect(root.children).to receive(:destroy_all)
199 | root.destroy_children
200 | end
201 | end
202 |
203 | describe ':delete_descendants' do
204 | it "should delete all descendants" do
205 | root = node(:root)
206 | expect(Node).to receive(:delete_all).with(:conditions => { :parent_ids => root.id })
207 | root.delete_descendants
208 | end
209 | end
210 |
211 | end
212 |
213 | describe 'utility methods' do
214 |
215 | before(:each) do
216 | setup_tree <<-ENDTREE
217 | - root:
218 | - child:
219 | - subchild
220 | - other_child
221 | - other_root
222 | ENDTREE
223 | end
224 |
225 | describe '.root' do
226 | it "should return the first root document" do
227 | expect(Node.root).to eq(node(:root))
228 | end
229 | end
230 |
231 | describe '.roots' do
232 | it "should return all root documents" do
233 | expect(Node.roots.to_a).to eq([node(:root), node(:other_root)])
234 | end
235 | end
236 |
237 | describe '.leaves' do
238 | it "should return all leaf documents" do
239 | expect(Node.leaves.to_a).to match_array([node(:subchild), node(:other_child), node(:other_root)])
240 | end
241 | end
242 |
243 | describe '#root?' do
244 | it "should return true for root documents" do
245 | expect(node(:root)).to be_root
246 | end
247 |
248 | it "should return false for non-root documents" do
249 | expect(node(:child)).not_to be_root
250 | end
251 | end
252 |
253 | describe '#leaf?' do
254 | it "should return true for leaf documents" do
255 | expect(node(:subchild)).to be_leaf
256 | expect(node(:other_child)).to be_leaf
257 | expect(Node.new).to be_leaf
258 | end
259 |
260 | it "should return false for non-leaf documents" do
261 | expect(node(:child)).not_to be_leaf
262 | expect(node(:root)).not_to be_leaf
263 | end
264 | end
265 |
266 | describe '#depth' do
267 | it "should return the depth of this document" do
268 | expect(node(:root).depth).to eq(0)
269 | expect(node(:child).depth).to eq(1)
270 | expect(node(:subchild).depth).to eq(2)
271 | end
272 |
273 | it "should be updated when the nodes ancestors change" do
274 | node(:child).update_attributes(:parent => nil)
275 | expect(node(:child).depth).to eq(0)
276 | expect(node(:subchild).depth).to eq(1)
277 | end
278 | end
279 |
280 | describe '#root' do
281 | it "should return the root for this document" do
282 | expect(node(:subchild).root).to eq(node(:root))
283 | end
284 |
285 | it "should return itself when there is no root" do
286 | new_node = Node.new
287 | expect(new_node.root).to be(new_node)
288 | end
289 |
290 | it "should return it root when it's not saved yet" do
291 | root = Node.new(:name => 'root')
292 | new_node = Node.new(:name => 'child')
293 | new_node.parent = root
294 | expect(new_node.root).to be(root)
295 | end
296 | end
297 |
298 | describe 'ancestors' do
299 | describe '#ancestors' do
300 | it "should return the documents ancestors" do
301 | expect(node(:subchild).ancestors.to_a).to eq([node(:root), node(:child)])
302 | end
303 |
304 | it "should return the ancestors in correct order even after rearranging" do
305 | setup_tree <<-ENDTREE
306 | - root:
307 | - child:
308 | - subchild
309 | ENDTREE
310 |
311 | child = node(:child); child.parent = nil; child.save!
312 | root = node(:root); root.parent = node(:child); root.save!
313 | subchild = node(:subchild); subchild.parent = root; subchild.save!
314 |
315 | expect(subchild.ancestors.to_a).to eq([child, root])
316 | end
317 |
318 | it 'should return nothing when there are no ancestors' do
319 | root = Node.new(:name => 'root')
320 | expect(root.ancestors).to be_empty
321 | end
322 |
323 | it 'should allow chaning of other `or`-criterias' do
324 | setup_tree <<-ENDTREE
325 | - root:
326 | - child:
327 | - subchild:
328 | - subsubchild
329 | ENDTREE
330 |
331 | filtered_ancestors = node(:subsubchild).ancestors.any_of(
332 | { :name => 'child' },
333 | { :name => 'subchild' }
334 | )
335 |
336 | expect(filtered_ancestors.to_a).to eq([node(:child), node(:subchild)])
337 | end
338 | end
339 |
340 | describe '#ancestors_and_self' do
341 | it "should return the documents ancestors and itself" do
342 | expect(node(:subchild).ancestors_and_self.to_a).to eq([node(:root), node(:child), node(:subchild)])
343 | end
344 | end
345 |
346 | describe '#ancestor_of?' do
347 | it "should return true for ancestors" do
348 | expect(node(:child)).to be_ancestor_of(node(:subchild))
349 | end
350 |
351 | it "should return false for non-ancestors" do
352 | expect(node(:other_child)).not_to be_ancestor_of(node(:subchild))
353 | end
354 | end
355 | end
356 |
357 | describe 'descendants' do
358 | describe '#descendants' do
359 | it "should return the documents descendants" do
360 | expect(node(:root).descendants.to_a).to match_array([node(:child), node(:other_child), node(:subchild)])
361 | end
362 | end
363 |
364 | describe '#descendants_and_self' do
365 | it "should return the documents descendants and itself" do
366 | expect(node(:root).descendants_and_self.to_a).to match_array([node(:root), node(:child), node(:other_child), node(:subchild)])
367 | end
368 | end
369 |
370 | describe '#descendant_of?' do
371 | it "should return true for descendants" do
372 | subchild = node(:subchild)
373 | expect(subchild).to be_descendant_of(node(:child))
374 | expect(subchild).to be_descendant_of(node(:root))
375 | end
376 |
377 | it "should return false for non-descendants" do
378 | expect(node(:subchild)).not_to be_descendant_of(node(:other_child))
379 | end
380 | end
381 | end
382 |
383 | describe 'siblings' do
384 | describe '#siblings' do
385 | it "should return the documents siblings" do
386 | expect(node(:child).siblings.to_a).to eq([node(:other_child)])
387 | end
388 | end
389 |
390 | describe '#siblings_and_self' do
391 | it "should return the documents siblings and itself" do
392 | expect(node(:child).siblings_and_self).to be_kind_of(Mongoid::Criteria)
393 | expect(node(:child).siblings_and_self.to_a).to eq([node(:child), node(:other_child)])
394 | end
395 | end
396 |
397 | describe '#sibling_of?' do
398 | it "should return true for siblings" do
399 | expect(node(:child)).to be_sibling_of(node(:other_child))
400 | end
401 |
402 | it "should return false for non-siblings" do
403 | expect(node(:root)).not_to be_sibling_of(node(:other_child))
404 | end
405 | end
406 | end
407 |
408 | describe '#leaves' do
409 | it "should return this documents leaves" do
410 | expect(node(:root).leaves.to_a).to match_array([node(:other_child), node(:subchild)])
411 | end
412 | end
413 |
414 | end
415 |
416 | describe 'callbacks' do
417 |
418 | after(:each) do
419 | Node.reset_callbacks(:rearrange)
420 | end
421 |
422 | it "should provide a before_rearrange callback" do
423 | expect(Node).to respond_to :before_rearrange
424 | end
425 |
426 | it "should provida an after_rearrange callback" do
427 | expect(Node).to respond_to :after_rearrange
428 | end
429 |
430 | describe 'before rearrange callback' do
431 |
432 | it "should be called before the document is rearranged" do
433 | Node.before_rearrange :callback
434 | node = Node.new
435 | expect(node).to receive(:callback).ordered
436 | expect(node).to receive(:rearrange).ordered
437 | node.save
438 | end
439 |
440 | end
441 |
442 | describe 'after rearrange callback' do
443 |
444 | it "should be called after the document is rearranged" do
445 | Node.after_rearrange :callback
446 | node = Node.new
447 | expect(node).to receive(:rearrange).ordered
448 | expect(node).to receive(:callback).ordered
449 | node.save
450 | end
451 |
452 | end
453 |
454 | describe 'cascading to embedded documents' do
455 |
456 | it 'should not raise a NoMethodError' do
457 | node = NodeWithEmbeddedDocument.new
458 | document = node.build_embedded_document
459 | expect { node.save }.to_not raise_error
460 | end
461 |
462 | end
463 |
464 | end
465 | end
466 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler/setup'
3 |
4 | require 'mongoid'
5 | require 'mongoid/tree'
6 | require 'mongoid-compatibility'
7 |
8 | require 'rspec'
9 |
10 | Mongoid.configure do |config|
11 | config.connect_to('mongoid_tree_test')
12 | end
13 |
14 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
15 |
16 | RSpec.configure do |config|
17 | config.expect_with :rspec do |c|
18 | c.syntax = :expect
19 | end
20 | config.mock_with :rspec
21 | config.after(:each) { Mongoid.purge! }
22 | end
23 |
--------------------------------------------------------------------------------
/spec/support/logger.rb:
--------------------------------------------------------------------------------
1 | FileUtils.mkdir_p File.expand_path('../../../log', __FILE__)
2 |
3 | logger = Logger.new('log/mongoid.log')
4 |
5 | if Mongoid::Compatibility::Version.mongoid5_or_newer?
6 | Mongoid.logger = Mongo::Logger.logger = logger
7 | else
8 | Mongoid.logger = logger
9 | end
10 |
--------------------------------------------------------------------------------
/spec/support/macros/tree_macros.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 |
3 | module Mongoid::Tree::TreeMacros
4 |
5 | def setup_tree(tree)
6 | create_tree(YAML.load(tree))
7 | end
8 |
9 | def node(name)
10 | @nodes[name].reload
11 | end
12 |
13 | def print_tree(node, inspect = false, depth = 0)
14 | print ' ' * depth
15 | print '- ' unless depth == 0
16 | print node.name
17 | print " (#{node.inspect})" if inspect
18 | print ':' if node.children.any?
19 | print "\n"
20 | node.children.each { |c| print_tree(c, inspect, depth + 1) }
21 | end
22 |
23 | private
24 |
25 | def create_tree(object)
26 | case object
27 | when String then return create_node(object)
28 | when Array then object.each { |tree| create_tree(tree) }
29 | when Hash then
30 | name, children = object.first
31 | node = create_node(name)
32 | children.each { |c| node.children << create_tree(c) }
33 | return node
34 | end
35 | end
36 |
37 | def create_node(name)
38 | @nodes ||= HashWithIndifferentAccess.new
39 | @nodes[name] = subject.create(:name => name)
40 | end
41 | end
42 |
43 | RSpec.configure do |config|
44 | config.include Mongoid::Tree::TreeMacros
45 | end
46 |
--------------------------------------------------------------------------------
/spec/support/models/node.rb:
--------------------------------------------------------------------------------
1 | class Node
2 | include Mongoid::Document
3 | include Mongoid::Tree
4 | include Mongoid::Tree::Traversal
5 |
6 | field :name
7 | end
8 |
9 | class SubclassedNode < Node
10 | end
11 |
12 | # Adding ordering on subclasses currently doesn't work as expected.
13 | #
14 | # class OrderedNode < Node
15 | # include Mongoid::Tree::Ordering
16 | # end
17 | class OrderedNode
18 | include Mongoid::Document
19 | include Mongoid::Tree
20 | include Mongoid::Tree::Traversal
21 | include Mongoid::Tree::Ordering
22 |
23 | field :name
24 | end
25 |
26 | class NodeWithEmbeddedDocument < Node
27 | embeds_one :embedded_document, :cascade_callbacks => true
28 | end
29 |
30 | class EmbeddedDocument
31 | include Mongoid::Document
32 | end
33 |
34 | class CounterCachedNode < Node
35 | include Mongoid::Tree::CounterCaching
36 | end
37 |
--------------------------------------------------------------------------------