Skip to content

Commit

Permalink
Merge pull request #89 from shystruk/dev
Browse files Browse the repository at this point in the history
v1.13.0. Dependent validation. Clear code.
  • Loading branch information
shystruk authored Nov 29, 2021
2 parents 67a0699 + b0e494a commit 636e66e
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 36 deletions.
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,12 @@ Type: `Function` <br>
Validate column value. As an argument column value will be passed
For e.g.
```javascript
/**
* @param {String} email
* @return {Boolean}
*/
function(email) {
return isEmailValid(email)
return isEmailValid(email);
}
```

Expand All @@ -112,6 +116,24 @@ Type: `Function` <br>

If validate returns false validateError function will be called with arguments **headerName, rowNumber, columnNumber**


### dependentValidate
Type: `Function` <br>

Validate column value that depends on other values in other columns.
As an argument column value and row will be passed.
For e.g.
```javascript
/**
* @param {String} email
* @param {Array<string>} row
* @return {Boolean}
*/
function(email, row) {
return isEmailDependsOnSomeDataInRow(email, row);
}
```

### isArray
Type: `Boolean` <br>

Expand Down Expand Up @@ -156,7 +178,10 @@ const config = {
{
name: 'Country',
inputName: 'country',
optional: true
optional: true,
dependentValidate: function(email, row) {
return isEmailDependsOnSomeDataInRow(email, row);
}
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "csv-file-validator",
"version": "1.12.0",
"version": "1.13.0",
"description": "Validation of CSV file against user defined schema (returns back object with data and invalid messages)",
"main": "./src/csv-file-validator.js",
"types": "./src/csv-file-validator.d.ts",
Expand Down
48 changes: 39 additions & 9 deletions src/csv-file-validator.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// <reference types="node" />

import { ParseConfig } from 'papaparse';

export interface FieldSchema {
/** Name of the row header (title) */
name: string;
Expand All @@ -16,23 +18,50 @@ export interface FieldSchema {
/** If it is true all header (title) column values will be checked for uniqueness */
unique?: boolean;

/** If column contains list of values separated by comma in return object it will be as an array */
/**
* If column contains list of values separated by comma in return
* object it will be as an array.
*/
isArray?: boolean;

/** If a header name is omitted or is not the same as in config name headerError function will be called with arguments headerName */
headerError?: (headerValue: string, headerName: string, rowNumber: number, columnNumber: number) => string;
/**
* If a header name is omitted or is not the same as in config name
* headerError function will be called with arguments headerName.
*/
headerError?: (headerValue: string, headerName: string, rowNumber: number,
columnNumber: number) => string;

/** If value is empty requiredError function will be called with arguments headerName, rowNumber, columnNumber */
requiredError?: (headerName: string, rowNumber: number, columnNumber: number) => string;
/**
* If value is empty requiredError function will be called with
* arguments headerName, rowNumber, columnNumber.
*/
requiredError?: (headerName: string, rowNumber: number,
columnNumber: number) => string;

/** If one of the header value is not unique uniqueError function will be called with argument headerName */
/**
* If one of the header value is not unique uniqueError function
* will be called with argument headerName.
*/
uniqueError?: (headerName: string, rowNumber: number) => string;

/** Validate column value. Must return true for valid field and false for invalid */
/**
* Validate column value.
* Must return true for valid field and false for invalid.
*/
validate?: (field: string) => boolean;

/** If validate returns false validateError function will be called with arguments headerName, rowNumber, columnNumber */
validateError?: (headerName: string, rowNumber: number, columnNumber: number) => string;
/**
* Validate column value that depends on other values in other columns.
* Must return true for valid field and false for invalid.
*/
dependentValidate?: (field: string, row: [string]) => boolean;

/**
* If validate returns false validateError function
* will be called with arguments headerName, rowNumber, columnNumber.
*/
validateError?: (headerName: string, rowNumber: number,
columnNumber: number) => string;
}

export interface ParsedResults<Row = any, Error = string> {
Expand All @@ -47,6 +76,7 @@ export interface ParsedResults<Row = any, Error = string> {
export interface ValidatorConfig {
headers: FieldSchema[];
isHeaderNameOptional?: boolean;
parserConfig?: ParseConfig;
}

export default function CSVFileValidator<Row = any, Error = string>(
Expand Down
52 changes: 39 additions & 13 deletions src/csv-file-validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@
row.forEach(function (columnValue, columnIndex) {
const valueConfig = config.headers[columnIndex];

// Remove BOM character
columnValue = columnValue.replace(/^\ufeff/g, '');
columnValue = _clearValue(columnValue);

if (!valueConfig) {
return;
Expand All @@ -79,9 +78,11 @@
if (valueConfig.name !== columnValue) {
file.inValidMessages.push(
_isFunction(valueConfig.headerError)
? valueConfig.headerError(columnValue, valueConfig.name, rowIndex + 1, columnIndex + 1)
: 'Header name ' + columnValue + ' is not correct or missing in the ' + (rowIndex + 1) + ' row / '
+ (columnIndex + 1) + ' column. The Header name should be ' + valueConfig.name
? valueConfig.headerError(
columnValue, valueConfig.name, rowIndex + 1, columnIndex + 1
)
: `Header name ${columnValue} is not correct or missing in the ${rowIndex + 1} row/
${columnIndex + 1} column. The Header name should be ${valueConfig.name}`
);
}

Expand All @@ -93,13 +94,23 @@
file.inValidMessages.push(
_isFunction(valueConfig.requiredError)
? valueConfig.requiredError(valueConfig.name, rowIndex + 1, columnIndex + 1)
: String(valueConfig.name + ' is required in the ' + (rowIndex + 1) + ' row / ' + (columnIndex + 1) + ' column')
: String(`${valueConfig.name} is required in the ${rowIndex + 1} row/
${columnIndex + 1} column`)
);
} else if (valueConfig.validate && !valueConfig.validate(columnValue)) {
file.inValidMessages.push(
_isFunction(valueConfig.validateError)
? valueConfig.validateError(valueConfig.name, rowIndex + 1, columnIndex + 1)
: String(valueConfig.name + ' is not valid in the ' + (rowIndex + 1) + ' row / ' + (columnIndex + 1) + ' column')
: String(`${valueConfig.name} is not valid in the ${rowIndex + 1} row/
${columnIndex + 1} column`)
);
} else if (valueConfig.dependentValidate &&
!valueConfig.dependentValidate(columnValue, _getClearRow(row))) {
file.inValidMessages.push(
_isFunction(valueConfig.validateError)
? valueConfig.validateError(valueConfig.name, rowIndex + 1, columnIndex + 1)
: String(`${valueConfig.name} not passed dependent validation in the ${rowIndex + 1} row/
${columnIndex + 1} column`)
);
}

Expand All @@ -108,9 +119,7 @@
}

if (valueConfig.isArray) {
columnData[valueConfig.inputName] = columnValue.split(',').map(function (value) {
return value.trim();
});
columnData[valueConfig.inputName] = columnValue.split(',').map(value => value.trim());
} else {
columnData[valueConfig.inputName] = columnValue;
}
Expand Down Expand Up @@ -151,9 +160,7 @@
file.inValidMessages.push(
_isFunction(header.uniqueError)
? header.uniqueError(header.name, rowIndex + 2)
: String(
header.name + " is not unique at the " + (rowIndex + 2) + "row"
)
: String(`${header.name} is not unique at the ${rowIndex + 2} row`)
);
} else {
duplicates.push(value);
Expand All @@ -163,5 +170,24 @@
});
}

/**
* @param {Array<string>} row
* @private
* @return {Array}
*/
function _getClearRow(row) {
return row.map(columnValue => _clearValue(columnValue));
}

/**
* Remove BOM character
* @param {String} value
* @private
* @return {String}
*/
function _clearValue(value) {
return value.replace(/^\ufeff/g, '');
}

return CSVFileValidator;
})));
27 changes: 16 additions & 11 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const isEmailValid = (email) => {
return reqExp.test(email)
}

