path_tree 1.0.12 → 1.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.
- checksums.yaml +4 -4
- data/.gitignore +5 -6
- data/Appraisals +18 -0
- data/Rakefile +20 -0
- data/gemfiles/rails_3.2.gemfile +7 -0
- data/gemfiles/rails_4.0.gemfile +7 -0
- data/gemfiles/rails_4.1.gemfile +7 -0
- data/gemfiles/rails_4.2.gemfile +7 -0
- data/lib/path_tree.rb +12 -13
- data/lib/{ruby_19_patterns.rb → path_tree/patterns.rb} +0 -0
- data/lib/path_tree/version.rb +1 -1
- data/path_tree.gemspec +6 -5
- data/spec/path_tree_spec.rb +55 -238
- data/spec/shared/path_tree_model_examples.rb +170 -0
- data/spec/spec_helper.rb +22 -1
- metadata +34 -8
- data/lib/ruby_18_patterns.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8fbfdfca21ebd5334ea1829ed80ed67675dca0a
|
4
|
+
data.tar.gz: 8f0d28fd3a6517030a207a65de808ce5f35c676c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3a63b1fb4e7d51d403fdc185e09454caa0c0d4e595a42bee6b5a9ec13a1a564494ca5cad8203470b1ea0138d712c90f2e46fe2108e2d96d591296ef0ebc43186
|
7
|
+
data.tar.gz: 1b64fd875d59d5a283b5066c0787f953bfd539da2205cad4f3f7099a66ed3c80c17d4b34f259a065dc40cae302bac9b3fc6dad1dd71947977dc4d4082edc6fff
|
data/.gitignore
CHANGED
data/Appraisals
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Install gems for all appraisal definitions:
|
2
|
+
#
|
3
|
+
# $ appraisal install
|
4
|
+
#
|
5
|
+
# To run tests on different versions:
|
6
|
+
#
|
7
|
+
# $ appraisal activerecord_x.x rspec spec
|
8
|
+
|
9
|
+
[
|
10
|
+
[ '3.2', '~> 3.2.0' ],
|
11
|
+
[ '4.0', '~> 4.0.0' ],
|
12
|
+
[ '4.1', '~> 4.1.0' ],
|
13
|
+
[ '4.2', '~> 4.2.0' ],
|
14
|
+
].each do |ver_name, ver_req|
|
15
|
+
appraise "rails_#{ver_name}" do
|
16
|
+
gem 'activerecord', ver_req
|
17
|
+
end
|
18
|
+
end
|
data/Rakefile
CHANGED
@@ -1 +1,21 @@
|
|
1
1
|
require 'bundler/gem_tasks'
|
2
|
+
require 'bundler/setup' # constrain rake version
|
3
|
+
|
4
|
+
# Note: if you get this error: "Bundler could not find compatible versions for gem ..."
|
5
|
+
# try deleting Gemfile.lock (usually happens when switching branches).
|
6
|
+
|
7
|
+
task default: :appraise_all
|
8
|
+
|
9
|
+
# This is slightly different from 'appraisal COMMAND' because it continues even if a definition fails.
|
10
|
+
desc "Run rspecs for all appraisals"
|
11
|
+
task :appraise_all do
|
12
|
+
success_map = {}
|
13
|
+
`bundle exec appraisal list`.lines.map(&:chomp).each do |appraise_def|
|
14
|
+
success = system('appraisal', appraise_def, 'rspec', 'spec')
|
15
|
+
success_map[appraise_def] = success
|
16
|
+
end
|
17
|
+
puts "\n===== Test Summary ====="
|
18
|
+
success_map.each do |appraise_def, success|
|
19
|
+
puts "#{appraise_def}: #{success ? 'no failures (but check pending)' : 'failed'}"
|
20
|
+
end
|
21
|
+
end
|
data/lib/path_tree.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require File.expand_path("../path_tree/patterns.rb", __FILE__)
|
2
|
+
|
1
3
|
# This module implements a tree structure by using a convention of converting a name into a path.
|
2
4
|
# Paths created by normalizing a name attribute and then separating levels with periods with
|
3
5
|
# the lowest level coming last.
|
@@ -6,11 +8,6 @@
|
|
6
8
|
# have support for after_destroy and after_save callbacks, validates_* macros and include attributes
|
7
9
|
# for name, node_path, path, and parent_path.
|
8
10
|
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
11
|
include Patterns
|
15
12
|
|
16
13
|
def self.included (base)
|
@@ -45,7 +42,7 @@ module PathTree
|
|
45
42
|
|
46
43
|
# Get all the root nodes (i.e. those without any parents)
|
47
44
|
def roots
|
48
|
-
|
45
|
+
where(parent_path: nil).to_a
|
49
46
|
end
|
50
47
|
|
51
48
|
# Set the path delimiter (default is '.').
|
@@ -61,7 +58,7 @@ module PathTree
|
|
61
58
|
# tree, this is the fastest way to load it. Returns the root node of the branch.
|
62
59
|
def branch (path)
|
63
60
|
raise ArgumentError.new("branch path must not be blank") if path.blank?
|
64
|
-
root =
|
61
|
+
root = where(path: path).first
|
65
62
|
return [] unless root
|
66
63
|
nodes = path_like(path).sort{|a,b| b.path <=> a.path}
|
67
64
|
nodes << root
|
@@ -76,7 +73,8 @@ module PathTree
|
|
76
73
|
end
|
77
74
|
end
|
78
75
|
|
79
|
-
# Replace accented characters with the closest ascii equivalent
|
76
|
+
# Replace accented characters with the closest ascii equivalent.
|
77
|
+
# NOTE: characters than can't be decomposed to a latin equivalent may not be replaced.
|
80
78
|
def asciify (value)
|
81
79
|
if value
|
82
80
|
value.gsub(UPPER_A_PATTERN, 'A').gsub(LOWER_A_PATTERN, 'a').
|
@@ -99,7 +97,7 @@ module PathTree
|
|
99
97
|
|
100
98
|
# Abstract way of finding paths that start with a value so it can be overridden by non-SQL implementations.
|
101
99
|
def path_like (value)
|
102
|
-
|
100
|
+
where("path LIKE ?", "#{value}#{path_delimiter}%").to_a
|
103
101
|
end
|
104
102
|
|
105
103
|
# Expand a path into an array of the path and all its ancestor paths.
|
@@ -156,7 +154,7 @@ module PathTree
|
|
156
154
|
def parent
|
157
155
|
unless instance_variable_defined?(:@parent)
|
158
156
|
if path.index(path_delimiter)
|
159
|
-
@parent = self.class.base_class.
|
157
|
+
@parent = self.class.base_class.where(path: parent_path).first
|
160
158
|
else
|
161
159
|
@parent = nil
|
162
160
|
end
|
@@ -199,7 +197,7 @@ module PathTree
|
|
199
197
|
def children
|
200
198
|
unless @children
|
201
199
|
childrens_path = new_record? ? path : path_was
|
202
|
-
@children = self.class.base_class.
|
200
|
+
@children = self.class.base_class.where(parent_path: childrens_path).to_a
|
203
201
|
@children.each{|c| c.parent = self}
|
204
202
|
end
|
205
203
|
@children
|
@@ -207,7 +205,8 @@ module PathTree
|
|
207
205
|
|
208
206
|
# Get all nodes that share the same parent as this node.
|
209
207
|
def siblings
|
210
|
-
|
208
|
+
# OPTIMIZE if this record has an ID, it may be more efficient to exclude it via query rather than using reject
|
209
|
+
self.class.base_class.where(parent_path: parent_path).to_a.reject{|node| node == self }
|
211
210
|
end
|
212
211
|
|
213
212
|
# Get all descendant of this node.
|
@@ -222,7 +221,7 @@ module PathTree
|
|
222
221
|
if ancestor_paths.empty?
|
223
222
|
[]
|
224
223
|
else
|
225
|
-
self.class.base_class.
|
224
|
+
self.class.base_class.where(path: ancestor_paths).to_a.sort{|a,b| a.path.length <=> b.path.length}
|
226
225
|
end
|
227
226
|
end
|
228
227
|
|
File without changes
|
data/lib/path_tree/version.rb
CHANGED
data/path_tree.gemspec
CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'path_tree/version'
|
5
5
|
Gem::Specification.new do |spec|
|
6
6
|
spec.name = 'path_tree'
|
7
|
-
spec.version = PathTree::VERSION
|
7
|
+
spec.version = PathTree::VERSION.dup # dup for ruby 1.9
|
8
8
|
spec.authors = ['Brian Durand', 'Milan Dobrota']
|
9
9
|
spec.email = ['mdobrota@tribpub.com']
|
10
10
|
spec.summary = 'Helper module for constructing tree data structures'
|
@@ -16,11 +16,12 @@ Gem::Specification.new do |spec|
|
|
16
16
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
17
17
|
spec.require_paths = ['lib']
|
18
18
|
|
19
|
-
spec.add_runtime_dependency 'activerecord', '
|
19
|
+
spec.add_runtime_dependency 'activerecord', '>= 3.2.0', '< 4.3'
|
20
20
|
|
21
|
-
spec.add_development_dependency 'rspec', '~> 2.
|
21
|
+
spec.add_development_dependency 'rspec', '~> 2.99'
|
22
22
|
spec.add_development_dependency 'sqlite3', '>= 0'
|
23
23
|
|
24
|
-
spec.add_development_dependency 'bundler', '~> 1.7'
|
25
|
-
spec.add_development_dependency 'rake', '~> 10.0'
|
24
|
+
spec.add_development_dependency 'bundler' , '~> 1.7'
|
25
|
+
spec.add_development_dependency 'rake' , '~> 10.0'
|
26
|
+
spec.add_development_dependency 'appraisal', '~> 2.0'
|
26
27
|
end
|
data/spec/path_tree_spec.rb
CHANGED
@@ -1,30 +1,9 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
3
|
require 'spec_helper'
|
4
|
+
require 'shared/path_tree_model_examples'
|
4
5
|
|
5
6
|
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
7
|
before :all do
|
29
8
|
PathTree::Test.create_tables
|
30
9
|
end
|
@@ -32,245 +11,83 @@ describe PathTree do
|
|
32
11
|
after :all do
|
33
12
|
PathTree::Test.drop_tables
|
34
13
|
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
14
|
|
117
|
-
|
118
|
-
|
119
|
-
node.descendants.should == [@child_a1, @child_a2, @grandchild]
|
120
|
-
end
|
15
|
+
context "validations" do
|
16
|
+
let(:new_rec) { PathTree::Test.new }
|
121
17
|
|
122
|
-
it "
|
123
|
-
|
124
|
-
|
18
|
+
it "is invalid without a name" do
|
19
|
+
expect(new_rec).not_to be_valid
|
20
|
+
expect(new_rec.errors[:name]).to include "can't be blank"
|
125
21
|
end
|
126
22
|
|
127
|
-
it "
|
128
|
-
|
129
|
-
|
23
|
+
it "is invalid without a node_path" do
|
24
|
+
new_rec.name = "I'm a leaf"
|
25
|
+
new_rec.node_path = ''
|
26
|
+
expect(new_rec).not_to be_valid
|
27
|
+
expect(new_rec.errors[:node_path]).to include "can't be blank"
|
130
28
|
end
|
131
29
|
|
132
|
-
it "
|
133
|
-
|
134
|
-
|
135
|
-
|
30
|
+
it "is invalid without a path" do
|
31
|
+
new_rec.name = "I'm a leaf"
|
32
|
+
new_rec.path = ''
|
33
|
+
expect(new_rec).not_to be_valid
|
34
|
+
expect(new_rec.errors[:path]).to include "can't be blank"
|
136
35
|
end
|
137
36
|
|
138
|
-
|
139
|
-
|
140
|
-
|
37
|
+
context "uniqueness" do
|
38
|
+
around :each do |ex|
|
39
|
+
PathTree::Test.transaction do
|
40
|
+
@root = PathTree::Test.create!(name: 'top-dog')
|
41
|
+
@child_1 = PathTree::Test.create!(name: 'underdog', parent: @root)
|
42
|
+
ex.run
|
43
|
+
raise ActiveRecord::Rollback
|
44
|
+
end
|
45
|
+
end
|
141
46
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
47
|
+
it "enforces unique path" do
|
48
|
+
new_rec = PathTree::Test.new(name: @child_1.node_path, parent: @root)
|
49
|
+
expect(new_rec).not_to be_valid
|
50
|
+
expect(new_rec.errors[:path]).to include 'has already been taken'
|
51
|
+
expect(new_rec.errors[:node_path]).to include 'has already been taken'
|
151
52
|
end
|
152
|
-
end
|
153
53
|
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
54
|
+
it "allows duplicate node_path under different parents" do
|
55
|
+
new_rec = PathTree::Test.new(name: @child_1.node_path)
|
56
|
+
expect(new_rec).to be_valid
|
163
57
|
end
|
164
58
|
end
|
165
59
|
end
|
166
|
-
|
167
60
|
|
168
|
-
context "
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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]
|
61
|
+
context "path construction" do
|
62
|
+
it "turns accented characters into ascii equivalents" do
|
63
|
+
accent_string = "ÀÂÄÃÅàâáããä-ÈÊÉËèêéë-ÌÎÍÏìîíï-ÒÔÖØÓÕòôöøóõ-ÚÜÙÛúüùû-ÝýÿÑñÇçÆæßÐ"
|
64
|
+
expected_string = "AAAAAaaaaaa-EEEEeeee-IIIIiiii-OOOOOOoooooo-UUUUuuuu-YyyNnCcAEaessD"
|
65
|
+
expect(PathTree::Test.asciify(accent_string)).to eq expected_string
|
231
66
|
end
|
232
67
|
|
233
|
-
it "
|
234
|
-
|
235
|
-
node.ancestors.should == [@root_1, @parent_a]
|
68
|
+
it "unquotes strings" do
|
69
|
+
expect(PathTree::Test.unquote(%Q("This is a 'test'"))).to eq "This is a test"
|
236
70
|
end
|
237
71
|
|
238
|
-
it "
|
239
|
-
|
240
|
-
node.node_path = "New Name"
|
241
|
-
node.path.should == "root-1/new-name"
|
72
|
+
it "translates a value to a path part" do
|
73
|
+
expect(PathTree::Test.pathify("This is the 1st / test À...")).to eq "this-is-the-1st-test-a"
|
242
74
|
end
|
243
75
|
|
244
|
-
it "
|
245
|
-
|
76
|
+
it "sets the parent path when setting the parent" do
|
77
|
+
parent = PathTree::Test.new(:name => "parent")
|
78
|
+
node = PathTree::Test.new(:name => "child")
|
79
|
+
node.parent = parent
|
80
|
+
expect(node.parent_path).to eq "parent"
|
81
|
+
node.parent = nil
|
82
|
+
expect(node.parent_path).to be nil
|
246
83
|
end
|
84
|
+
end
|
247
85
|
|
248
|
-
|
249
|
-
|
250
|
-
|
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
|
86
|
+
context "with default delimiter" do
|
87
|
+
include_examples "a PathTree model", nil, '/'
|
88
|
+
end
|
259
89
|
|
260
|
-
|
261
|
-
|
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
|
90
|
+
context "with custom delimiter" do
|
91
|
+
include_examples "a PathTree model", '/', ':'
|
275
92
|
end
|
276
93
|
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# path_delimiter is used for configuring the model; use nil for default
|
2
|
+
# fq_separator: separator for fully qualified name
|
3
|
+
shared_examples "a PathTree model" do |path_delimiter, fq_separator|
|
4
|
+
# actual_delim: the generated paths will be delimited by this
|
5
|
+
attr_reader :path_delim, :fq_sep, :actual_delim
|
6
|
+
|
7
|
+
before :all do
|
8
|
+
@path_delim = path_delimiter
|
9
|
+
@fq_sep = fq_separator or raise 'fq_separator must be set'
|
10
|
+
|
11
|
+
@actual_delim = path_delim || '.'.freeze
|
12
|
+
if $DEBUG
|
13
|
+
puts "OPTIONS: path_delim=#{path_delim.inspect}, fq_sep=#{fq_sep.inspect}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def delim_join(*strings)
|
18
|
+
strings.join(actual_delim)
|
19
|
+
end
|
20
|
+
def fq_join(*strings)
|
21
|
+
strings.join(fq_sep)
|
22
|
+
end
|
23
|
+
|
24
|
+
before :all do
|
25
|
+
PathTree::Test.path_delimiter = path_delim if path_delim
|
26
|
+
@root_1 = PathTree::Test.create!(:name => "Root 1")
|
27
|
+
@parent_a = PathTree::Test.create!(:name => "Parent A", :parent_path => "root-1")
|
28
|
+
@parent_b = PathTree::Test.create!(:name => "Parent B", :parent_path => "root-1")
|
29
|
+
@parent_c = PathTree::Test.create!(:name => "Parent C", :parent_path => "root-1")
|
30
|
+
@child_a1 = PathTree::Test.create!(:name => "Child A1", :parent_path => delim_join("root-1", "parent-a"))
|
31
|
+
@child_a2 = PathTree::Test.create!(:name => "Child A2", :parent_path => delim_join("root-1", "parent-a"))
|
32
|
+
@grandchild = PathTree::Test.create!(:name => "Grandchild A1.1", :parent_path => delim_join("root-1", "parent-a", "child-a1"))
|
33
|
+
@root_2 = PathTree::Test.create!(:name => "Root 2")
|
34
|
+
@parent_z = PathTree::Test.create!(:name => "Parent Z", :parent_path => "root-2")
|
35
|
+
end
|
36
|
+
|
37
|
+
after :all do
|
38
|
+
PathTree::Test.delete_all
|
39
|
+
PathTree::Test.path_delimiter = nil # reset to default
|
40
|
+
end
|
41
|
+
|
42
|
+
let(:unsaved_rec) { PathTree::Test.new(name: 'level-2', parent: @root_1) }
|
43
|
+
|
44
|
+
it "gets the root nodes" do
|
45
|
+
expect(PathTree::Test.roots).to match_array [@root_1, @root_2]
|
46
|
+
end
|
47
|
+
|
48
|
+
it "loads an entire branch structure" do
|
49
|
+
branch = PathTree::Test.branch(delim_join("root-1", "parent-a"))
|
50
|
+
expect(branch).to eq @parent_a
|
51
|
+
expect(branch.instance_variable_get(:@children)).to eq [@child_a1, @child_a2]
|
52
|
+
expect(branch.children.first.instance_variable_get(:@children)).to eq [@grandchild]
|
53
|
+
end
|
54
|
+
|
55
|
+
it "constructs a fully qualified name with a delimiter" do
|
56
|
+
expect(@grandchild.full_name).to eq "Root 1 > Parent A > Child A1 > Grandchild A1.1"
|
57
|
+
expect(@grandchild.full_name(:separator => fq_sep)).to eq(
|
58
|
+
fq_join("Root 1", "Parent A", "Child A1", "Grandchild A1.1") )
|
59
|
+
expect(@grandchild.full_name(:context => delim_join("root-1", "parent-a"))).to eq "Child A1 > Grandchild A1.1"
|
60
|
+
end
|
61
|
+
|
62
|
+
it "is able to get and set a parent node" do
|
63
|
+
node = PathTree::Test.find_by_path(delim_join("root-1", "parent-a"))
|
64
|
+
expect(node.parent).to eq @root_1
|
65
|
+
node.parent = @root_2
|
66
|
+
expect(node.parent_path).to eq "root-2"
|
67
|
+
expect(node.path).to eq delim_join("root-2", "parent-a")
|
68
|
+
end
|
69
|
+
|
70
|
+
it "is able to set the parent by path" do
|
71
|
+
node = PathTree::Test.find_by_path(delim_join("root-1", "parent-a"))
|
72
|
+
node.parent_path = "root-2"
|
73
|
+
expect(node.parent).to eq @root_2
|
74
|
+
expect(node.path).to eq delim_join("root-2", "parent-a")
|
75
|
+
end
|
76
|
+
|
77
|
+
it "has child nodes" do
|
78
|
+
node = PathTree::Test.find_by_path(delim_join("root-1", "parent-a"))
|
79
|
+
expect(node.children).to eq [@child_a1, @child_a2]
|
80
|
+
end
|
81
|
+
|
82
|
+
it "has descendant nodes" do
|
83
|
+
node = PathTree::Test.find_by_path(delim_join("root-1", "parent-a"))
|
84
|
+
expect(node.descendants).to eq [@child_a1, @child_a2, @grandchild]
|
85
|
+
end
|
86
|
+
|
87
|
+
it "has sibling nodes" do
|
88
|
+
node = PathTree::Test.find_by_path(delim_join("root-1", "parent-a"))
|
89
|
+
expect(node.siblings).to eq [@parent_b, @parent_c]
|
90
|
+
end
|
91
|
+
|
92
|
+
it "has ancestor nodes" do
|
93
|
+
node = PathTree::Test.find_by_path(delim_join("root-1", "parent-a", "child-a1"))
|
94
|
+
expect(node.ancestors).to eq [@root_1, @parent_a]
|
95
|
+
end
|
96
|
+
|
97
|
+
context "when name is changed" do
|
98
|
+
it "fills in a blank node_path when name is changed" do
|
99
|
+
unsaved_rec.node_path = ''
|
100
|
+
|
101
|
+
unsaved_rec.name = 'Changed Parent'
|
102
|
+
expect(unsaved_rec.node_path).to eq 'changed-parent'
|
103
|
+
expect(unsaved_rec.path).to eq delim_join('root-1', 'changed-parent')
|
104
|
+
end
|
105
|
+
|
106
|
+
it "does not overwrite a present node_path when name is changed" do
|
107
|
+
orig_node_path = unsaved_rec.node_path
|
108
|
+
unsaved_rec.name = 'Changed Parent'
|
109
|
+
expect(unsaved_rec.node_path).to eq orig_node_path
|
110
|
+
expect(unsaved_rec.path).to eq delim_join('root-1', orig_node_path)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
it "maintains the path with the node path" do
|
115
|
+
node = PathTree::Test.find_by_path(delim_join("root-1", "parent-a"))
|
116
|
+
node.node_path = "New Name"
|
117
|
+
expect(node.path).to eq delim_join("root-1", "new-name")
|
118
|
+
end
|
119
|
+
|
120
|
+
it "expands a path to its component paths" do
|
121
|
+
expect(PathTree::Test.expanded_paths(delim_join(*%w[this is a test]))).to eq [
|
122
|
+
"this",
|
123
|
+
delim_join(*%w[this is]),
|
124
|
+
delim_join(*%w[this is a]),
|
125
|
+
delim_join(*%w[this is a test]) ]
|
126
|
+
end
|
127
|
+
|
128
|
+
it "gets the expanded paths for a node" do
|
129
|
+
expect(@grandchild.expanded_paths).to eq [
|
130
|
+
"root-1",
|
131
|
+
delim_join("root-1", "parent-a"),
|
132
|
+
delim_join("root-1", "parent-a", "child-a1"),
|
133
|
+
delim_join("root-1", "parent-a", "child-a1", "grandchild-a1-1") ]
|
134
|
+
end
|
135
|
+
|
136
|
+
context "child paths" do
|
137
|
+
around :each do |ex|
|
138
|
+
PathTree::Test.transaction do
|
139
|
+
ex.run
|
140
|
+
raise ActiveRecord::Rollback
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
it "updates child paths when the path is changed" do
|
145
|
+
node = PathTree::Test.find_by_path(delim_join("root-1", "parent-a"))
|
146
|
+
node.node_path = "New Name"
|
147
|
+
node.save!
|
148
|
+
node.reload
|
149
|
+
expect(node.children.map(&:path)).to eq [
|
150
|
+
delim_join("root-1", "new-name", "child-a1"),
|
151
|
+
delim_join("root-1", "new-name", "child-a2") ]
|
152
|
+
expect(node.children.first.children.map(&:path)).to eq [
|
153
|
+
delim_join("root-1", "new-name", "child-a1", "grandchild-a1-1") ]
|
154
|
+
end
|
155
|
+
|
156
|
+
it "updates child paths when a node is destroyed" do
|
157
|
+
node = PathTree::Test.find_by_path(delim_join("root-1", "parent-a"))
|
158
|
+
node.name = "New Name"
|
159
|
+
node.destroy
|
160
|
+
root = PathTree::Test.find_by_path("root-1")
|
161
|
+
expect(root.children.map(&:path)).to eq [
|
162
|
+
delim_join("root-1", "parent-b"),
|
163
|
+
delim_join("root-1", "parent-c"),
|
164
|
+
delim_join("root-1", "child-a1"),
|
165
|
+
delim_join("root-1", "child-a2") ]
|
166
|
+
expect(root.children[2].children.map(&:path)).to eq [
|
167
|
+
delim_join("root-1", "child-a1", "grandchild-a1-1") ]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'rubygems'
|
2
1
|
require 'active_record'
|
3
2
|
require 'sqlite3'
|
4
3
|
|
@@ -14,3 +13,25 @@ end
|
|
14
13
|
ActiveRecord::Base.establish_connection("adapter" => "sqlite3", "database" => ":memory:")
|
15
14
|
|
16
15
|
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'path_tree'))
|
16
|
+
|
17
|
+
module PathTree
|
18
|
+
class Test < ActiveRecord::Base
|
19
|
+
self.table_name = :test_path_trees
|
20
|
+
|
21
|
+
def self.create_tables
|
22
|
+
connection.create_table(table_name) do |t|
|
23
|
+
t.string :name
|
24
|
+
t.string :node_path
|
25
|
+
t.string :path
|
26
|
+
t.string :parent_path
|
27
|
+
end unless table_exists?
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.drop_tables
|
31
|
+
connection.drop_table(table_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
# lib/path_tree must be loaded before this
|
35
|
+
include PathTree
|
36
|
+
end
|
37
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: path_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian Durand
|
@@ -9,36 +9,42 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-06-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- - "
|
18
|
+
- - ">="
|
19
19
|
- !ruby/object:Gem::Version
|
20
20
|
version: 3.2.0
|
21
|
+
- - "<"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '4.3'
|
21
24
|
type: :runtime
|
22
25
|
prerelease: false
|
23
26
|
version_requirements: !ruby/object:Gem::Requirement
|
24
27
|
requirements:
|
25
|
-
- - "
|
28
|
+
- - ">="
|
26
29
|
- !ruby/object:Gem::Version
|
27
30
|
version: 3.2.0
|
31
|
+
- - "<"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.3'
|
28
34
|
- !ruby/object:Gem::Dependency
|
29
35
|
name: rspec
|
30
36
|
requirement: !ruby/object:Gem::Requirement
|
31
37
|
requirements:
|
32
38
|
- - "~>"
|
33
39
|
- !ruby/object:Gem::Version
|
34
|
-
version: 2.
|
40
|
+
version: '2.99'
|
35
41
|
type: :development
|
36
42
|
prerelease: false
|
37
43
|
version_requirements: !ruby/object:Gem::Requirement
|
38
44
|
requirements:
|
39
45
|
- - "~>"
|
40
46
|
- !ruby/object:Gem::Version
|
41
|
-
version: 2.
|
47
|
+
version: '2.99'
|
42
48
|
- !ruby/object:Gem::Dependency
|
43
49
|
name: sqlite3
|
44
50
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,6 +87,20 @@ dependencies:
|
|
81
87
|
- - "~>"
|
82
88
|
- !ruby/object:Gem::Version
|
83
89
|
version: '10.0'
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: appraisal
|
92
|
+
requirement: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.0'
|
97
|
+
type: :development
|
98
|
+
prerelease: false
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.0'
|
84
104
|
description: Module that defines a tree data structure based on a path.
|
85
105
|
email:
|
86
106
|
- mdobrota@tribpub.com
|
@@ -89,16 +109,21 @@ extensions: []
|
|
89
109
|
extra_rdoc_files: []
|
90
110
|
files:
|
91
111
|
- ".gitignore"
|
112
|
+
- Appraisals
|
92
113
|
- Gemfile
|
93
114
|
- License.txt
|
94
115
|
- README.rdoc
|
95
116
|
- Rakefile
|
117
|
+
- gemfiles/rails_3.2.gemfile
|
118
|
+
- gemfiles/rails_4.0.gemfile
|
119
|
+
- gemfiles/rails_4.1.gemfile
|
120
|
+
- gemfiles/rails_4.2.gemfile
|
96
121
|
- lib/path_tree.rb
|
122
|
+
- lib/path_tree/patterns.rb
|
97
123
|
- lib/path_tree/version.rb
|
98
|
-
- lib/ruby_18_patterns.rb
|
99
|
-
- lib/ruby_19_patterns.rb
|
100
124
|
- path_tree.gemspec
|
101
125
|
- spec/path_tree_spec.rb
|
126
|
+
- spec/shared/path_tree_model_examples.rb
|
102
127
|
- spec/spec_helper.rb
|
103
128
|
homepage: ''
|
104
129
|
licenses: []
|
@@ -125,4 +150,5 @@ specification_version: 4
|
|
125
150
|
summary: Helper module for constructing tree data structures
|
126
151
|
test_files:
|
127
152
|
- spec/path_tree_spec.rb
|
153
|
+
- spec/shared/path_tree_model_examples.rb
|
128
154
|
- spec/spec_helper.rb
|
data/lib/ruby_18_patterns.rb
DELETED
@@ -1,24 +0,0 @@
|
|
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
|