├── .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 [![Build Status](https://github.com/benedikt/mongoid-tree/workflows/Tests/badge.svg)](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 | --------------------------------------------------------------------------------