eco-helpers 3.0.17 → 3.0.19

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -1
  3. data/eco-helpers.gemspec +3 -3
  4. data/lib/eco/api/common/loaders/parser.rb +10 -0
  5. data/lib/eco/api/common/people/default_parsers/csv_parser.rb +21 -208
  6. data/lib/eco/api/common/people/default_parsers/helpers/expected_headers.rb +206 -0
  7. data/lib/eco/api/common/people/default_parsers/helpers/null_parsing.rb +36 -0
  8. data/lib/eco/api/common/people/default_parsers/helpers.rb +15 -0
  9. data/lib/eco/api/common/people/default_parsers/json_parser.rb +56 -0
  10. data/lib/eco/api/common/people/default_parsers/xls_parser.rb +13 -14
  11. data/lib/eco/api/common/people/default_parsers.rb +2 -0
  12. data/lib/eco/api/common/people/entry_factory.rb +15 -4
  13. data/lib/eco/api/session/batch/launcher/mode_size.rb +65 -0
  14. data/lib/eco/api/session/batch/launcher/retry.rb +3 -3
  15. data/lib/eco/api/session/batch/launcher/status_handling.rb +4 -2
  16. data/lib/eco/api/session/batch/launcher.rb +42 -37
  17. data/lib/eco/api/session.rb +2 -0
  18. data/lib/eco/api/usecases/default/utils/cli/group_csv_cli.rb +26 -0
  19. data/lib/eco/api/usecases/default/utils/cli/json_to_csv_cli.rb +10 -0
  20. data/lib/eco/api/usecases/default/utils/cli/sort_csv_cli.rb +17 -0
  21. data/lib/eco/api/usecases/default/utils/cli/split_json_cli.rb +15 -0
  22. data/lib/eco/api/usecases/default/utils/group_csv_case.rb +213 -0
  23. data/lib/eco/api/usecases/default/utils/json_to_csv_case.rb +71 -0
  24. data/lib/eco/api/usecases/default/utils/sort_csv_case.rb +127 -0
  25. data/lib/eco/api/usecases/default/utils/split_json_case.rb +224 -0
  26. data/lib/eco/api/usecases/default/utils.rb +4 -0
  27. data/lib/eco/version.rb +1 -1
  28. metadata +22 -11
  29. data/lib/eco/api/session/batch/launcher/mode.rb +0 -23
  30. data/lib/eco/api/session/batch/launcher/size.rb +0 -40
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 884bc5e19544453088719de4fb1445b7fdb38de4b14b0a70d2dbe795d7082fb6
4
- data.tar.gz: 0ba7fa5b2367d5fe51e7a51571c5e031961ddb94ec863fa71a6ab2869aa7a353
3
+ metadata.gz: 0f6637f51c2d892eb93c881cca7613fba034cbb00ff2676966172c2277f583ce
4
+ data.tar.gz: 68703625905aa5ed2c00223a628de16612714a2c2cdd69a1c0a284c8c6096332
5
5
  SHA512:
6
- metadata.gz: 75eddb8557bf95b1174a2de8d18922aeac5bb3fb74fa8371c58d8c74be0413d7eef12a682549efe4a996c5413bc245e75dc2a6b06d9615aee520ab3be3247abd
7
- data.tar.gz: 898c76f69fd73fd89c261ff4989798c2dec9d845ce8b4aadb168ac1c44c0b7c67cd0d6a1aabead337ede761ef37dcf7d850e895057d04af211c8f19a6708cf94
6
+ metadata.gz: 89bbd57468fd7de3412240e3e44f61122aee9982896401494c4ee88c5e2dec97563e421cac2da0794c4f68ff90206422bc52b2868ee1e7ef3422fce50f3ec73d
7
+ data.tar.gz: 82703dafe2ccecf25994567926c80e7bd40fe0af70853cce664dbcbfc12b56b0b9ad72308ccf2789a386e053baadce2a8c29095130aa57dc560228d99c27267e
data/CHANGELOG.md CHANGED
@@ -2,14 +2,38 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [3.0.18] - 2024-10-xx
5
+ ## [3.0.19] - 2024-10-xx
6
6
 
