Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rework oneOf error output (#48) #50

Merged
merged 1 commit into from
Sep 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion src/ValidationError.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,91 @@ function filterChildren(children) {
return newChildren;
}

/**
* Find all children errors
* @param children
* @param {string[]} schemaPaths
* @return {number} returns index of first child
*/
function findAllChildren(children, schemaPaths) {
let i = children.length - 1;
const predicate = (schemaPath) =>
children[i].schemaPath.indexOf(schemaPath) !== 0;

while (i > -1 && !schemaPaths.every(predicate)) {
if (children[i].keyword === 'anyOf' || children[i].keyword === 'oneOf') {
const refs = extractRefs(children[i]);
const childrenStart = findAllChildren(
children.slice(0, i),
refs.concat(children[i].schemaPath)
);
i = childrenStart - 1;
} else {
i -= 1;
}
}

return i + 1;
}

/**
* Extracts all refs from schema
* @param error
* @return {string[]}
*/
function extractRefs(error) {
const { schema } = error;

if (!Array.isArray(schema)) {
return [];
}

return schema.map(({ $ref }) => $ref).filter((s) => s);
}

/**
* Groups children by their first level parent (assuming that error is root)
* @param children
* @return {any[]}
*/
function groupChildrenByFirstChild(children) {
const result = [];
let i = children.length - 1;

while (i > 0) {
const child = children[i];

if (child.keyword === 'anyOf' || child.keyword === 'oneOf') {
const refs = extractRefs(child);
const childrenStart = findAllChildren(
children.slice(0, i),
refs.concat(child.schemaPath)
);

if (childrenStart !== i) {
result.push(
Object.assign({}, child, {
children: children.slice(childrenStart, i),
})
);
i = childrenStart;
} else {
result.push(child);
}
} else {
result.push(child);
}

i -= 1;
}

if (i === 0) {
result.push(children[i]);
}

return result.reverse();
}

function indent(str, prefix) {
return str.replace(/\n(?!$)/g, `\n${prefix}`);
}
Expand Down Expand Up @@ -767,12 +852,14 @@ class ValidationError extends Error {
);
}

const children = filterChildren(error.children);
let children = filterChildren(error.children);

if (children.length === 1) {
return this.formatValidationError(children[0]);
}

children = groupChildrenByFirstChild(children);

return `${dataPath} should be one of these:\n${this.getSchemaPartText(
error.parentSchema
)}\nDetails:\n${children
Expand Down
63 changes: 42 additions & 21 deletions test/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,14 @@ exports[`Validation should fail validation for anyOf 1`] = `
Details:
* configuration.anyOfKeyword should be an object:
object { foo?, … }
* configuration.anyOfKeyword should be a non-empty string.
-> An entry point without name. The string is resolved to a module which is loaded upon startup.
* configuration.anyOfKeyword should be an array:
[non-empty string, ...] (should not have fewer than 1 item, should not have duplicate items)
-> A non-empty array of non-empty strings
* configuration.anyOfKeyword should be one of these:
[non-empty string, ...] (should not have fewer than 1 item, should not have duplicate items)
-> An entry point without name. All modules are loaded upon startup. The last one is exported.
* configuration.anyOfKeyword should be one of these:
non-empty string | [non-empty string, ...] (should not have fewer than 1 item, should not have duplicate items)"
non-empty string | [non-empty string, ...] (should not have fewer than 1 item, should not have duplicate items)
Details:
* configuration.anyOfKeyword should be a non-empty string.
-> An entry point without name. The string is resolved to a module which is loaded upon startup.
* configuration.anyOfKeyword should be an array:
[non-empty string, ...] (should not have fewer than 1 item, should not have duplicate items)
-> A non-empty array of non-empty strings"
`;

exports[`Validation should fail validation for array #1 1`] = `
Expand Down Expand Up @@ -905,8 +903,6 @@ exports[`Validation should fail validation for module 1`] = `
[RegExp | non-empty string | function | [(recursive), ...] | object { and?, exclude?, include?, not?, or?, test? }, ...]
* configuration.module.rules[0].compiler should be an object:
object { and?, exclude?, include?, not?, or?, test? }
* configuration.module.rules[0].compiler should be one of these:
RegExp | non-empty string | function | [(recursive), ...] | object { and?, exclude?, include?, not?, or?, test? }
* configuration.module.rules[0].compiler should be an array:
[RegExp | non-empty string | function | [(recursive), ...] | object { and?, exclude?, include?, not?, or?, test? }, ...]"
`;
Expand Down Expand Up @@ -1143,23 +1139,48 @@ exports[`Validation should fail validation for one const 1`] = `
- configuration.oneConst should be equal to constant [\\"foo\\"]"
`;

