eco-helpers 3.0.18 → 3.0.20

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +34 -3
  4. data/eco-helpers.gemspec +3 -3
  5. data/lib/eco/api/common/loaders/config/session.rb +12 -0
  6. data/lib/eco/api/common/loaders/config/workflow/mailer.rb +17 -4
  7. data/lib/eco/api/common/loaders/config.rb +10 -2
  8. data/lib/eco/api/common/loaders/parser.rb +10 -0
  9. data/lib/eco/api/common/people/default_parsers/csv_parser.rb +21 -208
  10. data/lib/eco/api/common/people/default_parsers/helpers/expected_headers.rb +206 -0
  11. data/lib/eco/api/common/people/default_parsers/helpers/null_parsing.rb +36 -0
  12. data/lib/eco/api/common/people/default_parsers/helpers.rb +15 -0
  13. data/lib/eco/api/common/people/default_parsers/json_parser.rb +56 -0
  14. data/lib/eco/api/common/people/default_parsers/xls_parser.rb +13 -14
  15. data/lib/eco/api/common/people/default_parsers.rb +2 -0
  16. data/lib/eco/api/common/people/entry_factory.rb +15 -4
  17. data/lib/eco/api/common/session/sftp.rb +5 -0
  18. data/lib/eco/api/custom/mailer.rb +1 -0
  19. data/lib/eco/api/error.rb +4 -0
  20. data/lib/eco/api/session/batch/job.rb +14 -16
  21. data/lib/eco/api/session/batch/jobs.rb +6 -8
  22. data/lib/eco/api/session/batch/launcher/mode_size.rb +5 -2
  23. data/lib/eco/api/session/batch/launcher/retry.rb +6 -1
  24. data/lib/eco/api/session/batch/launcher/status_handling.rb +4 -2
  25. data/lib/eco/api/session/batch/launcher.rb +3 -3
  26. data/lib/eco/api/session/config/api.rb +1 -0
  27. data/lib/eco/api/session/config/apis/one_off.rb +6 -6
  28. data/lib/eco/api/session/config/workflow.rb +16 -3
  29. data/lib/eco/api/session.rb +13 -7
  30. data/lib/eco/api/usecases/default/locations/tagtree_extract_case.rb +1 -0
  31. data/lib/eco/api/usecases/default/locations/tagtree_upload_case.rb +2 -0
  32. data/lib/eco/api/usecases/default/utils/cli/group_csv_cli.rb +26 -0
  33. data/lib/eco/api/usecases/default/utils/cli/json_to_csv_cli.rb +10 -0
  34. data/lib/eco/api/usecases/default/utils/cli/sort_csv_cli.rb +17 -0
  35. data/lib/eco/api/usecases/default/utils/cli/split_json_cli.rb +15 -0
  36. data/lib/eco/api/usecases/default/utils/group_csv_case.rb +213 -0
  37. data/lib/eco/api/usecases/default/utils/json_to_csv_case.rb +71 -0
  38. data/lib/eco/api/usecases/default/utils/sort_csv_case.rb +127 -0
  39. data/lib/eco/api/usecases/default/utils/split_json_case.rb +224 -0
  40. data/lib/eco/api/usecases/default/utils.rb +4 -0
  41. data/lib/eco/api/usecases/default_cases/samples/sftp_case.rb +22 -15
  42. data/lib/eco/api/usecases/ooze_cases/export_register_case.rb +6 -6
  43. data/lib/eco/api/usecases/ooze_samples/helpers/exportable_register.rb +1 -0
  44. data/lib/eco/api/usecases/ooze_samples/ooze_base_case.rb +1 -1
  45. data/lib/eco/api/usecases/ooze_samples/ooze_run_base_case.rb +8 -5
  46. data/lib/eco/cli_default/workflow.rb +10 -4
  47. data/lib/eco/csv/stream.rb +2 -0
  48. data/lib/eco/csv.rb +3 -2
  49. data/lib/eco/language/methods/delegate_missing.rb +4 -3
  50. data/lib/eco/version.rb +1 -1
  51. metadata +22 -9
@@ -71,7 +71,7 @@ module Eco
71
71
  tap_status(status: status, enviro: enviro, queue: data, method: method) do |overall_status|
72
72
  pending_for_server_error = data.to_a[0..]
73
73
 
