mongestry 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +64 -0
- data/LICENSE.txt +23 -0
- data/README.rdoc +161 -0
- data/Rakefile +42 -0
- data/VERSION +1 -0
- data/lib/mongestry.rb +214 -0
- data/mongestry.gemspec +78 -0
- data/spec/mongestry_spec.rb +450 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/category.rb +10 -0
- data/spec/support/connection.rb +3 -0
- metadata +206 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
|
9
|
+
gem 'bson_ext'
|
10
|
+
gem 'mongoid'
|
11
|
+
|
12
|
+
group :development do
|
13
|
+
gem "rspec", "~> 2.3.0"
|
14
|
+
gem "yard", "~> 0.6.0"
|
15
|
+
gem "bundler", "~> 1.0.0"
|
16
|
+
gem "jeweler", "~> 1.6.2"
|
17
|
+
gem "rcov", ">= 0"
|
18
|
+
gem 'linecache19', '0.5.11'
|
19
|
+
gem 'ruby-debug19'
|
20
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.0.7)
|
5
|
+
activesupport (= 3.0.7)
|
6
|
+
builder (~> 2.1.2)
|
7
|
+
i18n (~> 0.5.0)
|
8
|
+
activesupport (3.0.7)
|
9
|
+
archive-tar-minitar (0.5.2)
|
10
|
+
bson (1.3.1)
|
11
|
+
bson_ext (1.3.1)
|
12
|
+
builder (2.1.2)
|
13
|
+
columnize (0.3.2)
|
14
|
+
diff-lcs (1.1.2)
|
15
|
+
git (1.2.5)
|
16
|
+
i18n (0.5.0)
|
17
|
+
jeweler (1.6.2)
|
18
|
+
bundler (~> 1.0)
|
19
|
+
git (>= 1.2.5)
|
20
|
+
rake
|
21
|
+
linecache19 (0.5.11)
|
22
|
+
ruby_core_source (>= 0.1.4)
|
23
|
+
mongo (1.3.1)
|
24
|
+
bson (>= 1.3.1)
|
25
|
+
mongoid (2.0.2)
|
26
|
+
activemodel (~> 3.0)
|
27
|
+
mongo (~> 1.3)
|
28
|
+
tzinfo (~> 0.3.22)
|
29
|
+
rake (0.9.2)
|
30
|
+
rcov (0.9.9)
|
31
|
+
rspec (2.3.0)
|
32
|
+
rspec-core (~> 2.3.0)
|
33
|
+
rspec-expectations (~> 2.3.0)
|
34
|
+
rspec-mocks (~> 2.3.0)
|
35
|
+
rspec-core (2.3.1)
|
36
|
+
rspec-expectations (2.3.0)
|
37
|
+
diff-lcs (~> 1.1.2)
|
38
|
+
rspec-mocks (2.3.0)
|
39
|
+
ruby-debug-base19 (0.11.25)
|
40
|
+
columnize (>= 0.3.1)
|
41
|
+
linecache19 (>= 0.5.11)
|
42
|
+
ruby_core_source (>= 0.1.4)
|
43
|
+
ruby-debug19 (0.11.6)
|
44
|
+
columnize (>= 0.3.1)
|
45
|
+
linecache19 (>= 0.5.11)
|
46
|
+
ruby-debug-base19 (>= 0.11.19)
|
47
|
+
ruby_core_source (0.1.5)
|
48
|
+
archive-tar-minitar (>= 0.5.2)
|
49
|
+
tzinfo (0.3.27)
|
50
|
+
yard (0.6.8)
|
51
|
+
|
52
|
+
PLATFORMS
|
53
|
+
ruby
|
54
|
+
|
55
|
+
DEPENDENCIES
|
56
|
+
bson_ext
|
57
|
+
bundler (~> 1.0.0)
|
58
|
+
jeweler (~> 1.6.2)
|
59
|
+
linecache19 (= 0.5.11)
|
60
|
+
mongoid
|
61
|
+
rcov
|
62
|
+
rspec (~> 2.3.0)
|
63
|
+
ruby-debug19
|
64
|
+
yard (~> 0.6.0)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2011 DailyDeal
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
data/README.rdoc
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
= Mongestry
|
2
|
+
|
3
|
+
Mongestry is a gem that allows the records of a Mongoid model to be organized as a tree structure (or hierarchy). It uses a single, intuitively formatted database column, using a variation on the materialised path pattern. It exposes all the standard tree structure relations (ancestors, parent, root, children, siblings, descendants) and all of them can be fetched in a single query. Additional features are scopes, depth caching, depth constraints, easy migration from similar plugins/gems.
|
4
|
+
|
5
|
+
Mongestry is inspired by the famous {Ancestry}[https://github.com/stefankroes/ancestry] gem by {Stefan Kroes}[https://github.com/stefankroes]. It implements most of its functionality but lacks some. So be prepared.
|
6
|
+
|
7
|
+
= Installation
|
8
|
+
|
9
|
+
To apply Mongestry to any Mongoid model, follow these simple steps:
|
10
|
+
|
11
|
+
== Gem installation
|
12
|
+
|
13
|
+
Add Mongestry to your app's Gemfile:
|
14
|
+
|
15
|
+
gem 'mongestry'
|
16
|
+
|
17
|
+
Install required gems:
|
18
|
+
|
19
|
+
bundle install
|
20
|
+
|
21
|
+
Add mongestry to your model via the following declarative line:
|
22
|
+
|
23
|
+
has_mongestry
|
24
|
+
|
25
|
+
== Example
|
26
|
+
|
27
|
+
class TreeNode
|
28
|
+
include Mongoid::Document
|
29
|
+
include Mongoid::Timestamps
|
30
|
+
|
31
|
+
field :name, type: String
|
32
|
+
|
33
|
+
has_mongestry
|
34
|
+
end
|
35
|
+
|
36
|
+
Your model is now a tree!
|
37
|
+
|
38
|
+
= Organizing records into a tree
|
39
|
+
|
40
|
+
You can use the parent attribute to organize your records into a tree. If you have the id of the record you want to use as a parent and don't want to fetch it, you can also use parent_id. Like any virtual model attributes, parent and parent_id can be set using parent= and parent_id= on a record or by including them in the hash passed to new, create, create!. For example:
|
41
|
+
|
42
|
+
TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
|
43
|
+
|
44
|
+
As of now you can <b>NOT</b> create children through the children relation on a node, so be patient, this will come in an upcoming release.
|
45
|
+
|
46
|
+
= Navigating your tree
|
47
|
+
|
48
|
+
To navigate a Mongestry model, use the following methods on any instance / record:
|
49
|
+
|
50
|
+
parent Returns the parent of the record, nil for a root node
|
51
|
+
parent_id Returns the id of the parent of the record, nil for a root node
|
52
|
+
root Returns the root of the tree the record is in, self for a root node
|
53
|
+
root_id Returns the id of the root of the tree the record is in
|
54
|
+
is_root? Returns true if the record is a root node, false otherwise
|
55
|
+
ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
|
56
|
+
ancestors Scopes the model on ancestors of the record
|
57
|
+
children Scopes the model on children of the record
|
58
|
+
child_ids Returns a list of child ids
|
59
|
+
has_children? Returns true if the record has any children, false otherwise
|
60
|
+
is_childless? Returns true is the record has no childen, false otherwise
|
61
|
+
siblings Scopes the model on siblings of the record, the record itself is included
|
62
|
+
sibling_ids Returns a list of sibling ids
|
63
|
+
has_siblings? Returns true if the record's parent has more than one child
|
64
|
+
is_only_child? Returns true if the record is the only child of its parent
|
65
|
+
descendants Scopes the model on direct and indirect children of the record
|
66
|
+
descendant_ids Returns a list of a descendant ids
|
67
|
+
subtree Scopes the model on descendants and itself
|
68
|
+
subtree_ids Returns a list of all ids in the record's subtree
|
69
|
+
depth Return the depth of the node, root nodes are at depth 0
|
70
|
+
|
71
|
+
= Options for has_mongestry
|
72
|
+
|
73
|
+
Currently there are none.
|
74
|
+
|
75
|
+
= Scopes
|
76
|
+
|
77
|
+
Where possible, the navigation methods return scopes instead of records, this means additional ordering, conditions, limits, etc. can be applied and that the result can be either retrieved, counted or checked for existence. For example:
|
78
|
+
|
79
|
+
node.children.exists?(:name => 'Mary')
|
80
|
+
node.subtree.all(:order => :name, :limit => 10).each do; ...; end
|
81
|
+
node.descendants.count
|
82
|
+
|
83
|
+
For convenience, a couple of named scopes are included at the class level:
|
84
|
+
|
85
|
+
roots # Root nodes
|
86
|
+
ancestors_of(node) # Ancestors of node, node can be either a record or an id
|
87
|
+
children_of(node) # Children of node, node can be either a record or an id
|
88
|
+
descendants_of(node) # Descendants of node, node can be either a record or an id
|
89
|
+
subtree_of(node) # Subtree of node, node can be either a record or an id
|
90
|
+
siblings_of(node) # Siblings of node, node can be either a record or an id
|
91
|
+
|
92
|
+
== Selecting nodes by depth
|
93
|
+
|
94
|
+
In Mongestry depth caching is enabled by default. Therefore five more scopes can be used to select nodes on their depth:
|
95
|
+
|
96
|
+
before_depth(depth) # Return nodes that are less deep than depth (node.depth < depth)
|
97
|
+
to_depth(depth) # Return nodes up to a certain depth (node.depth <= depth)
|
98
|
+
at_depth(depth) # Return nodes that are at depth (node.depth == depth)
|
99
|
+
from_depth(depth) # Return nodes starting from a certain depth (node.depth >= depth)
|
100
|
+
after_depth(depth) # Return nodes that are deeper than depth (node.depth > depth)
|
101
|
+
|
102
|
+
The depth scopes are also available through calls to descendants, descendant_ids, subtree, subtree_ids, path and ancestors. In this case, depth values are interpreted relatively. Some examples:
|
103
|
+
|
104
|
+
node.subtree(:to_depth => 2) # Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
|
105
|
+
node.subtree.to_depth(5) # Subtree of node to an absolute depth of 5
|
106
|
+
node.descendants(:at_depth => 2) # Descendant of node, at depth node.depth + 2 (grandchildren)
|
107
|
+
node.descendants.at_depth(10) # Descendants of node at an absolute depth of 10
|
108
|
+
node.ancestors.to_depth(3) # The oldest 4 ancestors of node (its root and 3 more)
|
109
|
+
|
110
|
+
node.ancestors(:from_depth => -6, :to_depth => -4)
|
111
|
+
node.descendants(:from_depth => 2, :to_depth => 4)
|
112
|
+
node.subtree.from_depth(10).to_depth(12)
|
113
|
+
|
114
|
+
Please note that depth constraints cannot be passed to ancestor_ids and path_ids. The reason for this is that both these relations can be fetched directly from the ancestry column without performing a database query. It would require an entirely different method of applying the depth constraints which isn't worth the effort of implementing. You can use ancestors(depth_options).map(&:id) or ancestor_ids.slice(min_depth..max_depth) instead.
|
115
|
+
|
116
|
+
= Tests
|
117
|
+
|
118
|
+
The Mongestry gem comes with a RSpec test suite consisting of about 190+ assertions in about 45+ tests. It takes about 0.2 seconds to run on MongoDB. To run it yourself check out the repository from GitHub, check and fix <em>spec/support/connection.rb</em> to your needs and type:
|
119
|
+
|
120
|
+
rake
|
121
|
+
|
122
|
+
= Internals
|
123
|
+
|
124
|
+
As can be seen in the previous section, Mongestry stores a path from the root to the parent for every node. This is a variation on the materialised path database pattern. It allows Mongestry to fetch any relation (siblings, descendants, etc.) in a single db request without the complicated algorithms and incomprehensibility associated with left and right values. Additionally, any inserts, deletes and updates only affect nodes within the affected node's own subtree.
|
125
|
+
|
126
|
+
In the example above, the ancestry field is created as a string. This puts a limitation on the depth of the tree of about 40 or 50 levels, which I think may be enough for most users. To increase the maximum depth of the tree, increase the size of the string that is being used or change it to a text to remove the limitation entirely. Changing it to a text will however decrease performance because an index cannot be put on the column in that case.
|
127
|
+
|
128
|
+
= Limitations
|
129
|
+
|
130
|
+
Mongestry was created with Rails3 and Ruby >= 1.9.2 in mind. Sorry. You need Rails2 or Ruby prior to 1.9 support? Feel free to fork, fix and request a pull.
|
131
|
+
|
132
|
+
= Missing Features
|
133
|
+
|
134
|
+
Compared to {Ancestry}[https://github.com/stefankroes/ancestry] there are some missing features.
|
135
|
+
|
136
|
+
- Creation of nodes through relational scopes
|
137
|
+
- Integrity checking
|
138
|
+
- options for <em>has_mongestry</em> (don't know if we need any)
|
139
|
+
- STI support
|
140
|
+
- arrangement
|
141
|
+
- sorting by ancestry
|
142
|
+
- migration from other plugins
|
143
|
+
- integrity checking and fixing
|
144
|
+
- Rails2 support
|
145
|
+
- support for Ruby versions < 1.9
|
146
|
+
- instance methods: path, path_ids
|
147
|
+
|
148
|
+
= Contributing to Mongestry
|
149
|
+
|
150
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
151
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
152
|
+
* Fork the project
|
153
|
+
* Start a feature/bugfix branch
|
154
|
+
* Commit and push until you are happy with your contribution
|
155
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
156
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so we can cherry-pick around it.
|
157
|
+
|
158
|
+
= Copyright
|
159
|
+
|
160
|
+
Copyright (c) 2011 DailyDeal GmbH. See LICENSE.txt for further details.
|
161
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "mongestry"
|
18
|
+
gem.homepage = "http://github.com/DailyDeal/mongestry"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Mongestry is Ancestry for Mongo}
|
21
|
+
gem.description = %Q{Mongestry is Ancestry for Mongo, build for ORM Mongoid}
|
22
|
+
gem.email = %q{jan.roesner@dailydeal.de lars.kluge@dailydeal.de}
|
23
|
+
gem.authors = ["Jan Roesner", "Lars Kluge"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
31
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :default => :spec
|
40
|
+
|
41
|
+
require 'yard'
|
42
|
+
YARD::Rake::YardocTask.new
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5.5
|
data/lib/mongestry.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
module Mongestry
|
2
|
+
|
3
|
+
class << self.class.superclass
|
4
|
+
def has_mongestry
|
5
|
+
include Mongestry::InstanceMethods
|
6
|
+
field :ancestry, type: String
|
7
|
+
index :ancestry
|
8
|
+
field :persisted_depth, type: Integer
|
9
|
+
before_create :build_ancestry
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
|
15
|
+
def self.included base
|
16
|
+
base.extend Mongestry::ClassMethods
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_ancestry
|
20
|
+
raise "Either parent or parent_id can be given, not both at once" if self.attributes.keys.include?("parent") and self.attributes.keys.include?("parent_id")
|
21
|
+
return unless self.respond_to?(:parent) or self.respond_to?(:parent_id)
|
22
|
+
|
23
|
+
parent = self.class.object_for(self.attributes["parent"] || self.attributes["parent_id"])
|
24
|
+
|
25
|
+
self.ancestry = nil unless parent
|
26
|
+
self.ancestry = parent.ancestry.nil? ? parent.id.to_s : parent.ancestry.to_s + "/#{parent.id.to_s}" if parent
|
27
|
+
self.persisted_depth = parent.depth + 1 rescue 0
|
28
|
+
|
29
|
+
self.attributes.delete("parent")
|
30
|
+
self.attributes.delete("parent_id")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns a list of ancestor ids, starting with the root id and ending with the parent id
|
34
|
+
def ancestor_ids
|
35
|
+
self.ancestry.split('/').collect{ |s| BSON::ObjectId.from_string s }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Scopes the model on ancestors of the record
|
39
|
+
def ancestors
|
40
|
+
return [] if self.is_root?
|
41
|
+
self.class.where(_id: {"$in" => self.ancestor_ids})
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the parent of the record, nil for a root node
|
45
|
+
def parent
|
46
|
+
self.class.where(_id: self.ancestry.split('/').last).first rescue nil
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the id of the parent of the record, nil for a root node
|
50
|
+
def parent_id
|
51
|
+
self.parent.id rescue nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the root of the tree the record is in, self for a root node
|
55
|
+
def root
|
56
|
+
return self unless self.ancestry
|
57
|
+
self.class.where(_id: self.ancestry.split('/').first).first
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the id of the root of the tree the record is in
|
61
|
+
def root_id
|
62
|
+
self.root.id
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns true if the record is a root node, false otherwise
|
66
|
+
def is_root?
|
67
|
+
self.ancestry.nil?
|
68
|
+
end
|
69
|
+
|
70
|
+
# Scopes the model on children of the record
|
71
|
+
def children
|
72
|
+
case self.is_root?
|
73
|
+
when true
|
74
|
+
self.class.where(:ancestry => self.ancestry.to_s + "#{self.id.to_s}")
|
75
|
+
else
|
76
|
+
self.class.where(:ancestry => self.ancestry.to_s + "/#{self.id.to_s}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns a list of child ids
|
81
|
+
def child_ids
|
82
|
+
self.children.map(&:id)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns true if the record has any children, false otherwise
|
86
|
+
def has_children?
|
87
|
+
!self.children.to_a.blank?
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns true if the record has no childen, false otherwise
|
91
|
+
def is_childless?
|
92
|
+
!self.has_children?
|
93
|
+
end
|
94
|
+
|
95
|
+
# Scopes the model on siblings of the record, the record itself is included
|
96
|
+
def siblings
|
97
|
+
self.class.where(:ancestry => self.ancestry.to_s).and(:_id => {'$ne' => self.id})
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns a list of sibling ids
|
101
|
+
def sibling_ids
|
102
|
+
self.siblings.map(&:id)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns true if the record's parent has more than one child
|
106
|
+
def has_siblings?
|
107
|
+
self.siblings.present?
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns true if the record is the only child of its parent
|
111
|
+
def is_only_child?
|
112
|
+
!self.has_siblings?
|
113
|
+
end
|
114
|
+
|
115
|
+
# Scopes the model on direct and indirect children of the record
|
116
|
+
def descendants
|
117
|
+
expression = self.is_root? ? self.id.to_s : (self.ancestry + "/#{self.id.to_s}").split('/').join('\/')
|
118
|
+
self.class.where(:ancestry => Regexp.new(expression))
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns a list of a descendant ids
|
122
|
+
def descendant_ids
|
123
|
+
self.descendants.map(&:id)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Scopes the model on descendants and itself
|
127
|
+
def subtree
|
128
|
+
self.class.where(_id: {"$in" => self.descendant_ids.push(self.id)})
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns a list of all ids in the record's subtree
|
132
|
+
def subtree_ids
|
133
|
+
self.subtree.map(&:id)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Return the depth of the node, root nodes are at depth 0
|
137
|
+
def depth
|
138
|
+
self.ancestry.split('/').size rescue 0
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
module ClassMethods
|
144
|
+
|
145
|
+
def object_for identifier
|
146
|
+
return nil if identifier == ""
|
147
|
+
case identifier
|
148
|
+
when BSON::ObjectId
|
149
|
+
self.find identifier
|
150
|
+
when String
|
151
|
+
self.find(BSON::ObjectId.from_string(identifier))
|
152
|
+
when self
|
153
|
+
identifier
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
#Root nodes
|
158
|
+
def roots
|
159
|
+
self.where(ancestry: nil)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Ancestors of node, node can be either a record or an id
|
163
|
+
def ancestors_of node
|
164
|
+
node.ancestors
|
165
|
+
end
|
166
|
+
|
167
|
+
# Children of node, node can be either a record or an id
|
168
|
+
def children_of node
|
169
|
+
node.children
|
170
|
+
end
|
171
|
+
|
172
|
+
# Descendants of node, node can be either a record or an id
|
173
|
+
def descendants_of node
|
174
|
+
node.descendants
|
175
|
+
end
|
176
|
+
|
177
|
+
# Subtree of node, node can be either a record or an id
|
178
|
+
def subtree_of node
|
179
|
+
node.subtree
|
180
|
+
end
|
181
|
+
|
182
|
+
# Siblings of node, node can be either a record or an id
|
183
|
+
def siblings_of node
|
184
|
+
node.siblings
|
185
|
+
end
|
186
|
+
|
187
|
+
# Return nodes that are less deep than depth (node.depth < depth)
|
188
|
+
def before_depth depth
|
189
|
+
self.where(persisted_depth: {"$lt" => depth})
|
190
|
+
end
|
191
|
+
|
192
|
+
# Return nodes up to a certain depth (node.depth <= depth)
|
193
|
+
def to_depth depth
|
194
|
+
self.where(persisted_depth: {"$lte" => depth})
|
195
|
+
end
|
196
|
+
|
197
|
+
# Return nodes that are at depth (node.depth == depth)
|
198
|
+
def at_depth depth
|
199
|
+
self.where(persisted_depth: depth)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Return nodes starting from a certain depth (node.depth >= depth)
|
203
|
+
def from_depth depth
|
204
|
+
self.where(persisted_depth: {"$gte" => depth})
|
205
|
+
end
|
206
|
+
|
207
|
+
# Return nodes that are deeper than depth (node.depth > depth)
|
208
|
+
def after_depth depth
|
209
|
+
self.where(persisted_depth: {"$gt" => depth})
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
data/mongestry.gemspec
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{mongestry}
|
8
|
+
s.version = "0.5.5"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jan Roesner", "Lars Kluge"]
|
12
|
+
s.date = %q{2011-07-20}
|
13
|
+
s.description = %q{Mongestry is Ancestry for Mongo, build for ORM Mongoid}
|
14
|
+
s.email = %q{jan.roesner@dailydeal.de lars.kluge@dailydeal.de}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".rspec",
|
22
|
+
"Gemfile",
|
23
|
+
"Gemfile.lock",
|
24
|
+
"LICENSE.txt",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"lib/mongestry.rb",
|
29
|
+
"mongestry.gemspec",
|
30
|
+
"spec/mongestry_spec.rb",
|
31
|
+
"spec/spec_helper.rb",
|
32
|
+
"spec/support/category.rb",
|
33
|
+
"spec/support/connection.rb"
|
34
|
+
]
|
35
|
+
s.homepage = %q{http://github.com/DailyDeal/mongestry}
|
36
|
+
s.licenses = ["MIT"]
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubygems_version = %q{1.3.7}
|
39
|
+
s.summary = %q{Mongestry is Ancestry for Mongo}
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
43
|
+
s.specification_version = 3
|
44
|
+
|
45
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
46
|
+
s.add_runtime_dependency(%q<bson_ext>, [">= 0"])
|
47
|
+
s.add_runtime_dependency(%q<mongoid>, [">= 0"])
|
48
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
|
49
|
+
s.add_development_dependency(%q<yard>, ["~> 0.6.0"])
|
50
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
51
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.6.2"])
|
52
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
53
|
+
s.add_development_dependency(%q<linecache19>, ["= 0.5.11"])
|
54
|
+
s.add_development_dependency(%q<ruby-debug19>, [">= 0"])
|
55
|
+
else
|
56
|
+
s.add_dependency(%q<bson_ext>, [">= 0"])
|
57
|
+
s.add_dependency(%q<mongoid>, [">= 0"])
|
58
|
+
s.add_dependency(%q<rspec>, ["~> 2.3.0"])
|
59
|
+
s.add_dependency(%q<yard>, ["~> 0.6.0"])
|
60
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
61
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
|
62
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
63
|
+
s.add_dependency(%q<linecache19>, ["= 0.5.11"])
|
64
|
+
s.add_dependency(%q<ruby-debug19>, [">= 0"])
|
65
|
+
end
|
66
|
+
else
|
67
|
+
s.add_dependency(%q<bson_ext>, [">= 0"])
|
68
|
+
s.add_dependency(%q<mongoid>, [">= 0"])
|
69
|
+
s.add_dependency(%q<rspec>, ["~> 2.3.0"])
|
70
|
+
s.add_dependency(%q<yard>, ["~> 0.6.0"])
|
71
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
72
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
|
73
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
74
|
+
s.add_dependency(%q<linecache19>, ["= 0.5.11"])
|
75
|
+
s.add_dependency(%q<ruby-debug19>, [">= 0"])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
@@ -0,0 +1,450 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
def initialize_country_tree
|
4
|
+
@root = Category.create!(name: "Root", persisted_depth: 0)
|
5
|
+
@germany = Category.create!(name: "Germany", persisted_depth: 1, ancestry: "#{@root.id}")
|
6
|
+
@switzerland = Category.create!(name: "Switzerland", persisted_depth: 1, ancestry: "#{@root.id}")
|
7
|
+
@austria = Category.create!(name: "Austria", persisted_depth: 1, ancestry: "#{@root.id}")
|
8
|
+
@berlin = Category.create!(name: "Berlin", persisted_depth: 2, ancestry: "#{@root.id}/#{@germany.id}")
|
9
|
+
@munich = Category.create!(name: "Munich", persisted_depth: 2, ancestry: "#{@root.id}/#{@germany.id}")
|
10
|
+
@hamburg = Category.create!(name: "Hamburg", persisted_depth: 2, ancestry: "#{@root.id}/#{@germany.id}")
|
11
|
+
@bern = Category.create!(name: "Bern", persisted_depth: 2, ancestry: "#{@root.id}/#{@switzerland.id}")
|
12
|
+
@zurich = Category.create!(name: "Zurich", persisted_depth: 2, ancestry: "#{@root.id}/#{@switzerland.id}")
|
13
|
+
@vienna = Category.create!(name: "Vienna", persisted_depth: 2, ancestry: "#{@root.id}/#{@austria.id}")
|
14
|
+
@graz = Category.create!(name: "Graz", persisted_depth: 2, ancestry: "#{@root.id}/#{@austria.id}")
|
15
|
+
@pankow = Category.create!(name: "Pankow", persisted_depth: 3, ancestry: "#{@root.id}/#{@germany.id}/#{@berlin.id}")
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "Mongestry" do
|
19
|
+
context "with fixed tree" do
|
20
|
+
|
21
|
+
before :all do
|
22
|
+
Category.destroy_all
|
23
|
+
initialize_country_tree
|
24
|
+
Category.class_eval{ has_mongestry }
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#has_mongestry' do
|
28
|
+
|
29
|
+
it 'should include instance methods into class where invoked' do
|
30
|
+
class Foo
|
31
|
+
include Mongoid::Document
|
32
|
+
has_mongestry
|
33
|
+
end
|
34
|
+
|
35
|
+
[:build_ancestry, :ancestor_ids, :ancestors, :parent, :parent_id, :root, :root_id, :is_root?, :children, :child_ids, :has_children?, :is_childless?, :siblings, :sibling_ids, :has_siblings?, :is_only_child?, :descendants, :descendant_ids, :subtree, :subtree_ids, :depth].each do |method|
|
36
|
+
Foo.new.respond_to?(method).should be_true
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should include class methods into class where invoked' do
|
42
|
+
class Bar
|
43
|
+
include Mongoid::Document
|
44
|
+
has_mongestry
|
45
|
+
end
|
46
|
+
|
47
|
+
[:roots, :ancestors_of, :children_of, :descendants_of, :subtree_of, :siblings_of, :before_depth, :to_depth, :at_depth, :from_depth, :after_depth, :object_for].each do |method|
|
48
|
+
Bar.respond_to?(method).should be_true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#ancestor_ids' do
|
54
|
+
it 'should return the ancestor_ids of the given node' do
|
55
|
+
ids = Category.where(name:"Pankow").first.ancestor_ids
|
56
|
+
|
57
|
+
ids.size.should == 3
|
58
|
+
ids.include?(@berlin.id).should be_true
|
59
|
+
ids.include?(@germany.id).should be_true
|
60
|
+
ids.include?(@root.id).should be_true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#ancestors' do
|
65
|
+
it 'should return ancestors of the given node scoped' do
|
66
|
+
ancestors = Category.where(name:"Pankow").first.ancestors.to_a
|
67
|
+
|
68
|
+
ancestors.size.should == 3
|
69
|
+
ancestors.include?(@berlin).should be_true
|
70
|
+
ancestors.include?(@germany).should be_true
|
71
|
+
ancestors.include?(@root).should be_true
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should return an empty array if called on a root category' do
|
75
|
+
ancestors = Category.where(name: "Root").first.ancestors.to_a
|
76
|
+
ancestors.size.should == 0
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#parent' do
|
81
|
+
it 'should return the given nodes parent' do
|
82
|
+
Category.where(name: "Germany").first.parent.should == @root
|
83
|
+
Category.where(name: "Pankow").first.parent.should == @berlin
|
84
|
+
Category.roots.first.parent.should be_nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe '#parent_id' do
|
89
|
+
it 'should return the given nodes parents id' do
|
90
|
+
Category.where(name: "Germany").first.parent_id.should == @root.id
|
91
|
+
Category.where(name: "Pankow").first.parent_id.should == @berlin.id
|
92
|
+
Category.roots.first.parent_id.should be_nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#root' do
|
97
|
+
it 'should return the root of the tree of the given node' do
|
98
|
+
Category.where(name: "Pankow").first.root.should eql @root
|
99
|
+
Category.where(name: "Berlin").first.root.should eql @root
|
100
|
+
Category.where(name: "Germany").first.root.should eql @root
|
101
|
+
Category.where(name: "Root").first.root.should eql @root
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '#root_id' do
|
106
|
+
it 'should return the id of the root of the tree of the given node' do
|
107
|
+
Category.where(name: "Pankow").first.root_id.should eql @root.id
|
108
|
+
Category.where(name: "Berlin").first.root_id.should eql @root.id
|
109
|
+
Category.where(name: "Germany").first.root_id.should eql @root.id
|
110
|
+
Category.where(name: "Root").first.root_id.should eql @root.id
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe '#is_root?' do
|
115
|
+
it 'should return true if given node is a root' do
|
116
|
+
Category.where(name: "Root").first.is_root?.should be_true
|
117
|
+
end
|
118
|
+
it 'should return false if given node is no root' do
|
119
|
+
Category.where(name: "Pankow").first.is_root?.should be_false
|
120
|
+
Category.where(name: "Germany").first.is_root?.should be_false
|
121
|
+
Category.where(name: "Berlin").first.is_root?.should be_false
|
122
|
+
Category.where(name: "Bern").first.is_root?.should be_false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe '#children' do
|
127
|
+
it 'should return the given nodes children scoped' do
|
128
|
+
children_scope = Category.where(name: "Germany").first.children
|
129
|
+
children_scope.is_a?(Mongoid::Criteria).should be_true
|
130
|
+
children_scope.count.should == 3
|
131
|
+
children_scope.to_a.include?(@berlin).should be_true
|
132
|
+
children_scope.to_a.include?(@hamburg).should be_true
|
133
|
+
children_scope.to_a.include?(@munich).should be_true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe '#child_ids' do
|
138
|
+
it 'should return the given nodes childs ids' do
|
139
|
+
ids = Category.where(name: "Germany").first.child_ids
|
140
|
+
ids.count.should == 3
|
141
|
+
ids.include?(@berlin.id).should be_true
|
142
|
+
ids.include?(@hamburg.id).should be_true
|
143
|
+
ids.include?(@munich.id).should be_true
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe '#has_children?' do
|
148
|
+
it 'should return true if given node has children' do
|
149
|
+
Category.where(name: "Root").first.has_children?.should be_true
|
150
|
+
Category.where(name: "Germany").first.has_children?.should be_true
|
151
|
+
Category.where(name: "Berlin").first.has_children?.should be_true
|
152
|
+
end
|
153
|
+
it 'should return false if given node has no children' do
|
154
|
+
Category.where(name: "Pankow").first.has_children?.should be_false
|
155
|
+
Category.where(name: "Bern").first.has_children?.should be_false
|
156
|
+
Category.where(name: "Zurich").first.has_children?.should be_false
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe '#is_childless?' do
|
161
|
+
it 'should return true if given node has no children' do
|
162
|
+
Category.where(name: "Root").first.is_childless?.should be_false
|
163
|
+
Category.where(name: "Germany").first.is_childless?.should be_false
|
164
|
+
Category.where(name: "Berlin").first.is_childless?.should be_false
|
165
|
+
end
|
166
|
+
it 'should return false if given node has children' do
|
167
|
+
Category.where(name: "Pankow").first.is_childless?.should be_true
|
168
|
+
Category.where(name: "Bern").first.is_childless?.should be_true
|
169
|
+
Category.where(name: "Zurich").first.is_childless?.should be_true
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe '#siblings' do
|
174
|
+
it 'should return the given nodes siblings scoped' do
|
175
|
+
siblings_scope = Category.where(name: "Berlin").first.siblings
|
176
|
+
siblings_scope.is_a?(Mongoid::Criteria).should be_true
|
177
|
+
siblings_scope.to_a.size.should == 2
|
178
|
+
siblings_scope.to_a.include?(@hamburg).should be_true
|
179
|
+
siblings_scope.to_a.include?(@munich).should be_true
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe '#sibling_ids' do
|
184
|
+
it 'should return the given nodes siblings ids' do
|
185
|
+
ids = Category.where(name: "Berlin").first.sibling_ids
|
186
|
+
ids.size.should == 2
|
187
|
+
ids.include?(@hamburg.id).should be_true
|
188
|
+
ids.include?(@munich.id).should be_true
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe '#has_siblings?' do
|
193
|
+
it 'should return true if given node has siblings' do
|
194
|
+
Category.where(name: "Berlin").first.has_siblings?.should be_true
|
195
|
+
Category.where(name: "Bern").first.has_siblings?.should be_true
|
196
|
+
Category.where(name: "Germany").first.has_siblings?.should be_true
|
197
|
+
end
|
198
|
+
it 'should return false if given node has no siblings' do
|
199
|
+
Category.where(name: "Root").first.has_siblings?.should be_false
|
200
|
+
Category.where(name: "Pankow").first.has_siblings?.should be_false
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
describe '#is_only_child?' do
|
205
|
+
it 'should return true if given node has no siblings' do
|
206
|
+
Category.where(name: "Berlin").first.is_only_child?.should be_false
|
207
|
+
Category.where(name: "Bern").first.is_only_child?.should be_false
|
208
|
+
Category.where(name: "Germany").first.is_only_child?.should be_false
|
209
|
+
end
|
210
|
+
it 'should return false if given node has siblings' do
|
211
|
+
Category.where(name: "Root").first.is_only_child?.should be_true
|
212
|
+
Category.where(name: "Pankow").first.is_only_child?.should be_true
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
describe '#descendants' do
|
217
|
+
it 'should return the given nodes descendants scoped' do
|
218
|
+
desc_scope = Category.where(name: "Germany").first.descendants
|
219
|
+
desc_scope.is_a?(Mongoid::Criteria).should be_true
|
220
|
+
desc_scope.to_a.size.should == 4
|
221
|
+
desc_scope.to_a.include?(@berlin).should be_true
|
222
|
+
desc_scope.to_a.include?(@hamburg).should be_true
|
223
|
+
desc_scope.to_a.include?(@munich).should be_true
|
224
|
+
desc_scope.to_a.include?(@pankow).should be_true
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
describe '#descendant_ids' do
|
229
|
+
it 'should return the given nodes descendants ids' do
|
230
|
+
ids = Category.where(name: "Germany").first.descendant_ids
|
231
|
+
ids.size.should == 4
|
232
|
+
ids.include?(@berlin.id).should be_true
|
233
|
+
ids.include?(@hamburg.id).should be_true
|
234
|
+
ids.include?(@munich.id).should be_true
|
235
|
+
ids.include?(@pankow.id).should be_true
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
describe '#subtree' do
|
240
|
+
it 'should return the given nodes subtree including the node itself' do
|
241
|
+
subtree_scope = Category.where(name: "Germany").first.subtree
|
242
|
+
subtree_scope.is_a?(Mongoid::Criteria).should be_true
|
243
|
+
subtree_scope.to_a.size.should == 5
|
244
|
+
subtree_scope.to_a.include?(@germany).should be_true
|
245
|
+
subtree_scope.to_a.include?(@berlin).should be_true
|
246
|
+
subtree_scope.to_a.include?(@hamburg).should be_true
|
247
|
+
subtree_scope.to_a.include?(@munich).should be_true
|
248
|
+
subtree_scope.to_a.include?(@pankow).should be_true
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe '#subtree_ids' do
|
253
|
+
it 'should return the ids of the given nodes subtree including the code itself' do
|
254
|
+
ids = Category.where(name: "Germany").first.subtree_ids
|
255
|
+
ids.size.should == 5
|
256
|
+
ids.include?(@germany.id).should be_true
|
257
|
+
ids.include?(@berlin.id).should be_true
|
258
|
+
ids.include?(@hamburg.id).should be_true
|
259
|
+
ids.include?(@munich.id).should be_true
|
260
|
+
ids.include?(@pankow.id).should be_true
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
describe '#depth' do
|
265
|
+
it 'should return the computed depth of the given node' do
|
266
|
+
Category.all.each do |category|
|
267
|
+
category.depth.should == category.persisted_depth
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
describe '#object_for' do
|
273
|
+
it 'should return the correct object if object was given' do
|
274
|
+
Category.object_for(Category.where(name: "Berlin").first).should == @berlin
|
275
|
+
Category.object_for(Category.where(name: "Germany").first).should == @germany
|
276
|
+
Category.object_for(Category.where(name: "Root").first).should == @root
|
277
|
+
end
|
278
|
+
it 'should return the correct object if object_id was given as BSON::ObjectId' do
|
279
|
+
Category.object_for(Category.where(name: "Berlin").first.id).should == @berlin
|
280
|
+
Category.object_for(Category.where(name: "Germany").first.id).should == @germany
|
281
|
+
Category.object_for(Category.where(name: "Root").first.id).should == @root
|
282
|
+
end
|
283
|
+
it 'should return the correct object if object_id was given as String' do
|
284
|
+
Category.object_for(Category.where(name: "Berlin").first.id.to_s).should == @berlin
|
285
|
+
Category.object_for(Category.where(name: "Germany").first.id.to_s).should == @germany
|
286
|
+
Category.object_for(Category.where(name: "Root").first.id.to_s).should == @root
|
287
|
+
end
|
288
|
+
it 'should return nil in case an empty string is given as object_id' do
|
289
|
+
Category.object_for("").should be_nil
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
describe '#roots' do
|
294
|
+
it 'should return all available roots scoped' do
|
295
|
+
roots_scope = Category.roots
|
296
|
+
roots_scope.is_a?(Mongoid::Criteria).should be_true
|
297
|
+
roots_scope.to_a.size.should == 1
|
298
|
+
roots_scope.to_a.include?(@root).should be_true
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
describe '#ancestors_of node' do
|
303
|
+
it 'should return the given nodes ancestors scoped' do
|
304
|
+
anc_scope = Category.ancestors_of(Category.where(name: "Pankow").first)
|
305
|
+
anc_scope.is_a?(Mongoid::Criteria).should be_true
|
306
|
+
anc_scope.to_a.size.should == 3
|
307
|
+
anc_scope.to_a.include?(@berlin).should be_true
|
308
|
+
anc_scope.to_a.include?(@germany).should be_true
|
309
|
+
anc_scope.to_a.include?(@root).should be_true
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
describe '#children_of node' do
|
314
|
+
it 'should return the given nodes children scoped' do
|
315
|
+
child_scope = Category.children_of(Category.where(name: "Germany").first)
|
316
|
+
child_scope.is_a?(Mongoid::Criteria).should be_true
|
317
|
+
child_scope.to_a.size.should == 3
|
318
|
+
child_scope.to_a.include?(@berlin).should be_true
|
319
|
+
child_scope.to_a.include?(@hamburg).should be_true
|
320
|
+
child_scope.to_a.include?(@munich).should be_true
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
describe '#descendants_of node' do
|
325
|
+
it 'should return the given nodes descendants scoped' do
|
326
|
+
desc_scope = Category.descendants_of(Category.where(name: "Germany").first)
|
327
|
+
desc_scope.is_a?(Mongoid::Criteria).should be_true
|
328
|
+
desc_scope.to_a.size.should == 4
|
329
|
+
desc_scope.to_a.include?(@berlin).should be_true
|
330
|
+
desc_scope.to_a.include?(@munich).should be_true
|
331
|
+
desc_scope.to_a.include?(@hamburg).should be_true
|
332
|
+
desc_scope.to_a.include?(@pankow).should be_true
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
describe '#subtree_of node' do
|
337
|
+
it 'should return the given nodes subtree scoped' do
|
338
|
+
subtree_scope = Category.subtree_of(Category.where(name: "Germany").first)
|
339
|
+
subtree_scope.is_a?(Mongoid::Criteria).should be_true
|
340
|
+
subtree_scope.to_a.size.should == 5
|
341
|
+
subtree_scope.to_a.include?(@germany).should be_true
|
342
|
+
subtree_scope.to_a.include?(@berlin).should be_true
|
343
|
+
subtree_scope.to_a.include?(@munich).should be_true
|
344
|
+
subtree_scope.to_a.include?(@hamburg).should be_true
|
345
|
+
subtree_scope.to_a.include?(@pankow).should be_true
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
describe '#siblings_of node' do
|
350
|
+
it 'should return the given nodes siblings scoped' do
|
351
|
+
siblings_scope = Category.siblings_of(Category.where(name: "Berlin").first)
|
352
|
+
siblings_scope.is_a?(Mongoid::Criteria).should be_true
|
353
|
+
siblings_scope.to_a.size.should == 2
|
354
|
+
siblings_scope.to_a.include?(@hamburg).should be_true
|
355
|
+
siblings_scope.to_a.include?(@munich).should be_true
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
describe '#before_depth depth' do
|
360
|
+
it 'should return a scope finding objects with a depth less than given depth' do
|
361
|
+
amounts = { 0 => 0, 1 => 1, 2 => 4, 3 => 11, 4 => 12 }
|
362
|
+
0.upto(3) do |depth|
|
363
|
+
Category.before_depth(depth).size.should == amounts[depth]
|
364
|
+
Category.before_depth(depth).each do |category|
|
365
|
+
category.persisted_depth.should < depth
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
describe '#to_depth depth' do
|
372
|
+
it 'should return a scope finding objects with a depth less or equal than given depth' do
|
373
|
+
amounts = { 0 => 1, 1 => 4, 2 => 11, 3 => 12 }
|
374
|
+
0.upto(3) do |depth|
|
375
|
+
Category.to_depth(depth).size.should == amounts[depth]
|
376
|
+
Category.before_depth(depth).each do |category|
|
377
|
+
category.persisted_depth.should <= depth
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
describe '#at_depth depth' do
|
384
|
+
it 'should return a scope finding objects with that exact given depth' do
|
385
|
+
amounts = { 0 => 1, 1 => 3, 2 => 7, 3 => 1 }
|
386
|
+
0.upto(3) do |depth|
|
387
|
+
Category.at_depth(depth).size.should == amounts[depth]
|
388
|
+
Category.at_depth(depth).each do |category|
|
389
|
+
category.persisted_depth.should == depth
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
describe '#from_depth depth' do
|
396
|
+
it 'should return a scope finding objects with a depth greater or equal than given depth' do
|
397
|
+
amounts = { 0 => 12, 1 => 11, 2 => 8, 3 => 1 }
|
398
|
+
0.upto(3) do |depth|
|
399
|
+
Category.from_depth(depth).size.should == amounts[depth]
|
400
|
+
Category.from_depth(depth).each do |category|
|
401
|
+
category.persisted_depth.should >= depth
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
describe '#after_depth depth' do
|
408
|
+
it 'should return a scope finding objects with a depth greater than given depth' do
|
409
|
+
amounts = { 0 => 11, 1 => 8, 2 => 1, 3 => 0 }
|
410
|
+
0.upto(3) do |depth|
|
411
|
+
Category.after_depth(depth).size.should == amounts[depth]
|
412
|
+
Category.after_depth(depth).each do |category|
|
413
|
+
category.persisted_depth.should > depth
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
context "with new nodes" do
|
421
|
+
describe '#build_ancestry' do
|
422
|
+
it 'should raise an error in case parent and parent_id were given' do
|
423
|
+
lambda{ Category.create(name: "Error", parent: Category.first, parent_id: Category.first.id) }.should raise_error "Either parent or parent_id can be given, not both at once"
|
424
|
+
end
|
425
|
+
|
426
|
+
it 'should set the ancestry string' do
|
427
|
+
category = Category.create!(name: "Correct country", parent: Category.where(name: "Root").first)
|
428
|
+
category.reload.ancestry.should == Category.where(name: "Root").first.id.to_s
|
429
|
+
category = Category.create!(name: "Correct country2", parent_id: Category.where(name: "Root").first.id)
|
430
|
+
category.reload.ancestry.should == Category.where(name: "Root").first.id.to_s
|
431
|
+
end
|
432
|
+
|
433
|
+
it 'should set persisted depth' do
|
434
|
+
category = Category.create!(name: "Depth test", parent: Category.where(name: "Pankow").first)
|
435
|
+
category.reload.persisted_depth.should == 4
|
436
|
+
category = Category.create!(name: "Depth test2", parent: Category.where(name: "Root").first)
|
437
|
+
category.reload.persisted_depth.should == 1
|
438
|
+
end
|
439
|
+
|
440
|
+
it 'should not persist the parent object when given' do
|
441
|
+
category = Category.create!(name: "Persistence test", parent: Category.where(name: "Root").first)
|
442
|
+
category.reload.attributes.keys.include?(:parent).should be_false
|
443
|
+
end
|
444
|
+
it 'should not persist the parent_id when given' do
|
445
|
+
category = Category.create!(name: "Persistence test2", parent: Category.where(name: "Root").first)
|
446
|
+
category.reload.attributes.keys.include?(:parent_id).should be_false
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
|
4
|
+
require 'mongoid'
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + "/support/connection")
|
6
|
+
require File.expand_path(File.dirname(__FILE__) + "/support/category")
|
7
|
+
require 'rspec'
|
8
|
+
require 'mongestry'
|
9
|
+
|
10
|
+
# Requires supporting files with custom matchers and macros, etc,
|
11
|
+
# in ./support/ and its subdirectories.
|
12
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
13
|
+
|
14
|
+
RSpec.configure do |config|
|
15
|
+
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mongestry
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 5
|
8
|
+
- 5
|
9
|
+
version: 0.5.5
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Jan Roesner
|
13
|
+
- Lars Kluge
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-07-20 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: bson_ext
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
prerelease: false
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: mongoid
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *id002
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 2
|
56
|
+
- 3
|
57
|
+
- 0
|
58
|
+
version: 2.3.0
|
59
|
+
type: :development
|
60
|
+
prerelease: false
|
61
|
+
version_requirements: *id003
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: yard
|
64
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
- 6
|
72
|
+
- 0
|
73
|
+
version: 0.6.0
|
74
|
+
type: :development
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: *id004
|
77
|
+
- !ruby/object:Gem::Dependency
|
78
|
+
name: bundler
|
79
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ~>
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
segments:
|
85
|
+
- 1
|
86
|
+
- 0
|
87
|
+
- 0
|
88
|
+
version: 1.0.0
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: *id005
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: jeweler
|
94
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ~>
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
segments:
|
100
|
+
- 1
|
101
|
+
- 6
|
102
|
+
- 2
|
103
|
+
version: 1.6.2
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: *id006
|
107
|
+
- !ruby/object:Gem::Dependency
|
108
|
+
name: rcov
|
109
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: *id007
|
120
|
+
- !ruby/object:Gem::Dependency
|
121
|
+
name: linecache19
|
122
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
123
|
+
none: false
|
124
|
+
requirements:
|
125
|
+
- - "="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
segments:
|
128
|
+
- 0
|
129
|
+
- 5
|
130
|
+
- 11
|
131
|
+
version: 0.5.11
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: *id008
|
135
|
+
- !ruby/object:Gem::Dependency
|
136
|
+
name: ruby-debug19
|
137
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
segments:
|
143
|
+
- 0
|
144
|
+
version: "0"
|
145
|
+
type: :development
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: *id009
|
148
|
+
description: Mongestry is Ancestry for Mongo, build for ORM Mongoid
|
149
|
+
email: jan.roesner@dailydeal.de lars.kluge@dailydeal.de
|
150
|
+
executables: []
|
151
|
+
|
152
|
+
extensions: []
|
153
|
+
|
154
|
+
extra_rdoc_files:
|
155
|
+
- LICENSE.txt
|
156
|
+
- README.rdoc
|
157
|
+
files:
|
158
|
+
- .document
|
159
|
+
- .rspec
|
160
|
+
- Gemfile
|
161
|
+
- Gemfile.lock
|
162
|
+
- LICENSE.txt
|
163
|
+
- README.rdoc
|
164
|
+
- Rakefile
|
165
|
+
- VERSION
|
166
|
+
- lib/mongestry.rb
|
167
|
+
- mongestry.gemspec
|
168
|
+
- spec/mongestry_spec.rb
|
169
|
+
- spec/spec_helper.rb
|
170
|
+
- spec/support/category.rb
|
171
|
+
- spec/support/connection.rb
|
172
|
+
has_rdoc: true
|
173
|
+
homepage: http://github.com/DailyDeal/mongestry
|
174
|
+
licenses:
|
175
|
+
- MIT
|
176
|
+
post_install_message:
|
177
|
+
rdoc_options: []
|
178
|
+
|
179
|
+
require_paths:
|
180
|
+
- lib
|
181
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
182
|
+
none: false
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
hash: 4258898673227910918
|
187
|
+
segments:
|
188
|
+
- 0
|
189
|
+
version: "0"
|
190
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
191
|
+
none: false
|
192
|
+
requirements:
|
193
|
+
- - ">="
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
segments:
|
196
|
+
- 0
|
197
|
+
version: "0"
|
198
|
+
requirements: []
|
199
|
+
|
200
|
+
rubyforge_project:
|
201
|
+
rubygems_version: 1.3.7
|
202
|
+
signing_key:
|
203
|
+
specification_version: 3
|
204
|
+
summary: Mongestry is Ancestry for Mongo
|
205
|
+
test_files: []
|
206
|
+
|