forester 4.3.0 → 5.0.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 +16 -1
- data/.travis.yml +4 -5
- data/CHANGELOG.md +14 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +1 -1
- data/{LICENSE → LICENSE.txt} +5 -5
- data/README.md +60 -24
- data/Rakefile +4 -4
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/forester.gemspec +28 -20
- data/lib/forester.rb +13 -11
- data/lib/forester/tree_factory.rb +18 -49
- data/lib/forester/tree_node.rb +68 -80
- data/lib/forester/tree_node_ext/iterators.rb +57 -0
- data/lib/forester/tree_node_ext/mutators.rb +36 -43
- data/lib/forester/tree_node_ext/serializers.rb +48 -0
- data/lib/forester/tree_node_ext/validators.rb +21 -24
- data/lib/forester/version.rb +1 -15
- metadata +78 -50
- data/lib/forester/node_content/base.rb +0 -9
- data/lib/forester/node_content/dictionary.rb +0 -91
- data/lib/forester/node_content/factory.rb +0 -25
- data/lib/forester/node_content/list.rb +0 -31
- data/lib/forester/tree_node_ext/aggregators.rb +0 -101
- data/lib/forester/tree_node_ext/views.rb +0 -34
- data/test/minitest_helper.rb +0 -41
- data/test/test_ad_hoc_tree.rb +0 -48
- data/test/test_aggregators.rb +0 -105
- data/test/test_mutators.rb +0 -97
- data/test/test_tree_factory.rb +0 -30
- data/test/test_tree_node.rb +0 -44
- data/test/test_validators.rb +0 -240
- data/test/test_views.rb +0 -22
- data/test/trees/simple_tree.yml +0 -61
data/lib/forester/tree_node.rb
CHANGED
@@ -1,110 +1,98 @@
|
|
1
1
|
module Forester
|
2
2
|
class TreeNode < Tree::TreeNode
|
3
|
-
|
4
|
-
extend Forwardable
|
5
|
-
def_delegators :@content, :fields, :has?, :put!, :add!, :del!
|
6
|
-
|
7
|
-
include Aggregators
|
8
|
-
include Validators
|
3
|
+
include Iterators
|
9
4
|
include Mutators
|
10
|
-
include
|
5
|
+
include Validators
|
6
|
+
include Serializers
|
11
7
|
|
12
|
-
|
8
|
+
def node_level
|
9
|
+
node_depth + 1
|
10
|
+
end
|
11
|
+
|
12
|
+
def nodes_of_depth(d) # relative to this node
|
13
|
+
d.between?(0, node_height) ? each_level.take(d + 1).last : []
|
14
|
+
end
|
13
15
|
|
14
16
|
def nodes_of_level(l)
|
15
|
-
|
16
|
-
end
|
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
|
17
|
+
nodes_of_depth(l - 1)
|
36
18
|
end
|
37
19
|
|
38
|
-
def
|
39
|
-
|
20
|
+
def path_from_root
|
21
|
+
(parentage || []).reverse + [self]
|
22
|
+
end
|
40
23
|
|
41
|
-
|
42
|
-
|
43
|
-
until stop
|
44
|
-
begin
|
45
|
-
yielder << node_enumerator.next.content
|
46
|
-
rescue StopIteration
|
47
|
-
stop = true
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
24
|
+
def paths_to_leaves
|
25
|
+
paths_to(leaves)
|
51
26
|
end
|
52
27
|
|
53
|
-
def
|
54
|
-
|
55
|
-
level = [self]
|
56
|
-
until level.empty?
|
57
|
-
yielder << level
|
58
|
-
level = level.flat_map(&:children)
|
59
|
-
end
|
60
|
-
end
|
28
|
+
def paths_to(descendants)
|
29
|
+
descendants.map { |node| node.path_from_root.drop(node_depth) }
|
61
30
|
end
|
62
31
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
subtree: false, # if false, traversal is ignored
|
67
|
-
traversal: :depth_first
|
68
|
-
}
|
69
|
-
options = default_options.merge(options)
|
32
|
+
def leaf?
|
33
|
+
is_leaf?
|
34
|
+
end
|
70
35
|
|
71
|
-
|
36
|
+
def leaves
|
37
|
+
each_leaf
|
38
|
+
end
|
72
39
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
40
|
+
def paths_of_length(l)
|
41
|
+
paths_to(nodes_of_depth(l))
|
42
|
+
end
|
43
|
+
|
44
|
+
def leaves_when_pruned_to_depth(d)
|
45
|
+
ret = []
|
46
|
+
each_node(traversal: :breadth_first) do |node|
|
47
|
+
relative_depth_of_descendant = node.node_depth - node_depth
|
48
|
+
break if relative_depth_of_descendant > d
|
49
|
+
ret.push(node) if node.leaf? || (relative_depth_of_descendant == d)
|
81
50
|
end
|
51
|
+
|
52
|
+
ret
|
82
53
|
end
|
83
54
|
|
84
|
-
def
|
85
|
-
|
55
|
+
def leaves_when_pruned_to_level(l)
|
56
|
+
leaves_when_pruned_to_depth(l - 1)
|
86
57
|
end
|
87
58
|
|
88
|
-
def
|
89
|
-
|
90
|
-
return false unless size == other.size
|
91
|
-
nodes_of_other = other.each_node.to_a
|
92
|
-
each_node.with_index do |n, i|
|
93
|
-
next if i == 0
|
94
|
-
return false unless n.same_as?(nodes_of_other[i])
|
95
|
-
end
|
96
|
-
true
|
59
|
+
def paths_to_leaves_when_pruned_to_depth(d)
|
60
|
+
paths_to(leaves_when_pruned_to_depth(d))
|
97
61
|
end
|
98
62
|
|
99
|
-
|
63
|
+
def paths_to_leaves_when_pruned_to_level(l)
|
64
|
+
paths_to(leaves_when_pruned_to_level(l))
|
65
|
+
end
|
66
|
+
|
67
|
+
def get(field, default = :raise)
|
68
|
+
if has_field?(field)
|
69
|
+
content[field]
|
70
|
+
elsif block_given?
|
71
|
+
yield(field, self)
|
72
|
+
elsif default != :raise
|
73
|
+
default
|
74
|
+
else
|
75
|
+
missing_key =
|
76
|
+
if field.is_a?(Symbol)
|
77
|
+
":#{field}"
|
78
|
+
elsif field.is_a?(String)
|
79
|
+
"'#{field}'"
|
80
|
+
else
|
81
|
+
field
|
82
|
+
end
|
83
|
+
error_message = "key not found: #{missing_key} in node content \"#{content}\""
|
84
|
+
raise KeyError, error_message
|
85
|
+
end
|
86
|
+
end
|
100
87
|
|
101
|
-
def
|
102
|
-
|
88
|
+
def has_field?(field)
|
89
|
+
content.key?(field)
|
103
90
|
end
|
104
91
|
|
92
|
+
private
|
93
|
+
|
105
94
|
def as_array(object)
|
106
95
|
[object].flatten(1)
|
107
96
|
end
|
108
|
-
|
109
97
|
end
|
110
98
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Forester
|
2
|
+
module Iterators
|
3
|
+
TRAVERSAL_MODES = {
|
4
|
+
depth_first: :each,
|
5
|
+
breadth_first: :breadth_each,
|
6
|
+
preorder: :preordered_each,
|
7
|
+
postorder: :postordered_each
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
def each_node(options = {}, &block)
|
11
|
+
default_options = {
|
12
|
+
traversal: :depth_first
|
13
|
+
}
|
14
|
+
options = default_options.merge(options)
|
15
|
+
|
16
|
+
method_name = traversal_modes[options[:traversal]]
|
17
|
+
|
18
|
+
if method_name
|
19
|
+
send(method_name, &block)
|
20
|
+
else
|
21
|
+
available = traversal_modes.keys.join(', ')
|
22
|
+
raise ArgumentError, "invalid traversal mode: #{options[:traversal]} (#{available})"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_content(options = {})
|
27
|
+
node_enumerator = each_node(options)
|
28
|
+
|
29
|
+
Enumerator.new do |yielder|
|
30
|
+
stop = false
|
31
|
+
until stop
|
32
|
+
begin
|
33
|
+
yielder << node_enumerator.next.content
|
34
|
+
rescue StopIteration
|
35
|
+
stop = true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def each_level
|
42
|
+
Enumerator.new do |yielder|
|
43
|
+
level = [self]
|
44
|
+
until level.empty?
|
45
|
+
yielder << level
|
46
|
+
level = level.flat_map(&:children)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def traversal_modes
|
54
|
+
TRAVERSAL_MODES
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module Forester
|
2
2
|
module Mutators
|
3
|
-
|
4
|
-
def change_parent_to!(new_parent_node, options = {})
|
3
|
+
def change_parent_to(new_parent_node, options = {})
|
5
4
|
default_options = {
|
6
5
|
subtree: true
|
7
6
|
}
|
@@ -12,70 +11,64 @@ module Forester
|
|
12
11
|
new_parent_node.add(self) # always as its last child
|
13
12
|
end
|
14
13
|
|
15
|
-
def add_child_content
|
16
|
-
new_node =
|
14
|
+
def add_child_content(content)
|
15
|
+
new_node = Forester.tree_factory.node_from_content(content)
|
17
16
|
add(new_node)
|
18
17
|
end
|
19
18
|
|
20
|
-
def
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
def add_fields!(fields, options = {})
|
25
|
-
default_options = {
|
26
|
-
subtree: true
|
27
|
-
}
|
28
|
-
options = default_options.merge(options)
|
29
|
-
|
30
|
-
target_nodes = options[:subtree] ? each_node : [self]
|
19
|
+
def add_field_in_node(name, definition)
|
20
|
+
value = definition.respond_to?(:call) ? definition.call(self) : definition
|
31
21
|
|
32
|
-
|
22
|
+
content[name] = value
|
33
23
|
end
|
34
24
|
|
35
|
-
def
|
25
|
+
def add_fields_in_node(fields)
|
36
26
|
fields.each do |field|
|
37
|
-
|
38
|
-
value = value.call(self) if value.respond_to?(:call)
|
39
|
-
|
40
|
-
put!(field[:name], value)
|
27
|
+
add_field_in_node(field[:name], field[:definition])
|
41
28
|
end
|
42
29
|
end
|
43
30
|
|
44
|
-
def
|
31
|
+
def add_field_in_subtree(name, definition)
|
32
|
+
add_fields_in_subtree([name: name, definition: definition])
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_fields_in_subtree(fields)
|
36
|
+
each_node { |node| node.add_fields_in_node(fields) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete_values_in_node(field, values, options = {})
|
45
40
|
default_options = {
|
46
|
-
percolate: false
|
47
|
-
subtree: true
|
41
|
+
percolate: false
|
48
42
|
}
|
49
43
|
options = default_options.merge(options)
|
50
44
|
|
51
|
-
|
52
|
-
|
53
|
-
target_nodes.each { |node| node.delete_values_from_root!(field, values, options[:percolate]) }
|
54
|
-
end
|
55
|
-
|
56
|
-
def delete_values_from_root!(field, values, percolate)
|
57
|
-
return unless has?(field)
|
45
|
+
return unless has_field?(field)
|
58
46
|
current_value = get(field)
|
59
|
-
return unless current_value.is_a?(Array)
|
60
47
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
current_value - as_array(values)
|
66
|
-
end
|
48
|
+
operation = options[:percolate] ? :& : :-
|
49
|
+
return unless current_value.respond_to?(operation)
|
50
|
+
|
51
|
+
new_value = current_value.public_send(operation, as_array(values))
|
67
52
|
|
68
|
-
|
53
|
+
content[field] = new_value
|
69
54
|
end
|
70
55
|
|
71
|
-
def
|
72
|
-
|
56
|
+
def delete_values_in_subtree(field, values, options = {})
|
57
|
+
default_options = {
|
58
|
+
percolate: false
|
59
|
+
}
|
60
|
+
options = default_options.merge(options)
|
61
|
+
|
62
|
+
each_node { |node| node.delete_values_in_node(field, values, options) }
|
73
63
|
end
|
74
64
|
|
75
|
-
def remove_levels_past
|
65
|
+
def remove_levels_past(last_level_to_keep)
|
66
|
+
unless last_level_to_keep >= 1
|
67
|
+
raise ArgumentError, "expected a positive integer, got #{last_level_to_keep}"
|
68
|
+
end
|
69
|
+
|
76
70
|
nodes_of_level(last_level_to_keep).map(&:remove_all!)
|
77
71
|
self
|
78
72
|
end
|
79
|
-
|
80
73
|
end
|
81
74
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Forester
|
2
|
+
module Serializers
|
3
|
+
def as_root_hash(options = {})
|
4
|
+
default_options = {
|
5
|
+
max_depth: :none,
|
6
|
+
children_key: 'children',
|
7
|
+
stringify_keys: false,
|
8
|
+
symbolize_keys: false,
|
9
|
+
include_fields: :all,
|
10
|
+
exclude_fields: :none
|
11
|
+
}
|
12
|
+
options = default_options.merge(options)
|
13
|
+
|
14
|
+
max_depth = options[:max_depth]
|
15
|
+
max_depth = -1 if max_depth == :none
|
16
|
+
|
17
|
+
adjusted_content = content.each_with_object(content.class.new) do |(k, v), h|
|
18
|
+
adjusted_key = k
|
19
|
+
adjusted_key = k.to_s if options[:stringify_keys]
|
20
|
+
adjusted_key = k.to_sym if options[:symbolize_keys]
|
21
|
+
|
22
|
+
unless options[:include_fields] == :all
|
23
|
+
next unless options[:include_fields].include?(adjusted_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
unless options[:exclude_fields] == :none
|
27
|
+
next if options[:exclude_fields].include?(adjusted_key)
|
28
|
+
end
|
29
|
+
|
30
|
+
h[adjusted_key] = v
|
31
|
+
end
|
32
|
+
|
33
|
+
children_key = options[:children_key]
|
34
|
+
children_key = children_key.to_s if options[:stringify_keys]
|
35
|
+
children_key = children_key.to_sym if options[:symbolize_keys]
|
36
|
+
|
37
|
+
next_children =
|
38
|
+
if max_depth == 0
|
39
|
+
[]
|
40
|
+
else
|
41
|
+
next_options = options.merge(max_depth: max_depth - 1)
|
42
|
+
children.map { |node| node.as_root_hash(next_options) }
|
43
|
+
end
|
44
|
+
|
45
|
+
adjusted_content.merge(children_key => next_children)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -1,32 +1,21 @@
|
|
1
1
|
module Forester
|
2
2
|
module Validators
|
3
|
-
|
4
3
|
def validate_uniqueness_of_field(field, options = {})
|
5
4
|
validate_uniqueness_of_fields([field], options)
|
6
5
|
end
|
7
6
|
|
8
7
|
def validate_uniqueness_of_fields(fields, options = {})
|
9
|
-
|
10
|
-
combination: false,
|
11
|
-
first_failure_only: false,
|
12
|
-
within_subtrees_of_level: 0,
|
13
|
-
among_siblings_of_level: :not_siblings,
|
14
|
-
field_for_failures: :name,
|
15
|
-
as_failure: ->(node) { node.get(options[:field_for_failures]) }
|
16
|
-
}
|
17
|
-
options = default_options.merge(options)
|
18
|
-
|
19
|
-
return of_combination_of_fields(fields, options) if options[:combination]
|
8
|
+
options = default_validator_options.merge(options)
|
20
9
|
|
21
10
|
failures = Hash.new(Hash.new([]))
|
22
11
|
|
23
|
-
|
12
|
+
nodes_of_depth(options[:within_subtrees_of_depth]).each do |subtree|
|
24
13
|
visited_nodes = []
|
25
14
|
nodes_to_visit =
|
26
|
-
if options[:
|
15
|
+
if options[:among_siblings_of_depth] == :not_siblings
|
27
16
|
subtree.each_node
|
28
17
|
else
|
29
|
-
|
18
|
+
nodes_of_depth(options[:among_siblings_of_depth])
|
30
19
|
end
|
31
20
|
|
32
21
|
nodes_to_visit.each do |node|
|
@@ -53,18 +42,18 @@ module Forester
|
|
53
42
|
result(failures)
|
54
43
|
end
|
55
44
|
|
56
|
-
|
45
|
+
def validate_uniqueness_of_fields_combination(fields, options = {})
|
46
|
+
options = default_validator_options.merge(options)
|
57
47
|
|
58
|
-
def of_combination_of_fields(fields, options)
|
59
48
|
failures = Hash.new(Hash.new([]))
|
60
49
|
|
61
|
-
|
50
|
+
nodes_of_depth(options[:within_subtrees_of_depth]).each do |subtree|
|
62
51
|
visited_nodes = []
|
63
52
|
nodes_to_visit =
|
64
|
-
if options[:
|
53
|
+
if options[:among_siblings_of_depth] == :not_siblings
|
65
54
|
subtree.each_node
|
66
55
|
else
|
67
|
-
|
56
|
+
nodes_of_depth(options[:among_siblings_of_depth])
|
68
57
|
end
|
69
58
|
|
70
59
|
nodes_to_visit.each do |node|
|
@@ -91,12 +80,21 @@ module Forester
|
|
91
80
|
|
92
81
|
private
|
93
82
|
|
83
|
+
def default_validator_options
|
84
|
+
{
|
85
|
+
first_failure_only: false,
|
86
|
+
within_subtrees_of_depth: 0,
|
87
|
+
among_siblings_of_depth: :not_siblings,
|
88
|
+
as_failure: ->(node) { node }
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
94
92
|
def all_have?(field, nodes)
|
95
93
|
all_have_all?([field], nodes)
|
96
94
|
end
|
97
95
|
|
98
96
|
def all_have_all?(fields, nodes)
|
99
|
-
nodes.all? { |n| fields.all? { |f| n.
|
97
|
+
nodes.all? { |n| fields.all? { |f| n.has_field?(f) } }
|
100
98
|
end
|
101
99
|
|
102
100
|
def same_values?(field, nodes)
|
@@ -110,8 +108,8 @@ module Forester
|
|
110
108
|
end
|
111
109
|
|
112
110
|
def prepare_hash(hash, key, subkey)
|
113
|
-
hash[key] = {} unless hash.
|
114
|
-
hash[key][subkey] = [] unless hash[key].
|
111
|
+
hash[key] = {} unless hash.key?(key)
|
112
|
+
hash[key][subkey] = [] unless hash[key].key?(subkey)
|
115
113
|
end
|
116
114
|
|
117
115
|
def add_failure_if_new(failures, key, subkey, value)
|
@@ -129,6 +127,5 @@ module Forester
|
|
129
127
|
failures: failures
|
130
128
|
}
|
131
129
|
end
|
132
|
-
|
133
130
|
end
|
134
131
|
end
|