74
- batch_mode_on(*RETRY_ON, options: options, allow_job_mode: job_mode) do |job_mode, per_page|
74
+ batch_mode_on(*RETRY_ON, options: options, allow_job_mode: job_mode) do |as_job_mode, per_page|
75
75
  iteration = 0
76
76
  done = 0
77
77
  iterations = (data.length.to_f / per_page).ceil
@@ -79,7 +79,7 @@ module Eco
79
79
  start_time = Time.now
80
80
 
81
81
  data.each_slice(per_page) do |slice|
82
- iteration += 1
82
+ iteration += 1
83
83
 
84
84
  msg = "starting batch '#{method}' iteration #{iteration}/#{iterations}, "
85
85
  msg << "with #{slice.length} entries of #{data.length} -- #{done} done"
@@ -89,7 +89,7 @@ module Eco
89
89
  start_slice = Time.now
90
90
 
91
91
  offer_retry_on(*RETRY_ON, retries_left: TIMEOUT_RETRIES) do
92
- people_api.batch(job_mode: job_mode) do |batch|
92
+ people_api.batch(job_mode: as_job_mode) do |batch|
93
93
  slice.each do |person|
94
94
  batch.public_send(method, person) do |response|
95
95
  faltal("Request with no response") unless response
@@ -135,6 +135,7 @@ module Eco
135
135
  self.class.description(self)
136
136
  end
137
137
 
138
+ # @todo: deletage to `apis.one_off?`
138
139
  def one_off?
139
140
  name.is_a?(Symbol)
140
141
  end
@@ -4,14 +4,14 @@ module Eco
4
4
  class Config
5
5
  class Apis
6
6
  module OneOff
7
- private
8
-
9
7
  def one_off?
10
- @is_one_off ||=
8
+ @is_one_off ||= # rubocop:disable Naming/MemoizedInstanceVariableName
11
9
  SCR.get_arg('-api-key') ||
12
10
  SCR.get_arg('-one-off')
13
11
  end
14
12
 
13
+ private
14
+
15
15
  def one_off_key
16
16
  return @one_off_key if instance_variable_defined?(:@one_off_key)
17
17
 
@@ -48,10 +48,10 @@ module Eco
48
48
  return @one_off_org if instance_variable_defined?(:@one_off_org)
49
49
 
50
50
  msg = "You should specify -org NAME when using -api-key or -one-off"
51
- raise msg unless org = SCR.get_arg('-org', with_param: true)
51
+ raise msg unless (org = SCR.get_arg('-org', with_param: true))
52
52
 
53
53
  str_org = "#{org.downcase.split(/[^a-z]+/).join('_')}_#{one_off_enviro.gsub('.', '_')}"
54
- @one_off_org ||= str_org.to_sym
54
+ @one_off_org ||= str_org.to_sym
55
55
  end
56
56
 
57
57
  def one_off_enviro
@@ -83,7 +83,7 @@ module Eco
83
83
 
84
84
  true
85
85
  rescue StandardError => err
86
- puts "#{err}"
86
+ puts err.to_s
87
87
  false
88
88
  end
89
89
  end
@@ -141,7 +141,8 @@ module Eco
141
141
  # @yieldreturn [Eco::API::UseCases::BaseIO] the `io` input/output object carried througout all the _workflow_
142
142
  # @return [Eco::API::Session::Config::Workflow] the current stage object (to ease chainig).
143
143
  def rescue(&block)
144
- return @rescue unless block
144
+ return @rescue unless block_given?
145
+
145
146
  @rescue = block
146
147
  self
147
148
  end
@@ -150,7 +151,8 @@ module Eco
150
151
 
151
152
  # Called on `SystemExit` exception
152
153
  def exit_handle(&block)
153
- return @exit_handle unless block
154
+ return @exit_handle unless block_given?
155
+
154
156
  @exit_handle = block
155
157
  self
156
158
  end
@@ -171,6 +173,7 @@ module Eco
171
173
  # @return [Eco::API::Session::Config::Workflow] the current stage object (to ease chainig).
172
174
  def before(key = nil, &block)
173
175
  raise ArgumentError, "A block should be given." unless block_given?
176
+
174
177
  if key
175
178
  stage(key).before(&block)
176
179
  else
@@ -195,6 +198,7 @@ module Eco
195
198
  # @return [Eco::API::Session::Config::Workflow] the current stage object (to ease chainig).
196
199
  def after(key = nil, &block)
197
200
  raise ArgumentError, "A block should be given." unless block_given?
201
+
198
202
  if key
199
203
  stage(key).after(&block)
