eco-helpers 2.5.2 → 2.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +62 -2
- data/eco-helpers.gemspec +2 -2
- data/lib/eco/api/common/loaders/use_case.rb +0 -2
- data/lib/eco/api/common/people/person_entry_attribute_mapper.rb +0 -2
- data/lib/eco/api/common/session/logger.rb +22 -77
- data/lib/eco/api/microcases/with_each.rb +0 -1
- data/lib/eco/api/organization/tag_tree.rb +64 -15
- data/lib/eco/api/session/config/tagtree.rb +32 -10
- data/lib/eco/api/session/config/workflow.rb +0 -1
- data/lib/eco/api/session/config.rb +6 -2
- data/lib/eco/api/session.rb +2 -2
- data/lib/eco/api/usecases/default_cases/abstract_policygroup_abilities_case.rb +2 -3
- data/lib/eco/api/usecases/default_cases/analyse_people_case.rb +2 -3
- data/lib/eco/api/usecases/default_cases/append_usergroups_case.rb +0 -1
- data/lib/eco/api/usecases/default_cases/change_email_case.rb +1 -2
- data/lib/eco/api/usecases/default_cases/clean_unknown_tags_case.rb +0 -5
- data/lib/eco/api/usecases/default_cases/clear_abilities_case.rb +2 -2
- data/lib/eco/api/usecases/default_cases/codes_to_tags_case.rb +5 -7
- data/lib/eco/api/usecases/default_cases/create_case.rb +0 -5
- data/lib/eco/api/usecases/default_cases/create_details_case.rb +0 -5
- data/lib/eco/api/usecases/default_cases/create_details_with_supervisor_case.rb +0 -5
- data/lib/eco/api/usecases/default_cases/csv_to_tree_case/helper.rb +1 -1
- data/lib/eco/api/usecases/default_cases/csv_to_tree_case.rb +0 -4
- data/lib/eco/api/usecases/default_cases/delete_sync_case.rb +2 -4
- data/lib/eco/api/usecases/default_cases/delete_trans_case.rb +2 -3
- data/lib/eco/api/usecases/default_cases/email_as_id_case.rb +0 -1
- data/lib/eco/api/usecases/default_cases/entries_to_csv_case.rb +0 -4
- data/lib/eco/api/usecases/default_cases/hris_case.rb +2 -3
- data/lib/eco/api/usecases/default_cases/new_email_case.rb +0 -2
- data/lib/eco/api/usecases/default_cases/new_id_case.rb +0 -2
- data/lib/eco/api/usecases/default_cases/org_data_convert_case.rb +0 -5
- data/lib/eco/api/usecases/default_cases/refresh_case.rb +0 -1
- data/lib/eco/api/usecases/default_cases/reinvite_sync_case.rb +1 -3
- data/lib/eco/api/usecases/default_cases/reinvite_trans_case.rb +2 -2
- data/lib/eco/api/usecases/default_cases/remove_account_sync_case.rb +1 -2
- data/lib/eco/api/usecases/default_cases/remove_account_trans_case.rb +2 -3
- data/lib/eco/api/usecases/default_cases/reset_landing_page_case.rb +1 -7
- data/lib/eco/api/usecases/default_cases/restore_db_case.rb +0 -10
- data/lib/eco/api/usecases/default_cases/set_default_tag_case.rb +0 -1
- data/lib/eco/api/usecases/default_cases/set_supervisor_case.rb +0 -1
- data/lib/eco/api/usecases/default_cases/supers_cyclic_identify_case.rb +2 -3
- data/lib/eco/api/usecases/default_cases/supers_hierarchy_case.rb +2 -3
- data/lib/eco/api/usecases/default_cases/switch_supervisor_case.rb +2 -4
- data/lib/eco/api/usecases/default_cases/tagtree_case.rb +0 -2
- data/lib/eco/api/usecases/default_cases/to_csv_case.rb +4 -5
- data/lib/eco/api/usecases/default_cases/to_csv_detailed_case.rb +0 -1
- data/lib/eco/api/usecases/default_cases/transfer_account_case.rb +0 -2
- data/lib/eco/api/usecases/default_cases/update_case.rb +0 -2
- data/lib/eco/api/usecases/default_cases/update_details_case.rb +0 -2
- data/lib/eco/api/usecases/default_cases/upsert_case.rb +0 -4
- data/lib/eco/api/usecases/graphql/base.rb +6 -18
- data/lib/eco/api/usecases/graphql/helpers/base/case_env.rb +15 -0
- data/lib/eco/api/usecases/graphql/helpers/base.rb +23 -0
- data/lib/eco/api/usecases/graphql/helpers/location/base.rb +87 -0
- data/lib/eco/api/usecases/graphql/helpers/location/command/result.rb +69 -0
- data/lib/eco/api/usecases/graphql/helpers/location/command/results.rb +126 -0
- data/lib/eco/api/usecases/graphql/helpers/location/command.rb +92 -0
- data/lib/eco/api/usecases/graphql/helpers/location.rb +7 -0
- data/lib/eco/api/usecases/graphql/helpers.rb +2 -1
- data/lib/eco/api/usecases/graphql/samples/location/command/dsl.rb +54 -0
- data/lib/eco/api/usecases/graphql/samples/location/command/results.rb +125 -0
- data/lib/eco/api/usecases/graphql/samples/location/command.rb +10 -0
- data/lib/eco/api/usecases/graphql/samples/location/dsl.rb +6 -0
- data/lib/eco/api/usecases/graphql/samples/location.rb +10 -0
- data/lib/eco/api/usecases/graphql/samples.rb +6 -0
- data/lib/eco/api/usecases/graphql/utils/sftp.rb +74 -0
- data/lib/eco/api/usecases/graphql/utils.rb +6 -0
- data/lib/eco/api/usecases/graphql.rb +3 -1
- data/lib/eco/api/usecases/ooze_cases/export_register_case.rb +0 -1
- data/lib/eco/api/usecases/ooze_samples/ooze_base_case.rb +0 -2
- data/lib/eco/api/usecases/ooze_samples/register_migration_case.rb +0 -2
- data/lib/eco/api/usecases/use_case.rb +2 -2
- data/lib/eco/cli/config/default/workflow.rb +2 -4
- data/lib/eco/cli/scripting/args_helpers.rb +0 -2
- data/lib/eco/csv/table.rb +39 -3
- data/lib/eco/data/files/helpers.rb +4 -3
- data/lib/eco/data/hashes/array_diff.rb +21 -61
- data/lib/eco/data/hashes/diff_meta.rb +52 -0
- data/lib/eco/data/hashes/diff_result.rb +36 -25
- data/lib/eco/data/hashes.rb +1 -0
- data/lib/eco/data/locations/convert.rb +92 -0
- data/lib/eco/data/locations/dsl.rb +35 -0
- data/lib/eco/data/locations/node_base/builder.rb +26 -0
- data/lib/eco/data/locations/node_base/csv_convert.rb +57 -0
- data/lib/eco/data/locations/node_base/parsing.rb +30 -0
- data/lib/eco/data/locations/node_base/serial.rb +26 -0
- data/lib/eco/data/locations/node_base/tag_validations.rb +52 -0
- data/lib/eco/data/locations/node_base/treeify.rb +150 -0
- data/lib/eco/data/locations/node_base.rb +48 -0
- data/lib/eco/data/locations/node_diff/accessors.rb +46 -0
- data/lib/eco/data/locations/node_diff/nodes_diff.rb +90 -0
- data/lib/eco/data/locations/node_diff/selectors.rb +20 -0
- data/lib/eco/data/locations/node_diff.rb +55 -0
- data/lib/eco/data/locations/node_level/builder.rb +6 -0
- data/lib/eco/data/locations/node_level/cleaner.rb +74 -0
- data/lib/eco/data/locations/node_level/parsing.rb +63 -0
- data/lib/eco/data/locations/node_level/serial.rb +37 -0
- data/lib/eco/data/locations/node_level.rb +153 -0
- data/lib/eco/data/locations/node_plain/builder.rb +6 -0
- data/lib/eco/data/locations/node_plain/parsing.rb +36 -0
- data/lib/eco/data/locations/node_plain/serial.rb +14 -0
- data/lib/eco/data/locations/node_plain.rb +31 -0
- data/lib/eco/data/locations.rb +13 -0
- data/lib/eco/data.rb +1 -0
- data/lib/eco/language/auxiliar_logger.rb +9 -1
- data/lib/eco/language/basic_logger.rb +74 -0
- data/lib/eco/language.rb +2 -1
- data/lib/eco/version.rb +1 -1
- metadata +45 -8
- data/lib/eco/api/usecases/default_cases/new_id_case0.rb +0 -14
- data/lib/eco/api/usecases/graphql/helpers/locations/commands.rb +0 -4
- data/lib/eco/api/usecases/graphql/helpers/locations.rb +0 -6
@@ -0,0 +1,57 @@
|
|
1
|
+
module Eco::Data::Locations::NodeBase
|
2
|
+
module CsvConvert
|
3
|
+
include Eco::Data::Locations::NodeBase::Parsing
|
4
|
+
|
5
|
+
def tree_class
|
6
|
+
Eco::API::Organization::TagTree
|
7
|
+
end
|
8
|
+
|
9
|
+
# @yield [Node] optional custom serializer
|
10
|
+
# @yieldreturn [Hash] the serialized Node
|
11
|
+
# @param value [CSV::Table, Eco::API::Organization::TagTree]
|
12
|
+
# @return [Array<Hash>] a plain list of hash nodes
|
13
|
+
def hash_list(value, &block)
|
14
|
+
return hash_list(org_tree(value)) if value.is_a?(::CSV::Table)
|
15
|
+
return value.as_nodes_json if value.is_a?(tree_class)
|
16
|
+
raise ArgumentError, "Expecting Eco::API::Organization::TagTree or CSV::Table. Given: #{value.class}"
|
17
|
+
end
|
18
|
+
|
19
|
+
# @yield [Node] optional custom serializer
|
20
|
+
# @yieldreturn [Hash] the serialized Node
|
21
|
+
# @param value [CSV::Table, Eco::API::Organization::TagTree]
|
22
|
+
# @return [Array<Hash>] a hierarchical tree of hash nodes,
|
23
|
+
# ready to be parsed as an organization tagtree
|
24
|
+
def hash_tree(value, &block)
|
25
|
+
return hash_tree_from_csv(value, &block) if value.is_a?(::CSV::Table)
|
26
|
+
return value.as_json if value.is_a?(tree_class)
|
27
|
+
raise ArgumentError, "Expecting Eco::API::Organization::TagTree or CSV::Table. Given: #{value.class}"
|
28
|
+
end
|
29
|
+
|
30
|
+
# @yield [Node] optional custom serializer
|
31
|
+
# @yieldreturn [Hash] the serialized Node
|
32
|
+
# @param value [CSV::Table, Eco::API::Organization::TagTree]
|
33
|
+
# @return [Eco::API::Organization::TagTree]
|
34
|
+
def org_tree(value, &block)
|
35
|
+
return tree_class.new(hash_tree(value), &block) if value.is_a?(::CSV::Table)
|
36
|
+
return tree_class.new(value.as_json) if value.is_a?(tree_class)
|
37
|
+
raise ArgumentError, "Expecting Eco::API::Organization::TagTree or CSV::Table. Given: #{value.class}"
|
38
|
+
end
|
39
|
+
|
40
|
+
# @yield [Node] optional custom serializer
|
41
|
+
# @yieldreturn [Hash] the serialized Node
|
42
|
+
# @return [CSV::Table] a table with L1 to Ln columns ready for dump to csv
|
43
|
+
def csv_tree(value, encoding: 'utf-8', &block)
|
44
|
+
Eco::CSV::Table.new(hash_tree_to_tree_csv(hash_tree(value, &block)))
|
45
|
+
end
|
46
|
+
|
47
|
+
# @note it just converts to an organizational tagtree and uses a helper method.
|
48
|
+
# @yield [Node] optional custom serializer
|
49
|
+
# @yieldreturn [Hash] the serialized Node
|
50
|
+
# @param value [CSV::Table, Eco::API::Organization::TagTree]
|
51
|
+
# @return [CSV::Table] a table with a list of nodes and their parents
|
52
|
+
def csv_list(value, &block)
|
53
|
+
value = org_tree(value, &block) unless value.is_a?(tree_class)
|
54
|
+
Eco::CSV.Table.new(hash_list(value))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Eco::Data::Locations::NodeBase
|
2
|
+
module Parsing
|
3
|
+
include Eco::Data::Locations::Convert
|
4
|
+
include Eco::Data::Locations::NodeBase::Treeify
|
5
|
+
|
6
|
+
# @param csv [CSV::Table]
|
7
|
+
# @return [Array<NodePlain>, Array<NodeLevel>] with integrity issues resolved.
|
8
|
+
def nodes_from_csv(csv)
|
9
|
+
raise ArgumentError, "Expecting CSV::Table. Given: #{csv.class}" unless csv.is_a?(::CSV::Table)
|
10
|
+
return Eco::Data::Locations::NodePlain.nodes_from_csv(csv) if Eco::Data::Locations::NodePlain.csv_matches_format?(csv)
|
11
|
+
return Eco::Data::Locations::NodeLevel.nodes_from_csv(csv) if Eco::Data::Locations::NodeLevel.csv_matches_format?(csv)
|
12
|
+
raise ArgumentError, "The input csv does not have the required format to read a locations structure."
|
13
|
+
end
|
14
|
+
|
15
|
+
# @yield [Node] optional custom serializer
|
16
|
+
# @yieldreturn [Hash] the serialized Node
|
17
|
+
# @return [Array<Hash>] a hierarchical tree of nested Hashes via `nodes` key.
|
18
|
+
def hash_tree_from_csv(csv, &block)
|
19
|
+
raise ArgumentError, "Expecting CSV::Table. Given: #{csv.class}" unless csv.is_a?(::CSV::Table)
|
20
|
+
treeify(nodes_from_csv(csv), &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Shortcut to obtain a list of parsed nodes out of a file
|
24
|
+
# @param filename [String] the csv file.
|
25
|
+
# @return [Array<NodePlain>, Array<NodeLevel>] with integrity issues resolved.
|
26
|
+
def csv_nodes_from(filename, encoding: 'utf-8')
|
27
|
+
nodes_from_csv(csv_from(filename, encoding: 'utf-8'))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Eco::Data::Locations::NodeBase
|
2
|
+
module Serial
|
3
|
+
include Eco::Data::Locations::NodeBase::Treeify
|
4
|
+
include Eco::Data::Locations::Convert
|
5
|
+
|
6
|
+
# @param item [Eco::Data::Locations::NodeBase] an instance object of a child class.
|
7
|
+
# @return [Proc] the serializer to be used.
|
8
|
+
def serializer(item)
|
9
|
+
raise "Execting a chidren of NodeBase. Given: #{item.class}" unless item.class < Eco::Data::Locations::NodeBase
|
10
|
+
item.serializer
|
11
|
+
end
|
12
|
+
|
13
|
+
# @paran nodes [Array<NodeBase>]
|
14
|
+
# @return [CSV::Table] ready to dump into a hierarhical **csv** (columns are tree levels)
|
15
|
+
def nodes_to_csv_tree(nodes)
|
16
|
+
hash_tree_to_tree_csv(treeify(nodes))
|
17
|
+
end
|
18
|
+
|
19
|
+
# @paran nodes [Array<NodeBase>]
|
20
|
+
# @return [CSV::Table] ready to dump into a nodes list **csv** (rows are nodes; a column holds `parent_id`)
|
21
|
+
def nodes_to_csv_list(nodes)
|
22
|
+
tree = Eco::API::Organization::TagTree.new(treeify(nodes))
|
23
|
+
Eco::CSV::Table.new(tree.as_nodes_json)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Eco::Data::Locations::NodeBase
|
2
|
+
module TagValidations
|
3
|
+
ALLOWED_CHARACTERS = "A-Za-z0-9 &_'\/.-"
|
4
|
+
VALID_TAG_REGEX = /^[#{ALLOWED_CHARACTERS}]+$/
|
5
|
+
INVALID_TAG_REGEX = /[^#{ALLOWED_CHARACTERS}]+/
|
6
|
+
VALID_TAG_CHARS = /[#{ALLOWED_CHARACTERS}]+/
|
7
|
+
DOUBLE_BLANKS = /\s\s+/
|
8
|
+
|
9
|
+
def clean_id(str)
|
10
|
+
blanks_x2 = has_double_blanks?(str)
|
11
|
+
partial = replace_not_allowed(str)
|
12
|
+
remove_double_blanks(partial).tap do |result|
|
13
|
+
next if invalid_warned?
|
14
|
+
if partial != str
|
15
|
+
invalid_chars = identify_invalid_characters(str)
|
16
|
+
puts "• (Row: #{self.row_num}) Invalid characters _#{invalid_chars}_ (removed): '#{str}' (converted to '#{result}')"
|
17
|
+
elsif blanks_x2
|
18
|
+
puts "• (Row: #{self.row_num}) Double blanks (removed): '#{str}' (converted to '#{result}')"
|
19
|
+
end
|
20
|
+
invalid_warned!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def invalid_warned?
|
25
|
+
@invalid_warned ||= false
|
26
|
+
end
|
27
|
+
|
28
|
+
def invalid_warned!
|
29
|
+
@invalid_warned = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_double_blanks?(str)
|
33
|
+
return false if str.nil?
|
34
|
+
str.match(DOUBLE_BLANKS)
|
35
|
+
end
|
36
|
+
|
37
|
+
def remove_double_blanks(str)
|
38
|
+
return nil if str.nil?
|
39
|
+
str.gsub(DOUBLE_BLANKS, ' ').strip
|
40
|
+
end
|
41
|
+
|
42
|
+
def replace_not_allowed(str)
|
43
|
+
return nil if str.nil?
|
44
|
+
return str if str.match(VALID_TAG_REGEX)
|
45
|
+
str.gsub(INVALID_TAG_REGEX, ' ')
|
46
|
+
end
|
47
|
+
|
48
|
+
def identify_invalid_characters(str)
|
49
|
+
str.gsub(VALID_TAG_CHARS, '')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Eco::Data::Locations::NodeBase
|
2
|
+
# Generic treeifier
|
3
|
+
# @note expects nodes to have these properties:
|
4
|
+
# 1. `id`, `name` and `parentId`
|
5
|
+
# 2. `parent`
|
6
|
+
# 3. `tracked_level`
|
7
|
+
module Treeify
|
8
|
+
include Eco::Language::AuxiliarLogger
|
9
|
+
|
10
|
+
# @note if block is no given, it auto-detects the `serializer` **block**.
|
11
|
+
# @yield [NodeBase] for each included node
|
12
|
+
# @yieldreturn [Hash] custom hash model when treeifying (allows to set more keys/properties).
|
13
|
+
# @nodes [Array<NodeBase>] list of nodes
|
14
|
+
# @return [Array<Hash>] a hierarchical tree of nested Hashes via `nodes` key.
|
15
|
+
def treeify(nodes, &block)
|
16
|
+
return [] if nodes.empty?
|
17
|
+
block ||= nodes.first.class.serializer
|
18
|
+
get_children(nil, parents_hash(nodes), &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def parents_hash(nodes)
|
24
|
+
nodes.each_with_object({}) do |node, parents|
|
25
|
+
(parents[node.parentId] ||= []).push(node)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# @note
|
30
|
+
# 1. It tracks the `level` where nodes are discovered
|
31
|
+
# 2. If the node had already a tracked level, it warns and keeps the previous level
|
32
|
+
# 3. The above can translate into some
|
33
|
+
# @yield [node]
|
34
|
+
# @yieldreturn [Hash] custom hash model when treeifying
|
35
|
+
def get_children(node_id, parents, parent: nil, done_ids: {}, level: 0, &block)
|
36
|
+
level_ids = []
|
37
|
+
(parents[node_id] ||= []).each_with_object([]) do |child, results|
|
38
|
+
# Skipping done id. Add proper warnings...
|
39
|
+
# => rely on `done_ids` to identify if an `id` has already been done
|
40
|
+
next report_skipped_node(child, parent, done_ids, level, level_ids, parents) if done_ids[child.id]
|
41
|
+
|
42
|
+
# Fill in tracking data
|
43
|
+
child.parent = parent
|
44
|
+
child.tracked_level = level + 1
|
45
|
+
level_ids << child.id
|
46
|
+
|
47
|
+
node_hash = {
|
48
|
+
"id" => child.id,
|
49
|
+
"name" => child.name,
|
50
|
+
"parent_id" => node_id
|
51
|
+
}
|
52
|
+
node_hash.merge(yield(child)) if block_given?
|
53
|
+
# we must register the `id` before recursing down
|
54
|
+
done_ids[child.id] = child
|
55
|
+
results << node_hash.merge({
|
56
|
+
"nodes" => get_children(child.id, parents, parent: child, done_ids: done_ids, level: level + 1, &block).compact
|
57
|
+
})
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def parent_msg(parent)
|
62
|
+
parent ? "child of '#{parent.id}'" : "top level"
|
63
|
+
end
|
64
|
+
|
65
|
+
def level_msg(level)
|
66
|
+
"at lev: #{level}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def indent(level)
|
70
|
+
"#{" " * level}"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Gives different warnings, depending the case
|
74
|
+
def report_skipped_node(node, parent, done_ids, level, level_ids, parents)
|
75
|
+
lev = level + 1
|
76
|
+
done_node = done_ids[node.id]
|
77
|
+
prev_parent = node.parent
|
78
|
+
prev_level = node.tracked_level
|
79
|
+
node_dup = done_node && (done_node != node)
|
80
|
+
lev_dup = level_ids.include?(node.id)
|
81
|
+
multi_parent = (!prev_parent == !!parent) || (prev_parent && (prev_parent.id != parent.id))
|
82
|
+
|
83
|
+
row_num = node.respond_to?(:row_num) ? node.row_num : nil
|
84
|
+
row_str = row_num ? "(Row: #{row_num}) " : ''
|
85
|
+
node_str = "#{row_str}Node '#{node.id}' #{level_msg(lev)} (#{parent_msg(parent)})"
|
86
|
+
|
87
|
+
# Implementation integrity guard
|
88
|
+
# => as we don't register in `done_ids` those that are skipped,
|
89
|
+
# when a `node` has already a tracked `parent` or `level`,
|
90
|
+
# it should not happen that the `node.id` retrieves a different node in `node_ids`.
|
91
|
+
if (prev_parent || prev_level) && node_dup # && !done_node
|
92
|
+
str = "Integrity issue in Treeify. "
|
93
|
+
str << "A Node with tracked level or parent should be present in done_ids, but it isn't."
|
94
|
+
str << "\n • #{node_str}."
|
95
|
+
raise str
|
96
|
+
end
|
97
|
+
# From here on, do NOT expect `node_dup` where `node` has tracked `parent` or `level`.
|
98
|
+
|
99
|
+
# Implementation integrity guard
|
100
|
+
# => as`level_ids` only relates to the current `parent`,
|
101
|
+
# and as `done_ids` don't get those skipped,
|
102
|
+
# when we get an ID double-up in `level_ids`,
|
103
|
+
# there must be a `done_node` AND
|
104
|
+
# `done_node` can only have `tracked_level` matching the current one
|
105
|
+
# Moreover, they should have exactly the same parentId.
|
106
|
+
if lev_dup && (multi_parent || !done_node || done_node.tracked_level != lev)
|
107
|
+
str = "Integrity issue in Treeify. "
|
108
|
+
str << "A Node with ID already in level_ids should have same tracked_level as current level."
|
109
|
+
str << "\n • #{node_str}."
|
110
|
+
raise str
|
111
|
+
end
|
112
|
+
# From here on, do NOT expect `lev_up` where there isn't `done_node` or it has different level or parent.
|
113
|
+
|
114
|
+
cyclic = multi_parent && done_node == node
|
115
|
+
double_up = node_dup || lev_dup
|
116
|
+
|
117
|
+
msg = []
|
118
|
+
msg << "#{indent(level)}WARNING: Skipping #{node_str}."
|
119
|
+
|
120
|
+
if cyclic
|
121
|
+
str = "#{indent(level)+1}Cyclic definition. By skipping the node, "
|
122
|
+
str << "it will remain as #{parent_msg(done_node.parent)} (#{level_msg(prev_level)})."
|
123
|
+
msg << str
|
124
|
+
end
|
125
|
+
|
126
|
+
if double_up
|
127
|
+
str = "#{indent(level)+1}The node ID has been tracked as #{level_msg(done_node.tracked_level)}, "
|
128
|
+
str << "as #{parent_msg(node_dup.parent)} "
|
129
|
+
str << "(same parent)." if lev_dup
|
130
|
+
str << "(different parent)." if multi_parent
|
131
|
+
msg << str
|
132
|
+
end
|
133
|
+
|
134
|
+
unless cyclic || double_up
|
135
|
+
str = "Integrity issue in Treeify. "
|
136
|
+
str = "Skipping is only applicable to double_ups or cyclic nodes."
|
137
|
+
str << "\n • #{node_str}."
|
138
|
+
raise str
|
139
|
+
end
|
140
|
+
|
141
|
+
if children = parents[node.id]
|
142
|
+
str = "#{indent(level)+1}Immediate children of skipped node (will probably be missing): "
|
143
|
+
str << children.map {|gc| "'#{gc.id}'"}.join(", ")
|
144
|
+
msg << str
|
145
|
+
end
|
146
|
+
|
147
|
+
log(:warn) { msg.join('\n') }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Eco::Data::Locations
|
2
|
+
module NodeBase
|
3
|
+
require_relative 'node_base/tag_validations'
|
4
|
+
include Eco::Data::Locations::NodeBase::TagValidations
|
5
|
+
|
6
|
+
require_relative 'node_base/treeify'
|
7
|
+
require_relative 'node_base/parsing'
|
8
|
+
require_relative 'node_base/serial'
|
9
|
+
require_relative 'node_base/csv_convert'
|
10
|
+
require_relative 'node_base/builder'
|
11
|
+
extend Eco::Data::Locations::NodeBase::Builder
|
12
|
+
|
13
|
+
ALL_ATTRS = []
|
14
|
+
|
15
|
+
attr_accessor :tracked_level, :parent
|
16
|
+
|
17
|
+
def copy
|
18
|
+
self.class.new.set_attrs(**self.to_h)
|
19
|
+
end
|
20
|
+
|
21
|
+
def attr(sym)
|
22
|
+
self.send(sym.to_sym)
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_attrs(**kargs)
|
26
|
+
kargs.each {|attr, value| set_attr(attr, value)}
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_attr(attr, value)
|
31
|
+
self.send("#{attr}=", value)
|
32
|
+
end
|
33
|
+
|
34
|
+
def values_at(*attrs)
|
35
|
+
attrs.map {|a| attr(a)}
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_h(*attrs)
|
39
|
+
attrs = self.class::ALL_ATTRS if attrs.empty?
|
40
|
+
attrs.zip(values_at(*attrs)).to_h
|
41
|
+
end
|
42
|
+
|
43
|
+
def slice(*attrs)
|
44
|
+
return {} if attrs.empty?
|
45
|
+
to_h(*attrs)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Eco::Data::Locations::NodeDiff
|
2
|
+
module Accessors
|
3
|
+
class << self
|
4
|
+
def included(base)
|
5
|
+
super(base)
|
6
|
+
base.extend Eco::Language::Models::ClassHelpers
|
7
|
+
base.extend ClassMethods
|
8
|
+
base.inheritable_class_vars :exposed_attrs
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# Creates the defined accessor attributes against `NodeDiff`
|
14
|
+
# It also creates a method with question mark that evaluates true if **any** the attr changed.
|
15
|
+
# @note the defined attributes are expected to be the keys within
|
16
|
+
# the source Hashes that are being compared.
|
17
|
+
# @note accessing `src1` (prev) attributes, will have it's method as `prev_[attrName]`
|
18
|
+
def attr_expose(*attrs)
|
19
|
+
attrs.each do |attr|
|
20
|
+
meth = attr.to_sym
|
21
|
+
methp = "prev_#{meth}".to_sym
|
22
|
+
methq = "diff_#{meth}?".to_sym
|
23
|
+
|
24
|
+
define_method meth do
|
25
|
+
attr(meth)
|
26
|
+
end
|
27
|
+
|
28
|
+
define_method methp do
|
29
|
+
attr_prev(meth)
|
30
|
+
end
|
31
|
+
|
32
|
+
exposed_attrs |= [meth]
|
33
|
+
|
34
|
+
define_method methq do
|
35
|
+
diff_attr?(meth)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Keeps track on what attributes have been exposed.
|
41
|
+
def exposed_attrs
|
42
|
+
@exposed_attrs ||= []
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
class Eco::Data::Locations::NodeDiff
|
2
|
+
# Adjusts ArrayDiff for Nodes diffs in a location structure
|
3
|
+
class NodesDiff < Eco::Data::Hashes::ArrayDiff
|
4
|
+
extend Eco::Data::Locations::NodeDiff::Selectors
|
5
|
+
|
6
|
+
SELECTORS = %i[id name id_name insert unarchive update move archive]
|
7
|
+
selector *SELECTORS
|
8
|
+
|
9
|
+
class_resolver :diff_result_class, Eco::Data::Locations::NodeDiff
|
10
|
+
attr_reader :original_tree
|
11
|
+
|
12
|
+
def initialize(*args, original_tree:, **kargs, &block)
|
13
|
+
super(*args, **kargs, &block)
|
14
|
+
@original_tree = original_tree
|
15
|
+
end
|
16
|
+
|
17
|
+
def diffs
|
18
|
+
@diffs ||= super.select do |res|
|
19
|
+
res.unarchive? || res.id_name? || res.insert? || res.move? || res.archive?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def diffs_details
|
24
|
+
section = '#' * 10
|
25
|
+
msg = ''
|
26
|
+
if insert?
|
27
|
+
msg << " #{section} I N S E R T S #{section}\n"
|
28
|
+
msg << insert.map {|d| d.diff_hash.pretty_inspect}.join('')
|
29
|
+
end
|
30
|
+
|
31
|
+
if update?
|
32
|
+
msg << "\n #{section} U P D A T E S #{section}\n"
|
33
|
+
update.each do |d|
|
34
|
+
flags = ''
|
35
|
+
#flags << 'i' if d.id?
|
36
|
+
flags << 'n' if d.diff_name?
|
37
|
+
flags << 'm' if d.move?
|
38
|
+
flags << 'u' if d.unarchive?
|
39
|
+
msg << "<< #{flags} >> "
|
40
|
+
msg << d.diff_hash.pretty_inspect
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
if archive?
|
45
|
+
msg << "\n #{section} A R C H I V E S #{section}\n"
|
46
|
+
msg << archive.map {|d| d.diff_hash.pretty_inspect}.join('')
|
47
|
+
end
|
48
|
+
|
49
|
+
msg
|
50
|
+
end
|
51
|
+
|
52
|
+
def diffs_summary
|
53
|
+
return "There were no differences identified" if diffs.empty?
|
54
|
+
msg = "Identified #{diffs.count} differences:\n"
|
55
|
+
msg << when_present(insert, '') do |count|
|
56
|
+
" • #{count} nodes to insert\n"
|
57
|
+
end
|
58
|
+
msg << when_present(update, '') do |count|
|
59
|
+
" • #{count} nodes to update\n"
|
60
|
+
end
|
61
|
+
# msg << when_present(id, '') do |count|
|
62
|
+
# " • #{count} nodes to change id\n"
|
63
|
+
# end
|
64
|
+
msg << when_present(name, '') do |count|
|
65
|
+
" • #{count} nodes to change name\n"
|
66
|
+
end
|
67
|
+
msg << when_present(move, '') do |count|
|
68
|
+
" • #{count} nodes to move\n"
|
69
|
+
end
|
70
|
+
msg << when_present(unarchive, '') do |count|
|
71
|
+
" • #{count} nodes to unarchive\n"
|
72
|
+
end
|
73
|
+
msg << when_present(archive, '') do |count|
|
74
|
+
" • #{count} nodes to archive\n"
|
75
|
+
end
|
76
|
+
msg
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def when_present(list, default = nil)
|
82
|
+
count = list.count
|
83
|
+
if count > 0
|
84
|
+
yield(count)
|
85
|
+
else
|
86
|
+
default
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Eco::Data::Locations::NodeDiff
|
2
|
+
module Selectors
|
3
|
+
# Creates selector methods on `diffs` (nodes that changed)
|
4
|
+
# It also creates a method with question mark that evaluates true if **any** diff matches.
|
5
|
+
# @note the selector method name with a question mark should exist in the `diff_result_class`
|
6
|
+
def selector(*attrs)
|
7
|
+
attrs.each do |attr|
|
8
|
+
meth = attr.to_sym
|
9
|
+
methq = "#{meth}?".to_sym
|
10
|
+
define_method meth do
|
11
|
+
diffs.select(&methq)
|
12
|
+
end
|
13
|
+
|
14
|
+
define_method methq do
|
15
|
+
diffs.any(&methq)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Eco::Data::Locations
|
2
|
+
# Differences between node before and after
|
3
|
+
# @note this works with `Hash` object where basic keys are:
|
4
|
+
# - `nodeId`
|
5
|
+
# - `name`
|
6
|
+
# - `parentId`
|
7
|
+
# - `archived`
|
8
|
+
# @note other properties can be part of the `Hash` although
|
9
|
+
# they may not influence the results.
|
10
|
+
class NodeDiff < Eco::Data::Hashes::DiffResult
|
11
|
+
require_relative 'node_diff/accessors'
|
12
|
+
require_relative 'node_diff/selectors'
|
13
|
+
require_relative 'node_diff/nodes_diff'
|
14
|
+
|
15
|
+
include Eco::Data::Locations::NodeDiff::Accessors
|
16
|
+
|
17
|
+
key :nodeId
|
18
|
+
compare :parentId, :name, :archived
|
19
|
+
case_sensitive false
|
20
|
+
|
21
|
+
attr_expose :nodeId, :name, :parentId, :archived
|
22
|
+
|
23
|
+
alias_method :insert?, :new?
|
24
|
+
|
25
|
+
alias_method :diff_name_src?, :diff_name?
|
26
|
+
# Has the property `name` changed?
|
27
|
+
def diff_name?
|
28
|
+
diff_name_src? && update?
|
29
|
+
end
|
30
|
+
alias_method :name?, :diff_name?
|
31
|
+
alias_method :id? , :diff_name? # currently a change of name is a change of id (tag)
|
32
|
+
#alias_method :id? , :key?
|
33
|
+
#alias_method :nodeId? , :id?
|
34
|
+
|
35
|
+
# Has any of `id` or `name` properties changed?
|
36
|
+
def id_name?
|
37
|
+
id? || diff_name?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Has the parent id changed?
|
41
|
+
def move?
|
42
|
+
update? && diff_parentId?
|
43
|
+
end
|
44
|
+
|
45
|
+
# Has the `archived` property changed and it was `true`?
|
46
|
+
def unarchive?
|
47
|
+
!archived && update? && diff_archived?
|
48
|
+
end
|
49
|
+
|
50
|
+
# Has the `archived` property changed and it was `false`?
|
51
|
+
def archive?
|
52
|
+
!prev_archived && (del? || archived)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
class Eco::Data::Locations::NodeLevel
|
2
|
+
module Cleaner
|
3
|
+
include Eco::Language::AuxiliarLogger
|
4
|
+
include Eco::Data::Locations::Convert
|
5
|
+
|
6
|
+
# Prevents repeated node ids/tags, decouples merged levels,
|
7
|
+
# covers gaps (jumping multiple levels)
|
8
|
+
# @note
|
9
|
+
# 1. It first discards node ids/tags that have been already pulled (discard repeated)
|
10
|
+
# 2. For non repeated, it identifies if there's a gap (jump of multiple levels)
|
11
|
+
# 3. It covers the gap if present by decoupling merged parent(s) from the same node (see node.decouple)
|
12
|
+
# 4. Then, it delegates the filling in of parents to `fill_in_parents` function.
|
13
|
+
# @return [Array<NodeLevel>] child to parent relationships solved and no double-ups.
|
14
|
+
def tidy_nodes(nodes, prev_level: 0, main: true)
|
15
|
+
reset_trackers! if main
|
16
|
+
nodes.each_with_object([]) do |node, out|
|
17
|
+
node_id = node.id
|
18
|
+
if done_ids.include?(node_id)
|
19
|
+
repeated_ids << "#{node_id} (level: #{node.level})"
|
20
|
+
else
|
21
|
+
level = node.actual_level
|
22
|
+
if level > prev_level + 1
|
23
|
+
gap = level - (prev_level + 1)
|
24
|
+
msg = "(Row: #{node.row_num}) ID/Tag '#{node_id}' (lev #{level}) jumps #{gap} level(s) (expected #{prev_level + 1})."
|
25
|
+
#puts " " + node.tags_array.pretty_inspect
|
26
|
+
missing_nodes = node.decouple(gap)
|
27
|
+
|
28
|
+
msg << "\n Adding missing upper level(s): " + missing_nodes.map(&:raw_tag).pretty_inspect
|
29
|
+
log(:info) { msg }
|
30
|
+
|
31
|
+
out.push(*tidy_nodes(missing_nodes, prev_level: prev_level, main: false))
|
32
|
+
# puts node.actual_level
|
33
|
+
# pp node.tags_array
|
34
|
+
level = prev_level + 1
|
35
|
+
end
|
36
|
+
out << node
|
37
|
+
done_ids << node_id
|
38
|
+
prev_level = level
|
39
|
+
end
|
40
|
+
end.yield_self do |out|
|
41
|
+
report_repeated_node_ids(repeated_ids) if main
|
42
|
+
fill_in_parents(out)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Sets the `parentId` property.
|
47
|
+
def fill_in_parents(nodes)
|
48
|
+
nodes.tap do |nodes|
|
49
|
+
prev_nodes = empty_level_tracker_hash(11)
|
50
|
+
nodes.each do |node|
|
51
|
+
if parent_node = prev_nodes[node.actual_level - 1]
|
52
|
+
node.parentId = parent_node.id
|
53
|
+
end
|
54
|
+
prev_nodes[node.raw_level] = node
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Tracker helper (those repeated)
|
60
|
+
def repeated_ids
|
61
|
+
@repeated_ids ||= []
|
62
|
+
end
|
63
|
+
|
64
|
+
# Tracker helper (those done)
|
65
|
+
def done_ids
|
66
|
+
@done_ids ||= []
|
67
|
+
end
|
68
|
+
|
69
|
+
def reset_trackers!
|
70
|
+
@done_ids = []
|
71
|
+
@repeated_ids = []
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|