7
7
  ### Added
8
8
 
9
+ - Input entries `:json` parser
10
+ - **Cases**:
11
+ - `json-to-csv`
12
+ - `split-json`
13
+ - `sort-csv`
14
+ - `group-csv`
15
+
9
16
  ### Changed
10
17
 
18
+ - Refactored input file parsers (`csv`, `xls`)
19
+ - Upgraded gems:
20
+ - `ecoportal-api`
21
+ - `ecoportal-api-v2`
22
+ - `ecoportal-api-graphql`
23
+
11
24
  ### Fixed
12
25
 
26
+ ## [3.0.18] - 2024-10-28
27
+
28
+ ### Changed
29
+
30
+ - upgrade `ecoportal-api` gem
31
+ - upgrade `ecoportal-api-v2` gem
32
+ - `Eco::API::Session::Batch#launch_batch`
33
+ - **removed** `per_page` parameter
34
+ - **added** auto-swap endpoint from `job` to `batch` on TimeOut errors.
35
+ - **retries** also on `Ecoportal::API::Errors::StartTimeOut`
36
+
13
37
  ## [3.0.17] - 2024-10-18
14
38
 
15
39
  ### Added
data/eco-helpers.gemspec CHANGED
@@ -40,9 +40,9 @@ Gem::Specification.new do |spec|
40
40
  spec.add_dependency 'bcrypt_pbkdf', '~> 1.0'
41
41
  spec.add_dependency 'docx', '>= 0.8.0', '< 0.9'
42
42
  spec.add_dependency 'dotenv', '~> 3'
43
- spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.5'
44
- spec.add_dependency 'ecoportal-api-graphql', '~> 0.4', '>= 0.4.2'
45
- spec.add_dependency 'ecoportal-api-v2', '~> 2.0', '>= 2.0.10'
43
+ spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.7'
44
+ spec.add_dependency 'ecoportal-api-graphql', '~> 0.4', '>= 0.4.3'
45
+ spec.add_dependency 'ecoportal-api-v2', '~> 2.0', '>= 2.0.12'
46
46
  spec.add_dependency 'ed25519', '~> 1.2'
47
47
  spec.add_dependency 'fast_excel', '>= 0.5.0', '< 0.6'
48
48
  spec.add_dependency 'fuzzy_match', '>= 2.1.0', '< 2.2'
@@ -21,6 +21,7 @@ module Eco
21
21
  return [] if miss.empty?
22
22
  return attrs if match.empty?
23
23
  return miss if type == :all
24
+
24
25
  []
25
26
  end
26
27
  end
@@ -38,6 +39,7 @@ module Eco
38
39
  msg << "#{self.class}, is linked to"
39
40
  return @attribute || (raise msg)
40
41
  end
42
+
41
43
  name value
42
44
  @attribute = value
43
45
  end
@@ -62,6 +64,7 @@ module Eco
62
64
  def parsing_phase(phase = nil)
63
65
  @parsing_phase ||= :internal
64
66
  return @parsing_phase unless phase
67
+
65
68
  @parsing_phase = phase
66
69
  end
67
70
 
@@ -71,6 +74,7 @@ module Eco
71
74
  def serializing_phase(phase = nil)
72
75
  @serializing_phase ||= :person
73
76
  return @serializing_phase unless phase
77
+
74
78
  @serializing_phase = phase
75
79
  end
76
80
 
@@ -86,6 +90,7 @@ module Eco
86
90
  # Helper to build the `active_when` condition.
87
91
  def active_when_all(*attrs)
88
92
  @active_when_attrs = RequiredAttrs.new(attribute, :all, attrs)
93
+
89
94
  @active_when = proc do |source_data|
90
95
  keys = data_keys(source_data)
91
96
  attrs.all? {|key| keys.include?(key)}
@@ -149,6 +154,10 @@ module Eco
149
154
 
150
155
  private
151
156
 