200
204
  else
@@ -267,6 +271,7 @@ module Eco
267
271
  io.evaluate(self, io, &c)
268
272
  end
269
273
  end
274
+
270
275
  io
271
276
  end
272
277
 
@@ -276,6 +281,7 @@ module Eco
276
281
  io.evaluate(self, io, &c)
277
282
  end
278
283
  end
284
+
279
285
  io
280
286
  end
281
287
 
@@ -305,6 +311,7 @@ module Eco
305
311
  io.evaluate(self, io, &@on)
306
312
  end
307
313
  end
314
+
308
315
  io
309
316
  ensure
310
317
  @pending = false
@@ -341,7 +348,11 @@ module Eco
341
348
 
342
349
  def stage(key)
343
350
  self.class.validate_stage(key)
344
- @stages[key] ||= self.class.workflow_class(key).new(key, _parent: self, config: config)
351
+ @stages[key] ||= self.class.workflow_class(key).new(
352
+ key,
353
+ _parent: self,
354
+ config: config
355
+ )
345
356
  end
346
357
 
347
358
  # helper to treat trigger the exit and rescue handlers
@@ -354,6 +365,7 @@ module Eco
354
365
  io = io_result(io: io) do
355
366
  io.evaluate(err, io, &exit_handle)
356
367
  end
368
+
357
369
  exit err.status
358
370
  rescue Interrupt => _int
359
371
  raise
@@ -362,6 +374,7 @@ module Eco
362
374
  io = io_result(io: io) do
363
375
  io.evaluate(err, io, &self.rescue)
364
376
  end
377
+
365
378
  raise
366
379
  end
367
380
  end
@@ -70,6 +70,7 @@ module Eco
70
70
  )
71
71
  if live && api?(version: :graphql)
72
72
  return live_tree(include_archived: include_archived, **kargs, &block) unless merge
73
+
73
74
  live_trees(include_archived: include_archived, **kargs, &block).inject(&:merge)
74
75
  else
75
76
  config.tagtree(recache: recache)
@@ -118,10 +119,12 @@ module Eco
118
119
  # @return [Eco::Data::Mapper] the mappings between the internal and external attribute/property names.
119
120
  def fields_mapper
120
121
  return @fields_mapper if instance_variable_defined?(:@fields_mapper)
122
+
121
123
  mappings = []
122
124
  if (map_file = config.people.fields_mapper)
123
125
  mappings = map_file ? file_manager.load_json(map_file) : []
124
126
  end
127
+
125
128
  @fields_mapper = Eco::Data::Mapper.new(mappings)
126
129
  end
127
130
 
@@ -132,7 +135,9 @@ module Eco
132
135
  # If `schema` is `nil` or not provided it uses the currently associated to the `session`
133
136
  def entry_factory(schema: nil)
134
137
  schema = to_schema(schema) || self.schema
138
+
135
139
  return @entry_factories[schema&.id] if @entry_factories.key?(schema&.id)
140
+
136
141
  unless @entry_factories.empty?
137
142
  @entry_factories[schema&.id] = @entry_factories.values.first.newFactory(schema: schema)
138
143
  return @entry_factories[schema&.id]
@@ -164,9 +169,9 @@ module Eco
164
169
  # @param phase [Symbol] the phase when this parser should be active.
165
170
  # @return [Object] the parsed attribute.
166
171
  def parse_attribute(attr, source, phase = :internal, deps: {})
167
- unless (parsers = entry_factory.person_parser)
168
- raise "There are no parsers defined"
169
- end
172
+ msg = "There are no parsers defined"
173
+ raise msg unless (parsers = entry_factory.person_parser)
174
+
170
175
  parsers.parse(attr, source, phase, deps: deps)
171
176
  end
172
177
 
@@ -388,18 +393,19 @@ module Eco
388
393
 
389
394
  # from schema `id` or `name` to a PersonSchema object
390
395
  def to_schema(value)
391
- return nil unless value
392
396
  sch = nil
397
+ return unless value
398
+
393
399
  case value
394
400
  when String
395
- unless (sch = schemas.schema(value))
396
- fatal "The schema with id or name '#{value}' does not exist."
397
- end
401
+ msg = "The schema with id or name '#{value}' does not exist."
402
+ fatal msg unless (sch = schemas.schema(value))
398
403
  when Ecoportal::API::V1::PersonSchema
399
404
  sch = value
400
405
  else
