eco-helpers 2.6.0 → 2.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -4
  3. data/README.md +5 -0
  4. data/eco-helpers.gemspec +1 -1
  5. data/lib/eco/api/common/class_helpers.rb +1 -1
  6. data/lib/eco/api/common/loaders/case_base.rb +0 -2
  7. data/lib/eco/api/common/loaders/config/workflow/mailer.rb +78 -0
  8. data/lib/eco/api/common/loaders/config/workflow.rb +11 -0
  9. data/lib/eco/api/common/loaders/config.rb +29 -0
  10. data/lib/eco/api/common/loaders/error_handler.rb +0 -2
  11. data/lib/eco/api/common/loaders/parser.rb +0 -1
  12. data/lib/eco/api/common/loaders/policy.rb +0 -2
  13. data/lib/eco/api/common/loaders.rb +1 -0
  14. data/lib/eco/api/common/session/mailer.rb +3 -1
  15. data/lib/eco/api/common/version_patches/exception.rb +2 -2
  16. data/lib/eco/api/common/version_patches/ruby3/object.rb +18 -0
  17. data/lib/eco/api/common/version_patches/ruby3.rb +1 -0
  18. data/lib/eco/api/common/version_patches.rb +3 -0
  19. data/lib/eco/api/custom/config.rb +10 -0
  20. data/lib/eco/api/custom/mailer.rb +9 -0
  21. data/lib/eco/api/custom/namespace.rb +2 -0
  22. data/lib/eco/api/custom/workflow.rb +9 -0
  23. data/lib/eco/api/custom.rb +3 -0
  24. data/lib/eco/api/session/batch/base_policy.rb +13 -5
  25. data/lib/eco/api/session/batch/job.rb +10 -7
  26. data/lib/eco/api/session/config/workflow.rb +94 -58
  27. data/lib/eco/api/session/config.rb +2 -2
  28. data/lib/eco/api/usecases/base_io.rb +50 -4
  29. data/lib/eco/api/usecases/cli/dsl.rb +23 -13
  30. data/lib/eco/api/usecases/default/locations/cli/tagtree_extract_cli.rb +5 -0
  31. data/lib/eco/api/usecases/default/locations/tagtree_extract_case.rb +12 -4
  32. data/lib/eco/api/usecases/graphql/helpers/location/base.rb +1 -2
  33. data/lib/eco/api/usecases/ooze_samples/register_update_case.rb +3 -3
  34. data/lib/eco/api/usecases/use_case.rb +12 -2
  35. data/lib/eco/assets.rb +2 -2
  36. data/lib/eco/cli_default/workflow.rb +102 -120
  37. data/lib/eco/data/locations/node_base/tag_validations.rb +19 -9
  38. data/lib/eco/data/locations/node_base/treeify.rb +193 -18
  39. data/lib/eco/data/locations/node_level.rb +1 -1
  40. data/lib/eco/data/locations/node_plain/parsing.rb +1 -1
  41. data/lib/eco/data/locations/node_plain/serial.rb +1 -1
  42. data/lib/eco/data/locations/node_plain.rb +4 -3
  43. data/lib/eco/language/klass/when_inherited.rb +17 -0
  44. data/lib/eco/language/klass.rb +8 -0
  45. data/lib/eco/language/methods/delegate_missing.rb +28 -0
  46. data/lib/eco/language/methods/dsl_able.rb +25 -0
  47. data/lib/eco/language/methods.rb +9 -0
  48. data/lib/eco/language.rb +2 -0
  49. data/lib/eco/version.rb +1 -1
  50. metadata +16 -3
@@ -1,189 +1,171 @@
1
- ASSETS.cli.config do |config|
1
+ ASSETS.cli do |cli|
2
2
  ASSETS.config.workflow do |wf|
3
- io = nil
4
- rescued = false
5
- cases_with_input = nil
6
- cases_with_output = nil
7
3
 
4
+ rescued = false
8
5
  # default rescue
