forester 4.3.0 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|