401
406
  fatal "Required String or Ecoportal::API::V1::PersonSchema. Given: #{value}"
402
407
  end
408
+
403
409
  sch
404
410
  end
405
411
  end
@@ -195,6 +195,7 @@ class Eco::API::UseCases::Default::Locations::TagtreeExtract < Eco::API::UseCase
195
195
 
196
196
  def excel(filename)
197
197
  require 'fast_excel'
198
+
198
199
  FastExcel.open(filename, constant_memory: true).tap do |workbook|
199
200
  yield(workbook)
200
201
  workbook.close
@@ -27,6 +27,7 @@ class Eco::API::UseCases::Default::Locations::TagtreeUpload < Eco::API::UseCases
27
27
  comms << insert_command(tree, pid: pid) unless top_id?(tree.id)
28
28
  pid = tree.id
29
29
  end
30
+
30
31
  tree.nodes.map do |node|
31
32
  insert_commands(node, pid: pid)
32
33
  end.flatten(1).tap do |subs|
@@ -54,6 +55,7 @@ class Eco::API::UseCases::Default::Locations::TagtreeUpload < Eco::API::UseCases
54
55
 
55
56
  def top_id?(node_id = nil)
56
57
  return top_id.is_a?(String) if node_id.nil?
58
+
57
59
  node_id == top_id
58
60
  end
59
61
 
