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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -1
- data/eco-helpers.gemspec +3 -3
- data/lib/eco/api/common/loaders/parser.rb +10 -0
- data/lib/eco/api/common/people/default_parsers/csv_parser.rb +21 -208
- data/lib/eco/api/common/people/default_parsers/helpers/expected_headers.rb +206 -0
- data/lib/eco/api/common/people/default_parsers/helpers/null_parsing.rb +36 -0
- data/lib/eco/api/common/people/default_parsers/helpers.rb +15 -0
- data/lib/eco/api/common/people/default_parsers/json_parser.rb +56 -0
- data/lib/eco/api/common/people/default_parsers/xls_parser.rb +13 -14
- data/lib/eco/api/common/people/default_parsers.rb +2 -0
- data/lib/eco/api/common/people/entry_factory.rb +15 -4
- data/lib/eco/api/session/batch/launcher/mode_size.rb +65 -0
- data/lib/eco/api/session/batch/launcher/retry.rb +3 -3
- data/lib/eco/api/session/batch/launcher/status_handling.rb +4 -2
- data/lib/eco/api/session/batch/launcher.rb +42 -37
- data/lib/eco/api/session.rb +2 -0
- data/lib/eco/api/usecases/default/utils/cli/group_csv_cli.rb +26 -0
- data/lib/eco/api/usecases/default/utils/cli/json_to_csv_cli.rb +10 -0
- data/lib/eco/api/usecases/default/utils/cli/sort_csv_cli.rb +17 -0
- data/lib/eco/api/usecases/default/utils/cli/split_json_cli.rb +15 -0
- data/lib/eco/api/usecases/default/utils/group_csv_case.rb +213 -0
- data/lib/eco/api/usecases/default/utils/json_to_csv_case.rb +71 -0
- data/lib/eco/api/usecases/default/utils/sort_csv_case.rb +127 -0
- data/lib/eco/api/usecases/default/utils/split_json_case.rb +224 -0
- data/lib/eco/api/usecases/default/utils.rb +4 -0
- data/lib/eco/version.rb +1 -1
- metadata +22 -11
- data/lib/eco/api/session/batch/launcher/mode.rb +0 -23
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f6637f51c2d892eb93c881cca7613fba034cbb00ff2676966172c2277f583ce
|
4
|
+
data.tar.gz: 68703625905aa5ed2c00223a628de16612714a2c2cdd69a1c0a284c8c6096332
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
44
|
-
spec.add_dependency 'ecoportal-api-graphql', '~> 0.4', '>= 0.4.
|
45
|
-
spec.add_dependency 'ecoportal-api-v2', '~> 2.0', '>= 2.0.
|
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
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
21
|
+
|
22
|
+
hash[attr.strip] = parse_null(item[attr])
|
12
23
|
end
|
13
|
-
|
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,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
|