path_tree 1.0.11
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.txt +674 -0
- data/README.rdoc +11 -0
- data/Rakefile +37 -0
- data/lib/path_tree.rb +248 -0
- data/lib/ruby_18_patterns.rb +24 -0
- data/lib/ruby_19_patterns.rb +24 -0
- data/spec/path_tree_spec.rb +276 -0
- data/spec/spec_helper.rb +16 -0
- metadata +103 -0
data/README.rdoc
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
= PathTree
|
2
|
+
|
3
|
+
This gem provides support for creating tree data structures. The structure of the tree is defined with dot delimited paths on each node. This has a couple of advantages over the +acts_as_tree+ plugin.
|
4
|
+
|
5
|
+
1. Each node gets a unique character identifier that has semantic qualities and indicates the structure in the tree.
|
6
|
+
|
7
|
+
2. Queries for all ancestors or all descendants are far more efficient.
|
8
|
+
|
9
|
+
3. Out of the box the code works with ActiveRecord, but it can easily be made to work with other ORM's if they implement just a few methods.
|
10
|
+
|
11
|
+
See PathTree for more details.
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
desc 'Default: run unit tests'
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'rspec'
|
9
|
+
require 'rspec/core/rake_task'
|
10
|
+
desc 'Run the unit tests'
|
11
|
+
RSpec::Core::RakeTask.new(:test)
|
12
|
+
rescue LoadError
|
13
|
+
task :test do
|
14
|
+
raise "You must have rspec 2.0 installed to run the tests"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
begin
|
19
|
+
require 'jeweler'
|
20
|
+
Jeweler::Tasks.new do |gem|
|
21
|
+
gem.name = "path_tree"
|
22
|
+
gem.summary = %Q{Helper module for constructing tree data structures}
|
23
|
+
gem.description = %Q{Module that defines a tree data structure based on a path.}
|
24
|
+
gem.authors = ["Brian Durand"]
|
25
|
+
gem.email = ["mdobrota@tribune.com", "ddpr@tribune.com"]
|
26
|
+
gem.files = FileList["lib/**/*", "spec/**/*", "README.rdoc", "Rakefile", "License.txt"].to_a
|
27
|
+
gem.has_rdoc = true
|
28
|
+
gem.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
|
29
|
+
gem.extra_rdoc_files = ["README.rdoc"]
|
30
|
+
gem.add_dependency('activerecord')
|
31
|
+
gem.add_development_dependency('rspec', '>= 2.0.0')
|
32
|
+
gem.add_development_dependency('sqlite3')
|
33
|
+
gem.add_development_dependency('activerecord')
|
34
|
+
end
|
35
|
+
Jeweler::RubygemsDotOrgTasks.new
|
36
|
+
rescue LoadError
|
37
|
+
end
|
data/lib/path_tree.rb
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
# This module implements a tree structure by using a convention of converting a name into a path.
|
2
|
+
# Paths created by normalizing a name attribute and then separating levels with periods with
|
3
|
+
# the lowest level coming last.
|
4
|
+
#
|
5
|
+
# In order to use this module, the model must respond to the +first+ and +all+ methods like ActiveRecord,
|
6
|
+
# have support for after_destroy and after_save callbacks, validates_* macros and include attributes
|
7
|
+
# for name, node_path, path, and parent_path.
|
8
|
+
module PathTree
|
9
|
+
if RUBY_VERSION.match(/^1\.8/)
|
10
|
+
require File.expand_path("../ruby_18_patterns.rb", __FILE__)
|
11
|
+
else
|
12
|
+
require File.expand_path("../ruby_19_patterns.rb", __FILE__)
|
13
|
+
end
|
14
|
+
include Patterns
|
15
|
+
|
16
|
+
def self.included (base)
|
17
|
+
base.extend(ClassMethods)
|
18
|
+
|
19
|
+
base.validates_uniqueness_of :path
|
20
|
+
base.validates_uniqueness_of :node_path, :scope => :parent_path
|
21
|
+
base.validates_presence_of :name, :node_path, :path
|
22
|
+
|
23
|
+
base.after_save do |record|
|
24
|
+
if record.path_changed? and !record.path_was.nil?
|
25
|
+
record.children.each do |child|
|
26
|
+
child.update_attributes(:parent_path => record.path)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
record.instance_variable_set(:@children, nil)
|
30
|
+
end
|
31
|
+
|
32
|
+
base.after_destroy do |record|
|
33
|
+
record.children.each do |child|
|
34
|
+
child.update_attributes(:parent_path => record.parent_path)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module ClassMethods
|
40
|
+
include Patterns
|
41
|
+
|
42
|
+
NON_WORD_PATTERN = /[^a-z0-9_]+/.freeze
|
43
|
+
DASH_AT_START_PATTERN = /^-+/.freeze
|
44
|
+
DASH_AT_END_PATTERN = /-+$/.freeze
|
45
|
+
|
46
|
+
# Get all the root nodes (i.e. those without any parents)
|
47
|
+
def roots
|
48
|
+
all(:conditions => {:parent_path => nil})
|
49
|
+
end
|
50
|
+
|
51
|
+
# Set the path delimiter (default is '.').
|
52
|
+
def path_delimiter= (char)
|
53
|
+
@path_delimiter = char
|
54
|
+
end
|
55
|
+
|
56
|
+
def path_delimiter
|
57
|
+
@path_delimiter ||= '.'
|
58
|
+
end
|
59
|
+
|
60
|
+
# Load the entire branch of the tree under path at once. If you will be traversing the
|
61
|
+
# tree, this is the fastest way to load it. Returns the root node of the branch.
|
62
|
+
def branch (path)
|
63
|
+
raise ArgumentError.new("branch path must not be blank") if path.blank?
|
64
|
+
root = first(:conditions => {:path => path})
|
65
|
+
return [] unless root
|
66
|
+
nodes = path_like(path).sort{|a,b| b.path <=> a.path}
|
67
|
+
nodes << root
|
68
|
+
return populate_tree_structure!(nodes.pop, nodes)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Translate a value into a valid path part. By default this will translate it into an ascii
|
72
|
+
# lower case value with words delimited by dashes. Implementations can override this logic.
|
73
|
+
def pathify (value)
|
74
|
+
if value
|
75
|
+
asciify(unquote(value)).strip.downcase.gsub(NON_WORD_PATTERN, '-').gsub(DASH_AT_START_PATTERN, '').gsub(DASH_AT_END_PATTERN, '')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Replace accented characters with the closest ascii equivalent
|
80
|
+
def asciify (value)
|
81
|
+
if value
|
82
|
+
value.gsub(UPPER_A_PATTERN, 'A').gsub(LOWER_A_PATTERN, 'a').
|
83
|
+
gsub(UPPER_E_PATTERN, 'E').gsub(LOWER_E_PATTERN, 'e').
|
84
|
+
gsub(UPPER_I_PATTERN, 'I').gsub(LOWER_I_PATTERN, 'i').
|
85
|
+
gsub(UPPER_O_PATTERN, 'O').gsub(LOWER_O_PATTERN, 'o').
|
86
|
+
gsub(UPPER_U_PATTERN, 'U').gsub(LOWER_U_PATTERN, 'u').
|
87
|
+
gsub(UPPER_Y_PATTERN, 'Y').gsub(LOWER_Y_PATTERN, 'y').
|
88
|
+
gsub(UPPER_N_PATTERN, 'N').gsub(LOWER_N_PATTERN, 'n').
|
89
|
+
gsub(UPPER_C_PATTERN, 'C').gsub(LOWER_C_PATTERN, 'c').
|
90
|
+
gsub(UPPER_AE_PATTERN, 'AE').gsub(LOWER_AE_PATTERN, 'ae').
|
91
|
+
gsub(SS_PATTERN, 'ss').gsub(UPPER_D_PATTERN, 'D')
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Remove quotation marks from a string.
|
96
|
+
def unquote (value)
|
97
|
+
value.gsub(/['"]/, '') if value
|
98
|
+
end
|
99
|
+
|
100
|
+
# Abstract way of finding paths that start with a value so it can be overridden by non-SQL implementations.
|
101
|
+
def path_like (value)
|
102
|
+
all(:conditions => ["path LIKE ?", "#{value}#{path_delimiter}%"])
|
103
|
+
end
|
104
|
+
|
105
|
+
# Expand a path into an array of the path and all its ancestor paths.
|
106
|
+
def expanded_paths (path)
|
107
|
+
expanded = []
|
108
|
+
path.split(path_delimiter).each do |part|
|
109
|
+
if expanded.empty?
|
110
|
+
expanded << part
|
111
|
+
else
|
112
|
+
expanded << "#{expanded.last}#{path_delimiter}#{part}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
expanded
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def populate_tree_structure! (root, sorted_nodes)
|
121
|
+
while !sorted_nodes.empty? do
|
122
|
+
node = sorted_nodes.last
|
123
|
+
if node.parent_path == root.path
|
124
|
+
sorted_nodes.pop
|
125
|
+
node.parent = root
|
126
|
+
root.send(:append_child, node)
|
127
|
+
else
|
128
|
+
last_child = root.children.last
|
129
|
+
if last_child and node.parent_path == last_child.path
|
130
|
+
populate_tree_structure!(last_child, sorted_nodes)
|
131
|
+
else
|
132
|
+
break
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
return root
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Get the full name of a node including the names of all it's parent nodes. Specify the separator string to use
|
141
|
+
# between values with :separator (defaults to " > "). You can also specify the context for the full name by
|
142
|
+
# specifying a path in :context. This will only render the names up to and not including that part of the tree.
|
143
|
+
def full_name (options = {})
|
144
|
+
separator = options[:separator] || " > "
|
145
|
+
n = ""
|
146
|
+
n << parent.full_name(options) if parent_path and parent_path != options[:context]
|
147
|
+
n << separator unless n.blank?
|
148
|
+
n << name
|
149
|
+
end
|
150
|
+
|
151
|
+
def path_delimiter
|
152
|
+
self.class.path_delimiter
|
153
|
+
end
|
154
|
+
|
155
|
+
# Get the parent node.
|
156
|
+
def parent
|
157
|
+
unless instance_variable_defined?(:@parent)
|
158
|
+
if path.index(path_delimiter)
|
159
|
+
@parent = self.class.base_class.first(:conditions => {:path => parent_path})
|
160
|
+
else
|
161
|
+
@parent = nil
|
162
|
+
end
|
163
|
+
end
|
164
|
+
@parent
|
165
|
+
end
|
166
|
+
|
167
|
+
# Set the parent node.
|
168
|
+
def parent= (node)
|
169
|
+
node_path = node.path if node
|
170
|
+
self.parent_path = node_path unless parent_path == node_path
|
171
|
+
@parent = node
|
172
|
+
end
|
173
|
+
|
174
|
+
# Set the parent path
|
175
|
+
def parent_path= (value)
|
176
|
+
unless value == parent_path
|
177
|
+
self[:parent_path] = value
|
178
|
+
recalculate_path
|
179
|
+
remove_instance_variable(:@parent) if instance_variable_defined?(:@parent)
|
180
|
+
end
|
181
|
+
value
|
182
|
+
end
|
183
|
+
|
184
|
+
def name= (value)
|
185
|
+
unless value == name
|
186
|
+
self[:name] = value
|
187
|
+
self.node_path = value if node_path.blank?
|
188
|
+
end
|
189
|
+
value
|
190
|
+
end
|
191
|
+
|
192
|
+
def node_path= (value)
|
193
|
+
pathified = self.class.pathify(value)
|
194
|
+
self[:node_path] = pathified
|
195
|
+
recalculate_path
|
196
|
+
end
|
197
|
+
|
198
|
+
# Get all nodes that are direct children of this node.
|
199
|
+
def children
|
200
|
+
unless @children
|
201
|
+
childrens_path = new_record? ? path : path_was
|
202
|
+
@children = self.class.base_class.all(:conditions => {:parent_path => childrens_path})
|
203
|
+
@children.each{|c| c.parent = self}
|
204
|
+
end
|
205
|
+
@children
|
206
|
+
end
|
207
|
+
|
208
|
+
# Get all nodes that share the same parent as this node.
|
209
|
+
def siblings
|
210
|
+
self.class.base_class.all(:conditions => {:parent_path => parent_path}).reject{|node| node == self}
|
211
|
+
end
|
212
|
+
|
213
|
+
# Get all descendant of this node.
|
214
|
+
def descendants
|
215
|
+
self.class.base_class.path_like(path)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Get all ancestors of this node with the root node first.
|
219
|
+
def ancestors
|
220
|
+
ancestor_paths = expanded_paths
|
221
|
+
ancestor_paths.pop
|
222
|
+
if ancestor_paths.empty?
|
223
|
+
[]
|
224
|
+
else
|
225
|
+
self.class.base_class.all(:conditions => {:path => ancestor_paths}).sort{|a,b| a.path.length <=> b.path.length}
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Returns an array containing the paths of this node and those of all its ancestors.
|
230
|
+
def expanded_paths
|
231
|
+
self.class.expanded_paths(path)
|
232
|
+
end
|
233
|
+
|
234
|
+
protected
|
235
|
+
|
236
|
+
def append_child (node)
|
237
|
+
@children ||= []
|
238
|
+
@children << node
|
239
|
+
node.parent = self
|
240
|
+
end
|
241
|
+
|
242
|
+
def recalculate_path
|
243
|
+
path = ""
|
244
|
+
path << "#{parent_path}#{path_delimiter}" unless parent_path.blank?
|
245
|
+
path << node_path if node_path
|
246
|
+
self.path = path
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module PathTree
|
2
|
+
module Patterns
|
3
|
+
UPPER_A_PATTERN = /\xC3[\x80-\x85]/.freeze
|
4
|
+
LOWER_A_PATTERN = /\xC3[\xA0-\xA5]/.freeze
|
5
|
+
UPPER_E_PATTERN = /\xC3[\x88-\x8B]/.freeze
|
6
|
+
LOWER_E_PATTERN = /\xC3[\xA8-\xAB]/.freeze
|
7
|
+
UPPER_I_PATTERN = /\xC3[\x8C-\x8F]/.freeze
|
8
|
+
LOWER_I_PATTERN = /\xC3[\xAC-\xAF]/.freeze
|
9
|
+
UPPER_O_PATTERN = /\xC3[\x92-\x96\x98]/.freeze
|
10
|
+
LOWER_O_PATTERN = /\xC3[\xB2-\xB6\xB8]/.freeze
|
11
|
+
UPPER_U_PATTERN = /\xC3[\x99-\x9C]/.freeze
|
12
|
+
LOWER_U_PATTERN = /\xC3[\xB9-\xBC]/.freeze
|
13
|
+
UPPER_Y_PATTERN = /\xC3\x9D/.freeze
|
14
|
+
LOWER_Y_PATTERN = /\xC3[\xBD\xBF]/.freeze
|
15
|
+
UPPER_C_PATTERN = /\xC3\x87/.freeze
|
16
|
+
LOWER_C_PATTERN = /\xC3\xA7/.freeze
|
17
|
+
UPPER_N_PATTERN = /\xC3\x91/.freeze
|
18
|
+
LOWER_N_PATTERN = /\xC3\xB1/.freeze
|
19
|
+
UPPER_D_PATTERN = /\xC3\x90/.freeze
|
20
|
+
UPPER_AE_PATTERN = /\xC3\x86/.freeze
|
21
|
+
LOWER_AE_PATTERN = /\xC3\xA6/.freeze
|
22
|
+
SS_PATTERN = /\xC3\x9F/.freeze
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module PathTree
|
2
|
+
module Patterns
|
3
|
+
UPPER_A_PATTERN = /[\xC3\x80-\xC3\x85]/u.freeze
|
4
|
+
LOWER_A_PATTERN = /[\xC3\xA0-\xC3\xA5]/u.freeze
|
5
|
+
UPPER_E_PATTERN = /[\xC3\x88-\xC3\x8B]/u.freeze
|
6
|
+
LOWER_E_PATTERN = /[\xC3\xA8-\xC3\xAB]/u.freeze
|
7
|
+
UPPER_I_PATTERN = /[\xC3\x8C-\xC3\x8F]/u.freeze
|
8
|
+
LOWER_I_PATTERN = /[\xC3\xAC-\xC3\xAF]/u.freeze
|
9
|
+
UPPER_O_PATTERN = /[\xC3\x92-\xC3\x96\xC3\x98]/u.freeze
|
10
|
+
LOWER_O_PATTERN = /[\xC3\xB2-\xC3\xB6\xC3\xB8]/u.freeze
|
11
|
+
UPPER_U_PATTERN = /[\xC3\x99-\xC3\x9C]/u.freeze
|
12
|
+
LOWER_U_PATTERN = /[\xC3\xB9-\xC3\xBC]/u.freeze
|
13
|
+
UPPER_Y_PATTERN = /\xC3\x9D/u.freeze
|
14
|
+
LOWER_Y_PATTERN = /[\xC3\xBD\xC3\xBF]/u.freeze
|
15
|
+
UPPER_C_PATTERN = /\xC3\x87/u.freeze
|
16
|
+
LOWER_C_PATTERN = /\xC3\xA7/u.freeze
|
17
|
+
UPPER_N_PATTERN = /\xC3\x91/u.freeze
|
18
|
+
LOWER_N_PATTERN = /\xC3\xB1/u.freeze
|
19
|
+
UPPER_D_PATTERN = /\xC3\x90/u.freeze
|
20
|
+
UPPER_AE_PATTERN = /\xC3\x86/u.freeze
|
21
|
+
LOWER_AE_PATTERN = /\xC3\xA6/u.freeze
|
22
|
+
SS_PATTERN = /\xC3\x9F/u.freeze
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,276 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe PathTree do
|
6
|
+
|
7
|
+
module PathTree
|
8
|
+
class Test < ActiveRecord::Base
|
9
|
+
self.table_name = :test_path_trees
|
10
|
+
|
11
|
+
def self.create_tables
|
12
|
+
connection.create_table(table_name) do |t|
|
13
|
+
t.string :name
|
14
|
+
t.string :node_path
|
15
|
+
t.string :path
|
16
|
+
t.string :parent_path
|
17
|
+
end unless table_exists?
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.drop_tables
|
21
|
+
connection.drop_table(table_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
include PathTree
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
before :all do
|
29
|
+
PathTree::Test.create_tables
|
30
|
+
end
|
31
|
+
|
32
|
+
after :all do
|
33
|
+
PathTree::Test.drop_tables
|
34
|
+
end
|
35
|
+
|
36
|
+
context "path construction" do
|
37
|
+
it "should turn accented characters into ascii equivalents" do
|
38
|
+
PathTree::Test.asciify("ÀÂÄÃÅàâáããä-ÈÊÉËèêéë-ÌÎÍÏìîíï-ÒÔÖØÓÕòôöøóõ-ÚÜÙÛúüùû-ÝýÿÑñÇçÆæßÐ").should == "AAAAAaaaaaa-EEEEeeee-IIIIiiii-OOOOOOoooooo-UUUUuuuu-YyyNnCcAEaessD"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should unquote strings" do
|
42
|
+
PathTree::Test.unquote(%Q("This is a 'test'")).should == "This is a test"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should translate a value to a path part" do
|
46
|
+
PathTree::Test.pathify("This is the 1st / test À...").should == "this-is-the-1st-test-a"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should expand a path to its component paths" do
|
50
|
+
PathTree::Test.expanded_paths("this.is.a.test").should == ["this", "this.is", "this.is.a", "this.is.a.test"]
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should set the parent path when setting the parent" do
|
54
|
+
parent = PathTree::Test.new(:name => "parent")
|
55
|
+
node = PathTree::Test.new(:name => "child")
|
56
|
+
node.parent = parent
|
57
|
+
node.parent_path.should == "parent"
|
58
|
+
node.parent = nil
|
59
|
+
node.parent_path.should == nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "with default delimiter" do
|
64
|
+
before :all do
|
65
|
+
@root_1 = PathTree::Test.create!(:name => "Root 1")
|
66
|
+
@parent_a = PathTree::Test.create!(:name => "Parent A", :parent_path => "root-1")
|
67
|
+
@parent_b = PathTree::Test.create!(:name => "Parent B", :parent_path => "root-1")
|
68
|
+
@parent_c = PathTree::Test.create!(:name => "Parent C", :parent_path => "root-1")
|
69
|
+
@child_a1 = PathTree::Test.create!(:name => "Child A1", :parent_path => "root-1.parent-a")
|
70
|
+
@child_a2 = PathTree::Test.create!(:name => "Child A2", :parent_path => "root-1.parent-a")
|
71
|
+
@grandchild = PathTree::Test.create!(:name => "Grandchild A1.1", :parent_path => "root-1.parent-a.child-a1")
|
72
|
+
@root_2 = PathTree::Test.create!(:name => "Root 2")
|
73
|
+
@parent_z = PathTree::Test.create!(:name => "Parent Z", :parent_path => "root-2")
|
74
|
+
end
|
75
|
+
|
76
|
+
after :all do
|
77
|
+
PathTree::Test.delete_all
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should get the root nodes" do
|
81
|
+
PathTree::Test.roots.sort{|a,b| a.path <=> b.path}.should == [@root_1, @root_2]
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should load an entire branch structure" do
|
85
|
+
branch = PathTree::Test.branch("root-1.parent-a")
|
86
|
+
branch.should == @parent_a
|
87
|
+
branch.instance_variable_get(:@children).should == [@child_a1, @child_a2]
|
88
|
+
branch.children.first.instance_variable_get(:@children).should == [@grandchild]
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should construct a fully qualified name with a delimiter" do
|
92
|
+
@grandchild.full_name.should == "Root 1 > Parent A > Child A1 > Grandchild A1.1"
|
93
|
+
@grandchild.full_name(:separator => "/").should == "Root 1/Parent A/Child A1/Grandchild A1.1"
|
94
|
+
@grandchild.full_name(:context => "root-1.parent-a").should == "Child A1 > Grandchild A1.1"
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should be able to get and set a parent node" do
|
98
|
+
node = PathTree::Test.find_by_path("root-1.parent-a")
|
99
|
+
node.parent.should == @root_1
|
100
|
+
node.parent = @root_2
|
101
|
+
node.parent_path.should == "root-2"
|
102
|
+
node.path.should == "root-2.parent-a"
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should be able to set the parent by path" do
|
106
|
+
node = PathTree::Test.find_by_path("root-1.parent-a")
|
107
|
+
node.parent_path = "root-2"
|
108
|
+
node.parent.should == @root_2
|
109
|
+
node.path.should == "root-2.parent-a"
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should have child nodes" do
|
113
|
+
node = PathTree::Test.find_by_path("root-1.parent-a")
|
114
|
+
node.children.should == [@child_a1, @child_a2]
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should have descendant nodes" do
|
118
|
+
node = PathTree::Test.find_by_path("root-1.parent-a")
|
119
|
+
node.descendants.should == [@child_a1, @child_a2, @grandchild]
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should have sibling nodes" do
|
123
|
+
node = PathTree::Test.find_by_path("root-1.parent-a")
|
124
|
+
node.siblings.should == [@parent_b, @parent_c]
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should have ancestor nodes" do
|
128
|
+
node = PathTree::Test.find_by_path("root-1.parent-a.child-a1")
|
129
|
+
node.ancestors.should == [@root_1, @parent_a]
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should maintain the path with the name path" do
|
133
|
+
node = PathTree::Test.find_by_path("root-1.parent-a")
|
134
|
+
node.node_path = "New Name"
|
135
|
+
node.path.should == "root-1.new-name"
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should get the expanded paths for a node" do
|
139
|
+
@grandchild.expanded_paths.should == ["root-1", "root-1.parent-a", "root-1.parent-a.child-a1", "root-1.parent-a.child-a1.grandchild-a1-1"]
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should update child paths when the path is changed" do
|
143
|
+
PathTree::Test.transaction do
|
144
|
+
node = PathTree::Test.find_by_path("root-1.parent-a")
|
145
|
+
node.node_path = "New Name"
|
146
|
+
node.save!
|
147
|
+
node.reload
|
148
|
+
node.children.collect{|c| c.path}.should == ["root-1.new-name.child-a1", "root-1.new-name.child-a2"]
|
149
|
+
node.children.first.children.collect{|c| c.path}.should == ["root-1.new-name.child-a1.grandchild-a1-1"]
|
150
|
+
raise ActiveRecord::Rollback
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should update child paths when a node is destroyed" do
|
155
|
+
PathTree::Test.transaction do
|
156
|
+
node = PathTree::Test.find_by_path("root-1.parent-a")
|
157
|
+
node.name = "New Name"
|
158
|
+
node.destroy
|
159
|
+
root = PathTree::Test.find_by_path("root-1")
|
160
|
+
root.children.collect{|c| c.path}.should == ["root-1.parent-b", "root-1.parent-c", "root-1.child-a1", "root-1.child-a2"]
|
161
|
+
root.children[2].children.collect{|c| c.path}.should == ["root-1.child-a1.grandchild-a1-1"]
|
162
|
+
raise ActiveRecord::Rollback
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
context "with default delimiter" do
|
169
|
+
before :all do
|
170
|
+
PathTree::Test.path_delimiter = '/'
|
171
|
+
@root_1 = PathTree::Test.create!(:name => "Root 1")
|
172
|
+
@parent_a = PathTree::Test.create!(:name => "Parent A", :parent_path => "root-1")
|
173
|
+
@parent_b = PathTree::Test.create!(:name => "Parent B", :parent_path => "root-1")
|
174
|
+
@parent_c = PathTree::Test.create!(:name => "Parent C", :parent_path => "root-1")
|
175
|
+
@child_a1 = PathTree::Test.create!(:name => "Child A1", :parent_path => "root-1/parent-a")
|
176
|
+
@child_a2 = PathTree::Test.create!(:name => "Child A2", :parent_path => "root-1/parent-a")
|
177
|
+
@grandchild = PathTree::Test.create!(:name => "Grandchild A1.1", :parent_path => "root-1/parent-a/child-a1")
|
178
|
+
@root_2 = PathTree::Test.create!(:name => "Root 2")
|
179
|
+
@parent_z = PathTree::Test.create!(:name => "Parent Z", :parent_path => "root-2")
|
180
|
+
end
|
181
|
+
|
182
|
+
after :all do
|
183
|
+
PathTree::Test.path_delimiter = nil
|
184
|
+
end
|
185
|
+
|
186
|
+
it "should get the root nodes" do
|
187
|
+
PathTree::Test.roots.sort{|a,b| a.path <=> b.path}.should == [@root_1, @root_2]
|
188
|
+
end
|
189
|
+
|
190
|
+
it "should load an entire branch structure" do
|
191
|
+
branch = PathTree::Test.branch("root-1/parent-a")
|
192
|
+
branch.should == @parent_a
|
193
|
+
branch.instance_variable_get(:@children).should == [@child_a1, @child_a2]
|
194
|
+
branch.children.first.instance_variable_get(:@children).should == [@grandchild]
|
195
|
+
end
|
196
|
+
|
197
|
+
it "should construct a fully qualified name with a delimiter" do
|
198
|
+
@grandchild.full_name.should == "Root 1 > Parent A > Child A1 > Grandchild A1.1"
|
199
|
+
@grandchild.full_name(:separator => ":").should == "Root 1:Parent A:Child A1:Grandchild A1.1"
|
200
|
+
@grandchild.full_name(:context => "root-1/parent-a").should == "Child A1 > Grandchild A1.1"
|
201
|
+
end
|
202
|
+
|
203
|
+
it "should be able to get and set a parent node" do
|
204
|
+
node = PathTree::Test.find_by_path("root-1/parent-a")
|
205
|
+
node.parent.should == @root_1
|
206
|
+
node.parent = @root_2
|
207
|
+
node.parent_path.should == "root-2"
|
208
|
+
node.path.should == "root-2/parent-a"
|
209
|
+
end
|
210
|
+
|
211
|
+
it "should be able to set the parent by path" do
|
212
|
+
node = PathTree::Test.find_by_path("root-1/parent-a")
|
213
|
+
node.parent_path = "root-2"
|
214
|
+
node.parent.should == @root_2
|
215
|
+
node.path.should == "root-2/parent-a"
|
216
|
+
end
|
217
|
+
|
218
|
+
it "should have child nodes" do
|
219
|
+
node = PathTree::Test.find_by_path("root-1/parent-a")
|
220
|
+
node.children.should == [@child_a1, @child_a2]
|
221
|
+
end
|
222
|
+
|
223
|
+
it "should have descendant nodes" do
|
224
|
+
node = PathTree::Test.find_by_path("root-1/parent-a")
|
225
|
+
node.descendants.should == [@child_a1, @child_a2, @grandchild]
|
226
|
+
end
|
227
|
+
|
228
|
+
it "should have sibling nodes" do
|
229
|
+
node = PathTree::Test.find_by_path("root-1/parent-a")
|
230
|
+
node.siblings.should == [@parent_b, @parent_c]
|
231
|
+
end
|
232
|
+
|
233
|
+
it "should have ancestor nodes" do
|
234
|
+
node = PathTree::Test.find_by_path("root-1/parent-a/child-a1")
|
235
|
+
node.ancestors.should == [@root_1, @parent_a]
|
236
|
+
end
|
237
|
+
|
238
|
+
it "should maintain the path with the name path" do
|
239
|
+
node = PathTree::Test.find_by_path("root-1/parent-a")
|
240
|
+
node.node_path = "New Name"
|
241
|
+
node.path.should == "root-1/new-name"
|
242
|
+
end
|
243
|
+
|
244
|
+
it "should get the expanded paths for a node" do
|
245
|
+
@grandchild.expanded_paths.should == ["root-1", "root-1/parent-a", "root-1/parent-a/child-a1", "root-1/parent-a/child-a1/grandchild-a1-1"]
|
246
|
+
end
|
247
|
+
|
248
|
+
it "should update child paths when the path is changed" do
|
249
|
+
PathTree::Test.transaction do
|
250
|
+
node = PathTree::Test.find_by_path("root-1/parent-a")
|
251
|
+
node.node_path = "New Name"
|
252
|
+
node.save!
|
253
|
+
node.reload
|
254
|
+
node.children.collect{|c| c.path}.should == ["root-1/new-name/child-a1", "root-1/new-name/child-a2"]
|
255
|
+
node.children.first.children.collect{|c| c.path}.should == ["root-1/new-name/child-a1/grandchild-a1-1"]
|
256
|
+
raise ActiveRecord::Rollback
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
it "should update child paths when a node is destroyed" do
|
261
|
+
begin
|
262
|
+
PathTree::Test.transaction do
|
263
|
+
node = PathTree::Test.find_by_path("root-1/parent-a")
|
264
|
+
node.name = "New Name"
|
265
|
+
node.destroy
|
266
|
+
root = PathTree::Test.find_by_path("root-1")
|
267
|
+
root.children.collect{|c| c.path}.should == ["root-1/parent-b", "root-1/parent-c", "root-1/child-a1", "root-1/child-a2"]
|
268
|
+
root.children[2].children.collect{|c| c.path}.should == ["root-1/child-a1/grandchild-a1-1"]
|
269
|
+
raise ActiveRecord::Rollback
|
270
|
+
end
|
271
|
+
rescue
|
272
|
+
puts $@.join("\n")
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_record'
|
3
|
+
require 'sqlite3'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'simplecov'
|
7
|
+
SimpleCov.start do
|
8
|
+
add_filter "/spec/"
|
9
|
+
end
|
10
|
+
rescue LoadError
|
11
|
+
# simplecov not installed
|
12
|
+
end
|
13
|
+
|
14
|
+
ActiveRecord::Base.establish_connection("adapter" => "sqlite3", "database" => ":memory:")
|
15
|
+
|
16
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'path_tree'))
|