9
- wf.rescue do |exception, io|
10
- begin
11
- next io if rescued
12
- rescued = true
13
-
14
- io.session.logger.debug(exception.patch_full_message)
15
- wf.run(:close, io: io)
16
- rescue Exception => e
17
- puts "Some problem in workflow.rescue: #{e}"
18
- end
19
- io
6
+ wf.rescue do |err, io|
7
+ next io if rescued
8
+ rescued = true
9
+ log(:debug) { err.patch_full_message }
10
+ wf.run(:close, io: io)
11
+ rescue StandardError => e
12
+ puts "Some problem in workflow.rescue: #{e}"
20
13
  end
21
14
 
22
- wf.on(:options) do |wf_options, io|
23
- config.usecases.cli_apply(io: io)
24
- io = io.new(options: config.options_set.process(io: io))
15
+ wf.on(:options) do |_wf_opt, io|
16
+ cli.config.usecases.cli_apply(io: io)
17
+ io.new(options: cli.config.options_set.process(io: io))
25
18
  end
26
19
 
27
20
  wf.for(:load) do |wf_load|
28
- wf_load.for(:input) do |wf_input|
29
- wf_input.on(:get) do |wf_input_get, io|
30
- cases_with_input = config.usecases.active(io: io).select do |usecase, data|
21
+ wf_load.for(:input) do |wf_in|
22
+ wf_in.on(:get) do |_wf_ig, io|
23
+ cases_with_input = cli.config.usecases.active(io: io).select do |usecase, data|
31
24
  io.class.input_required?(usecase.type)
32
25
  end
33
26
 
34
- input_is_required = !cases_with_input.empty? || io.options.dig(:input, :entries_from)
35
- missing_input = !io.input || io.input.empty?
36
- next io unless missing_input && input_is_required
27
+ input_is_required = !cases_with_input.empty? || options.dig(:input, :entries_from)
28
+ missing_input = !input || input.empty?
29
+ next unless missing_input && input_is_required
37
30
 
38
- if io.options.dig(:input, :entries_from)
39
- io = io.new(input: config.input.get(io: io))
31
+ if options.dig(:input, :entries_from)
32
+ io.new(input: cli.config.input.get(io: io))
40
33
  else
41
34
  opt_case = cases_with_input.values.first.option
42
- io = io.new(input: config.input.get(io: io, option: opt_case))
35
+ io.new(input: cli.config.input.get(io: io, option: opt_case))
43
36
  end
44
- io
45
37
  end
46
38
 
47
- wf_input.on(:filter) do |wf_input_filter, io|
48
- next io unless io.input && !io.input.empty?
49
- io = io.new(input: config.input_filters.process(io: io))
39
+ wf_in.on(:filter) do |_wf_if, io|
40
+ next unless input && !input.empty?
41
+ io.new(input: cli.config.input_filters.process(io: io))
50
42
  end
51
43
  end
52
44
 
53
- wf_load.for(:people) do |wf_people|
54
- wf_people.on(:get) do |wf_people_get, io|
55
- cases_with_people = config.usecases.active(io: io).select do |usecase, data|
45
+ wf_load.for(:people) do |wf_peo|
46
+ wf_peo.on(:get) do |_wf_pg, io|
47
+ cases_with_people = cli.config.usecases.active(io: io).select do |usecase, data|
56
48
  io.class.people_required?(usecase.type)
57
49
  end
58
- next io if cases_with_people.empty? && !io.options.dig(:people, :get)
59
- io = io.new(people: config.people(io: io))
50
+ next if cases_with_people.empty? && !options.dig(:people, :get)
51
+ io.new(people: cli.config.people(io: io))
60
52
  end
61
53
 
62
- wf_people.on(:filter) do |wf_people_filter, io|
63
- next io unless io.people && !io.people.empty?
64
- io = io.new(people: config.people_filters.process(io: io))
54
+ wf_peo.on(:filter) do |_wf_pf, io|
55
+ next unless people && !people.empty?
56
+ io.new(people: cli.config.people_filters.process(io: io))
65
57
  end
66
58
  end
67
59
  end
68
60
 
69
- wf.before(:usecases) do |wf_cases, io|
61
+ wf.before(:usecases) do |_wf_ca, io|
70
62
  # save partial entries -> should be native to session.workflow
