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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -2
  3. data/eco-helpers.gemspec +2 -2
  4. data/lib/eco/api/common/loaders/use_case.rb +0 -2
  5. data/lib/eco/api/common/people/person_entry_attribute_mapper.rb +0 -2
  6. data/lib/eco/api/common/session/logger.rb +22 -77
  7. data/lib/eco/api/microcases/with_each.rb +0 -1
  8. data/lib/eco/api/organization/tag_tree.rb +64 -15
  9. data/lib/eco/api/session/config/tagtree.rb +32 -10
  10. data/lib/eco/api/session/config/workflow.rb +0 -1
  11. data/lib/eco/api/session/config.rb +6 -2
  12. data/lib/eco/api/session.rb +2 -2
  13. data/lib/eco/api/usecases/default_cases/abstract_policygroup_abilities_case.rb +2 -3
  14. data/lib/eco/api/usecases/default_cases/analyse_people_case.rb +2 -3
  15. data/lib/eco/api/usecases/default_cases/append_usergroups_case.rb +0 -1
  16. data/lib/eco/api/usecases/default_cases/change_email_case.rb +1 -2
  17. data/lib/eco/api/usecases/default_cases/clean_unknown_tags_case.rb +0 -5
  18. data/lib/eco/api/usecases/default_cases/clear_abilities_case.rb +2 -2
  19. data/lib/eco/api/usecases/default_cases/codes_to_tags_case.rb +5 -7
  20. data/lib/eco/api/usecases/default_cases/create_case.rb +0 -5
  21. data/lib/eco/api/usecases/default_cases/create_details_case.rb +0 -5
  22. data/lib/eco/api/usecases/default_cases/create_details_with_supervisor_case.rb +0 -5
  23. data/lib/eco/api/usecases/default_cases/csv_to_tree_case/helper.rb +1 -1
  24. data/lib/eco/api/usecases/default_cases/csv_to_tree_case.rb +0 -4
  25. data/lib/eco/api/usecases/default_cases/delete_sync_case.rb +2 -4
  26. data/lib/eco/api/usecases/default_cases/delete_trans_case.rb +2 -3
  27. data/lib/eco/api/usecases/default_cases/email_as_id_case.rb +0 -1
  28. data/lib/eco/api/usecases/default_cases/entries_to_csv_case.rb +0 -4
  29. data/lib/eco/api/usecases/default_cases/hris_case.rb +2 -3
  30. data/lib/eco/api/usecases/default_cases/new_email_case.rb +0 -2
  31. data/lib/eco/api/usecases/default_cases/new_id_case.rb +0 -2
  32. data/lib/eco/api/usecases/default_cases/org_data_convert_case.rb +0 -5
  33. data/lib/eco/api/usecases/default_cases/refresh_case.rb +0 -1
  34. data/lib/eco/api/usecases/default_cases/reinvite_sync_case.rb +1 -3
  35. data/lib/eco/api/usecases/default_cases/reinvite_trans_case.rb +2 -2
  36. data/lib/eco/api/usecases/default_cases/remove_account_sync_case.rb +1 -2
  37. data/lib/eco/api/usecases/default_cases/remove_account_trans_case.rb +2 -3
  38. data/lib/eco/api/usecases/default_cases/reset_landing_page_case.rb +1 -7
  39. data/lib/eco/api/usecases/default_cases/restore_db_case.rb +0 -10
  40. data/lib/eco/api/usecases/default_cases/set_default_tag_case.rb +0 -1
  41. data/lib/eco/api/usecases/default_cases/set_supervisor_case.rb +0 -1
  42. data/lib/eco/api/usecases/default_cases/supers_cyclic_identify_case.rb +2 -3
  43. data/lib/eco/api/usecases/default_cases/supers_hierarchy_case.rb +2 -3
  44. data/lib/eco/api/usecases/default_cases/switch_supervisor_case.rb +2 -4
  45. data/lib/eco/api/usecases/default_cases/tagtree_case.rb +0 -2
  46. data/lib/eco/api/usecases/default_cases/to_csv_case.rb +4 -5
  47. data/lib/eco/api/usecases/default_cases/to_csv_detailed_case.rb +0 -1
  48. data/lib/eco/api/usecases/default_cases/transfer_account_case.rb +0 -2
  49. data/lib/eco/api/usecases/default_cases/update_case.rb +0 -2
  50. data/lib/eco/api/usecases/default_cases/update_details_case.rb +0 -2
  51. data/lib/eco/api/usecases/default_cases/upsert_case.rb +0 -4
  52. data/lib/eco/api/usecases/graphql/base.rb +6 -18
  53. data/lib/eco/api/usecases/graphql/helpers/base/case_env.rb +15 -0
  54. data/lib/eco/api/usecases/graphql/helpers/base.rb +23 -0
  55. data/lib/eco/api/usecases/graphql/helpers/location/base.rb +87 -0
  56. data/lib/eco/api/usecases/graphql/helpers/location/command/result.rb +69 -0
  57. data/lib/eco/api/usecases/graphql/helpers/location/command/results.rb +126 -0
  58. data/lib/eco/api/usecases/graphql/helpers/location/command.rb +92 -0
  59. data/lib/eco/api/usecases/graphql/helpers/location.rb +7 -0
  60. data/lib/eco/api/usecases/graphql/helpers.rb +2 -1
  61. data/lib/eco/api/usecases/graphql/samples/location/command/dsl.rb +54 -0
  62. data/lib/eco/api/usecases/graphql/samples/location/command/results.rb +125 -0
  63. data/lib/eco/api/usecases/graphql/samples/location/command.rb +10 -0
  64. data/lib/eco/api/usecases/graphql/samples/location/dsl.rb +6 -0
  65. data/lib/eco/api/usecases/graphql/samples/location.rb +10 -0
  66. data/lib/eco/api/usecases/graphql/samples.rb +6 -0
  67. data/lib/eco/api/usecases/graphql/utils/sftp.rb +74 -0
  68. data/lib/eco/api/usecases/graphql/utils.rb +6 -0
  69. data/lib/eco/api/usecases/graphql.rb +3 -1
  70. data/lib/eco/api/usecases/ooze_cases/export_register_case.rb +0 -1
  71. data/lib/eco/api/usecases/ooze_samples/ooze_base_case.rb +0 -2
  72. data/lib/eco/api/usecases/ooze_samples/register_migration_case.rb +0 -2
  73. data/lib/eco/api/usecases/use_case.rb +2 -2
  74. data/lib/eco/cli/config/default/workflow.rb +2 -4
  75. data/lib/eco/cli/scripting/args_helpers.rb +0 -2
  76. data/lib/eco/csv/table.rb +39 -3
  77. data/lib/eco/data/files/helpers.rb +4 -3
  78. data/lib/eco/data/hashes/array_diff.rb +21 -61
  79. data/lib/eco/data/hashes/diff_meta.rb +52 -0
  80. data/lib/eco/data/hashes/diff_result.rb +36 -25
  81. data/lib/eco/data/hashes.rb +1 -0
  82. data/lib/eco/data/locations/convert.rb +92 -0
  83. data/lib/eco/data/locations/dsl.rb +35 -0
  84. data/lib/eco/data/locations/node_base/builder.rb +26 -0
  85. data/lib/eco/data/locations/node_base/csv_convert.rb +57 -0
  86. data/lib/eco/data/locations/node_base/parsing.rb +30 -0
  87. data/lib/eco/data/locations/node_base/serial.rb +26 -0
  88. data/lib/eco/data/locations/node_base/tag_validations.rb +52 -0
  89. data/lib/eco/data/locations/node_base/treeify.rb +150 -0
  90. data/lib/eco/data/locations/node_base.rb +48 -0
  91. data/lib/eco/data/locations/node_diff/accessors.rb +46 -0
  92. data/lib/eco/data/locations/node_diff/nodes_diff.rb +90 -0
  93. data/lib/eco/data/locations/node_diff/selectors.rb +20 -0
  94. data/lib/eco/data/locations/node_diff.rb +55 -0
  95. data/lib/eco/data/locations/node_level/builder.rb +6 -0
  96. data/lib/eco/data/locations/node_level/cleaner.rb +74 -0
  97. data/lib/eco/data/locations/node_level/parsing.rb +63 -0
  98. data/lib/eco/data/locations/node_level/serial.rb +37 -0
  99. data/lib/eco/data/locations/node_level.rb +153 -0
  100. data/lib/eco/data/locations/node_plain/builder.rb +6 -0
  101. data/lib/eco/data/locations/node_plain/parsing.rb +36 -0
  102. data/lib/eco/data/locations/node_plain/serial.rb +14 -0
  103. data/lib/eco/data/locations/node_plain.rb +31 -0
  104. data/lib/eco/data/locations.rb +13 -0
  105. data/lib/eco/data.rb +1 -0
  106. data/lib/eco/language/auxiliar_logger.rb +9 -1
  107. data/lib/eco/language/basic_logger.rb +74 -0
  108. data/lib/eco/language.rb +2 -1
  109. data/lib/eco/version.rb +1 -1
  110. metadata +45 -8
  111. data/lib/eco/api/usecases/default_cases/new_id_case0.rb +0 -14
  112. data/lib/eco/api/usecases/graphql/helpers/locations/commands.rb +0 -4
  113. 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,6 @@
1
+ class Eco::Data::Locations::NodeLevel
2
+ module Builder
3
+ include Eco::Data::Locations::NodeLevel::Parsing
4
+ include Eco::Data::Locations::NodeLevel::Serial
5
+ end
6
+ 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