Skip to content

Commit edb9bb2

Browse files
author
Matthew Gramigna
committed
Update to switch to custom implementation of validation with tests
1 parent a671a00 commit edb9bb2

File tree

6 files changed

+148
-76
lines changed

6 files changed

+148
-76
lines changed

package-lock.json

Lines changed: 0 additions & 37 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
"ajv": "^6.12.6",
2323
"antlr4": "4.8.0",
2424
"commander": "^6.2.0",
25-
"csv-file-validator": "^1.10.1",
2625
"csv-parse": "^4.8.8",
2726
"fhir-crud-client": "^1.2.2",
2827
"fhirpath": "2.1.5",

src/client/BaseClient.js

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ class BaseClient {
2525
}
2626

2727
// Given an extractor configuration, initialize all the necessary extractors
28-
async initializeExtractors(extractorConfig, commonExtractorArgs) {
29-
const extractorInits = extractorConfig.map(async (curExtractorConfig) => {
28+
initializeExtractors(extractorConfig, commonExtractorArgs) {
29+
let allExtractorsValid = true;
30+
31+
extractorConfig.forEach((curExtractorConfig) => {
3032
const { label, type, constructorArgs } = curExtractorConfig;
3133
logger.debug(`Initializing ${label} extractor with type ${type}`);
3234
const ExtractorClass = this.extractorClasses[type];
@@ -35,20 +37,26 @@ class BaseClient {
3537
const newExtractor = new ExtractorClass({ ...commonExtractorArgs, ...constructorArgs });
3638

3739
if (newExtractor.validate) {
38-
await newExtractor.validate();
40+
const isExtractorValid = newExtractor.validate();
41+
allExtractorsValid = (allExtractorsValid && isExtractorValid);
42+
if (isExtractorValid) {
43+
logger.debug(`Extractor ${label} PASSED CSV validation`);
44+
} else {
45+
logger.debug(`Extractor ${label} FAILED CSV validation`);
46+
}
3947
}
4048

41-
return newExtractor;
49+
this.extractors.push(newExtractor);
4250
} catch (e) {
43-
throw new Error(`Unable to initialize ${label} extractor with type ${type}`);
51+
throw new Error(`Unable to initialize ${label} extractor with type ${type}: ${e.message}`);
4452
}
4553
});
4654

47-
await Promise.all(extractorInits).then((extractors) => {
48-
this.extractors.push(...extractors);
49-
});
50-
51-
logger.info('Validation succeeded');
55+
if (allExtractorsValid) {
56+
logger.info('Validation succeeded');
57+
} else {
58+
throw new Error('Error occurred during CSV validation');
59+
}
5260
}
5361

5462
// NOTE: Async because in other clients that extend this, we need async helper functions (ex. auth)

src/extractors/BaseCSVExtractor.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ class BaseCSVExtractor extends Extractor {
1212
this.csvModule = new CSVModule(this.filePath);
1313
}
1414

15-
async validate() {
15+
validate() {
1616
if (this.csvSchema) {
1717
logger.info(`Validating CSV file for ${this.filePath}`);
18-
await validateCSV(this.filePath, this.csvSchema);
19-
} else {
20-
logger.warn(`No CSV schema provided for ${this.filePath}`);
18+
return validateCSV(this.filePath, this.csvSchema, this.csvModule.data);
2119
}
20+
logger.warn(`No CSV schema provided for ${this.filePath}`);
21+
return true;
2222
}
2323
}
2424

src/helpers/csvValidator.js

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,43 @@
11
const _ = require('lodash');
2-
const fs = require('fs');
3-
const validate = require('csv-file-validator');
4-
const parse = require('csv-parse/lib/sync');
52
const logger = require('./logger');
63

7-
async function validateCSV(pathToCSVFile, csvSchema) {
8-
const csv = fs.readFileSync(pathToCSVFile, { columns: true, encoding: 'utf8' });
4+
function validateCSV(pathToCSVFile, csvSchema, csvData) {
5+
let isValid = true;
96

10-
// Use CSV parser to determine actual number of columns in file
11-
const csvJson = parse(csv);
7+
// Check headers
8+
const headers = Object.keys(csvData[0]);
9+
const schemaDiff = _.difference(csvSchema.headers.map((h) => h.name), headers);
10+
const fileDiff = _.difference(headers, csvSchema.headers.map((h) => h.name));
1211

13-
if (csvJson[0].length !== csvSchema.headers.length) {
14-
// Report which erroneous columns exist in provided CSV
15-
const difference = _.difference(csvJson[0], csvSchema.headers.map((h) => h.name));
12+
if (fileDiff.length > 0) {
13+
logger.warn(`Found extra column(s) in CSV ${pathToCSVFile}: "${fileDiff.join(',')}"`);
14+
}
1615

17-
logger.error(`Validation error in CSV ${pathToCSVFile}: found extra column(s) "${difference}"`);
18-
process.exit(1);
16+
if (schemaDiff.length > 0) {
17+
schemaDiff.forEach((sd) => {
18+
const headerSchema = csvSchema.headers.find((h) => h.name === sd);
19+
if (headerSchema.required) {
20+
logger.error(`Column ${sd} is marked as required but is missing in CSV ${pathToCSVFile}`);
21+
isValid = false;
22+
} else {
23+
logger.warn(`Column ${sd} is missing in CSV ${pathToCSVFile}`);
24+
}
25+
});
1926
}
2027

21-
try {
22-
const { inValidMessages } = await validate(csv, csvSchema);
28+
// Check values
29+
csvData.forEach((row, i) => {
30+
Object.entries(row).forEach(([key, value], j) => {
31+
const schema = csvSchema.headers.find((h) => h.name === key);
2332

24-
if (inValidMessages.length > 0) {
25-
inValidMessages.forEach((errorMsg) => {
26-
logger.error(`Validation error in CSV ${pathToCSVFile}: ${errorMsg}`);
27-
});
33+
if (schema && schema.required && !value) {
34+
logger.error(`Column ${key} marked as required but missing value in row ${i + 1} column ${j + 1} in CSV ${pathToCSVFile}`);
35+
isValid = false;
36+
}
37+
});
38+
});
2839

29-
process.exit(1);
30-
}
31-
} catch (e) {
32-
logger.error(`Error occurred during CSV validation: ${e.message}`);
33-
process.exit(1);
34-
}
40+
return isValid;
3541
}
3642

3743
module.exports = {

test/helpers/csvValidator.test.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
const { validateCSV } = require('../../src/helpers/csvValidator');
2+
3+
const SIMPLE_DATA = [
4+
{
5+
header1: '1',
6+
header2: '2',
7+
header3: '3',
8+
},
9+
{
10+
header1: '4',
11+
header2: '',
12+
header3: '6',
13+
},
14+
];
15+
16+
const SIMPLE_DATA_MISSING_REQUIRED_VALUE = [
17+
{
18+
header1: '',
19+
header2: '2',
20+
header3: '3',
21+
},
22+
{
23+
header1: '4',
24+
header2: '2',
25+
header3: '3',
26+
},
27+
];
28+
29+
const SIMPLE_DATA_MISSING_HEADER = [
30+
{
31+
wrongHeader1: '1',
32+
header2: '2',
33+
header3: '3',
34+
},
35+
{
36+
wrongHeader1: '4',
37+
header2: '2',
38+
header3: '3',
39+
},
40+
];
41+
42+
const SIMPLE_DATA_EXTRA_COLUMNS = [
43+
{
44+
header1: '1',
45+
header2: '2',
46+
header3: '3',
47+
header4: '4',
48+
},
49+
{
50+
header1: '5',
51+
header2: '6',
52+
header3: '7',
53+
header4: '',
54+
},
55+
];
56+
57+
const SIMPLE_DATA_MISSING_OPTIONAL_COLUMN = [
58+
{
59+
header1: '1',
60+
header2: '2',
61+
},
62+
{
63+
header1: '3',
64+
header2: '4',
65+
},
66+
];
67+
68+
const schema = {
69+
headers: [
70+
{ name: 'header1', required: true },
71+
{ name: 'header2' },
72+
{ name: 'header3' },
73+
],
74+
};
75+
76+
describe('csvValidator', () => {
77+
test('simple data validates', () => {
78+
expect(validateCSV('', schema, SIMPLE_DATA)).toBe(true);
79+
});
80+
81+
test('data missing required value does not validate', () => {
82+
expect(validateCSV('', schema, SIMPLE_DATA_MISSING_REQUIRED_VALUE)).toBe(false);
83+
});
84+
85+
test('data missing required header does not validate', () => {
86+
expect(validateCSV('', schema, SIMPLE_DATA_MISSING_HEADER)).toBe(false);
87+
});
88+
89+
test('data with erroneous column should still validate', () => {
90+
expect(validateCSV('', schema, SIMPLE_DATA_EXTRA_COLUMNS)).toBe(true);
91+
});
92+
93+
test('data missing an optional column should still validate', () => {
94+
expect(validateCSV('', schema, SIMPLE_DATA_MISSING_OPTIONAL_COLUMN)).toBe(true);
95+
});
96+
});

0 commit comments

Comments
 (0)