forester 4.2.1 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +35 -0
- data/.travis.yml +10 -0
- data/README.md +2 -0
- data/Rakefile +10 -1
- data/forester.gemspec +8 -8
- data/lib/forester/node_content/dictionary.rb +1 -1
- data/lib/forester/node_content/factory.rb +13 -15
- data/lib/forester/tree_factory.rb +56 -43
- data/lib/forester/tree_node.rb +46 -7
- data/lib/forester/tree_node_ext/aggregators.rb +12 -2
- data/lib/forester/tree_node_ext/mutators.rb +17 -1
- data/lib/forester/tree_node_ext/views.rb +1 -1
- data/lib/forester/version.rb +2 -2
- data/test/minitest_helper.rb +41 -0
- data/test/test_ad_hoc_tree.rb +48 -0
- data/test/test_aggregators.rb +33 -22
- data/test/test_mutators.rb +41 -34
- data/test/test_tree_factory.rb +9 -12
- data/test/test_tree_node.rb +14 -17
- data/test/test_validators.rb +15 -20
- data/test/test_views.rb +4 -9
- metadata +28 -10
- data/test/simple_tree_helper.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 10d063cbc60a90d3e5ebe19867e2b47ae74f59fe
|
4
|
+
data.tar.gz: b2fb62970c91d0f75c18f01efb0f7e502da1e05e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53099d57189bde522ff5310b9532ba91f9338beec8f2676735ea837a1c0e23977f333ad177e15d6268981bb0ba24dd104d89b1d5e75ec1c660e0288db87a1cd5
|
7
|
+
data.tar.gz: 8a1e31896c0469536e4279e3095d573cb84f048e689f2693e70134dbaa0263720034cf97f4ceac72628e45720a10388dac085e133afb1292ffa3375a005fa648
|
data/.gitignore
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/test/tmp/
|
9
|
+
/test/version_tmp/
|
10
|
+
/tmp/
|
11
|
+
|
12
|
+
## Specific to RubyMotion:
|
13
|
+
.dat*
|
14
|
+
.repl_history
|
15
|
+
build/
|
16
|
+
|
17
|
+
## Documentation cache and generated files:
|
18
|
+
/.yardoc/
|
19
|
+
/_yardoc/
|
20
|
+
/doc/
|
21
|
+
/rdoc/
|
22
|
+
|
23
|
+
## Environment normalisation:
|
24
|
+
/.bundle/
|
25
|
+
/vendor/bundle
|
26
|
+
/lib/bundler/man/
|
27
|
+
|
28
|
+
# for a library or gem, you might want to ignore these files since the code is
|
29
|
+
# intended to run in multiple environments; otherwise, check them in:
|
30
|
+
Gemfile.lock
|
31
|
+
.ruby-version
|
32
|
+
.ruby-gemset
|
33
|
+
|
34
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
35
|
+
.rvmrc
|
data/.travis.yml
ADDED
data/README.md
CHANGED
data/Rakefile
CHANGED
@@ -1,8 +1,17 @@
|
|
1
1
|
require 'rake/testtask'
|
2
2
|
|
3
3
|
Rake::TestTask.new do |t|
|
4
|
+
t.libs << 'test'
|
4
5
|
t.pattern = "test/**/test_*.rb"
|
5
6
|
end
|
6
7
|
|
7
8
|
desc "Run tests"
|
8
|
-
task :
|
9
|
+
task default: :test
|
10
|
+
|
11
|
+
desc 'Start a REPL session'
|
12
|
+
task :console do
|
13
|
+
require 'forester'
|
14
|
+
require 'pry'
|
15
|
+
ARGV.clear
|
16
|
+
Pry.start
|
17
|
+
end
|
data/forester.gemspec
CHANGED
@@ -7,24 +7,24 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.name = 'forester'
|
8
8
|
s.version = Forester::Version
|
9
9
|
s.date = '2017-02-26'
|
10
|
-
s.summary = "A
|
11
|
-
s.description = "
|
12
|
-
s.authors =
|
10
|
+
s.summary = "A trees library"
|
11
|
+
s.description = "Forester is a collection of utilities to represent and interact with tree data structures."
|
12
|
+
s.authors = "Eugenio Bruno"
|
13
13
|
s.email = 'eugeniobruno@gmail.com'
|
14
14
|
s.homepage = 'https://github.com/eugeniobruno/forester'
|
15
15
|
s.license = 'MIT'
|
16
16
|
|
17
17
|
s.files = `git ls-files`.split($/)
|
18
|
-
s.executables = s.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
19
18
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
20
19
|
s.require_paths = ['lib']
|
21
20
|
|
22
21
|
s.required_ruby_version = '>= 2.0.0'
|
23
22
|
|
24
|
-
s.add_runtime_dependency 'rubytree',
|
23
|
+
s.add_runtime_dependency 'rubytree', '0.9.7'
|
25
24
|
|
26
|
-
s.add_development_dependency 'rake',
|
27
|
-
s.add_development_dependency 'minitest',
|
28
|
-
s.add_development_dependency '
|
25
|
+
s.add_development_dependency 'rake', '~> 12.0'
|
26
|
+
s.add_development_dependency 'minitest', '~> 5.10'
|
27
|
+
s.add_development_dependency 'simplecov', '~> 0.14'
|
28
|
+
s.add_development_dependency 'pry-byebug', '~> 3.4'
|
29
29
|
|
30
30
|
end
|
@@ -1,25 +1,23 @@
|
|
1
1
|
module Forester
|
2
2
|
module NodeContent
|
3
|
-
|
3
|
+
module Factory
|
4
4
|
|
5
|
-
|
5
|
+
extend self
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
def from_array(array)
|
14
|
-
List.new(array)
|
15
|
-
end
|
7
|
+
def from_hash(hash, children_key, indifferent = true)
|
8
|
+
ret = without_key(hash, children_key)
|
9
|
+
ret = Dictionary.new(ret) if indifferent
|
10
|
+
ret
|
11
|
+
end
|
16
12
|
|
17
|
-
|
13
|
+
def from_array(array)
|
14
|
+
List.new(array)
|
15
|
+
end
|
18
16
|
|
19
|
-
|
20
|
-
hash.reject { |k, _| k.to_s == key.to_s }
|
21
|
-
end
|
17
|
+
private
|
22
18
|
|
19
|
+
def without_key(hash, key)
|
20
|
+
hash.reject { |k, _| k.to_s == key.to_s }
|
23
21
|
end
|
24
22
|
|
25
23
|
end
|
@@ -1,62 +1,75 @@
|
|
1
1
|
module Forester
|
2
|
-
|
2
|
+
module TreeFactory
|
3
3
|
|
4
|
-
|
4
|
+
extend self
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
def node_from_hash(hash, options = {}, &block)
|
7
|
+
# TODO remove the whole node_content folder in next major version
|
8
|
+
# and let the user choose a class for the contents via a custom parser.
|
9
|
+
# That class should have the interface of the one used by the default parser.
|
10
|
+
# The default parser should be the constructor of Hash::Accessible, which
|
11
|
+
# is defined in the gem 'hash_ext'. Add it as a runtime dependency.
|
12
|
+
do_node_from_hash(hash, nil, options, &block)
|
13
|
+
end
|
9
14
|
|
10
|
-
|
11
|
-
|
12
|
-
|
15
|
+
def from_yaml_file(file, options = {})
|
16
|
+
from_hash_with_root_key(YAML.load_file(file), options)
|
17
|
+
end
|
13
18
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
children_key: :children,
|
18
|
-
root_key: :root
|
19
|
-
}
|
20
|
-
options = default_options.merge(options)
|
19
|
+
def from_root_hash(hash, options = {})
|
20
|
+
from_hash_with_root_key({ root: hash }, options)
|
21
|
+
end
|
21
22
|
|
22
|
-
|
23
|
-
|
23
|
+
def from_hash_with_root_key(hash, options = {})
|
24
|
+
default_options = {
|
25
|
+
max_level: :last,
|
26
|
+
children_key: :children,
|
27
|
+
root_key: :root
|
28
|
+
}
|
29
|
+
options = default_options.merge(options)
|
24
30
|
|
25
|
-
|
26
|
-
max_level = -2 if max_level == :last
|
31
|
+
options[:max_level] = -2 if options[:max_level] == :last
|
27
32
|
|
28
|
-
|
29
|
-
|
30
|
-
|
33
|
+
dummy_root = TreeNode.new('<TEMP>')
|
34
|
+
real_root = fetch_indifferently(hash, options[:root_key])
|
35
|
+
|
36
|
+
tree = with_children(dummy_root, [real_root], options[:children_key], options[:max_level] + 1).first_child
|
37
|
+
tree.detached_subtree_copy
|
38
|
+
end
|
31
39
|
|
32
|
-
|
40
|
+
private
|
33
41
|
|
34
|
-
|
35
|
-
|
36
|
-
|
42
|
+
def fetch_indifferently(hash, key, default = nil)
|
43
|
+
[key, key.to_s, key.to_s.to_sym].uniq.map { |k| hash[k] }.compact.first || default || hash.fetch(root_key)
|
44
|
+
end
|
37
45
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
46
|
+
def with_children(tree_node, children, children_key, levels_remaining)
|
47
|
+
return tree_node if levels_remaining == 0
|
48
|
+
children.each do |child_hash|
|
49
|
+
child_node = do_node_from_hash(child_hash, children_key)
|
50
|
+
child_children = fetch_indifferently(child_hash, children_key, [])
|
43
51
|
|
44
|
-
|
45
|
-
end
|
46
|
-
tree_node
|
52
|
+
tree_node << with_children(child_node, child_children, children_key, levels_remaining - 1)
|
47
53
|
end
|
54
|
+
tree_node
|
55
|
+
end
|
48
56
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
options = default_options.merge(options)
|
57
|
+
def do_node_from_hash(hash, children_key, options = {}, &block)
|
58
|
+
content = NodeContent::Factory.from_hash(hash, children_key)
|
59
|
+
node(content, options, &block)
|
60
|
+
end
|
54
61
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
62
|
+
def node(content, options = {})
|
63
|
+
default_options = {
|
64
|
+
uid: SecureRandom.uuid
|
65
|
+
}
|
66
|
+
options = default_options.merge(options)
|
67
|
+
|
68
|
+
name = options[:uid]
|
69
|
+
new_node = TreeNode.new(name, content)
|
59
70
|
|
71
|
+
yield new_node if block_given?
|
72
|
+
new_node
|
60
73
|
end
|
61
74
|
|
62
75
|
end
|
data/lib/forester/tree_node.rb
CHANGED
@@ -10,12 +10,46 @@ module Forester
|
|
10
10
|
include Views
|
11
11
|
|
12
12
|
alias_method :max_level, :node_height
|
13
|
-
alias_method :each_node, :breadth_each
|
14
13
|
|
15
14
|
def nodes_of_level(l)
|
16
15
|
l.between?(0, max_level) ? each_level.take(l + 1).last : []
|
17
16
|
end
|
18
17
|
|
18
|
+
def each_node(options = {})
|
19
|
+
default_options = {
|
20
|
+
traversal: :breadth_first
|
21
|
+
}
|
22
|
+
options = default_options.merge(options)
|
23
|
+
|
24
|
+
case options[:traversal]
|
25
|
+
when :breadth_first
|
26
|
+
breadth_each
|
27
|
+
when :depth_first
|
28
|
+
each
|
29
|
+
when :postorder
|
30
|
+
postordered_each
|
31
|
+
when :preorder
|
32
|
+
preordered_each
|
33
|
+
else
|
34
|
+
raise ArgumentError, "invalid traversal mode: #{options[:traversal]}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def each_content(options = {})
|
39
|
+
node_enumerator = each_node(options)
|
40
|
+
|
41
|
+
Enumerator.new do |yielder|
|
42
|
+
stop = false
|
43
|
+
until stop
|
44
|
+
begin
|
45
|
+
yielder << node_enumerator.next.content
|
46
|
+
rescue StopIteration
|
47
|
+
stop = true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
19
53
|
def each_level
|
20
54
|
Enumerator.new do |yielder|
|
21
55
|
level = [self]
|
@@ -29,11 +63,12 @@ module Forester
|
|
29
63
|
def get(field, options = {}, &if_missing)
|
30
64
|
default_options = {
|
31
65
|
default: :raise,
|
32
|
-
subtree: false
|
66
|
+
subtree: false, # if false, traversal is ignored
|
67
|
+
traversal: :depth_first
|
33
68
|
}
|
34
69
|
options = default_options.merge(options)
|
35
70
|
|
36
|
-
return own_and_descendants(field, &if_missing) if options[:subtree]
|
71
|
+
return own_and_descendants(field, { traversal: options[:traversal] }, &if_missing) if options[:subtree]
|
37
72
|
|
38
73
|
if has?(field)
|
39
74
|
content.get(field)
|
@@ -42,12 +77,12 @@ module Forester
|
|
42
77
|
elsif options[:default] != :raise
|
43
78
|
options[:default]
|
44
79
|
else
|
45
|
-
raise ArgumentError
|
80
|
+
raise ArgumentError, "the node \"#{best_name}\" does not have \"#{field}\""
|
46
81
|
end
|
47
82
|
end
|
48
83
|
|
49
|
-
def contents
|
50
|
-
each_node.map(&:content)
|
84
|
+
def contents(options = {})
|
85
|
+
each_node(options).map(&:content)
|
51
86
|
end
|
52
87
|
|
53
88
|
def same_as?(other)
|
@@ -61,7 +96,11 @@ module Forester
|
|
61
96
|
true
|
62
97
|
end
|
63
98
|
|
64
|
-
|
99
|
+
private
|
100
|
+
|
101
|
+
def best_name
|
102
|
+
get(:name, default: name)
|
103
|
+
end
|
65
104
|
|
66
105
|
def as_array(object)
|
67
106
|
[object].flatten(1)
|
@@ -1,10 +1,17 @@
|
|
1
1
|
module Forester
|
2
2
|
module Aggregators
|
3
3
|
|
4
|
-
def own_and_descendants(field, &if_missing)
|
4
|
+
def own_and_descendants(field, options = {}, &if_missing)
|
5
|
+
default_options = {
|
6
|
+
traversal: :depth_first
|
7
|
+
}
|
8
|
+
options = default_options.merge(options)
|
9
|
+
|
5
10
|
if_missing = -> (node) { [] } unless block_given?
|
6
11
|
|
7
|
-
flat_map
|
12
|
+
each_node(traversal: options[:traversal]).flat_map do |node|
|
13
|
+
as_array(node.get(field, &if_missing))
|
14
|
+
end
|
8
15
|
end
|
9
16
|
|
10
17
|
def nodes_with(field, values, options = {})
|
@@ -49,6 +56,9 @@ module Forester
|
|
49
56
|
found_nodes = nodes_with(options[:by_field], options[:keywords], single: options[:single_node] )
|
50
57
|
|
51
58
|
return found_nodes if options[:then_get] == :nodes
|
59
|
+
# TODO this method should never return [nil]. This happens when single_node
|
60
|
+
# is true and no matches are found. Moreover, if then_get is not :nodes,
|
61
|
+
# it should not raise. Both cases should return [].
|
52
62
|
|
53
63
|
found_nodes.flat_map do |node|
|
54
64
|
node.get(options[:then_get], subtree: options[:subtree])
|
@@ -1,8 +1,24 @@
|
|
1
1
|
module Forester
|
2
2
|
module Mutators
|
3
3
|
|
4
|
+
def change_parent_to!(new_parent_node, options = {})
|
5
|
+
default_options = {
|
6
|
+
subtree: true
|
7
|
+
}
|
8
|
+
options = default_options.merge(options)
|
9
|
+
|
10
|
+
children.each { |child| parent.add(child) } unless options[:subtree]
|
11
|
+
|
12
|
+
new_parent_node.add(self) # always as its last child
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_child_content!(content, options = {}, &block)
|
16
|
+
new_node = TreeFactory.node_from_hash(content, options, &block)
|
17
|
+
add(new_node)
|
18
|
+
end
|
19
|
+
|
4
20
|
def add_field!(name, definition, options = {})
|
5
|
-
add_fields!([
|
21
|
+
add_fields!([name: name, definition: definition], options)
|
6
22
|
end
|
7
23
|
|
8
24
|
def add_fields!(fields, options = {})
|
data/lib/forester/version.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
SimpleCov.start do
|
3
|
+
add_filter 'test/'
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'minitest/autorun'
|
7
|
+
|
8
|
+
require 'pry-byebug'
|
9
|
+
|
10
|
+
require 'forester'
|
11
|
+
|
12
|
+
class Forester::Test < Minitest::Test
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
PATH_TO_TREES = "#{File.dirname(__FILE__)}/trees"
|
17
|
+
PATH_TO_SIMPLE_TREE = "#{PATH_TO_TREES}/simple_tree.yml"
|
18
|
+
TREE = Forester::TreeFactory.from_yaml_file(PATH_TO_SIMPLE_TREE)
|
19
|
+
|
20
|
+
BINARY_TREE = Forester::TreeFactory.node_from_hash(name: :top) do |parent|
|
21
|
+
parent.add_child_content!(name: :left) do |left|
|
22
|
+
left.add_child_content!(name: :left_left) do |left_left|
|
23
|
+
left_left.add_child_content!(name: :left_left_left)
|
24
|
+
end
|
25
|
+
left.add_child_content!(name: :left_right)
|
26
|
+
end
|
27
|
+
parent.add_child_content!(name: :right) do |right|
|
28
|
+
right.add_child_content!(name: :right_left)
|
29
|
+
right.add_child_content!(name: :right_right)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def tree
|
34
|
+
TREE
|
35
|
+
end
|
36
|
+
|
37
|
+
def binary_tree
|
38
|
+
BINARY_TREE
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
class TestAdHocTree < Forester::Test
|
4
|
+
|
5
|
+
def test_content
|
6
|
+
assert_equal({ number: 1 }, Forester::TreeFactory.node_from_hash(number: 1).content)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_each_node_type
|
10
|
+
assert_instance_of Enumerator, binary_tree.each_node
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_each_content_type
|
14
|
+
assert_instance_of Enumerator, binary_tree.each_content
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_each_content_depth_first
|
18
|
+
expected = %i(top left left_left left_left_left left_right right right_left right_right)
|
19
|
+
assert_equal expected, binary_tree.get(:name, subtree: true, traversal: :depth_first)
|
20
|
+
assert_equal expected, binary_tree.each_node(traversal: :depth_first).map { |n| n.get(:name) }
|
21
|
+
assert_equal expected, binary_tree.each_content(traversal: :depth_first).map { |c| c[:name] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_each_content_breadth_first
|
25
|
+
expected = %i(top left right left_left left_right right_left right_right left_left_left)
|
26
|
+
|
27
|
+
assert_equal expected, binary_tree.get(:name, subtree: true, traversal: :breadth_first)
|
28
|
+
assert_equal expected, binary_tree.each_node(traversal: :breadth_first).map { |n| n.get(:name) }
|
29
|
+
assert_equal expected, binary_tree.each_node.map { |n| n.get(:name) }
|
30
|
+
assert_equal expected, binary_tree.each_content(traversal: :breadth_first).map { |c| c[:name] }
|
31
|
+
assert_equal expected, binary_tree.each_content.map { |c| c[:name] }
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_each_content_postorder
|
35
|
+
expected = %i(left_left_left left_left left_right left right_left right_right right top)
|
36
|
+
assert_equal expected, binary_tree.get(:name, subtree: true, traversal: :postorder)
|
37
|
+
assert_equal expected, binary_tree.each_node(traversal: :postorder).map { |n| n.get(:name) }
|
38
|
+
assert_equal expected, binary_tree.each_content(traversal: :postorder).map { |c| c[:name] }
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_each_content_preorder
|
42
|
+
expected = %i(top left left_left left_left_left left_right right right_left right_right)
|
43
|
+
assert_equal expected, binary_tree.get(:name, subtree: true, traversal: :preorder)
|
44
|
+
assert_equal expected, binary_tree.each_node(traversal: :preorder).map { |n| n.get(:name) }
|
45
|
+
assert_equal expected, binary_tree.each_content(traversal: :preorder).map { |c| c[:name] }
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/test/test_aggregators.rb
CHANGED
@@ -1,21 +1,29 @@
|
|
1
|
-
require '
|
2
|
-
require 'forester'
|
1
|
+
require 'minitest_helper'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
class TestAggregators < Minitest::Test
|
7
|
-
|
8
|
-
include SimpleTreeHelper
|
3
|
+
class TestAggregators < Forester::Test
|
9
4
|
|
10
5
|
def test_group_by_sibling_subtrees
|
11
|
-
|
12
6
|
expected = {
|
13
|
-
"First node of level 2" => [
|
14
|
-
|
15
|
-
|
7
|
+
"First node of level 2" => [
|
8
|
+
"Already in level 2",
|
9
|
+
"I want to be the very best",
|
10
|
+
"like no one ever was"
|
11
|
+
],
|
12
|
+
"Second node of level 2" => [
|
13
|
+
"I have a sibling to my left",
|
14
|
+
"She wants to catch them all"
|
15
|
+
],
|
16
|
+
"Third node of level 2" => [
|
17
|
+
"Reached level 3",
|
18
|
+
"It's dark",
|
19
|
+
"A hidden secret lies in the deepest leaves...",
|
20
|
+
"Just kidding.",
|
21
|
+
"Could forester handle trees with hundreds of levels?",
|
22
|
+
"Maybe."
|
23
|
+
]
|
16
24
|
}
|
17
25
|
|
18
|
-
actual =
|
26
|
+
actual = tree.group_by_sibling_subtrees(
|
19
27
|
level: 2,
|
20
28
|
aggregation_field: 'strings'
|
21
29
|
)
|
@@ -24,14 +32,13 @@ class TestAggregators < Minitest::Test
|
|
24
32
|
end
|
25
33
|
|
26
34
|
def test_group_by_sibling_subtrees_with_ancestry
|
27
|
-
|
28
35
|
expected = {
|
29
36
|
["First node of level 1", "First node of level 2"] => ["Already in level 2", "I want to be the very best", "like no one ever was"],
|
30
37
|
["First node of level 1", "Second node of level 2"] => ["I have a sibling to my left", "She wants to catch them all"],
|
31
38
|
["Second node of level 1", "Third node of level 2"] => ["Reached level 3", "It's dark", "A hidden secret lies in the deepest leaves...", "Just kidding.", "Could forester handle trees with hundreds of levels?", "Maybe."]
|
32
39
|
}
|
33
40
|
|
34
|
-
actual =
|
41
|
+
actual = tree.group_by_sibling_subtrees(
|
35
42
|
level: 2,
|
36
43
|
aggregation_field: 'strings',
|
37
44
|
ancestry_in_keys: true
|
@@ -41,9 +48,14 @@ class TestAggregators < Minitest::Test
|
|
41
48
|
end
|
42
49
|
|
43
50
|
def test_nodes_with
|
44
|
-
expected_names = [
|
45
|
-
|
46
|
-
|
51
|
+
expected_names = [
|
52
|
+
"A hidden secret lies in the deepest leaves...",
|
53
|
+
"Just kidding.",
|
54
|
+
"Could forester handle trees with hundreds of levels?",
|
55
|
+
"Maybe."
|
56
|
+
]
|
57
|
+
|
58
|
+
found_nodes = tree.nodes_with('name', 'Second node of level 3')
|
47
59
|
assert_equal 1, found_nodes.length
|
48
60
|
|
49
61
|
actual_names = found_nodes.flat_map do |node|
|
@@ -54,24 +66,23 @@ class TestAggregators < Minitest::Test
|
|
54
66
|
end
|
55
67
|
|
56
68
|
def test_search
|
57
|
-
|
58
69
|
expected = [7]
|
59
70
|
|
60
|
-
actual_1 =
|
71
|
+
actual_1 = tree.search({
|
61
72
|
by_field: 'name',
|
62
73
|
keywords: 'Second node of level 3',
|
63
74
|
then_get: 'value',
|
64
75
|
subtree: false
|
65
76
|
})
|
66
77
|
|
67
|
-
actual_2 =
|
78
|
+
actual_2 = tree.search({
|
68
79
|
by_field: 'name',
|
69
80
|
keywords: ['Second node of level 3', 'Not present name'],
|
70
81
|
then_get: 'value',
|
71
82
|
subtree: false
|
72
83
|
})
|
73
84
|
|
74
|
-
actual_3 =
|
85
|
+
actual_3 = tree.search({
|
75
86
|
by_field: 'strings',
|
76
87
|
keywords: 'A hidden secret lies in the deepest leaves...',
|
77
88
|
then_get: 'value',
|
@@ -82,7 +93,7 @@ class TestAggregators < Minitest::Test
|
|
82
93
|
assert_equal expected, actual_3
|
83
94
|
|
84
95
|
expected_values = [7, 8, 9]
|
85
|
-
actual_values =
|
96
|
+
actual_values = tree.search({
|
86
97
|
by_field: 'name',
|
87
98
|
keywords: 'Second node of level 3',
|
88
99
|
then_get: 'value',
|
data/test/test_mutators.rb
CHANGED
@@ -1,89 +1,96 @@
|
|
1
|
-
require '
|
2
|
-
require 'forester'
|
1
|
+
require 'minitest_helper'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
class TestMutators < Minitest::Test
|
7
|
-
|
8
|
-
def setup
|
9
|
-
path_to_trees = "#{File.dirname(__FILE__)}/trees"
|
10
|
-
path_to_simple_tree = "#{path_to_trees}/simple_tree.yml"
|
11
|
-
@tree = Forester::TreeFactory.from_yaml_file(path_to_simple_tree)
|
12
|
-
end
|
3
|
+
class TestMutators < Forester::Test
|
13
4
|
|
14
5
|
def test_add_field
|
6
|
+
tree.add_field!('number_four', 4)
|
15
7
|
|
16
|
-
|
8
|
+
assert_equal 4, tree.get(:number_four)
|
9
|
+
assert_equal 4, tree.get('number_four')
|
17
10
|
|
18
|
-
|
19
|
-
assert_equal 4, @tree.get('number_four')
|
11
|
+
tree.add_field!(:number_five, 5)
|
20
12
|
|
21
|
-
|
22
|
-
|
23
|
-
assert_equal 5, @tree.get(:number_five)
|
24
|
-
assert_equal 5, @tree.get('number_five')
|
13
|
+
assert_equal 5, tree.get(:number_five)
|
14
|
+
assert_equal 5, tree.get('number_five')
|
25
15
|
|
26
16
|
number_one = 1
|
27
17
|
|
28
|
-
|
29
|
-
|
30
|
-
assert_equal 6, @tree.get(:number_six)
|
18
|
+
tree.add_field!(:number_six, -> (node) { node.get(:number_five) + number_one })
|
31
19
|
|
20
|
+
assert_equal 6, tree.get(:number_six)
|
32
21
|
end
|
33
22
|
|
34
23
|
def test_delete_values
|
35
|
-
|
36
24
|
node_1, node_2, node_3 = nodes_with_tags
|
37
25
|
|
38
|
-
|
26
|
+
tree.delete_values!(:tags, [])
|
39
27
|
assert_equal ['First tag', 'Second tag', 'Third tag'], node_1.get(:tags)
|
40
28
|
assert_equal ['Second tag', 'Third tag'], node_2.get(:tags)
|
41
29
|
assert_equal ['Third tag'], node_3.get(:tags)
|
42
30
|
|
43
|
-
|
31
|
+
tree.delete_values!(:tags, ['First tag'])
|
44
32
|
assert_equal ['Second tag', 'Third tag'], node_1.get(:tags)
|
45
33
|
assert_equal ['Second tag', 'Third tag'], node_2.get(:tags)
|
46
34
|
assert_equal ['Third tag'], node_3.get(:tags)
|
47
35
|
|
48
|
-
|
36
|
+
tree.delete_values!(:tags, ['First tag', 'Second tag', 'Third tag'])
|
49
37
|
assert_equal [], node_1.get(:tags)
|
50
38
|
assert_equal [], node_2.get(:tags)
|
51
39
|
assert_equal [], node_3.get(:tags)
|
52
|
-
|
53
40
|
end
|
54
41
|
|
55
42
|
def test_percolate_values
|
56
|
-
|
57
43
|
node_1, node_2, node_3 = nodes_with_tags
|
58
44
|
|
59
|
-
|
45
|
+
tree.percolate_values!(:tags, ['First tag', 'Second tag', 'Third tag'])
|
60
46
|
assert_equal ['First tag', 'Second tag', 'Third tag'], node_1.get(:tags)
|
61
47
|
assert_equal ['Second tag', 'Third tag'], node_2.get(:tags)
|
62
48
|
assert_equal ['Third tag'], node_3.get(:tags)
|
63
49
|
|
64
|
-
|
50
|
+
tree.percolate_values!(:tags, ['First tag'])
|
65
51
|
assert_equal ['First tag'], node_1.get(:tags)
|
66
52
|
assert_equal [], node_2.get(:tags)
|
67
53
|
assert_equal [], node_3.get(:tags)
|
68
54
|
|
69
|
-
|
55
|
+
tree.percolate_values!(:tags, [])
|
70
56
|
assert_equal [], node_1.get(:tags)
|
71
57
|
assert_equal [], node_2.get(:tags)
|
72
58
|
assert_equal [], node_3.get(:tags)
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_change_parent_to
|
62
|
+
node_to_move = binary_tree.search(single_node: true, by_field: :name, keywords: [:left_left]).first
|
63
|
+
old_parent = binary_tree.search(single_node: true, by_field: :name, keywords: [:left]).first
|
64
|
+
new_parent = binary_tree.search(single_node: true, by_field: :name, keywords: [:right]).first
|
73
65
|
|
66
|
+
node_to_move.change_parent_to!(new_parent)
|
67
|
+
|
68
|
+
expected = %i(top left left_right right right_left right_right left_left left_left_left)
|
69
|
+
assert_equal expected, binary_tree.get(:name, subtree: true, traversal: :depth_first)
|
70
|
+
|
71
|
+
node_to_move.change_parent_to!(old_parent, subtree: false)
|
72
|
+
|
73
|
+
expected = %i(top left left_right left_left right right_left right_right left_left_left)
|
74
|
+
assert_equal expected, binary_tree.get(:name, subtree: true, traversal: :depth_first)
|
74
75
|
end
|
75
76
|
|
76
|
-
|
77
|
+
private
|
78
|
+
|
79
|
+
def tree
|
80
|
+
@mutable_tree ||= super.detached_subtree_copy
|
81
|
+
end
|
82
|
+
|
83
|
+
def binary_tree
|
84
|
+
@mutable_binary_tree ||= super.detached_subtree_copy
|
85
|
+
end
|
77
86
|
|
78
87
|
def nodes_with_tags
|
79
88
|
[1, 6, 9].map do |n|
|
80
|
-
|
81
|
-
@tree.search({
|
89
|
+
tree.search({
|
82
90
|
single_node: true,
|
83
91
|
by_field: :value,
|
84
92
|
keywords: n
|
85
93
|
}).first
|
86
|
-
|
87
94
|
end
|
88
95
|
end
|
89
96
|
|
data/test/test_tree_factory.rb
CHANGED
@@ -1,20 +1,17 @@
|
|
1
|
-
require '
|
2
|
-
require 'forester'
|
1
|
+
require 'minitest_helper'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
class TestTreeFactory < Minitest::Test
|
7
|
-
|
8
|
-
include SimpleTreeHelper
|
3
|
+
class TestTreeFactory < Forester::Test
|
9
4
|
|
10
5
|
def test_from_root_hash
|
11
6
|
hash = YAML.load_file(PATH_TO_SIMPLE_TREE)
|
12
7
|
|
13
|
-
|
8
|
+
whole_tree = from_root_hash(hash)
|
9
|
+
|
10
|
+
whole_trees = [whole_tree] + [29, 4].map { |ml| from_root_hash(hash, max_level: ml) }
|
14
11
|
|
15
12
|
assert(whole_trees.product(whole_trees).all? { |t1, t2| t1.same_as?(t2) })
|
16
13
|
|
17
|
-
pruned_trees = (0..2).map { |ml|
|
14
|
+
pruned_trees = (0..2).map { |ml| from_root_hash(hash, max_level: ml) }
|
18
15
|
|
19
16
|
pruned_trees.each_with_index do |t, i|
|
20
17
|
assert_equal(i, t.max_level)
|
@@ -24,10 +21,10 @@ class TestTreeFactory < Minitest::Test
|
|
24
21
|
end
|
25
22
|
end
|
26
23
|
|
27
|
-
|
24
|
+
private
|
28
25
|
|
29
|
-
def
|
30
|
-
Forester::TreeFactory.from_root_hash(hash['root'],
|
26
|
+
def from_root_hash(hash, options = {})
|
27
|
+
Forester::TreeFactory.from_root_hash(hash['root'], options)
|
31
28
|
end
|
32
29
|
|
33
30
|
end
|
data/test/test_tree_node.rb
CHANGED
@@ -1,34 +1,31 @@
|
|
1
|
-
require '
|
2
|
-
require 'forester'
|
1
|
+
require 'minitest_helper'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
class TestTreeNode < Minitest::Test
|
7
|
-
|
8
|
-
include SimpleTreeHelper
|
3
|
+
class TestTreeNode < Forester::Test
|
9
4
|
|
10
5
|
def test_size
|
11
|
-
assert_equal 10,
|
6
|
+
assert_equal 10, tree.size
|
12
7
|
end
|
13
8
|
|
14
9
|
def test_values
|
15
10
|
values = (0..9).to_a
|
16
11
|
|
17
12
|
expected = values.reduce(:+)
|
18
|
-
actual =
|
13
|
+
actual = tree.reduce(0) { |acum, node| acum + node.get('value') }
|
19
14
|
|
20
15
|
assert_equal expected, actual
|
21
16
|
|
22
|
-
assert_equal values,
|
17
|
+
assert_equal values, tree.get('value', subtree: true)
|
23
18
|
end
|
24
19
|
|
25
20
|
def test_missing_values
|
26
|
-
assert_equal 0,
|
27
|
-
assert_equal 'no',
|
28
|
-
assert_equal 'no',
|
29
|
-
assert_equal 'no',
|
30
|
-
assert_equal 'no',
|
31
|
-
assert_equal 1,
|
21
|
+
assert_equal 0, tree.get('value')
|
22
|
+
assert_equal 'no', tree.get('whatever', default: 'no')
|
23
|
+
assert_equal 'no', tree.get('whatever', default: 'missing') { 'no' }
|
24
|
+
assert_equal 'no', tree.get('whatever') { 'no' }
|
25
|
+
assert_equal 'no', tree.get('whatever') { |n| 'no' }
|
26
|
+
assert_equal 1, tree.get('whatever') { |n| n.get('value') + 1 }
|
27
|
+
|
28
|
+
assert_raises(ArgumentError) { tree.get('whatever') }
|
32
29
|
end
|
33
30
|
|
34
31
|
def test_levels
|
@@ -39,7 +36,7 @@ class TestTreeNode < Minitest::Test
|
|
39
36
|
["First node of level 3", "Second node of level 3"],
|
40
37
|
["First node of level 4", "Second node of level 4"]
|
41
38
|
]
|
42
|
-
actual =
|
39
|
+
actual = tree.each_level.map { |l| l.map { |n| n.get('name') } }.to_a
|
43
40
|
|
44
41
|
assert_equal expected, actual
|
45
42
|
end
|
data/test/test_validators.rb
CHANGED
@@ -1,11 +1,6 @@
|
|
1
|
-
require '
|
2
|
-
require 'forester'
|
1
|
+
require 'minitest_helper'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
class TestValidators < Minitest::Test
|
7
|
-
|
8
|
-
include SimpleTreeHelper
|
3
|
+
class TestValidators < Forester::Test
|
9
4
|
|
10
5
|
def test_validate_uniqueness_of_field_uniques
|
11
6
|
expected = {
|
@@ -15,7 +10,7 @@ class TestValidators < Minitest::Test
|
|
15
10
|
}
|
16
11
|
|
17
12
|
['name', :name, 'special', 'ghost'].each do |field|
|
18
|
-
actual =
|
13
|
+
actual = tree.validate_uniqueness_of_field(field)
|
19
14
|
assert_equal expected, actual
|
20
15
|
end
|
21
16
|
|
@@ -35,7 +30,7 @@ class TestValidators < Minitest::Test
|
|
35
30
|
}
|
36
31
|
}
|
37
32
|
|
38
|
-
actual =
|
33
|
+
actual = tree.validate_uniqueness_of_field(:color)
|
39
34
|
assert_equal expected, actual
|
40
35
|
end
|
41
36
|
|
@@ -52,7 +47,7 @@ class TestValidators < Minitest::Test
|
|
52
47
|
}
|
53
48
|
}
|
54
49
|
|
55
|
-
actual =
|
50
|
+
actual = tree.validate_uniqueness_of_field(:color, {
|
56
51
|
first_failure_only: true
|
57
52
|
})
|
58
53
|
assert_equal expected, actual
|
@@ -71,7 +66,7 @@ class TestValidators < Minitest::Test
|
|
71
66
|
}
|
72
67
|
}
|
73
68
|
|
74
|
-
actual =
|
69
|
+
actual = tree.validate_uniqueness_of_field(:color, {
|
75
70
|
among_siblings_of_level: 1
|
76
71
|
})
|
77
72
|
|
@@ -85,7 +80,7 @@ class TestValidators < Minitest::Test
|
|
85
80
|
failures: {}
|
86
81
|
}
|
87
82
|
|
88
|
-
actual =
|
83
|
+
actual = tree.validate_uniqueness_of_field(:color, {
|
89
84
|
among_siblings_of_level: 2
|
90
85
|
})
|
91
86
|
|
@@ -105,7 +100,7 @@ class TestValidators < Minitest::Test
|
|
105
100
|
}
|
106
101
|
}
|
107
102
|
|
108
|
-
actual =
|
103
|
+
actual = tree.validate_uniqueness_of_field(:color, {
|
109
104
|
within_subtrees_of_level: 1,
|
110
105
|
})
|
111
106
|
|
@@ -125,7 +120,7 @@ class TestValidators < Minitest::Test
|
|
125
120
|
}
|
126
121
|
}
|
127
122
|
|
128
|
-
actual =
|
123
|
+
actual = tree.validate_uniqueness_of_field(:color, {
|
129
124
|
within_subtrees_of_level: 3,
|
130
125
|
})
|
131
126
|
|
@@ -139,7 +134,7 @@ class TestValidators < Minitest::Test
|
|
139
134
|
failures: {}
|
140
135
|
}
|
141
136
|
|
142
|
-
actual =
|
137
|
+
actual = tree.validate_uniqueness_of_field(:color, {
|
143
138
|
within_subtrees_of_level: 4,
|
144
139
|
})
|
145
140
|
|
@@ -160,7 +155,7 @@ class TestValidators < Minitest::Test
|
|
160
155
|
}
|
161
156
|
}
|
162
157
|
|
163
|
-
actual =
|
158
|
+
actual = tree.validate_uniqueness_of_fields(['name', 'color'])
|
164
159
|
|
165
160
|
assert_equal expected, actual
|
166
161
|
end
|
@@ -172,7 +167,7 @@ class TestValidators < Minitest::Test
|
|
172
167
|
failures: {}
|
173
168
|
}
|
174
169
|
|
175
|
-
actual =
|
170
|
+
actual = tree.validate_uniqueness_of_fields(['name', 'color'], {
|
176
171
|
combination: true
|
177
172
|
})
|
178
173
|
|
@@ -192,7 +187,7 @@ class TestValidators < Minitest::Test
|
|
192
187
|
}
|
193
188
|
}
|
194
189
|
|
195
|
-
actual =
|
190
|
+
actual = tree.validate_uniqueness_of_fields(['color', 'tone'], {
|
196
191
|
first_failure_only: true
|
197
192
|
})
|
198
193
|
|
@@ -217,7 +212,7 @@ class TestValidators < Minitest::Test
|
|
217
212
|
}
|
218
213
|
}
|
219
214
|
|
220
|
-
actual =
|
215
|
+
actual = tree.validate_uniqueness_of_fields(['color', 'tone'])
|
221
216
|
|
222
217
|
assert_equal expected, actual
|
223
218
|
end
|
@@ -235,7 +230,7 @@ class TestValidators < Minitest::Test
|
|
235
230
|
}
|
236
231
|
}
|
237
232
|
|
238
|
-
actual =
|
233
|
+
actual = tree.validate_uniqueness_of_fields(['color', 'tone'], {
|
239
234
|
combination: true
|
240
235
|
})
|
241
236
|
|
data/test/test_views.rb
CHANGED
@@ -1,18 +1,13 @@
|
|
1
|
-
require '
|
2
|
-
require 'forester'
|
1
|
+
require 'minitest_helper'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
class TestViews < Minitest::Test
|
7
|
-
|
8
|
-
include SimpleTreeHelper
|
3
|
+
class TestViews < Forester::Test
|
9
4
|
|
10
5
|
def test_as_root_hash
|
11
|
-
hash =
|
6
|
+
hash = YAML.load_file(PATH_TO_SIMPLE_TREE)
|
12
7
|
add_empty_children_keys(hash['root'])
|
13
8
|
|
14
9
|
expected = hash['root']
|
15
|
-
actual =
|
10
|
+
actual = tree.as_root_hash(stringify_keys: true)
|
16
11
|
|
17
12
|
assert_equal expected, actual
|
18
13
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: forester
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eugenio Bruno
|
@@ -30,28 +30,42 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ~>
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '12.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ~>
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '12.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: minitest
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ~>
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '5.
|
47
|
+
version: '5.10'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '5.
|
54
|
+
version: '5.10'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: simplecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.14'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.14'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: pry-byebug
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,13 +80,15 @@ dependencies:
|
|
66
80
|
- - ~>
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '3.4'
|
69
|
-
description:
|
70
|
-
|
83
|
+
description: Forester is a collection of utilities to represent and interact with
|
84
|
+
tree data structures.
|
71
85
|
email: eugeniobruno@gmail.com
|
72
86
|
executables: []
|
73
87
|
extensions: []
|
74
88
|
extra_rdoc_files: []
|
75
89
|
files:
|
90
|
+
- .gitignore
|
91
|
+
- .travis.yml
|
76
92
|
- Gemfile
|
77
93
|
- LICENSE
|
78
94
|
- README.md
|
@@ -90,7 +106,8 @@ files:
|
|
90
106
|
- lib/forester/tree_node_ext/validators.rb
|
91
107
|
- lib/forester/tree_node_ext/views.rb
|
92
108
|
- lib/forester/version.rb
|
93
|
-
- test/
|
109
|
+
- test/minitest_helper.rb
|
110
|
+
- test/test_ad_hoc_tree.rb
|
94
111
|
- test/test_aggregators.rb
|
95
112
|
- test/test_mutators.rb
|
96
113
|
- test/test_tree_factory.rb
|
@@ -121,9 +138,10 @@ rubyforge_project:
|
|
121
138
|
rubygems_version: 2.4.8
|
122
139
|
signing_key:
|
123
140
|
specification_version: 4
|
124
|
-
summary: A
|
141
|
+
summary: A trees library
|
125
142
|
test_files:
|
126
|
-
- test/
|
143
|
+
- test/minitest_helper.rb
|
144
|
+
- test/test_ad_hoc_tree.rb
|
127
145
|
- test/test_aggregators.rb
|
128
146
|
- test/test_mutators.rb
|
129
147
|
- test/test_tree_factory.rb
|