Skip to content

Commit

Permalink
feat: rework oneOf error output (webpack#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
vankop committed Aug 16, 2019
1 parent 08d8147 commit 954b91d
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 23 deletions.
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

0 comments on commit 954b91d

Please sign in to comment.