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.
@@ -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 Views
5
+ include Validators
6
+ include Serializers
11
7
 
12
- alias_method :max_level, :node_height
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
- l.between?(0, max_level) ? each_level.take(l + 1).last : []
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 each_content(options = {})
39
- node_enumerator = each_node(options)
20
+ def path_from_root
21
+ (parentage || []).reverse + [self]
22
+ end
40
23
 
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
24
+ def paths_to_leaves
25
+ paths_to(leaves)
51
26
  end
52
27
 
53
- def each_level
54
- Enumerator.new do |yielder|
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 get(field, options = {}, &if_missing)
64
- default_options = {
65
- default: :raise,
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
- return own_and_descendants(field, { traversal: options[:traversal] }, &if_missing) if options[:subtree]
36
+ def leaves
37
+ each_leaf
38
+ end
72
39
 
73
- if has?(field)
74
- content.get(field)
75
- elsif block_given?
76
- yield self
77
- elsif options[:default] != :raise
78
- options[:default]
79
- else
80
- raise ArgumentError, "the node \"#{best_name}\" does not have \"#{field}\""
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 contents(options = {})
85
- each_node(options).map(&:content)
55
+ def leaves_when_pruned_to_level(l)
56
+ leaves_when_pruned_to_depth(l - 1)
86
57
  end
87
58
 
88
- def same_as?(other)
89
- return false unless content == other.content
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
- private
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 best_name
102
- get(:name, default: name)
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!(content, options = {}, &block)
16
- new_node = TreeFactory.node_from_hash(content, options, &block)
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 add_field!(name, definition, options = {})
21
- add_fields!([name: name, definition: definition], options)
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
- target_nodes.each { |node| node.add_fields_to_root!(fields) }
22
+ content[name] = value
33
23
  end
34
24
 
35
- def add_fields_to_root!(fields)
25
+ def add_fields_in_node(fields)
36
26
  fields.each do |field|
37
- value = field[:definition]
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 delete_values!(field, values, options = {})
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
- target_nodes = options[:subtree] ? each_node : [self]
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
- new_value =
62
- if percolate
63
- current_value & as_array(values)
64
- else
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
- put!(field, new_value)
53
+ content[field] = new_value
69
54
  end
70
55
 
71
- def percolate_values!(field, values, options = {})
72
- delete_values!(field, values, options.merge(percolate: true))
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!(last_level_to_keep)
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
- default_options = {
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
- nodes_of_level(options[:within_subtrees_of_level]).each do |subtree|
12
+ nodes_of_depth(options[:within_subtrees_of_depth]).each do |subtree|
24
13
  visited_nodes = []
25
14
  nodes_to_visit =
26
- if options[:among_siblings_of_level] == :not_siblings
15
+ if options[:among_siblings_of_depth] == :not_siblings
27
16
  subtree.each_node
28
17
  else
29
- nodes_of_level(options[:among_siblings_of_level])
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
- private
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
- nodes_of_level(options[:within_subtrees_of_level]).each do |subtree|
50
+ nodes_of_depth(options[:within_subtrees_of_depth]).each do |subtree|
62
51
  visited_nodes = []
63
52
  nodes_to_visit =
64
- if options[:among_siblings_of_level] == :not_siblings
53
+ if options[:among_siblings_of_depth] == :not_siblings
65
54
  subtree.each_node
66
55
  else
67
- nodes_of_level(options[:among_siblings_of_level])
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.has?(f) } }
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.has_key?(key)
114
- hash[key][subkey] = [] unless hash[key].has_key?(subkey)
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