71
- get_people = io.options.dig(:people, :get)
63
+ get_people = options.dig(:people, :get)
72
64
  partial_update = get_people && get_people.dig(:type) == :partial
73
- if !io.options[:dry_run] && partial_update
74
- partial_file = io.session.config.people.partial_cache
75
- io.session.file_manager.save_json(io.people, partial_file, :timestamp)
65
+ if !options[:dry_run] && partial_update
66
+ partial_file = session.config.people.partial_cache
67
+ session.file_manager.save_json(io.people, partial_file, :timestamp)
76
68
  end
77
- io
78
69
  end
79
70
 
80
- wf.on(:usecases) do |wf_cases, io|
81
- unless config.usecases.process(io: io)
82
- msg = "No update operation specified... quitting"
83
- io.session.logger.info(msg)
84
- exit(0)
71
+ wf.on(:usecases) do |_wf_ca, io|
72
+ unless cli.config.usecases.process(io: io)
73
+ log(:info) { "No update operation specified... quitting" }
74
+ exit 0
85
75
  end
86
- io
87
76
  end
88
77
 
89
- wf.before(:launch_jobs) do |wf_jobs, io|
78
+ wf.before(:launch_jobs) do
90
79
  SCR.stop_on_unknown!
91
- io
92
80
  end
93
81
 
94
- wf.on(:launch_jobs) do |wf_jobs, io|
95
- io.session.jobs_launch(simulate: io.options[:dry_run])
96
- io
82
+ wf.on(:launch_jobs) do
83
+ session.jobs_launch(simulate: options[:dry_run])
97
84
  end
98
85
 
99
86
  wf.before(:post_launch) do |wf_post, io|
100
- if io.session.post_launch.empty?
87
+ next wf_post.skip! if session.post_launch.empty?
88
+
89
+ run_it = !options[:dry_run] || options.dig(:post_launch, :run)
90
+ unless run_it
101
91
  wf_post.skip!
102
- else
103
- get_people = io.options.dig(:people, :get)
104
- partial_update = get_people && get_people.dig(:type) == :partial
105
- run_it = !io.options[:dry_run] || io.options.dig(:post_launch, :run)
106
- refresh_data = !io.options[:dry_run] && partial_update
107
- if run_it
108
- if refresh_data
109
- # get target people afresh
110
- people = io.session.micro.people_refresh(people: io.people, include_created: true)
111
- io = io.base.new(people: people)
112
- else
113
- msg = "Although there are post_launch cases, data will not be refreshed before their run"
114
- if io.options[:dry_run]
115
- msg += ", because we are in dry-run (simulate)."
116
- elsif !partial_update
117
- msg += ", because it is not a partial update (-get-partial option not present)."
118
- end
119
- io.session.logger.info(msg)
120
- end
121
- else
122
- wf_post.skip!
123
- msg = "Although there are post_launch cases, they will NOT be RUN"
92
+ log(:info) {
93
+ msg = "Although there are post_launch cases, they will NOT be RUN"
94
+ msg += ", because we are in dry-run (simulate)." if options[:dry_run]
95
+ msg
96
+ }
97
+ next
98
+ end
99
+
100
+ get_people = options.dig(:people, :get)
101
+ partial_update = get_people && get_people.dig(:type) == :partial
102
+ refresh_data = !options[:dry_run] && partial_update
103
+
104
+ unless refresh_data
105
+ log(:info) {
106
+ msg = "Although there are post_launch cases, data will not be refreshed before their run"
124
107
  if io.options[:dry_run]
125
- msg+= ", because we are in dry-run (simulate)."
108
+ msg += ", because we are in dry-run (simulate)."
109
+ elsif !partial_update
110
+ msg += ", because it is not a partial update (-get-partial option not present)."
126
111
  end
127
- io.session.logger.info(msg)
128
- end
112
+ msg
113
+ }
114
+ next
129
115
  end
130
- io
116
+
117
+ # get target people afresh
118
+ peo_aux = session.micro.people_refresh(people: people, include_created: true)
119
+ io.base.new(people: peo_aux)
131
120
  end
132
121
 
133
122
  wf.for(:post_launch) do |wf_post|
