Skip to content

Commit

Permalink
feat: improve error messaging for invalid inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
juanjoDiaz committed Apr 9, 2023
1 parent f9962b6 commit 471849f
Show file tree
Hide file tree
Showing 18 changed files with 337 additions and 65 deletions.
15 changes: 11 additions & 4 deletions dist/cdn/plainjs/Parser.js
Expand Up @@ -37,10 +37,17 @@ var JSON2CSVParser = class extends JSON2CSVBase {
*/
preprocessData(data) {
const processedData = Array.isArray(data) ? data : [data];
if (!this.opts.fields && (processedData.length === 0 || typeof processedData[0] !== "object")) {
throw new Error(
'Data should not be empty or the "fields" option should be included'
);
if (!this.opts.fields) {
if (data === void 0 || data === null || processedData.length === 0) {
throw new Error(
'Data should not be empty or the "fields" option should be included'
);
}
if (typeof processedData[0] !== "object") {
throw new Error(
'Data items should be objects or the "fields" option should be included'
);
}
}
if (this.opts.transforms.length === 0)
return processedData;
Expand Down
34 changes: 26 additions & 8 deletions dist/cdn/plainjs/StreamParser.js
Expand Up @@ -19,7 +19,12 @@ var __spreadValues = (a, b) => {
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));

// packages/plainjs/src/StreamParser.ts
import { Tokenizer, TokenParser, TokenType } from "https://cdn.jsdelivr.net/npm/@streamparser/json@0.0.12/dist/mjs/index.mjs";
import {
Tokenizer,
TokenParser,
TokenType,
TokenizerError
} from "https://cdn.jsdelivr.net/npm/@streamparser/json@0.0.12/dist/mjs/index.mjs";
import JSON2CSVBase from "./BaseParser.js";
var JSON2CSVStreamParser = class extends JSON2CSVBase {
constructor(opts, asyncOpts) {
Expand Down Expand Up @@ -75,7 +80,7 @@ var JSON2CSVStreamParser = class extends JSON2CSVBase {
return tokenizer;
}
getBinaryModeTokenizer(asyncOpts) {
const tokenizer = new Tokenizer();
const tokenizer = new Tokenizer(asyncOpts);
tokenizer.onToken = ({ token, value }) => {
if (token === TokenType.LEFT_BRACKET) {
this.tokenParser = new TokenParser({
Expand All @@ -85,13 +90,19 @@ var JSON2CSVStreamParser = class extends JSON2CSVBase {
} else if (token === TokenType.LEFT_BRACE) {
this.tokenParser = new TokenParser({ paths: ["$"], keepStack: false });
} else {
this.onError(new Error("Data should be a JSON object or array"));
this.onError(
new Error(
'Data items should be objects or the "fields" option should be included'
)
);
return;
}
this.configureCallbacks(tokenizer, this.tokenParser);
this.tokenParser.write({ token, value });
};
tokenizer.onError = () => this.onError(new Error("Data should be a JSON object or array"));
tokenizer.onError = (err) => this.onError(
err instanceof TokenizerError ? new Error("Data should be a valid JSON object or array") : err
);
tokenizer.onEnd = () => {
this.pushHeaderIfNotWritten();
this.onEnd();
Expand Down Expand Up @@ -140,10 +151,17 @@ var JSON2CSVStreamParser = class extends JSON2CSVBase {
pushLine(data) {
const processedData = this.preprocessRow(data);
if (!this._hasWritten) {
this.opts.fields = this.preprocessFieldsInfo(
this.opts.fields || Object.keys(processedData[0]),
this.opts.defaultValue
);
if (!this.opts.fields) {
if (typeof processedData[0] !== "object") {
throw new Error(
'Data items should be objects or the "fields" option should be included'
);
}
this.opts.fields = this.preprocessFieldsInfo(
Object.keys(processedData[0]),
this.opts.defaultValue
);
}
this.pushHeader();
}
processedData.forEach((row) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/json2csv.ts
Expand Up @@ -332,7 +332,7 @@ async function processStream<TRaw extends object, T extends object>(
processedError = new Error(`Invalid config file. (${err.message})`);
}
// eslint-disable-next-line no-console
console.error('test', processedError);
console.error(processedError);
process.exit(1);
}
})(programOpts);
14 changes: 9 additions & 5 deletions packages/cli/src/utils/parseNdjson.ts
@@ -1,7 +1,11 @@
export default function parseNdJson<T>(input: string, eol: string): Array<T> {
return input
.split(eol)
.map((line) => line.trim())
.filter((line) => line !== '')
.map((line) => JSON.parse(line));
try {
return input
.split(eol)
.map((line) => line.trim())
.filter((line) => line !== '')
.map((line) => JSON.parse(line));
} catch (err: any) {
throw new Error("Invalid ND-JSON couldn't be parsed");
}
}
60 changes: 52 additions & 8 deletions packages/cli/test/CLI.js
Expand Up @@ -41,19 +41,21 @@ export default function (jsonFixtures, csvFixtures) {
});

testRunner.add(
'should error on invalid ndjson input path without streaming',
'should error if ndjson input data is empty and fields are not set',
async (t) => {
const opts =
'--fields carModel,price,color,manual --ndjson --no-streaming';
const opts = '--ndjson';

try {
await execAsync(
`${cli} -i "${getFixturePath('/json2/ndjsonInvalid.json')}" ${opts}`
`${cli} -i "${getFixturePath('/json/empty.json')}" ${opts}`
);

t.fail('Exception expected.');
t.fail('Exception expected');
} catch (err) {
t.ok(err.message.includes('Invalid input file.'));
t.equal(
err.stderr.split('\n')[2].substring(7),
'Data should not be empty or the "fields" option should be included'
);
}
}
);
Expand Down Expand Up @@ -89,6 +91,27 @@ export default function (jsonFixtures, csvFixtures) {
t.equal(csv, csvFixtures.ndjson);
});

testRunner.add(
'should error on invalid ndjson input path without streaming',
async (t) => {
const opts =
'--fields carModel,price,color,manual --ndjson --no-streaming';

try {
await execAsync(
`${cli} -i "${getFixturePath('/json/ndjsonInvalid.json')}" ${opts}`
);

t.fail('Exception expected.');
} catch (err) {
t.equal(
err.stderr.split('\n')[2].substring(7),
"Invalid ND-JSON couldn't be parsed"
);
}
}
);

testRunner.add('should error on invalid input file path', async (t) => {
try {
await execAsync(`${cli} -i "${getFixturePath('/json2/default.json')}"`);
Expand Down Expand Up @@ -116,15 +139,36 @@ export default function (jsonFixtures, csvFixtures) {
}
);

testRunner.add(
'should error if input data is single item and not an object',
async (t) => {
try {
await execAsync(
`${cli} -i "${getFixturePath('/json/notObjectSingleItem.json')}"`
);

t.fail('Exception expected');
} catch (err) {
t.equal(
err.stderr.split('\n')[2].substring(7),
'Data items should be objects or the "fields" option should be included'
);
}
}
);

testRunner.add('should error if input data is not an object', async (t) => {
try {
await execAsync(
`${cli} -i "${getFixturePath('/json2/notAnObject.json')}"`
`${cli} -i "${getFixturePath('/json/notObjectArray.json')}"`
);

t.fail('Exception expected.');
} catch (err) {
t.ok(err.message.includes('Invalid input file.'));
t.equal(
err.stderr.split('\n')[2].substring(7),
'Data items should be objects or the "fields" option should be included'
);
}
});

Expand Down
26 changes: 23 additions & 3 deletions packages/node/test/AsyncParser.js
Expand Up @@ -152,14 +152,34 @@ export default function (jsonFixtures, csvFixtures) {
}
);

testRunner.add(
'should error if input data is single item and not an object',
async (t) => {
try {
const parser = new Parser();
await parseInput(parser, `"${jsonFixtures.notObjectSingleItem()}"`);

t.fail('Exception expected');
} catch (err) {
t.equal(
err.message,
'Data items should be objects or the "fields" option should be included'
);
}
}
);

testRunner.add('should error if input data is not an object', async (t) => {
try {
const parser = new Parser();
await parseInput(parser, `"${jsonFixtures.notAnObject()}"`);
await parseInput(parser, jsonFixtures.notObjectArray());

t.fail('Exception expected');
} catch (err) {
t.equal(err.message, 'Data should be a JSON object or array');
t.equal(
err.message,
'Data items should be objects or the "fields" option should be included'
);
}
});

Expand Down Expand Up @@ -191,7 +211,7 @@ export default function (jsonFixtures, csvFixtures) {

t.fail('Exception expected');
} catch (err) {
t.equal(err.message, 'Data should be a JSON object or array');
t.equal(err.message, 'Data should be a valid JSON object or array');
}
}
);
Expand Down
26 changes: 23 additions & 3 deletions packages/node/test/AsyncParserInMemory.js
Expand Up @@ -152,14 +152,34 @@ export default function (jsonFixtures, csvFixtures) {
}
);

testRunner.add(
'should error if input data is single item and not an object',
async (t) => {
try {
const parser = new Parser();
await parseInput(parser, `"${jsonFixtures.notObjectSingleItem()}"`);

t.fail('Exception expected');
} catch (err) {
t.equal(
err.message,
'Data items should be objects or the "fields" option should be included'
);
}
}
);

testRunner.add('should error if input data is not an object', async (t) => {
try {
const parser = new Parser();
await parseInput(parser, `"${jsonFixtures.notAnObject()}"`);
await parseInput(parser, jsonFixtures.notObjectArray());

t.fail('Exception expected');
} catch (err) {
t.equal(err.message, 'Data should be a JSON object or array');
t.equal(
err.message,
'Data items should be objects or the "fields" option should be included'
);
}
});

Expand Down Expand Up @@ -191,7 +211,7 @@ export default function (jsonFixtures, csvFixtures) {

t.fail('Exception expected');
} catch (err) {
t.equal(err.message, 'Data should be a JSON object or array');
t.equal(err.message, 'Data should be a valid JSON object or array');
}
}
);
Expand Down
26 changes: 23 additions & 3 deletions packages/node/test/Transform.js
Expand Up @@ -147,14 +147,34 @@ export default function (jsonFixtures, csvFixtures) {
}
);

testRunner.add(
'should error if input data is single item and not an object',
async (t) => {
try {
const parser = new Parser();
await parseInput(parser, jsonFixtures.notObjectSingleItem());

t.fail('Exception expected');
} catch (err) {
t.equal(
err.message,
'Data items should be objects or the "fields" option should be included'
);
}
}
);

testRunner.add('should error if input data is not an object', async (t) => {
try {
const parser = new Parser();
await parseInput(parser, jsonFixtures.notAnObject());
await parseInput(parser, jsonFixtures.notObjectArray());

t.fail('Exception expected');
} catch (err) {
t.equal(err.message, 'Data should be a JSON object or array');
t.equal(
err.message,
'Data items should be objects or the "fields" option should be included'
);
}
});

Expand Down Expand Up @@ -186,7 +206,7 @@ export default function (jsonFixtures, csvFixtures) {

t.fail('Exception expected');
} catch (err) {
t.equal(err.message, 'Data should be a JSON object or array');
t.equal(err.message, 'Data should be a valid JSON object or array');
}
}
);
Expand Down
18 changes: 11 additions & 7 deletions packages/plainjs/src/Parser.ts
Expand Up @@ -55,13 +55,17 @@ export default class JSON2CSVParser<
preprocessData(data: Array<TRaw> | TRaw): Array<T> {
const processedData = Array.isArray(data) ? data : [data];

if (
!this.opts.fields &&
(processedData.length === 0 || typeof processedData[0] !== 'object')
) {
throw new Error(
'Data should not be empty or the "fields" option should be included'
);
if (!this.opts.fields) {
if (data === undefined || data === null || processedData.length === 0) {
throw new Error(
'Data should not be empty or the "fields" option should be included'
);
}
if (typeof processedData[0] !== 'object') {
throw new Error(
'Data items should be objects or the "fields" option should be included'
);
}
}

if (this.opts.transforms.length === 0)
Expand Down

0 comments on commit 471849f

Please sign in to comment.