157
+ def options
158
+ ASSETS.cli.options
159
+ end
160
+
152
161
  def _define_parser(attr_parser)
153
162
  if (active_when = self.class.active_when)
154
163
  attr_parser.def_parser(
@@ -166,6 +175,7 @@ module Eco
166
175
 
167
176
  def _define_serializer(attr_parser)
168
177
  return unless respond_to?(:serializer, true)
178
+
169
179
  attr_parser.def_serializer(
170
180
  self.class.serializing_phase,
171
181
  &method(:serializer)
@@ -1,235 +1,48 @@
1
1
  class Eco::API::Common::People::DefaultParsers::CSVParser < Eco::API::Common::Loaders::Parser
2
2
  attribute :csv
3
3
 
4
+ include Eco::API::Common::People::DefaultParsers::Helpers::ExpectedHeaders
5
+ include Eco::API::Common::People::DefaultParsers::Helpers::NullParsing
6
+
4
7
  def parser(data, deps)
5
8
  Eco::CSV.parse(data, headers: true, skip_blanks: true).tap do |table|
6
- require_headers!(table)
7
- check_headers(table) if deps[:check_headers] && check_headers?
8
- end.each_with_object([]) do |row, arr_hash|
9
- row_hash = row.headers.uniq.each_with_object({}) do |attr, hash|
9
+ require_headers!(table.headers)
10
+
11
+ next unless deps[:check_headers]
12
+ next unless check_headers?
13
+
14
+ check_headers!(
15
+ table.headers,
16
+ order_check: options.dig(:input, :header_check, :order)
17
+ )
18
+ end.each_with_object([]) do |item, arr_hash|
19
+ item_hash = item.headers.uniq.each_with_object({}) do |attr, hash|
10
20
  next if attr.to_s.strip.empty?
11
- hash[attr.strip] = parse_string(row[attr])
21
+
22
+ hash[attr.strip] = parse_null(item[attr])
12
23
  end
13
- arr_hash.push(row_hash)
24
+
25
+ arr_hash.push(item_hash)
14
26
  end
15
27
  end
16
28
 
17
29
  def serializer(array_hash, _deps)
18
30
  arr_rows = []
31
+
19
32
  unless array_hash.empty?
20
33
  header = array_hash.first.keys
34
+
21
35
  arr_rows = array_hash.map do |csv_row|
22
36
  CSV::Row.new(header, csv_row.values_at(*header))
23
37
  end
24
38
  end
39
+
25
40
  CSV::Table.new(arr_rows).to_csv
26
41
  end
27
42
 
28
43
  private
29
44
 
30
- def abort(msg)
31
- super(msg, raising: false)
32
- end
33
-
34
- def require_headers!(table)
35
- headers = table.headers
36
-
37
- abort("Missing headers in CSV") unless headers&.any?
38
-
39
- empty = []
40
- headers.each_with_index do |header, idx|
41
- empty << idx if header.to_s.strip.empty?
42
- end
43
-
44
- abort("Empty headers in column(s): #{empty.join(', ')}") if empty.any?
45
-
46
- true
47
- end
48
-
49
45
  def check_headers?
50
46
  !options.dig(:input, :header_check, :skip)
51
47
  end
52
-
53
- def options
54
- ASSETS.cli.options
55
- end
56
-
57
- def parse_string(value)
58
- return nil if value.to_s.empty?
59
- return nil if null?(value)
60
- value
61
- end
62
-
63
- def null?(value)
64
- return true unless value
65
-
66
- str = value.strip.upcase
67
- ["NULL"].any? {|token| str == token}
68
- end
69
-
70
- def check_headers(table) # rubocop:disable Metrics/AbcSize
71
- headers = table.headers
72
- unmatch = []
73
- unmatch = unmatched_headers(headers) if options.dig(:input, :header_check, :order)
74
- missing = missing_headers(headers)
75
- unknown = unknown_headers(headers)
76
- criteria = [unknown, missing[:direct], missing[:indirect], unmatch]
77
- return if criteria.all?(&:empty?)
78
-
79
- msg = "Detected possible HEADER ISSUES !!!\n"
80
-
81
- # requires exact match
82
- unless unmatch.empty?
83
- msg << "CSV headers do NOT exactly match the expected:\n"
84
- msg << " * Expected: #{known_headers}\n"
85
- expected, given = unmatch.first
86
- msg << " * First unmatch => Given: '#{given}' where expected '#{expected}'\n"
87
- missed = known_headers - headers
88
- unless missed.empty?
89
- msg << " * Missing headers:\n"
90
- msg << " - #{missed.join("\n - ")}\n"
91
- end
92
- end
93
-
94
- msg << "Missing or Wrong HEADER names in the CSV file:\n"
95
- msg << " * UNKNOWN (or not used?): #{unknown}\n" unless unknown.empty?
96
- msg << " * MISSING HEADER: #{missing[:direct]}\n" unless missing[:direct].empty?
97
-
98
- unless (data = missing[:indirect]).empty?
99
- msg << " * MISSING INDIRECTLY:\n"
100
- data.each do |ext, info|
101
- msg << " - '#{ext}' => "
102
- msg << (info[:attrs] || {}).map do |status, attrs|
103
- if status == :inactive
104
- "makes inactive: #{attrs}"
105
- elsif status == :active
106
- "there could be missing info in: #{attrs}"
107
- end
108
- end.compact.join("; ")
109
- msg << "\n"
110
- end
111
- end
112
-
113
- log(:warn) { msg }
114
-
115
- msg = "There were issues identified on the CSV header names. Aborting..."
116
- abort(msg) if options.dig(:input, :header_check, :must_be_valid)
117
-
118
- sleep(2)
119
- end
120
-
121
- def unmatched_headers(headers)
122
- known_headers.zip(headers).reject do |(expected, given)|
123
- expected == given
124
- end
125
- end
126
-
127
- def unknown_headers(headers)
128
- (headers - known_headers) - all_internal_attrs
129
- end
130
-
131
- def missing_headers(headers) # rubocop:disable Metrics/AbcSize
132
- int_head = internal_present_or_active(headers)
133
- external = headers.select do |e|
134
- i = fields_mapper.to_internal(e)
135
- int_head.include?(i)
136
- end
137
-
138
- ext_present = known_headers_present(int_head) | external
139
- ext_miss = known_headers - ext_present
140
-
141
- {
142
- direct: [],
143
- indirect: {}
144
- }.tap do |missing|
145
- ext_miss.each do |ext|
146
- next unless (int = fields_mapper.to_internal(ext))
147
-
148
- missing[:direct] << ext if all_internal_attrs.include?(int)
149
- related_attrs_requirements = required_attrs.values.select do |req|
150
- dep = req.dependant?(int)
151
- affects = dep && !int_head.include?(int)
152
- in_header = int_head.include?(req.attr)
153
- affects || (dep && !in_header)
154
- end
155
-
156
- next if related_attrs_requirements.empty?
157
-
158
- data = missing[:indirect][ext] = {}
159
- data[:int] = int
160
- data[:attrs] = {}
161
-
162
- related_attrs_requirements.each_with_object(data[:attrs]) do |req, attrs|
163
- status = req.active?(*int_head) ? :active : :inactive
164
- attrs[status] ||= []
165
- attrs[status] << req.attr
166
- end
167
- end
168
- end
169
- end
170
-
171
- def known_headers_present(headers_internal)
172
- known_headers.select do |ext|
173
- int = fields_mapper.to_internal(ext)
174
- headers_internal.include?(int)
175
- end
176
- end
177
-
178
- # Scopes what internal attrs appear in headers as they are
179
- def internal_present_or_active(headers, inactive_requirements = {}) # rubocop:disable Metrics/AbcSize
180
- # internal attrs that are not being mapped
181
- int_all = all_internal_attrs.reject {|i| fields_mapper.external?(i)}
182
- hint = headers & int_all
183
- hext = headers - hint
184
- int_present = hint + hext.map {|e| fields_mapper.to_internal(e)}.compact
185
-
186
- update_inactive = proc do
187
- inactive_requirements.dup.each do |attr, req|
188
- next unless req.active?(*int_present)
189
-
190
- inactive_requirements.delete(attr)
191
- int_present << attr
192
- update_inactive.call
193
- end
194
- end
195
-
196
- required_attrs.each_value do |req|
197
- next if int_present.include?(req)
198
-
199
- if req.active?(*int_present)
200
- inactive_requirements.delete(req.attr)
201
- int_present << req.attr
202
- update_inactive.call
203
- else
204
- inactive_requirements[req.attr] = req
205
- end
206
- end
207
-
208
- int_present
209
- end
210
-
211
- # The csv header names as expected
212
- def known_headers
213
- @known_headers ||= fields_mapper.list(:external).compact.uniq
214
- end
215
-
216
- def fields_mapper
217
- session.fields_mapper
218
- end
219
-
220
- def required_attrs
221
- @required_attrs ||= person_parser.required_attrs.to_h {|ra| [ra.attr, ra]}
222
- end
223
-
224
- def all_internal_attrs
225
- @all_internal_attrs ||= [].tap do |int_attrs|
226
- known_int_attrs = person_parser.all_attrs(include_defined_parsers: true)
227
- known_int_attrs |= fields_mapper.list(:internal).compact
228
- int_attrs.concat(known_int_attrs)
229
- end
230
- end
231
-
232
- def person_parser
233
- session.entry_factory.person_parser
234
- end
235
48
  end
@@ -0,0 +1,206 @@
1
+ module Eco::API::Common::People
2
+ class DefaultParsers
3
+ module Helpers
4
+ module ExpectedHeaders
5
+ include Eco::Language::AuxiliarLogger
6
+
7
+ private
8
+
9
+ def require_headers!(raw_headers)
10
+ abort("Missing headers in CSV") unless raw_headers&.any?
11
+
12
+ empty = []
13
+ raw_headers.each_with_index do |header, idx|
14
+ empty << idx if header.to_s.strip.empty?
15
+ end
16
+
17
+ abort("Empty headers in column(s): #{empty.join(', ')}") if empty.any?
18
+
19
+ true
20
+ end
21
+
22
+ def check_headers!(raw_headers, order_check: false) # rubocop:disable Metrics/AbcSize
23
+ unmatch = []
24
+ unmatch = unmatched_headers(raw_headers) if order_check
25
+ missing = missing_headers(raw_headers)
26
+ unknown = unknown_headers(raw_headers)
27
+
28
+ criteria = [unknown, missing[:direct], missing[:indirect], unmatch]
29
+ return if criteria.all?(&:empty?)
30
+
31
+ msg = "Detected possible HEADER / FIELD ISSUES !!!\n"
32
+
33
+ # requires exact match
34
+ unless unmatch.empty?
35
+ msg << "File headers/fields do NOT exactly match the expected:\n"
36
+ msg << " * Expected: #{expected_headers}\n"
37
+
38
+ expected, given = unmatch.first
39
+ msg << " * First unmatch => Given: '#{given}' where expected '#{expected}'\n"
40
+
41
+ missed = expected_headers - raw_headers
42
+
43
+ unless missed.empty?
44
+ msg << " * Missing headers/fields:\n"
45
+ msg << " - #{missed.join("\n - ")}\n"
46
+ end
47
+ end
48
+
49
+ msg << "Missing or Wrong HEADER names in the file:\n"
50
+ msg << " * UNKNOWN (or not used?): #{unknown}\n" unless unknown.empty?
51
+ msg << " * MISSING HEADER/FIELD: #{missing[:direct]}\n" unless missing[:direct].empty?
52
+
53
+ unless (data = missing[:indirect]).empty?
54
+ msg << " * MISSING INDIRECTLY:\n"
55
+
56
+ data.each do |ext, info|
57
+ msg << " - '#{ext}' => "
58
+ msg << (info[:attrs] || {}).map do |status, attrs|
59
+ if status == :inactive
60
+ "makes inactive: #{attrs}"
61
+ elsif status == :active
62
+ "there could be missing info in: #{attrs}"
63
+ end
64
+ end.compact.join("; ")
65
+
66
+ msg << "\n"
67
+ end
68
+ end
69
+
70
+ log(:warn) { msg }
71
+
72
+ msg = "There were issues identified on the file header/field names. Aborting..."
73
+ abort(msg) if options.dig(:input, :header_check, :must_be_valid)
74
+
75
+ sleep(2)
76
+ end
77
+
78
+
79
+ # @return [Hash] with missing `:direct` and `:indirect` attrs, where
80
+ # - `:direct` [Array] refers to direct attrs
81
+ # - `:indirect` [Hash] refers to indirect attrs that are `:active` or `:inactive`.
82
+ def missing_headers(raw_headers) # rubocop:disable Metrics/AbcSize
83
+ int_head = internal_present_or_active(raw_headers)
84
+
85
+ external = raw_headers.select do |e|
86
+ i = fields_mapper.to_internal(e)
87
+ int_head.include?(i)
88
+ end
89
+
90
+ ext_present = present_internal_expected_headers(int_head) | external
91
+ ext_miss = expected_headers - ext_present
92
+
93
+ {
94
+ direct: [],
95
+ indirect: {}
96
+ }.tap do |missing|
97
+ ext_miss.each do |ext|
98
+ next unless (int = fields_mapper.to_internal(ext))
99
+
100
+ missing[:direct] << ext if all_internal_attrs.include?(int)
101
+
102
+ related_attrs_requirements = required_attrs.values.select do |req|
103
+ dep = req.dependant?(int)
104
+ affects = dep && !int_head.include?(int)
105
+ in_header = int_head.include?(req.attr)
106
+ affects || (dep && !in_header)
107
+ end
108
+
109
+ next if related_attrs_requirements.empty?
110
+
111
+ data = missing[:indirect][ext] = {}
112
+ data[:int] = int
113
+ data[:attrs] = {}
114
+
115
+ related_attrs_requirements.each_with_object(data[:attrs]) do |req, attrs|
116
+ status = req.active?(*int_head) ? :active : :inactive
117
+ attrs[status] ||= []
118
+ attrs[status] << req.attr
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+
125
+ # The input file header names as expected
126
+ def expected_headers
127
+ @expected_headers ||= fields_mapper.list(:external).compact.uniq
128
+ end
129
+
130
+ def present_internal_expected_headers(internal_headers)
131
+ expected_headers.select do |ext|
132
+ int = fields_mapper.to_internal(ext)
133
+ internal_headers.include?(int)
134
+ end
135
+ end
136
+
137
+ def unmatched_headers(raw_headers)
138
+ expected_headers.zip(raw_headers).reject do |(expected, given)|
139
+ expected == given
140
+ end
141
+ end
142
+
143
+ def unknown_headers(raw_headers)
144
+ (raw_headers - expected_headers) - all_internal_attrs
145
+ end
146
+
147
+ def fields_mapper
148
+ session.fields_mapper
149
+ end
150
+
151
+ def required_attrs
152
+ @required_attrs ||= person_parser.required_attrs.to_h {|ra| [ra.attr, ra]}
153
+ end
154
+
155
+ def all_internal_attrs
156
+ @all_internal_attrs ||= [].tap do |int_attrs|
157
+ known_int_attrs = person_parser.all_attrs(include_defined_parsers: true)
158
+ known_int_attrs |= fields_mapper.list(:internal).compact
159
+ int_attrs.concat(known_int_attrs)
160
+ end
161
+ end
162
+
163
+ # Scopes what internal attrs appear in headers as they are
164
+ def internal_present_or_active(raw_headers, inactive_requirements = {}) # rubocop:disable Metrics/AbcSize
165
+ # internal attrs that are not being mapped
166
+ int_all = all_internal_attrs.reject {|i| fields_mapper.external?(i)}
167
+ hint = raw_headers & int_all
168
+ hext = raw_headers - hint
169
+ int_present = hint + hext.map {|e| fields_mapper.to_internal(e)}.compact
170
+
171
+ update_inactive = proc do
172
+ inactive_requirements.dup.each do |attr, req|
173
+ next unless req.active?(*int_present)
174
+
175
+ inactive_requirements.delete(attr)
176
+ int_present << attr
177
+ update_inactive.call
178
+ end
179
+ end
180
+
181
+ required_attrs.each_value do |req|
182
+ next if int_present.include?(req)
183
+
184
+ if req.active?(*int_present)
185
+ inactive_requirements.delete(req.attr)
186
+ int_present << req.attr
187
+ update_inactive.call
188
+ else
189
+ inactive_requirements[req.attr] = req
190
+ end
191
+ end
192
+
193
+ int_present
194
+ end
195
+
196
+ def person_parser
197
+ session.entry_factory.person_parser
198
+ end
199
+
200
+ def abort(msg)
201
+ super(msg, raising: false) if defined?(super)
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,36 @@
1
+ module Eco::API::Common::People
2
+ class DefaultParsers
3
+ module Helpers
4
+ module NullParsing
5
+ private
6
+
7
+ def parse_null(value)
8
+ return if null?(value)
9
+ return parse_null_on_hash(value) if value.is_a?(Hash)
10
+ return value unless value.is_a?(Array)
11
+
12
+ value.map {|val| parse_null(val)}
13
+ end
14
+
15
+ def parse_null_on_hash(value)
16
+ return value unless value.is_a?(Hash)
17
+
18
+ value.dup do |out|
19
+ value.each do |key, val|
20
+ next out.delete(key) unless (out[key] = parse_null(val))
21
+ end
22
+ end
23
+ end
24
+
25
+ def null?(value)
26
+ return true if value.nil?
27
+ return false unless value.is_a?(String)
28
+ return true if value.strip.to_s.empty?
29
+
30
+ str = value.strip.upcase
31
+ ['NULL'].any? {|token| str == token}
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'helpers/expected_headers'
2
+ require_relative 'helpers/null_parsing'
3
+
4
+ module Eco
5
+ module API
6
+ module Common
7
+ module People
8
+ class DefaultParsers
9
+ module Helpers
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,56 @@
1
+ class Eco::API::Common::People::DefaultParsers::JsonParser < Eco::API::Common::Loaders::Parser
2
+ attribute :json
3
+
4
+ include Eco::API::Common::People::DefaultParsers::Helpers::ExpectedHeaders
5
+ include Eco::API::Common::People::DefaultParsers::Helpers::NullParsing
6
+
7
+ def parser(filename, deps)
8
+ parse_json_file(filename).tap do |data|
9
+ # Don't enable header checks for now
10
+ next
11
+
12
+ puts 'Identifying unified json object keys...'
13
+ raw_headers = data.each_with_object([]) do |item, head|
14
+ head.concat(item.keys - head)
15
+ end
16
+
17
+ require_headers!(raw_headers)
18
+
19
+ next unless deps[:check_headers]
20
+ next unless check_headers?
21
+
22
+ check_headers!(
23
+ data,
24
+ order_check: false
25
+ )
26
+ end.each_with_object([]) do |item, arr_hash|
27
+ item_hash = item.keys.each_with_object({}) do |attr, hash|
28
+ next if attr.to_s.strip.empty?
29
+
30
+ hash[attr.strip] = parse_null(item[attr])
31
+ end
32
+
33
+ arr_hash.push(item_hash)
34
+ end
35
+ end
36
+
37
+ def serializer(array_hash, _deps)
38
+ array_hash.to_json
39
+ end
40
+
41
+ private
42
+
43
+ def check_headers?
44
+ !options.dig(:input, :header_check, :skip)
45
+ end
46
+
47
+ def parse_json_file(filename)
48
+ fd = File.open(filename)
49
+ JSON.load fd # rubocop:disable Security/JSONLoad
50
+ rescue JSON::ParserError => err
51
+ log(:error) { "Parsing error on file '#{filename}'" }
52
+ raise err
53
+ ensure
54
+ fd&.close
55
+ end
56
+ end