134
-
135
- wf_post.on(:usecases) do |wf_postcases, io|
136
- io.session.post_launch.each do |use|
137
- begin
138
- io = use.launch(io: io).base
139
- rescue Eco::API::UseCases::BaseIO::MissingParameter => e
140
- if e.required == :people
141
- io.session.logger.debug("Skipping use case '#{use.name}' -- no base people detected for the current run")
142
- else
143
- raise
144
- end
145
- end
123
+ wf_post.on(:usecases) do |_wf_pu, io|
124
+ session.post_launch.each do |use|
125
+ use.launch(io: io).base
126
+ rescue Eco::API::UseCases::BaseIO::MissingParameter => e
127
+ raise unless e.required == :people
128
+ log(:debug) {
129
+ "Skipping use case '#{use.name}' -- no base people detected for the current run"
130
+ }
146
131
  end
147
- io
148
132
  end
149
133
 
150
- wf_post.on(:launch_jobs) do |wf_postlaunch, io|
151
- io.session.jobs_launch(simulate: io.options[:dry_run])
152
- io
134
+ wf_post.on(:launch_jobs) do |_wf_pl, io|
135
+ session.jobs_launch(simulate: options[:dry_run])
153
136
  end
154
137
  end
155
138
 
156
- wf.on(:report) do |wf_report, io|
157
- io.tap do |_io|
158
- if file = io.options.dig(:report, :people, :csv)
159
- io.options.deep_merge!(export: {
160
- options: {internal_names: true, nice_header: true, split_schemas: true},
161
- file: {name: file, format: :csv}
162
- })
163
- aux_io = io.new(people: io.people.updated_or_created)
164
- io.session.process_case("to-csv", io: aux_io, type: :export)
165
- end
139
+ wf.on(:report) do |_wf_rep, io|
140
+ if file = options.dig(:report, :people, :csv)
141
+ options.deep_merge!(export: {
142
+ options: {internal_names: true, nice_header: true, split_schemas: true},
143
+ file: {name: file, format: :csv}
144
+ })
145
+ aux_io = io.new(people: people.updated_or_created)
146
+ session.process_case("to-csv", io: aux_io, type: :export)
166
147
  end
167
148
  end
168
149
 
169
- wf.on(:end) do |wf_end, io|
170
- get_people = io.options.dig(:people, :get)
150
+ wf.on(:end) do |_wf_end, io|
151
+ get_people = options.dig(:people, :get)
171
152
  partial_update = get_people && get_people.dig(:type) == :partial
172
153
 
173
- unless !io.options[:end_get] || io.options[:dry_run] || partial_update
174
- people_update_cases = config.usecases.active(io: io).any? do |usecase, data|
175
- [:transform, :sync].any? {|type| usecase.type == type}
154
+ unless !options[:end_get] || options[:dry_run] || partial_update
155
+ people_update_cases = cli.config.usecases.active(io: io).any? do |usecase, data|
156
+ [:transform, :sync].any? { |type| usecase.type == type }
176
157
  end
177
158
 
178
159
  if !people_update_cases
179
160
  # Prevent getting people when there were no use cases that used them
180
- io.session.logger.info("Won't be recaching people, as there haven't been any targetted updates")
181
- elsif !io.people
182
- people = io.session.micro.people_cache
183
- io = io.new(people: people)
161
+ log(:info) {
162
+ "Won't be recaching people, as there haven't been any targetted updates"
163
+ }
164
+ elsif !people
165
+ people = session.micro.people_cache
166
+ io.new(people: people)
184
167
  end
185
168
  end
186
- io
187
169
  end
188
170
  end
189
171
  end
@@ -1,33 +1,43 @@
1
1
  module Eco::Data::Locations::NodeBase
2
2
  module TagValidations
3
+ include Eco::Language::AuxiliarLogger
4
+
3
5
  ALLOWED_CHARACTERS = "A-Za-z0-9 &_'\/.-"
