forester 4.2.1 → 4.3.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 +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
|