diff --git a/src/ValidationError.js b/src/ValidationError.js index d9648db..764ea21 100644 --- a/src/ValidationError.js +++ b/src/ValidationError.js @@ -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}`); } @@ -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 diff --git a/test/__snapshots__/index.test.js.snap b/test/__snapshots__/index.test.js.snap index 424352d..467632f 100644 --- a/test/__snapshots__/index.test.js.snap +++ b/test/__snapshots__/index.test.js.snap @@ -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`] = ` @@ -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? }, ...]" `; @@ -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 { : 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`] = ` diff --git a/test/index.test.js b/test/index.test.js index 5813d48..5d548e3 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -212,6 +212,14 @@ describe('Validation', () => { nonEmptyObject2: { foo: 'test' }, }); + createSuccessTestCase('oneOf', { + optimization: { + runtimeChunk: { + name: 'fef', + }, + }, + }); + // The "name" option createFailedTestCase( 'webpack name', @@ -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', {