4
6
  VALID_TAG_REGEX = /^[#{ALLOWED_CHARACTERS}]+$/
5
7
  INVALID_TAG_REGEX = /[^#{ALLOWED_CHARACTERS}]+/
6
8
  VALID_TAG_CHARS = /[#{ALLOWED_CHARACTERS}]+/
7
9
  DOUBLE_BLANKS = /\s\s+/
8
10
 
9
- def clean_id(str, notify: true)
11
+ def clean_id(str, notify: true, ref: '')
10
12
  blanks_x2 = has_double_blanks?(str)
11
13
  partial = replace_not_allowed(str)
12
14
  remove_double_blanks(partial).tap do |result|
13
15
  next unless notify
14
- next if invalid_warned?
16
+ next if invalid_warned?(str)
15
17
  if partial != str
16
18
  invalid_chars = identify_invalid_characters(str)
17
- puts "• (Row: #{self.row_num}) Invalid characters _#{invalid_chars}_ (removed): '#{str}' (converted to '#{result}')"
19
+ log(:warn) {
20
+ "• #{ref}Invalid characters _#{invalid_chars}_ <<_removed_: '#{str}' :_converted_>> '#{result}'"
21
+ }
18
22
  elsif blanks_x2
19
- puts "• (Row: #{self.row_num}) Double blanks (removed): '#{str}' (converted to '#{result}')"
23
+ log(:warn) {
24
+ "• #{ref}Double blanks removed: '#{str}' :_converted_>> '#{result}'"
25
+ }
20
26
  end
21
- invalid_warned!
27
+ invalid_warned!(str)
22
28
  end
23
29
  end
24
30
 
25
- def invalid_warned?
26
- @invalid_warned ||= false
31
+ def invalid_warned?(str)
32
+ invalid_warned[str] ||= false
33
+ end
34
+
35
+ def invalid_warned!(str)
36
+ invalid_warned[str] = true
27
37
  end
28
38
 
29
- def invalid_warned!
30
- @invalid_warned = true
39
+ def invalid_warned
40
+ @invalid_warned ||= {}
31
41
  end
32
42
 
33
43
  def has_double_blanks?(str)
@@ -12,14 +12,31 @@ module Eco::Data::Locations::NodeBase
12
12
  # @yieldreturn [Hash] custom hash model when treeifying (allows to set more keys/properties).
13
13
  # @nodes [Array<NodeBase>] list of nodes
14
14
  # @return [Array<Hash>] a hierarchical tree of nested Hashes via `nodes` key.
15
- def treeify(nodes, &block)
15
+ def treeify(nodes, skipped: [], unlinked_trees: [], &block)
16
16
  return [] if nodes.empty?
17
17
  block ||= nodes.first.class.serializer
18
- get_children(nil, parents_hash(nodes), &block)
18
+ done_ids = {}
19
+ warns = []
20
+ parents = parents_hash(nodes)
21
+ get_children(nil, parents, done_ids: done_ids, skipped: skipped, warns: warns, &block).tap do |tree|
22
+ check_results(
23
+ tree,
24
+ nodes,
25
+ parents,
26
+ done_ids: done_ids,
27
+ skipped: skipped,
28
+ unlinked_trees: unlinked_trees,
29
+ warns: warns,
30
+ &block
31
+ )
32
+ log(:warn) { warns.join("\n") } unless warns.empty?
33
+ end
19
34
  end
20
35
 
21
36
  private
22
37
 
38
+ # @return [Hash] where `key`s are all the `parentId` of the nodes
39
+ # and `value` and `Array` of those nodes that have that `parentId`
23
40
  def parents_hash(nodes)
24
41
  nodes.each_with_object({}) do |node, parents|
25
42
  (parents[node.parentId] ||= []).push(node)
@@ -32,12 +49,21 @@ module Eco::Data::Locations::NodeBase
32
49
  # 3. The above can translate into some
33
50
  # @yield [node]
34
51
  # @yieldreturn [Hash] custom hash model when treeifying
35
- def get_children(node_id, parents, parent: nil, done_ids: {}, level: 0, &block)
52
+ def get_children(node_id, parents, parent: nil, level: 0, done_ids: {}, skipped: [], warns: [], &block)
36
53
  level_ids = []
37
54
  (parents[node_id] ||= []).each_with_object([]) do |child, results|
38
55
  # Skipping done id. Add proper warnings...
39
56
  # => 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]
57
+ next report_skipped_node(
58
+ child,
59
+ parent,
60
+ done_ids,
61
+ level,
62
+ level_ids,
63
+ parents,
64
+ skipped: skipped,
65
+ warns: warns
66
+ ) if done_ids[child.id]
41
67
 
42
68
  # Fill in tracking data
43
69
  child.parent = parent
@@ -52,8 +78,26 @@ module Eco::Data::Locations::NodeBase
52
78
  node_hash.merge(yield(child)) if block_given?
53
79
  # we must register the `id` before recursing down
54
80
  done_ids[child.id] = child
81
+
82
+ children = get_children(
83
+ child.id,
84
+ parents,
85
+ parent: child,
86
+ done_ids: done_ids,
87
+ level: level + 1,
88
+ skipped: skipped,
89
+ warns: warns,
90
+ &block
91
+ ).tap do |desc|
92
+ if (nil_count = desc.count(nil)) > 0
93
+ log(:debug) {
94
+ "get_children gave #{nil_count} nil values for nodes of #{child.id}"
95
+ }
96
+ end
97
+ end
98
+
55
99
  results << node_hash.merge({
56
- "nodes" => get_children(child.id, parents, parent: child, done_ids: done_ids, level: level + 1, &block).compact
100
+ "nodes" => children.compact
57
101
  })
58
102
  end
59
103
  end
@@ -70,8 +114,138 @@ module Eco::Data::Locations::NodeBase
70
114
  "#{" " * level}"
71
115
  end
72
116
 
73
- # Gives different warnings, depending the case
74
- def report_skipped_node(node, parent, done_ids, level, level_ids, parents)
117
+ # Method to ensure the results are consistent
118
+ # @param skipped [Array<NodePlain>] those skipped because repeated
119
+ # 1. It will add children of them that were skipped. This won't clash with unlinked nodes
120
+ # because otherwise would be part of `done_ids` anyway.
121
+ # @param unlinked_trees [Array<Hash>] by excluding those done and skipped,
122
+ # it will treeify the unlinked nodes (the exclusion applies to `parants_hash`)
123
+ def check_results(tree, nodes, parents, done_ids: {}, skipped: [], unlinked_trees: [], warns: [], &block)
124
+ update_skipped(skipped, parents, done_ids: done_ids) unless skipped.empty?
125
+
126
+ if done_ids.count != nodes.count
127
+ tracked_nodes = done_ids.values
128
+ untracked_nodes = nodes - tracked_nodes - skipped
129
+ # skipped keys is inherent, as they were excluded because of id clash with done_ids
130
+ unlinked_parent_ids = (parents.keys - done_ids.keys).compact
131
+
132
+ msg = []
133
+
134
+ # The reason of missing nodes in the output tree is unknown!
135
+ if skipped.empty? && unlinked_parent_ids.empty?
136
+ msg << "BUG in this library (open issue with maintainers)."
137
+ msg << "There were no skipped nodes nor missin referred parents, and yet:"
138
+ msg << " • the tree nodes count: #{done_ids.count} ..."
139
+ msg << " • doesn't match the original nodes count: #{nodes.count}"
140
+ raise msg.join("\n")
141
+ end
142
+
143
+ unless unlinked_parent_ids.empty?
144
+ msg << "There are #{unlinked_parent_ids.count} referred parent_id's NOT linked to the root:"
145
+ msg << " • total_nodes: #{nodes.count}"
146
+ msg << " • tracked_nodes: #{tracked_nodes.count}"
147
+ msg << " • untracked_nodes: #{untracked_nodes.count}"
148
+ msg << " • unlinked_parents: #{unlinked_parent_ids.count}"
149
+ msg << " • skipped (repeated) nodes: #{skipped.count}" unless skipped.empty?
150
+
151
+ unlinked_nodes = nodes - skipped
152
+ unlinked_parents = parents.slice(*unlinked_parent_ids) # doesn'thave skipped ones
153
+
154
+ residual_skipped = []
155
+ unlinked_trees.concat \
156
+ get_unlinked_trees(
157
+ unlinked_nodes,
158
+ unlinked_parents,
159
+ done_ids: done_ids,
160
+ skipped: residual_skipped,
161
+ warns: warns,
162
+ &block
163
+ )
164
+
165
+ update_skipped(skipped, parents, with: residual_skipped, done_ids: done_ids) unless residual_skipped.empty?
166
+
167
+ tracked_nodes = done_ids.values
168
+ untracked_nodes = nodes - tracked_nodes - skipped
169
+ unlinked_parent_ids = (parents.keys - done_ids.keys).compact
170
+
171
+ msg << "After treeifying via the unlinked_parents:"
172
+ msg << " • total_nodes: #{nodes.count}"
173
+ msg << " • tracked_nodes: #{tracked_nodes.count}"
174
+ msg << " • untracked_nodes: #{untracked_nodes.count}"
175
+ msg << " • unlinked_parents: #{unlinked_parent_ids.count}"
176
+ msg << " • skipped in this step: #{residual_skipped.count}"
177
+ end
178
+
179
+ msg << " • total skipped (repeated) nodes: #{skipped.count} !!" unless skipped.empty?
180
+ warns << msg.join("\n")
181
+ nil
182
+ end
183
+ end
184
+
185
+ # Treeifies the unlinked nodes by scoping existing parent ids.
186
+ def get_unlinked_trees(nodes, parents, done_ids: {}, skipped: [], warns: [], &block)
187
+ node_ids = nodes.map(&:id)
188
+ parent_ids = parents.keys & node_ids
189
+ missing_parent_ids = parents.keys - parent_ids
190
+ missing_parents = parents.slice(*missing_parent_ids)
191
+ warns << " • missing_parents: #{missing_parents.count}"
192
+ nil_parent_nodes = missing_parents.each_with_object([]) do |(id, nodes), mem|
193
+ nodes.each {|node| node.parent_id = nil}
194
+ mem.concat(nodes)
195
+ end
196
+ rest_parents = parents.slice(*parent_ids).merge({
197
+ nil => nil_parent_nodes
198
+ })
199
+ get_children(nil, rest_parents, done_ids: done_ids, skipped: skipped, warns: warns, &block)
200
+ end
201
+
202
+ # Same as `get_children` but not performing checks and with
203
+ # option to retrieve the source nodes (rather than parsing to `Hash`).
204
+ # @note serves the purpose to identify what linked children got inherently
205
+ # skipped, because their parent was skipped.
206
+ def get_tree_nodes_raw(node_id, parents, src_plain: true, &block)
207
+ (parents[node_id] ||= []).each_with_object([]) do |child, results|
208
+ unless src_plain
209
+ node_hash = {
210
+ "id" => child.id,
211
+ "name" => child.name,
212
+ "parent_id" => node_id
213
+ }
214
+ node_hash.merge(yield(child)) if block_given?
215
+ end
216
+
217
+ descendants = get_tree_nodes_raw(child.id, parents, src_plain: src_plain, &block).tap do |desc|
218
+ if (nil_count = desc.count(nil)) > 0
219
+ puts "get_tree_nodes_raw gave #{nil_count} nil values for nodes of #{child.id}"
220
+ end
221
+ end
222
+
223
+ if src_plain
224
+ results.concat(descendants)
225
+ else
226
+ results << node_hash.merge({
227
+ "nodes" => descendants.compact
228
+ })
229
+ end
230
+ end
231
+ end
232
+
233
+ # It goes through the `with` skipped nodes, and adds them to the `skipped` ones
234
+ # by including their not tracked/done/included children.
235
+ def update_skipped(skipped, parents, with: skipped, done_ids: {})
236
+ raw_skipped_children = with.each_with_object([]) do |node, mem|
237
+ mem << node
238
+ mem.concat get_tree_nodes_raw(node.id, parents)
239
+ end.uniq
240
+ skipped_children = raw_skipped_children - done_ids.values
241
+ skipped.concat(skipped_children).uniq!
242
+ skipped
243
+ end
244
+
245
+ # With given a skipped `node` (repeated `id`), it gives different warnings,
246
+ # provided that the context in which the double-up `id` happened is identified.
247
+ def report_skipped_node(node, parent, done_ids, level, level_ids, parents, skipped: [], warns: [])
248
+ skipped << node
75
249
  lev = level + 1
76
250
  done_node = done_ids[node.id]
77
251
  prev_parent = node.parent
@@ -84,6 +258,9 @@ module Eco::Data::Locations::NodeBase
84
258
  row_str = row_num ? "(Row: #{row_num}) " : ''
85
259
  node_str = "#{row_str}Node '#{node.id}' #{level_msg(lev)} (#{parent_msg(parent)})"
86
260
 
261
+ msg = []
262
+ msg << "#{indent(level)}Skipping #{node_str}."
263
+
87
264
  # Implementation integrity guard
88
265
  # => as we don't register in `done_ids` those that are skipped,
89
266
  # when a `node` has already a tracked `parent` or `level`,
@@ -114,18 +291,15 @@ module Eco::Data::Locations::NodeBase
114
291
  cyclic = multi_parent && done_node == node
115
292
  double_up = node_dup || lev_dup
116
293
 
117
- msg = []
118
- msg << "#{indent(level)}WARNING: Skipping #{node_str}."
119
-
120
294
  if cyclic
121
- str = "#{indent(level)+1}Cyclic definition. By skipping the node, "
295
+ str = "#{indent(level + 1)}Cyclic definition. By skipping the node, "
122
296
  str << "it will remain as #{parent_msg(done_node.parent)} (#{level_msg(prev_level)})."
123
297
  msg << str
124
298
  end
125
299
 
126
300
  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)} "
301
+ str = "#{indent(level + 1)}Node ID was already tracked as #{level_msg(done_node.tracked_level)}, "
302
+ str << "as #{parent_msg(done_node.parent)} "
129
303
  str << "(same parent)." if lev_dup
130
304
  str << "(different parent)." if multi_parent
131
305
  msg << str
@@ -133,18 +307,19 @@ module Eco::Data::Locations::NodeBase
133
307
 
134
308
  unless cyclic || double_up
135
309
  str = "Integrity issue in Treeify. "
136
- str = "Skipping is only applicable to double_ups or cyclic nodes."
310
+ str << "Skipping is only applicable to double_ups or cyclic nodes."
137
311
  str << "\n • #{node_str}."
138
312
  raise str
139
313
  end
140
314
 
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(", ")
315
+ unless (children = parents[node.id] || []).empty?
316
+ str = "#{indent(level + 1)}Immediate children of skipped node (will probably be missing): "
317
+ str << children.map {|ch| "'#{ch.id}'"}.join(", ")
144
318
  msg << str
145
319
  end
146
320
 
147
- log(:warn) { msg.join('\n') }
321
+ warns << msg.join("\n")
322
+ nil
148
323
  end
149
324
  end
150
325
  end
@@ -28,7 +28,7 @@ module Eco::Data::Locations
28
28
  end
29
29
 
30
30
  def tag
31
- clean_id(raw_tag)
31
+ clean_id(raw_tag, ref: "(Row: #{self.row_num}) ")
32
32
  end
33
33
 
34
34
  def raw_tag
@@ -20,7 +20,7 @@ class Eco::Data::Locations::NodePlain
20
20
  end
21
21
 
22
22
  # It builds each NodePlain from the input csv.
23
- # @param `csv` [CSV::Table]
23
+ # @param `csv` [CSV::Table] with specific headers
24
24
  # @return [Array<NodePlain>]
25
25
  def nodes_from_csv(csv)
26
26
  raise ArgumentError, "Expecting CSV::Table. Given: #{csv.class}" unless csv.is_a?(::CSV::Table)
@@ -6,7 +6,7 @@ class Eco::Data::Locations::NodePlain
6
6
  def serializer
7
7
  @serializer ||= proc do |node|
8
8
  raise "Expecting NodePlain. Given: #{node.class}" unless node.is_a?(Eco::Data::Locations::NodePlain)
9
- keys = Eco::Data::Locations::NodePlain::NODE_PLAIN_ATTRS
9
+ keys = Eco::Data::Locations::NodePlain::ALL_ATTRS
10
10
  node.to_h(*keys)
11
11
  end
12
12
  end