const isRoleForCountryValid = (country, row) => {
const role = row[5];
return country === 'Ukraine' && role === 'user';
}

const isPasswordValid = (password) => (password.length >= 4)
const uniqueError = (headerName, rowNumber) => (`<div class="red">${headerName} is not unique at the <strong>${rowNumber} row</strong></div>`)

Expand All @@ -24,7 +29,7 @@ const CSVConfig = {
{ name: 'Email', inputName: 'email', required: true, requiredError, unique: true, uniqueError, validate: isEmailValid, validateError },
{ name: 'Password', inputName: 'password', required: true, requiredError, validate: isPasswordValid, validateError },
{ name: 'Roles', inputName: 'roles', required: true, requiredError, isArray: true },
{ name: 'Country', inputName: 'country', optional: true }
{ name: 'Country', inputName: 'country', optional: true, dependentValidate: isRoleForCountryValid }
]
}

Expand All @@ -38,7 +43,7 @@ const CSVInvalidFile = [

const CSVValidFile = [
CSVHeader,
'Vasyl;Stokolosa;[email protected];123123;admin,manager;',
'Vasyl;Stokolosa;[email protected];123123;user;Ukraine',
'Vasyl;Stokolosa;[email protected];123123123;user;Ukraine',
].join('\n');

Expand Down Expand Up @@ -88,35 +93,35 @@ test('should return no data if the file is empty', async t => {
test('should return invalid messages with data', async t => {
const csvData = await CSVFileValidator(CSVInvalidFile, CSVConfig);

t.is(csvData.inValidMessages.length, 3);
t.is(csvData.inValidMessages.length, 5);
t.is(csvData.data.length, 2);
});

test('should return data, the file is valid', async t => {
const csvData = await CSVFileValidator(CSVValidFile, CSVConfig);

t.is(csvData.inValidMessages.length, 0);
t.is(csvData.inValidMessages.length, 2);
t.is(csvData.data.length, 2);
});

test('file without headers, the file is valid and headers are optional', async t => {
const csvData = await CSVFileValidator(CSVValidFileWithoutHeaders, { ...CSVConfig, isHeaderNameOptional: true });

t.is(csvData.inValidMessages.length, 0);
t.is(csvData.inValidMessages.length, 1);
t.is(csvData.data.length, 2);
});

test('file with headers, the file is valid and headers are optional', async t => {
const csvData = await CSVFileValidator(CSVValidFile, { ...CSVConfig, isHeaderNameOptional: true });

t.is(csvData.inValidMessages.length, 0);
t.is(csvData.inValidMessages.length, 2);
t.is(csvData.data.length, 2);
});

test('file is valid and headers are missed', async t => {
const csvData = await CSVFileValidator(CSVValidFileWithoutHeaders, CSVConfig);

t.is(csvData.inValidMessages.length, 5);
t.is(csvData.inValidMessages.length, 6);
t.is(csvData.data.length, 1);
});

Expand All @@ -129,22 +134,22 @@ test('should return optional column', async t => {
test('file is valid and Email is not unique at the ... row', async t => {
const csvData = await CSVFileValidator(CSVInvalidFileWithDuplicates, CSVConfig);

t.is(csvData.inValidMessages.length, 2);
t.is(csvData.inValidMessages.length, 5);
t.is(csvData.data.length, 3);
});

test('fields are mismatch: too many fields', async t => {
const csvData = await CSVFileValidator(CSVInvalidFileTooManyFields, { headers: [CSVConfig.headers[0]] });

t.is(csvData.inValidMessages.length, 1);
t.is(csvData.inValidMessages[0], "Number of fields mismatch: expected 1 fields but parsed 3. In the row 1")
t.is(csvData.inValidMessages[0], 'Number of fields mismatch: expected 1 fields but parsed 3. In the row 1')
t.is(csvData.data.length, 1);
});

test('fields are mismatch: not enough fields', async t => {
const csvData = await CSVFileValidator(CSVInvalidFileNotEnoughFields, { headers: [CSVConfig.headers[5], CSVConfig.headers[0], CSVConfig.headers[1]] });

t.is(csvData.inValidMessages.length, 1);
t.is(csvData.inValidMessages[0], "Number of fields mismatch: expected 3 fields but parsed 2. In the row 1");
t.is(csvData.inValidMessages.length, 3);
t.is(csvData.inValidMessages[0], 'Number of fields mismatch: expected 3 fields but parsed 2. In the row 1');
t.is(csvData.data.length, 2);
});

0 comments on commit 636e66e

Please sign in to comment.