@@ -0,0 +1,26 @@
1
+ class Eco::API::UseCases::Default::Utils::GroupCsv
2
+ class Cli < Eco::API::UseCases::Cli
3
+ str_desc = 'Groups the csv rows by a pivot field. '
4
+ str_desc << 'It assumes the sorting field is sorted '
5
+ str_desc << '(same values should be consecutive)'
6
+
7
+ desc str_desc
8
+
9
+ callback do |_session, options, _usecase|
10
+ if (file = SCR.get_file(cli_name, required: true, should_exist: true))
11
+ options.deep_merge!(input: {file: {name: file}})
12
+ end
13
+ end
14
+
15
+ add_option("-start-at", "Get only the last N-start_at rows") do |options|
16
+ count = SCR.get_arg("-start-at", with_param: true)
17
+ options.deep_merge!(input: {file: {start_at: count}})
18
+ end
19
+
20
+ add_option('-by', 'The column that should be used to group') do |options|
21
+ if (file = SCR.get_arg("-by", with_param: true))
22
+ options.deep_merge!(input: {group_by_field: file})
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,10 @@
1
+ class Eco::API::UseCases::Default::Utils::JsonToCsv
2
+ class Cli < Eco::API::UseCases::Cli
3
+ desc "Transforms an input JSON file into a CSV one."
4
+
5
+ callback do |_sess, options, _case|
6
+ file = SCR.get_file(cli_name, required: true, should_exist: true)
7
+ options.deep_merge!(source: {file: file})
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ class Eco::API::UseCases::Default::Utils::SortCsv
2
+ class Cli < Eco::API::UseCases::Cli
3
+ desc 'Sorts the CSV by column -by'
4
+
5
+ callback do |_session, options, _usecase|
6
+ if (file = SCR.get_file(cli_name, required: true, should_exist: true))
7
+ options.deep_merge!(input: {file: file})
8
+ end
9
+ end
10
+
11
+ add_option('-by', 'The column that should be used to sorting') do |options|
12
+ if (file = SCR.get_arg("-by", with_param: true))
13
+ options.deep_merge!(input: {sort_by: file})
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ class Eco::API::UseCases::Default::Utils::SplitJson
2
+ class Cli < Eco::API::UseCases::Cli
3
+ desc 'Splits a json input file into multiple files'
4
+
5
+ callback do |_sess, options, _case|
6
+ file = SCR.get_file(cli_name, required: true, should_exist: true)
7
+ options.deep_merge!(source: {file: file})
8
+ end
9
+
10
+ add_option("-max-items", "The max count of items of the output files") do |options|
11
+ count = SCR.get_arg("-max-items", with_param: true)
12
+ options.deep_merge!(output: {file: {max_items: count}})
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,213 @@
1
+ # This script assumes that for the `GROUP_BY_FIELD` rows are consecutive.
2
+ # @note you might run first the `sort-csv` case.
3
+ # @note you must inherit from this case and define the constants.
4
+ #
5
+ # GROUP_BY_FIELD = 'target_csv_field'.freeze
6
+ # GROUPED_FIELDS = [
7
+ # 'joined_field_1',
8
+ # 'joined_field_2',
9
+ # 'joined_field_3',
10
+ # ].freeze
11
+ #
12
+ class Eco::API::UseCases::Default::Utils::GroupCsv < Eco::API::Custom::UseCase
13
+ name 'group-csv'
14
+ type :other
15
+
16
+ require_relative 'cli/group_csv_cli'
17
+
18
+ def main(*_args)
19
+ if simulate?
20
+ count = Eco::CSV.count(input_file)
21
+ log(:info) { "CSV '#{input_file}' has #{count} rows." }
22
+ else
23
+ generate_file
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def generate_file # rubocop:disable Metrics/AbcSize
30
+ row_count = 0
31
+ in_index = nil
32
+
33
+ CSV.open(output_filename, 'wb') do |out_csv|
34
+ first = true
35
+
36
+ puts "\n"
37
+
38
+ streamed_input.for_each(start_at_idx: start_at) do |row, idx|
39
+ if first
40
+ first = false
41
+ headers!(row)
42
+ out_csv << headers
43
+ require_group_by_field!(row, file: input_file)
44
+ end
45
+
46
+ in_index = idx
47
+ next unless !block_given? || yield(row, idx)
48
+
49
+ next unless pivotable?(row, idx)
50
+ next unless (last_group = pivot_row(row))
51
+
52
+ row_count += 1
53
+
54
+ if (row_count % 500).zero?
55
+ print "... Done #{row_count} rows \r"
56
+ $stdout.flush
57
+ end
58
+
59
+ out_csv << last_group.values_at(*headers)
60
+ end
61
+
62
+ # finalize
63
+ if (lrow = pivot_row)
64
+ row_count += 1
65
+ out_csv << lrow.values_at(*headers)
66
+ end
67
+ ensure
68
+ msg = "Generated file '#{output_filename}' "
69
+ msg << "with #{row_count} rows (out of #{in_index})."
70
+
71
+ log(:info) { msg } unless simulate?
72
+ end
73
+ end
74
+
75
+ # It tracks the current grouped row
76
+ # @return [Nil, Hash] the last grouped row when `row` doesn't belong
77
+ # or `nil` otherwise
78
+ def pivot_row(row = nil)
79
+ @group ||= {}
80
+ return @group unless row
81
+
82
+ pivot_value = row[group_by_field]
83
+
84
+ unless (last_pivot = @group[group_by_field])
85
+ last_pivot = @group[group_by_field] = pivot_value
86
+ end
87
+
88
+ last = @group
89
+ @group = {group_by_field => pivot_value} unless pivot_value == last_pivot
90
+
91
+ headers_rest.each do |field|
92
+ curr_values = row[field].to_s.split('|').compact.uniq
93
+ pivot_values = @group[field].to_s.split('|').compact.uniq
94
+ @group[field] = (pivot_values | curr_values).join('|')
95
+ end
96
+
97
+ last unless last == @group
98
+ end
99
+
100
+ attr_reader :group
101
+ attr_reader :headers, :headers_rest
102
+
103
+ def headers!(row)
104
+ return if headers?
105
+
106
+ @headers_rest = grouped_fields & row.headers
107
+ @headers_rest -= [group_by_field]
108
+ @headers = [group_by_field, *headers_rest]
109
+ end
110
+
111
+ def headers?
112
+ instance_variable_defined?(:@headers)
113
+ end
114
+
115
+ def pivotable?(row, idx)
116
+ return true unless row[group_by_field].to_s.strip.empty?
117
+
118
+ msg = "Row #{idx} doesn't have value for pivot field '#{group_by_field}'"
119
+ msg << ". Skipping (discared) ..."
120
+ log(:warn) { msg }
121
+ false
122
+ end
123
+
124
+ def streamed_input
125
+ @streamed_input ||= Eco::CSV::Stream.new(input_file)
126
+ end
127
+
128
+ def input_file
129
+ options.dig(:input, :file, :name)
130
+ end
131
+
132
+ def start_at
133
+ return nil unless (num = options.dig(:input, :file, :start_at))
134
+
135
+ num = num.to_i
136
+ num = nil if num.zero?
137
+ num
138
+ end
139
+
140
+ def output_filename
141
+ return nil unless input_name
142
+
143
+ File.join(input_dir, "#{input_name}_grouped#{input_ext}")
144
+ end
145
+
146
+ def input_name
147
+ @input_name ||= File.basename(input_basename, input_ext)
148
+ end
149
+
150
+ def input_ext
151
+ @input_ext ||= input_basename.split('.')[1..].join('.').then do |name|
152
+ ".#{name}"
153
+ end
154
+ end
155
+
156
+ def input_basename
157
+ @input_basename ||= File.basename(input_full_filename)
158
+ end
159
+
160
+ def input_dir
161
+ @input_dir = File.dirname(input_full_filename)
162
+ end
163
+
164
+ def input_full_filename
165
+ @input_full_filename ||= File.expand_path(input_file)
166
+ end
167
+
168
+ def require_group_by_field!(row, file:)
169
+ return true if row.key?(group_by_field)
170
+
171
+ msg = "Pivot field '#{group_by_field}' missing in header of file '#{file}'"
172
+ log(:error) { msg }
173
+ raise msg
174
+ end
175
+
176
+ def group_by_field
177
+ return @group_by_field if instance_variable_defined?(:@group_by_field)
178
+
179
+ return (@group_by_field = opts_group_by) if opts_group_by
180
+
181
+ unless self.class.const_defined?(:GROUP_BY_FIELD)
182
+ msg = "(#{self.class}) You must define GROUP_BY_FIELD constant"
183
+ log(:error) { msg }
184
+ raise msg
185
+ end
186
+
187
+ @group_by_field = self.class::GROUP_BY_FIELD
188
+ end
189
+
190
+ def grouped_fields
191
+ return @grouped_fields if instance_variable_defined?(:@grouped_fields)
192
+
193
+ unless self.class.const_defined?(:GROUPED_FIELDS)
194
+ msg = "(#{self.class}) You must define GROUPED_FIELDS constant"
195
+ log(:error) { msg }
196
+ raise msg
197
+ end
198
+
199
+ @grouped_fields ||= [self.class::GROUPED_FIELDS].flatten.compact.tap do |flds|
200
+ next unless flds.empty?
201
+
202
+ log(:warn) {
203
+ msg = "There were no fields to be grouped/joined. "
204
+ msg << "This is equivalent to launch a unique operation."
205
+ msg
206
+ }
207
+ end
208
+ end
209
+
210
+ def opts_group_by
211
+ options.dig(:input, :group_by_field)
212
+ end
213
+ end
@@ -0,0 +1,71 @@
1
+ class Eco::API::UseCases::Default::Utils::JsonToCsv < Eco::API::Common::Loaders::UseCase
2
+ require_relative 'cli/json_to_csv_cli'
3
+
4
+ name 'json-to-csv'
5
+ type :other
6
+
7
+ def main(*_args)
8
+ return if simulate?
9
+
10
+ CSV.open(out_filename, 'w') do |csv|
11
+ csv << all_keys
12
+ data.each do |item|
13
+ csv << item.values_at(*all_keys)
14
+ end
15
+ ensure
16
+ log(:info) {
17
+ "Generated output file: '#{File.expand_path(out_filename)}'."
18
+ }
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def all_keys
25
+ @all_keys ||= data.each_with_object([]) do |item, head|
26
+ head.concat(item.keys - head)
27
+ end
28
+ end
29
+
30
+ def data
31
+ @data ||= parse_json_file.tap do |dt|
32
+ ensure_array!(dt)
33
+
34
+ log(:info) {
35
+ "Loaded #{dt.count} items (from file '#{File.basename(input_file)}')"
36
+ }
37
+
38
+ exit 0 if simulate?
39
+ end
40
+ end
41
+
42
+ def out_filename
43
+ @out_filename ||= ''.then do
44
+ input_basename = File.basename(input_file)
45
+ base_name = File.basename(input_basename, '.json')
46
+ "#{base_name}.csv"
47
+ end
48
+ end
49
+
50
+ def input_file
51
+ options.dig(:source, :file)
52
+ end
53
+
54
+ def ensure_array!(data)
55
+ return if data.is_a?(Array)
56
+
57
+ msg = "Expecting JSON file to contain an Array. Given: #{data.class}"
58
+ log(:error) { msg }
59
+ raise msg
60
+ end
61
+
62
+ def parse_json_file(filename = input_file)
63
+ fd = File.open(filename)
64
+ JSON.load fd # rubocop:disable Security/JSONLoad
65
+ rescue JSON::ParserError => err
66
+ log(:error) { "Parsing error on file '#{filename}'" }
67
+ raise err
68
+ ensure
69
+ fd&.close
70
+ end
71
+ end