exports[`Validation should fail validation for oneOf 1`] = `
exports[`Validation should fail validation for oneOf #1 1`] = `
"Invalid configuration object. Object has been initialised using a configuration object that does not match the API schema.
- configuration.entry should be one of these:
function | object { <key>: non-empty string | [non-empty string, ...] (should not have fewer than 1 item, should not have duplicate items) } (should not have fewer than 1 property) | non-empty string | [non-empty string, ...] (should not have fewer than 1 item, should not have duplicate items)
-> The entry point(s) of the compilation.
Details:
* configuration.entry['foo'] should be a non-empty string.
-> The string is resolved to a module which is loaded upon startup.
* configuration.entry['foo'] should be an array:
[non-empty string, ...] (should not have fewer than 1 item, should not have duplicate items)
-> A non-empty array of non-empty strings
* configuration.entry['foo'] should be one of these:
[non-empty string, ...] (should not have fewer than 1 item, should not have duplicate items)
-> All modules are loaded upon startup. The last one is exported.
* configuration.entry['foo'] should be one of these:
non-empty string | [non-empty string, ...] (should not have fewer than 1 item, should not have duplicate items)
-> An entry point with name"
-> An entry point with name
Details:
* configuration.entry['foo'] should be a non-empty string.
-> The string is resolved to a module which is loaded upon startup.
* configuration.entry['foo'] should be an array:
[non-empty string, ...] (should not have fewer than 1 item, should not have duplicate items)
-> A non-empty array of non-empty strings"
`;

exports[`Validation should fail validation for oneOf #2 1`] = `
"Invalid configuration object. Object has been initialised using a configuration object that does not match the API schema.
- configuration.optimization.runtimeChunk should be one of these:
boolean | \\"single\\" | \\"multiple\\" | object { name? }
-> Create an additional chunk which contains only the webpack runtime and chunk hash maps
Details:
* configuration.optimization.runtimeChunk.name should be one of these:
string | function
-> The name or name factory for the runtime chunks
Details:
* configuration.optimization.runtimeChunk.name should be a string.
* configuration.optimization.runtimeChunk.name should be an instance of function."
`;

exports[`Validation should fail validation for oneOf #3 1`] = `
"Invalid configuration object. Object has been initialised using a configuration object that does not match the API schema.
- configuration.optimization.runtimeChunk should be one of these:
boolean | \\"single\\" | \\"multiple\\" | object { name? }
-> Create an additional chunk which contains only the webpack runtime and chunk hash maps
Details:
* configuration.optimization.runtimeChunk should be a boolean.
* configuration.optimization.runtimeChunk should be one of these:
\\"single\\" | \\"multiple\\"
* configuration.optimization.runtimeChunk should be an object:
object { name? }"
`;

exports[`Validation should fail validation for oneOf with description 1`] = `
Expand Down
32 changes: 31 additions & 1 deletion test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,14 @@ describe('Validation', () => {
nonEmptyObject2: { foo: 'test' },
});

createSuccessTestCase('oneOf', {
optimization: {
runtimeChunk: {
name: 'fef',
},
},
});

// The "name" option
createFailedTestCase(
'webpack name',
Expand Down Expand Up @@ -781,13 +789,35 @@ describe('Validation', () => {
);

createFailedTestCase(
'oneOf',
'oneOf #1',
{
entry: { foo: () => [] },
},
(msg) => expect(msg).toMatchSnapshot()
);

createFailedTestCase(
'oneOf #2',
{
optimization: {
runtimeChunk: {
name: /fef/,
},
},
},
(msg) => expect(msg).toMatchSnapshot()
);

createFailedTestCase(
'oneOf #3',
{
optimization: {
runtimeChunk: (name) => name,
},
},
(msg) => expect(msg).toMatchSnapshot()
);

createFailedTestCase(
'allOf',
{
Expand Down