mongo_tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Michael Parrish
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.
@@ -0,0 +1,15 @@
1
+ # mongo_tree
2
+
3
+ mongo_tree is a plugin for [MongoMapper](http://github.com/jnunemaker/mongomapper) that implements [a number of tree modeling strategies](http://www.mongodb.org/display/DOCS/Trees+in+MongoDB) for MongoDB.
4
+
5
+ Probably not quite ready for use just yet.
6
+
7
+ ## Note on Patches/Pull Requests
8
+
9
+ * Fork the project.
10
+ * Make your feature addition or bug fix.
11
+ * Add tests for it. This is important so I don't break it in a
12
+ future version unintentionally.
13
+ * Commit, do not mess with rakefile, version, or history.
14
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
15
+ * Send me a pull request. Bonus points for topic branches.
@@ -0,0 +1,46 @@
1
+ require 'mongo_mapper'
2
+ require 'active_support/all'
3
+
4
+ module MongoTree
5
+ autoload :Children, 'mongo_tree/children'
6
+ autoload :Base, 'mongo_tree/base'
7
+
8
+ module Strategies
9
+ autoload :FullTreeRoot, 'mongo_tree/strategies/full_tree_root'
10
+ autoload :FullTreeNode, 'mongo_tree/strategies/full_tree_node'
11
+ autoload :ChildLink, 'mongo_tree/strategies/child_link'
12
+ autoload :ParentLink, 'mongo_tree/strategies/parent_link'
13
+ autoload :AncestorArray, 'mongo_tree/strategies/ancestor_array'
14
+ autoload :MaterializedPath, 'mongo_tree/strategies/materialized_path'
15
+ end
16
+
17
+ module ClassMethods
18
+ def acts_as_tree(strategy, options = {})
19
+ @mongo_tree_options = options
20
+
21
+ case strategy
22
+ when :full_tree
23
+ if options.has_key?(:embeds)
24
+ self.send :include, MongoTree::Strategies::FullTreeRoot
25
+ elsif options.has_key?(:root)
26
+ self.send :include, MongoTree::Strategies::FullTreeNode
27
+ else
28
+ raise 'The full_tree strategy needs to have either the :root or the :embeds class specified'
29
+ end
30
+ when :child_link
31
+ self.send :include, MongoTree::Strategies::ChildLink
32
+ when :parent_link
33
+ self.send :include, MongoTree::Strategies::ParentLink
34
+ when :ancestor_array
35
+ self.send :include, MongoTree::Strategies::AncestorArray
36
+ when :materialized_path
37
+ self.send :include, MongoTree::Strategies::MaterializedPath
38
+ else
39
+ # I guess this is an okay default?
40
+ self.send :include, MongoTree::Strategies::ParentLink
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ MongoMapper::Plugins.send :include, MongoTree
@@ -0,0 +1,63 @@
1
+ module MongoTree
2
+ module Base
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.send :include, InstanceMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def roots
10
+ self.all(:parent_id => nil)
11
+ end
12
+ end
13
+
14
+ module InstanceMethods
15
+ def root
16
+ current = self
17
+
18
+ until current.parent.nil?
19
+ current = current.parent
20
+ end
21
+
22
+ current
23
+ end
24
+
25
+ def depth
26
+ ancestors.length
27
+ end
28
+
29
+ def children
30
+ MongoTree::Children.new(self.class.all(:parent_id => self.id), self)
31
+ end
32
+
33
+ def children=(nodes)
34
+ nodes.each{ |node| children << node }
35
+ end
36
+
37
+ def ancestors
38
+ collected = []
39
+ current = self.parent
40
+
41
+ until current.nil?
42
+ collected << current
43
+ current = current.parent
44
+ end
45
+
46
+ collected
47
+ end
48
+
49
+ def descendants
50
+ collected = []
51
+ nodes = self.children
52
+
53
+ until nodes.empty?
54
+ current = nodes.shift
55
+ collected << current
56
+ nodes += current.children
57
+ end
58
+
59
+ collected
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,26 @@
1
+ module MongoTree
2
+ class Children < Array
3
+ def initialize(array, parent, &update_method)
4
+ @parent = parent
5
+ @update_method = block_given? ? update_method : Proc.new { |child, parent| child.parent_id = parent.id }
6
+ super(array)
7
+ end
8
+
9
+ def <<(*docs)
10
+ @parent.save if @parent.new?
11
+ flatten_deeper(docs).collect do |doc|
12
+ @update_method.call(doc, @parent)
13
+ doc.save if doc.changed? || doc.new?
14
+ end
15
+
16
+ super.uniq
17
+ end
18
+ alias_method :push, :<<
19
+ alias_method :concat, :<<
20
+
21
+ private
22
+ def flatten_deeper(docs)
23
+ docs.collect{ |doc| (doc.respond_to?(:flatten) && !doc.is_a?(Hash)) ? doc.flatten : doc }.flatten
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,76 @@
1
+ module MongoTree
2
+ module Strategies
3
+ module AncestorArray
4
+ include MongoTree::Base
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ class << self
9
+ attr_accessor :mongo_tree_options
10
+ end
11
+
12
+ @mongo_tree_options = { :path_attribute => :id }.merge(@mongo_tree_options)
13
+
14
+ key :parent_id, ObjectId
15
+ key :ancestor_ids, Array, :index => true
16
+ belongs_to :parent, :class_name => self.name
17
+ before_save :update_ancestors, :if => Proc.new{ |doc| doc.changes.has_key? "parent_id" }
18
+ after_save :update_children, :if => Proc.new{ |doc| doc.changes.has_key? "parent_id" }
19
+ end
20
+
21
+ base.extend ClassMethods
22
+ base.send :include, InstanceMethods
23
+ end
24
+
25
+ module ClassMethods
26
+ def roots
27
+ self.all(:parent_id => nil)
28
+ end
29
+ end
30
+
31
+ module InstanceMethods
32
+ def root
33
+ self.class.first(path_attribute => self.ancestor_ids.first)
34
+ end
35
+
36
+ def depth
37
+ ancestor_ids.length
38
+ end
39
+
40
+ def siblings
41
+ self.class.all(:parent_id => self.parent_id, path_attribute.ne => self.send(path_attribute))
42
+ end
43
+
44
+ def ancestors
45
+ self.ancestor_ids.map{ |id| self.class.first(path_attribute => id) }
46
+ end
47
+
48
+ def descendants
49
+ self.class.all(:ancestor_ids.all => self.ancestor_ids << self.send(path_attribute))
50
+ end
51
+
52
+ protected
53
+ def update_ancestors
54
+ self.ancestor_ids = []
55
+ current = self
56
+
57
+ until current.parent.nil?
58
+ self.ancestor_ids.unshift(current.parent.send(path_attribute))
59
+ current = current.parent
60
+ end
61
+ end
62
+
63
+ def update_children
64
+ self.class.all(:parent_id => self.id).map do |child|
65
+ child.instance_eval{ update_ancestors }
66
+ child.save
67
+ end
68
+ end
69
+
70
+ def path_attribute
71
+ self.class.mongo_tree_options[:path_attribute].to_sym
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,53 @@
1
+ module MongoTree
2
+ module Strategies
3
+ module ChildLink
4
+ include MongoTree::Base
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ key :child_id, ObjectId, :index => true
9
+ many :children, :class_name => self.name, :foreign_key => "child_id"
10
+ end
11
+
12
+ base.extend ClassMethods
13
+ base.send :include, InstanceMethods
14
+ end
15
+
16
+ module ClassMethods
17
+ def roots
18
+ self.all(:child_id => nil)
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ def parent
24
+ @parent ||= self.class.find(self.child_id)
25
+ end
26
+
27
+ def parent=(node)
28
+ @parent = node
29
+ self.child_id = node.id
30
+ save if changed?
31
+ end
32
+
33
+ def siblings
34
+ return nil if parent.nil?
35
+ parent.children.reject{ |node| node == self }
36
+ end
37
+
38
+ def descendants
39
+ collected = []
40
+ nodes = self.children
41
+
42
+ until nodes.empty?
43
+ current = nodes.shift
44
+ collected << current
45
+ nodes += current.children
46
+ end
47
+
48
+ collected
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,97 @@
1
+ module MongoTree
2
+ module Strategies
3
+ module FullTreeNode
4
+ include MongoTree::Base
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ class << self
9
+ attr_accessor :mongo_tree_options
10
+ end
11
+
12
+ @mongo_tree_options = {
13
+ :root => "#{self.name}".sub(/Node/, '')
14
+ }.merge(@mongo_tree_options)
15
+
16
+ many :children, :class_name => self.name
17
+ end
18
+
19
+ base.extend ClassMethods
20
+ base.send :include, InstanceMethods
21
+ end
22
+
23
+ module ClassMethods
24
+ end
25
+
26
+ module InstanceMethods
27
+ def root
28
+ self._root_document
29
+ end
30
+
31
+ def parent
32
+ self._parent_document
33
+ end
34
+
35
+ def parent=(target)
36
+ bson = root_class.collection.find_one(root.id)
37
+ my_bson = nil
38
+
39
+ modify_children(bson, self) do |parent, found|
40
+ my_bson = found
41
+ parent['children'].delete(found)
42
+ end
43
+
44
+ modify_children(bson, target) do |parent, found|
45
+ found['children'] << my_bson
46
+ end
47
+
48
+ root_class.collection.update({'_id' => root.id}, bson)
49
+ root.reload
50
+ end
51
+
52
+ def siblings
53
+ self._parent_document.children.reject{ |n| n.id == self.id }
54
+ end
55
+
56
+ def ancestors
57
+ collected = []
58
+ current = parent
59
+
60
+ until current == root
61
+ collected.unshift(current)
62
+ current = current.parent
63
+ end
64
+
65
+ collected.unshift(root)
66
+ end
67
+
68
+ protected
69
+ def root_class
70
+ self.class.mongo_tree_options[:root].to_s.constantize
71
+ end
72
+
73
+ def modify_children(tree, node, &block)
74
+ current = tree
75
+ found = nil
76
+
77
+ ancestors = node.ancestors
78
+ ancestors.shift
79
+
80
+ until ancestors.empty?
81
+ ancestor = ancestors.shift
82
+ current = current['children'].select{ |child| child['_id'] == ancestor['_id'] }.first
83
+ end
84
+
85
+ if current['_id'] == node.id
86
+ found = current
87
+ else
88
+ found = current['children'].select{ |child| child['_id'] == node.id }.first
89
+ end
90
+
91
+ yield(current, found)
92
+ return tree
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,48 @@
1
+ module MongoTree
2
+ module Strategies
3
+ module FullTreeRoot
4
+ include MongoTree::Base
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ class << self
9
+ attr_accessor :mongo_tree_options
10
+ end
11
+
12
+ @mongo_tree_options = {
13
+ :embeds => "#{self.name}Node"
14
+ }.merge(@mongo_tree_options)
15
+
16
+ many :children, :class_name => @mongo_tree_options[:embeds].to_s
17
+ end
18
+
19
+ base.extend ClassMethods
20
+ base.send :include, InstanceMethods
21
+ end
22
+
23
+ module ClassMethods
24
+ def roots
25
+ self.all
26
+ end
27
+ end
28
+
29
+ module InstanceMethods
30
+ def depth
31
+ 0
32
+ end
33
+
34
+ def parent
35
+ nil
36
+ end
37
+
38
+ def siblings
39
+ []
40
+ end
41
+
42
+ def ancestors
43
+ []
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,119 @@
1
+ module MongoTree
2
+ module Strategies
3
+ module MaterializedPath
4
+ include MongoTree::Base
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ class << self
9
+ attr_accessor :mongo_tree_options
10
+ end
11
+
12
+ @mongo_tree_options = {
13
+ :path_attribute => :id,
14
+ :path_delimiter => ','
15
+ }.merge(@mongo_tree_options)
16
+
17
+ key :path, String, :index => true
18
+ key :depth, Integer, :default => 0
19
+ validates_presence_of @mongo_tree_options[:path_attribute].to_sym
20
+ before_save :update_path_and_depth, :if => Proc.new{ |doc| doc.changes.has_key? "path" }
21
+ after_save :update_children, :if => Proc.new{ |doc| doc.changes.has_key? "path" }
22
+ end
23
+
24
+ base.extend ClassMethods
25
+ base.send :include, InstanceMethods
26
+ end
27
+
28
+ module ClassMethods
29
+ def roots
30
+ self.all(:depth => 0)
31
+ end
32
+ end
33
+
34
+ module InstanceMethods
35
+ def path
36
+ if self[:path].nil?
37
+ self.send(path_attribute).to_s
38
+ else
39
+ self[:path]
40
+ end
41
+ end
42
+
43
+ def root
44
+ find_by_path_attribute(path_array.first)
45
+ end
46
+
47
+ def parent
48
+ @parent ||= find_parent_from_path
49
+ end
50
+
51
+ def parent=(node)
52
+ @parent = node
53
+ update_path_and_depth
54
+ end
55
+
56
+ def children
57
+ docs = self.class.all(:path => /^#{ self.path },/, :depth => self.depth + 1)
58
+ MongoTree::Children.new(docs, self) do |child, parent|
59
+ child.parent = parent
60
+ end
61
+ end
62
+
63
+ def siblings
64
+ return [] if @parent.nil?
65
+ self.class.all(:path => /^#{ @parent.path },/, :depth => self.depth, path_attribute.ne => self.send(path_attribute).to_s)
66
+ end
67
+
68
+ def ancestors
69
+ return [] if self.path.nil?
70
+ path_array[0..-2].map{ |n| find_by_path_attribute(n) }
71
+ end
72
+
73
+ def descendants
74
+ return [] if self.path.nil?
75
+ self.class.all(:path => /^#{ self.path },/)
76
+ end
77
+
78
+ protected
79
+ def path_attribute
80
+ self.class.mongo_tree_options[:path_attribute].to_sym
81
+ end
82
+
83
+ def path_delimiter
84
+ self.class.mongo_tree_options[:path_delimiter].to_s
85
+ end
86
+
87
+ def path_array
88
+ self.path.split(path_delimiter)
89
+ end
90
+
91
+ def find_by_path_attribute(id)
92
+ self.class.first(path_attribute => id)
93
+ end
94
+
95
+ def find_parent_from_path
96
+ return nil if self.path.nil? || self.path.length < 2
97
+ @parent = find_by_path_attribute(path_array[-2])
98
+ end
99
+
100
+ def update_path_and_depth
101
+ @parent ||= find_parent_from_path
102
+ self.path = @parent.nil? ? send(path_attribute).to_s : "#{ @parent.path }#{ path_delimiter }#{ send(path_attribute).to_s }"
103
+ self.depth = path_array.length - 1
104
+ end
105
+
106
+ def update_children
107
+ old_path = self.changes['path'].first
108
+ old_depth = self.changes.has_key?('depth') ? self.changes['depth'].first : self.depth
109
+ return if old_path.nil? || old_depth.nil?
110
+
111
+ self.class.all(:path => /^#{ old_path },/, :depth => old_depth + 1).map do |child|
112
+ child.instance_eval{ update_path_and_depth }
113
+ child.save
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,43 @@
1
+ module MongoTree
2
+ module Strategies
3
+ module ParentLink
4
+ include MongoTree::Base
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ key :parent_id, ObjectId, :index => true
9
+ belongs_to :parent, :class_name => self.name
10
+ end
11
+
12
+ base.extend ClassMethods
13
+ base.send :include, InstanceMethods
14
+ end
15
+
16
+ module ClassMethods
17
+ def roots
18
+ self.all(:parent_id => nil)
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ def siblings
24
+ self.class.all(:parent_id => self.parent_id, :id.ne => self.id)
25
+ end
26
+
27
+ def descendants
28
+ collected = []
29
+ nodes = [self]
30
+
31
+ until nodes.empty?
32
+ current = nodes.shift
33
+ current_children = self.class.all(:parent_id => current.id)
34
+ nodes += current_children
35
+ collected += current_children
36
+ end
37
+
38
+ collected
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'mongo_tree'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestMongoTree < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongo_tree
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Michael Parrish
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-08-02 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: thoughtbot-shoulda
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: mongo_mapper
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 59
44
+ segments:
45
+ - 0
46
+ - 8
47
+ - 2
48
+ version: 0.8.2
49
+ type: :runtime
50
+ version_requirements: *id002
51
+ description: A MongoMapper plugin that adds a number of tree strategies.
52
+ email: mtparrish@gmail.com
53
+ executables: []
54
+
55
+ extensions: []
56
+
57
+ extra_rdoc_files:
58
+ - LICENSE
59
+ - README.markdown
60
+ files:
61
+ - LICENSE
62
+ - README.markdown
63
+ - lib/mongo_tree.rb
64
+ - lib/mongo_tree/base.rb
65
+ - lib/mongo_tree/children.rb
66
+ - lib/mongo_tree/strategies/ancestor_array.rb
67
+ - lib/mongo_tree/strategies/child_link.rb
68
+ - lib/mongo_tree/strategies/full_tree_node.rb
69
+ - lib/mongo_tree/strategies/full_tree_root.rb
70
+ - lib/mongo_tree/strategies/materialized_path.rb
71
+ - lib/mongo_tree/strategies/parent_link.rb
72
+ - test/helper.rb
73
+ - test/test_mongo_tree.rb
74
+ has_rdoc: true
75
+ homepage: http://github.com/parrish/mongo_tree
76
+ licenses: []
77
+
78
+ post_install_message:
79
+ rdoc_options:
80
+ - --charset=UTF-8
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ hash: 3
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ hash: 3
98
+ segments:
99
+ - 0
100
+ version: "0"
101
+ requirements: []
102
+
103
+ rubyforge_project:
104
+ rubygems_version: 1.3.7
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: A MongoMapper plugin that adds a number of tree strategies.
108
+ test_files:
109
+ - test/helper.rb
110
+ - test/test_mongo_tree.rb