schema-utils
Advanced tools
Comparing version 2.5.0 to 2.6.0
@@ -5,2 +5,14 @@ # Changelog | ||
## [2.6.0](https://github.com/webpack/schema-utils/compare/v2.5.0...v2.6.0) (2019-11-27) | ||
### Features | ||
* support configuration via title ([#81](https://github.com/webpack/schema-utils/issues/81)) ([afddc10](https://github.com/webpack/schema-utils/commit/afddc109f6891cd37a9f1835d50862d119a072bf)) | ||
### Bug Fixes | ||
* typescript definitions ([#70](https://github.com/webpack/schema-utils/issues/70)) ([f38158d](https://github.com/webpack/schema-utils/commit/f38158d6d040e2c701622778ae8122fb26a4f990)) | ||
## [2.5.0](https://github.com/webpack/schema-utils/compare/v2.4.1...v2.5.0) (2019-10-15) | ||
@@ -7,0 +19,0 @@ |
@@ -8,3 +8,14 @@ "use strict"; | ||
function errorMessage(schema, data, message) { | ||
/** @typedef {import("ajv").Ajv} Ajv */ | ||
/** @typedef {import("../validate").SchemaUtilErrorObject} SchemaUtilErrorObject */ | ||
/** | ||
* | ||
* @param {string} data | ||
* @param {object} schema | ||
* @param {string} message | ||
* @returns {object} // Todo `returns` should be `SchemaUtilErrorObject` | ||
*/ | ||
function errorMessage(message, schema, data) { | ||
return { | ||
@@ -19,37 +30,60 @@ keyword: 'absolutePath', | ||
} | ||
/** | ||
* @param {boolean} shouldBeAbsolute | ||
* @param {object} schema | ||
* @param {string} data | ||
* @returns {object} | ||
*/ | ||
function getErrorFor(shouldBeAbsolute, data, schema) { | ||
function getErrorFor(shouldBeAbsolute, schema, data) { | ||
const message = shouldBeAbsolute ? `The provided value ${JSON.stringify(data)} is not an absolute path!` : `A relative path is expected. However, the provided value ${JSON.stringify(data)} is an absolute path!`; | ||
return errorMessage(schema, data, message); | ||
return errorMessage(message, schema, data); | ||
} | ||
/** | ||
* | ||
* @param {Ajv} ajv | ||
* @returns {Ajv} | ||
*/ | ||
var _default = ajv => ajv.addKeyword('absolutePath', { | ||
errors: true, | ||
type: 'string', | ||
compile(expected, schema) { | ||
function callback(data) { | ||
let passes = true; | ||
const isExclamationMarkPresent = data.includes('!'); | ||
const isCorrectAbsoluteOrRelativePath = expected === /^(?:[A-Za-z]:\\|\/)/.test(data); | ||
function addAbsolutePathKeyword(ajv) { | ||
ajv.addKeyword('absolutePath', { | ||
errors: true, | ||
type: 'string', | ||
if (isExclamationMarkPresent) { | ||
callback.errors = [errorMessage(schema, data, `The provided value ${JSON.stringify(data)} contains exclamation mark (!) which is not allowed because it's reserved for loader syntax.`)]; | ||
passes = false; | ||
} | ||
compile(schema, parentSchema) { | ||
/** | ||
* @param {any} data | ||
* @returns {boolean} | ||
*/ | ||
function callback(data) { | ||
let passes = true; | ||
const isExclamationMarkPresent = data.includes('!'); | ||
const isCorrectAbsoluteOrRelativePath = schema === /^(?:[A-Za-z]:\\|\/)/.test(data); | ||
if (!isCorrectAbsoluteOrRelativePath) { | ||
callback.errors = [getErrorFor(expected, data, schema)]; | ||
passes = false; | ||
if (isExclamationMarkPresent) { | ||
callback.errors = [errorMessage(`The provided value ${JSON.stringify(data)} contains exclamation mark (!) which is not allowed because it's reserved for loader syntax.`, parentSchema, data)]; | ||
passes = false; | ||
} | ||
if (!isCorrectAbsoluteOrRelativePath) { | ||
callback.errors = [getErrorFor(schema, parentSchema, data)]; | ||
passes = false; | ||
} | ||
return passes; | ||
} | ||
/** @type {null | Array<SchemaUtilErrorObject>}*/ | ||
return passes; | ||
callback.errors = []; | ||
return callback; | ||
} | ||
callback.errors = []; | ||
return callback; | ||
} | ||
}); | ||
return ajv; | ||
} | ||
}); | ||
var _default = addAbsolutePathKeyword; | ||
exports.default = _default; |
"use strict"; | ||
const left = Symbol('left'); | ||
const right = Symbol('right'); | ||
/** | ||
* @typedef {[number, boolean]} RangeValue | ||
*/ | ||
/** | ||
* @callback RangeValueCallback | ||
* @param {RangeValue} rangeValue | ||
* @returns {boolean} | ||
*/ | ||
class Range { | ||
@@ -10,3 +16,3 @@ /** | ||
* @param {boolean} exclusive | ||
* @returns {">" | ">" | ">=" | "<="} | ||
* @returns {">" | ">=" | "<" | "<="} | ||
*/ | ||
@@ -68,5 +74,5 @@ static getOperator(side, exclusive) { | ||
/** | ||
* @param {[number, boolean][]} values | ||
* @param {Array<RangeValue>} values | ||
* @param {boolean} logic is not logic applied | ||
* @return {[number, boolean]} computed value and it's exclusive flag | ||
* @return {RangeValue} computed value and it's exclusive flag | ||
*/ | ||
@@ -78,7 +84,11 @@ | ||
let j = -1; | ||
const predicate = logic ? ([value]) => value <= minMax : ([value]) => value >= minMax; | ||
const predicate = logic ? | ||
/** @type {RangeValueCallback} */ | ||
([value]) => value <= minMax : | ||
/** @type {RangeValueCallback} */ | ||
([value]) => value >= minMax; | ||
for (let i = 0; i < values.length; i++) { | ||
if (predicate(values[i])) { | ||
minMax = values[i][0]; | ||
[minMax] = values[i]; | ||
j = i; | ||
@@ -96,12 +106,25 @@ } | ||
constructor() { | ||
this[left] = []; | ||
this[right] = []; | ||
/** @type {Array<RangeValue>} */ | ||
this._left = []; | ||
/** @type {Array<RangeValue>} */ | ||
this._right = []; | ||
} | ||
/** | ||
* @param {number} value | ||
* @param {boolean=} exclusive | ||
*/ | ||
left(value, exclusive = false) { | ||
this[left].push([value, exclusive]); | ||
this._left.push([value, exclusive]); | ||
} | ||
/** | ||
* @param {number} value | ||
* @param {boolean=} exclusive | ||
*/ | ||
right(value, exclusive = false) { | ||
this[right].push([value, exclusive]); | ||
this._right.push([value, exclusive]); | ||
} | ||
@@ -115,4 +138,4 @@ /** | ||
format(logic = true) { | ||
const [start, leftExclusive] = Range.getRangeValue(this[left], logic); | ||
const [end, rightExclusive] = Range.getRangeValue(this[right], !logic); | ||
const [start, leftExclusive] = Range.getRangeValue(this._left, logic); | ||
const [end, rightExclusive] = Range.getRangeValue(this._right, !logic); | ||
@@ -119,0 +142,0 @@ if (!Number.isFinite(start) && !Number.isFinite(end)) { |
@@ -18,2 +18,27 @@ "use strict"; | ||
/** @typedef {import("json-schema").JSONSchema4} JSONSchema4 */ | ||
/** @typedef {import("json-schema").JSONSchema6} JSONSchema6 */ | ||
/** @typedef {import("json-schema").JSONSchema7} JSONSchema7 */ | ||
/** @typedef {import("ajv").ErrorObject} ErrorObject */ | ||
/** @typedef {(JSONSchema4 | JSONSchema6 | JSONSchema7)} Schema */ | ||
/** @typedef {ErrorObject & { children?: Array<ErrorObject>}} SchemaUtilErrorObject */ | ||
/** | ||
* @callback PostFormatter | ||
* @param {string} formattedError | ||
* @param {SchemaUtilErrorObject} error | ||
* @returns {string} | ||
*/ | ||
/** | ||
* @typedef {Object} ValidationErrorConfiguration | ||
* @property {string=} name | ||
* @property {string=} baseDataPath | ||
* @property {PostFormatter=} postFormatter | ||
*/ | ||
const ajv = new _ajv.default({ | ||
@@ -27,4 +52,10 @@ allErrors: true, | ||
(0, _absolutePath.default)(ajv); | ||
/** | ||
* @param {Schema} schema | ||
* @param {Array<object> | object} options | ||
* @param {ValidationErrorConfiguration=} configuration | ||
* @returns {void} | ||
*/ | ||
function validate(schema, options, configuration = {}) { | ||
function validate(schema, options, configuration) { | ||
let errors = []; | ||
@@ -35,3 +66,7 @@ | ||
errors.forEach((list, idx) => { | ||
const applyPrefix = error => { | ||
const applyPrefix = | ||
/** | ||
* @param {SchemaUtilErrorObject} error | ||
*/ | ||
error => { | ||
// eslint-disable-next-line no-param-reassign | ||
@@ -55,19 +90,38 @@ error.dataPath = `[${idx}]${error.dataPath}`; | ||
} | ||
return errors; | ||
} | ||
/** | ||
* @param {Schema} schema | ||
* @param {Array<object> | object} options | ||
* @returns {Array<SchemaUtilErrorObject>} | ||
*/ | ||
function validateObject(schema, options) { | ||
const compiledSchema = ajv.compile(schema); | ||
const valid = compiledSchema(options); | ||
if (!compiledSchema.errors) { | ||
return []; | ||
} | ||
return valid ? [] : filterErrors(compiledSchema.errors); | ||
} | ||
/** | ||
* @param {Array<ErrorObject>} errors | ||
* @returns {Array<SchemaUtilErrorObject>} | ||
*/ | ||
function filterErrors(errors) { | ||
/** @type {Array<SchemaUtilErrorObject>} */ | ||
let newErrors = []; | ||
for (const error of errors) { | ||
for (const error of | ||
/** @type {Array<SchemaUtilErrorObject>} */ | ||
errors) { | ||
const { | ||
dataPath | ||
} = error; | ||
/** @type {Array<SchemaUtilErrorObject>} */ | ||
let children = []; | ||
@@ -74,0 +128,0 @@ newErrors = newErrors.filter(oldError => { |
@@ -9,3 +9,17 @@ "use strict"; | ||
const Range = require('./util/Range'); | ||
/** @typedef {import("json-schema").JSONSchema6} JSONSchema6 */ | ||
/** @typedef {import("json-schema").JSONSchema7} JSONSchema7 */ | ||
/** @typedef {import("./validate").Schema} Schema */ | ||
/** @typedef {import("./validate").ValidationErrorConfiguration} ValidationErrorConfiguration */ | ||
/** @typedef {import("./validate").PostFormatter} PostFormatter */ | ||
/** @typedef {import("./validate").SchemaUtilErrorObject} SchemaUtilErrorObject */ | ||
/** @enum {number} */ | ||
const SPECIFICITY = { | ||
@@ -45,2 +59,8 @@ type: 1, | ||
}; | ||
/** | ||
* | ||
* @param {Array<SchemaUtilErrorObject>} array | ||
* @param {(item: SchemaUtilErrorObject) => number} fn | ||
* @returns {Array<SchemaUtilErrorObject>} | ||
*/ | ||
@@ -51,7 +71,26 @@ function filterMax(array, fn) { | ||
} | ||
/** | ||
* | ||
* @param {Array<SchemaUtilErrorObject>} children | ||
* @returns {Array<SchemaUtilErrorObject>} | ||
*/ | ||
function filterChildren(children) { | ||
let newChildren = children; | ||
newChildren = filterMax(newChildren, error => error.dataPath ? error.dataPath.length : 0); | ||
newChildren = filterMax(newChildren, error => SPECIFICITY[error.keyword] || 2); | ||
newChildren = filterMax(newChildren, | ||
/** | ||
* | ||
* @param {SchemaUtilErrorObject} error | ||
* @returns {number} | ||
*/ | ||
error => error.dataPath ? error.dataPath.length : 0); | ||
newChildren = filterMax(newChildren, | ||
/** | ||
* @param {SchemaUtilErrorObject} error | ||
* @returns {number} | ||
*/ | ||
error => SPECIFICITY[ | ||
/** @type {keyof typeof SPECIFICITY} */ | ||
error.keyword] || 2); | ||
return newChildren; | ||
@@ -61,4 +100,4 @@ } | ||
* Find all children errors | ||
* @param children | ||
* @param {string[]} schemaPaths | ||
* @param {Array<SchemaUtilErrorObject>} children | ||
* @param {Array<string>} schemaPaths | ||
* @return {number} returns index of first child | ||
@@ -71,3 +110,8 @@ */ | ||
const predicate = schemaPath => children[i].schemaPath.indexOf(schemaPath) !== 0; | ||
const predicate = | ||
/** | ||
* @param {string} schemaPath | ||
* @returns {boolean} | ||
*/ | ||
schemaPath => children[i].schemaPath.indexOf(schemaPath) !== 0; | ||
@@ -88,4 +132,4 @@ while (i > -1 && !schemaPaths.every(predicate)) { | ||
* Extracts all refs from schema | ||
* @param error | ||
* @return {string[]} | ||
* @param {SchemaUtilErrorObject} error | ||
* @return {Array<string>} | ||
*/ | ||
@@ -109,4 +153,4 @@ | ||
* Groups children by their first level parent (assuming that error is root) | ||
* @param children | ||
* @return {any[]} | ||
* @param {Array<SchemaUtilErrorObject>} children | ||
* @return {Array<SchemaUtilErrorObject>} | ||
*/ | ||
@@ -147,39 +191,90 @@ | ||
} | ||
/** | ||
* @param {string} str | ||
* @param {string} prefix | ||
* @returns {string} | ||
*/ | ||
function indent(str, prefix) { | ||
return str.replace(/\n(?!$)/g, `\n${prefix}`); | ||
} | ||
/** | ||
* @param {any} maybeObj | ||
* @returns {boolean} | ||
*/ | ||
function isObject(maybyObj) { | ||
return typeof maybyObj === 'object' && maybyObj !== null; | ||
function isObject(maybeObj) { | ||
return typeof maybeObj === 'object' && maybeObj !== null; | ||
} | ||
/** | ||
* @param {Schema} schema | ||
* @returns {boolean} | ||
*/ | ||
function likeNumber(schema) { | ||
return schema.type === 'number' || typeof schema.minimum !== 'undefined' || typeof schema.exclusiveMinimum !== 'undefined' || typeof schema.maximum !== 'undefined' || typeof schema.exclusiveMaximum !== 'undefined' || typeof schema.multipleOf !== 'undefined'; | ||
} | ||
/** | ||
* @param {Schema} schema | ||
* @returns {boolean} | ||
*/ | ||
function likeInteger(schema) { | ||
return schema.type === 'integer' || typeof schema.minimum !== 'undefined' || typeof schema.exclusiveMinimum !== 'undefined' || typeof schema.maximum !== 'undefined' || typeof schema.exclusiveMaximum !== 'undefined' || typeof schema.multipleOf !== 'undefined'; | ||
} | ||
/** | ||
* @param {Schema & {formatMinimum?: string; formatMaximum?: string;}} schema | ||
* @returns {boolean} | ||
*/ | ||
function likeString(schema) { | ||
return schema.type === 'string' || typeof schema.minLength !== 'undefined' || typeof schema.maxLength !== 'undefined' || typeof schema.pattern !== 'undefined' || typeof schema.format !== 'undefined' || typeof schema.formatMinimum !== 'undefined' || typeof schema.formatMaximum !== 'undefined'; | ||
} | ||
/** | ||
* @param {Schema} schema | ||
* @returns {boolean} | ||
*/ | ||
function likeBoolean(schema) { | ||
return schema.type === 'boolean'; | ||
} | ||
/** | ||
* @param {Schema} schema | ||
* @returns {boolean} | ||
*/ | ||
function likeArray(schema) { | ||
return schema.type === 'array' || typeof schema.minItems === 'number' || typeof schema.maxItems === 'number' || typeof schema.uniqueItems !== 'undefined' || typeof schema.items !== 'undefined' || typeof schema.additionalItems !== 'undefined' || typeof schema.contains !== 'undefined'; | ||
} | ||
/** | ||
* @param {Schema & {patternRequired?: Array<string>}} schema | ||
* @returns {boolean} | ||
*/ | ||
function likeObject(schema) { | ||
return schema.type === 'object' || typeof schema.minProperties !== 'undefined' || typeof schema.maxProperties !== 'undefined' || typeof schema.required !== 'undefined' || typeof schema.properties !== 'undefined' || typeof schema.patternProperties !== 'undefined' || typeof schema.additionalProperties !== 'undefined' || typeof schema.dependencies !== 'undefined' || typeof schema.propertyNames !== 'undefined' || typeof schema.patternRequired !== 'undefined'; | ||
} | ||
/** | ||
* @param {Schema} schema | ||
* @returns {boolean} | ||
*/ | ||
function likeNull(schema) { | ||
return schema.type === 'null'; | ||
} | ||
/** | ||
* @param {string} type | ||
* @returns {string} | ||
*/ | ||
function getArticle(type) { | ||
@@ -192,4 +287,13 @@ if (/^[aeiou]/i.test(type)) { | ||
} | ||
/** | ||
* @param {Schema=} schema | ||
* @returns {string} | ||
*/ | ||
function getSchemaNonTypes(schema) { | ||
if (!schema) { | ||
return ''; | ||
} | ||
if (!schema.type) { | ||
@@ -215,4 +319,13 @@ if (likeNumber(schema) || likeInteger(schema)) { | ||
} | ||
/** | ||
* @param {Schema=} schema | ||
* @returns {Array<string>} | ||
*/ | ||
function numberHints(schema) { | ||
if (!schema) { | ||
return []; | ||
} | ||
const hints = []; | ||
@@ -249,7 +362,17 @@ const range = new Range(); | ||
} | ||
/** | ||
* @param {Array<string>} hints | ||
* @returns {string} | ||
*/ | ||
function formatHints(hints) { | ||
return hints.length > 0 ? `(${hints.join(', ')})` : ''; | ||
} | ||
/** | ||
* @param {Schema} schema | ||
* @returns {string} | ||
*/ | ||
function getHints(schema) { | ||
@@ -264,15 +387,56 @@ if (likeNumber(schema) || likeInteger(schema)) { | ||
class ValidationError extends Error { | ||
/** | ||
* @param {Array<SchemaUtilErrorObject>} errors | ||
* @param {Schema} schema | ||
* @param {ValidationErrorConfiguration} configuration | ||
*/ | ||
constructor(errors, schema, configuration = {}) { | ||
super(); | ||
/** @type {string} */ | ||
this.name = 'ValidationError'; | ||
/** @type {Array<SchemaUtilErrorObject>} */ | ||
this.errors = errors; | ||
/** @type {Schema} */ | ||
this.schema = schema; | ||
this.headerName = configuration.name || 'Object'; | ||
this.baseDataPath = configuration.baseDataPath || 'configuration'; | ||
let headerNameFromSchema; | ||
let baseDataPathFromSchema; | ||
if (schema.title && (!configuration.name || !configuration.baseDataPath)) { | ||
const splittedTitleFromSchema = schema.title.match(/^(.+) (.+)$/); | ||
if (splittedTitleFromSchema) { | ||
if (!configuration.name) { | ||
[, headerNameFromSchema] = splittedTitleFromSchema; | ||
} | ||
if (!configuration.name) { | ||
[,, baseDataPathFromSchema] = splittedTitleFromSchema; | ||
} | ||
} | ||
} | ||
/** @type {string} */ | ||
this.headerName = configuration.name || headerNameFromSchema || 'Object'; | ||
/** @type {string} */ | ||
this.baseDataPath = configuration.baseDataPath || baseDataPathFromSchema || 'configuration'; | ||
/** @type {PostFormatter | null} */ | ||
this.postFormatter = configuration.postFormatter || null; | ||
const header = `Invalid ${this.baseDataPath} object. ${this.headerName} has been initialised using ${getArticle(this.baseDataPath)} ${this.baseDataPath} object that does not match the API schema.\n`; | ||
/** @type {string} */ | ||
this.message = `${header}${this.formatValidationErrors(errors)}`; | ||
Error.captureStackTrace(this, this.constructor); | ||
} | ||
/** | ||
* @param {string} path | ||
* @returns {Schema} | ||
*/ | ||
getSchemaPart(path) { | ||
@@ -283,3 +447,5 @@ const newPath = path.split('/'); | ||
for (let i = 1; i < newPath.length; i++) { | ||
const inner = schemaPart[newPath[i]]; | ||
const inner = schemaPart[ | ||
/** @type {keyof Schema} */ | ||
newPath[i]]; | ||
@@ -295,5 +461,18 @@ if (!inner) { | ||
} | ||
/** | ||
* @param {Schema} schema | ||
* @param {Array<Object>} prevSchemas | ||
* @returns {string} | ||
*/ | ||
formatSchema(schema, prevSchemas = []) { | ||
const formatInnerSchema = (innerSchema, addSelf) => { | ||
const formatInnerSchema = | ||
/** | ||
* | ||
* @param {Object} innerSchema | ||
* @param {boolean=} addSelf | ||
* @returns {string} | ||
*/ | ||
(innerSchema, addSelf) => { | ||
if (!addSelf) { | ||
@@ -314,19 +493,24 @@ return this.formatSchema(innerSchema, prevSchemas); | ||
if (schema.instanceof) { | ||
if (Array.isArray(schema.instanceof)) { | ||
return schema.instanceof.map(formatInnerSchema).join(' | '); | ||
} // eslint-disable-next-line default-case | ||
switch (schema.instanceof) { | ||
case 'Function': | ||
return 'function'; | ||
case 'RegExp': | ||
return 'RegExp'; | ||
} | ||
if ( | ||
/** @type {Schema & {instanceof: string | Array<string>}} */ | ||
schema.instanceof) { | ||
const { | ||
instanceof: value | ||
} = | ||
/** @type {Schema & {instanceof: string | Array<string>}} */ | ||
schema; | ||
const values = !Array.isArray(value) ? [value] : value; | ||
return values.map( | ||
/** | ||
* @param {string} item | ||
* @returns {string} | ||
*/ | ||
item => item === 'Function' ? 'function' : item).join(' | '); | ||
} | ||
if (schema.enum) { | ||
return schema.enum.map(item => JSON.stringify(item)).join(' | '); | ||
return ( | ||
/** @type {Array<any>} */ | ||
schema.enum.map(item => JSON.stringify(item)).join(' | ') | ||
); | ||
} | ||
@@ -339,15 +523,33 @@ | ||
if (schema.oneOf) { | ||
return schema.oneOf.map(formatInnerSchema).join(' | '); | ||
return ( | ||
/** @type {Array<Schema>} */ | ||
schema.oneOf.map(item => formatInnerSchema(item, true)).join(' | ') | ||
); | ||
} | ||
if (schema.anyOf) { | ||
return schema.anyOf.map(formatInnerSchema).join(' | '); | ||
return ( | ||
/** @type {Array<Schema>} */ | ||
schema.anyOf.map(item => formatInnerSchema(item, true)).join(' | ') | ||
); | ||
} | ||
if (schema.allOf) { | ||
return schema.allOf.map(formatInnerSchema).join(' & '); | ||
return ( | ||
/** @type {Array<Schema>} */ | ||
schema.allOf.map(item => formatInnerSchema(item, true)).join(' & ') | ||
); | ||
} | ||
if (schema.if) { | ||
return `if ${formatInnerSchema(schema.if)} then ${formatInnerSchema(schema.then)}${schema.else ? ` else ${formatInnerSchema(schema.else)}` : ''}`; | ||
if ( | ||
/** @type {JSONSchema7} */ | ||
schema.if) { | ||
const { | ||
if: ifValue, | ||
then: thenValue, | ||
else: elseValue | ||
} = | ||
/** @type {JSONSchema7} */ | ||
schema; | ||
return `${ifValue ? `if ${formatInnerSchema(ifValue)}` : ''}${thenValue ? ` then ${formatInnerSchema(thenValue)}` : ''}${elseValue ? ` else ${formatInnerSchema(elseValue)}` : ''}`; | ||
} | ||
@@ -395,8 +597,24 @@ | ||
if (schema.formatMinimum) { | ||
hints.push(`should be ${schema.formatExclusiveMinimum ? '>' : '>='} ${JSON.stringify(schema.formatMinimum)}`); | ||
if ( | ||
/** @type {Schema & {formatMinimum?: string; formatExclusiveMinimum?: boolean;}} */ | ||
schema.formatMinimum) { | ||
const { | ||
formatExclusiveMinimum, | ||
formatMinimum | ||
} = | ||
/** @type {Schema & {formatMinimum?: string; formatExclusiveMinimum?: boolean;}} */ | ||
schema; | ||
hints.push(`should be ${formatExclusiveMinimum ? '>' : '>='} ${JSON.stringify(formatMinimum)}`); | ||
} | ||
if (schema.formatMaximum) { | ||
hints.push(`should be ${schema.formatExclusiveMaximum ? '<' : '<='} ${JSON.stringify(schema.formatMaximum)}`); | ||
if ( | ||
/** @type {Schema & {formatMaximum?: string; formatExclusiveMaximum?: boolean;}} */ | ||
schema.formatMaximum) { | ||
const { | ||
formatExclusiveMaximum, | ||
formatMaximum | ||
} = | ||
/** @type {Schema & {formatMaximum?: string; formatExclusiveMaximum?: boolean;}} */ | ||
schema; | ||
hints.push(`should be ${formatExclusiveMaximum ? '<' : '<='} ${JSON.stringify(formatMaximum)}`); | ||
} | ||
@@ -431,6 +649,8 @@ | ||
if (Array.isArray(schema.items) && schema.items.length > 0) { | ||
items = `${schema.items.map(formatInnerSchema).join(', ')}`; | ||
items = `${ | ||
/** @type {Array<Schema>} */ | ||
schema.items.map(item => formatInnerSchema(item)).join(', ')}`; | ||
if (hasAdditionalItems) { | ||
if (isObject(schema.additionalItems) && Object.keys(schema.additionalItems).length > 0) { | ||
if (schema.additionalItems && isObject(schema.additionalItems) && Object.keys(schema.additionalItems).length > 0) { | ||
hints.push(`additional items should be ${formatInnerSchema(schema.additionalItems)}`); | ||
@@ -466,3 +686,3 @@ } | ||
if (typeof schema.maxProperties === 'number') { | ||
hints.push(`should not have more than ${schema.maxProperties} ${schema.minProperties > 1 ? 'properties' : 'property'}`); | ||
hints.push(`should not have more than ${schema.maxProperties} ${schema.minProperties && schema.minProperties > 1 ? 'properties' : 'property'}`); | ||
} | ||
@@ -477,4 +697,5 @@ | ||
const required = schema.required ? schema.required : []; | ||
const allProperties = [...new Set([].concat(required).concat(properties))]; | ||
const hasAdditionalProperties = typeof schema.additionalProperties === 'undefined' || Boolean(schema.additionalProperties); | ||
const allProperties = [...new Set( | ||
/** @type {Array<string>} */ | ||
[].concat(required).concat(properties))]; | ||
const objectStructure = allProperties.map(property => { | ||
@@ -485,7 +706,14 @@ const isRequired = required.includes(property); // Some properties need quotes, maybe we should add check | ||
return `${property}${isRequired ? '' : '?'}`; | ||
}).concat(hasAdditionalProperties ? isObject(schema.additionalProperties) ? [`<key>: ${formatInnerSchema(schema.additionalProperties)}`] : ['…'] : []).join(', '); | ||
}).concat(typeof schema.additionalProperties === 'undefined' || Boolean(schema.additionalProperties) ? schema.additionalProperties && isObject(schema.additionalProperties) ? [`<key>: ${formatInnerSchema(schema.additionalProperties)}`] : ['…'] : []).join(', '); | ||
const { | ||
dependencies, | ||
propertyNames, | ||
patternRequired | ||
} = | ||
/** @type {Schema & {patternRequired?: Array<string>;}} */ | ||
schema; | ||
if (schema.dependencies) { | ||
Object.keys(schema.dependencies).forEach(dependencyName => { | ||
const dependency = schema.dependencies[dependencyName]; | ||
if (dependencies) { | ||
Object.keys(dependencies).forEach(dependencyName => { | ||
const dependency = dependencies[dependencyName]; | ||
@@ -500,8 +728,13 @@ if (Array.isArray(dependency)) { | ||
if (schema.propertyNames && Object.keys(schema.propertyNames).length > 0) { | ||
if (propertyNames && Object.keys(propertyNames).length > 0) { | ||
hints.push(`each property name should match format ${JSON.stringify(schema.propertyNames.format)}`); | ||
} | ||
if (schema.patternRequired && schema.patternRequired.length > 0) { | ||
hints.push(`should have property matching pattern ${schema.patternRequired.map(item => JSON.stringify(item))}`); | ||
if (patternRequired && patternRequired.length > 0) { | ||
hints.push(`should have property matching pattern ${patternRequired.map( | ||
/** | ||
* @param {string} item | ||
* @returns {string} | ||
*/ | ||
item => JSON.stringify(item))}`); | ||
} | ||
@@ -517,3 +750,3 @@ | ||
if (Array.isArray(schema.type)) { | ||
return `${schema.type.map(item => item).join(' | ')}`; | ||
return `${schema.type.join(' | ')}`; | ||
} // Fallback for unknown keywords | ||
@@ -526,7 +759,21 @@ | ||
} | ||
/** | ||
* @param {Schema=} schemaPart | ||
* @param {(boolean | Array<string>)=} additionalPath | ||
* @param {boolean=} needDot | ||
* @returns {string} | ||
*/ | ||
getSchemaPartText(schemaPart, additionalPath, needDot = false) { | ||
if (additionalPath) { | ||
if (!schemaPart) { | ||
return ''; | ||
} | ||
if (Array.isArray(additionalPath)) { | ||
for (let i = 0; i < additionalPath.length; i++) { | ||
const inner = schemaPart[additionalPath[i]]; | ||
/** @type {Schema | undefined} */ | ||
const inner = schemaPart[ | ||
/** @type {keyof Schema} */ | ||
additionalPath[i]]; | ||
@@ -555,4 +802,13 @@ if (inner) { | ||
} | ||
/** | ||
* @param {Schema=} schemaPart | ||
* @returns {string} | ||
*/ | ||
getSchemaPartDescription(schemaPart) { | ||
if (!schemaPart) { | ||
return ''; | ||
} | ||
while (schemaPart.$ref) { | ||
@@ -569,47 +825,103 @@ // eslint-disable-next-line no-param-reassign | ||
} | ||
/** | ||
* @param {SchemaUtilErrorObject} error | ||
* @returns {string} | ||
*/ | ||
formatValidationError(error) { | ||
const dataPath = `${this.baseDataPath}${error.dataPath}`; | ||
const { | ||
keyword, | ||
dataPath: errorDataPath | ||
} = error; | ||
const dataPath = `${this.baseDataPath}${errorDataPath}`; | ||
switch (error.keyword) { | ||
switch (keyword) { | ||
case 'type': | ||
// eslint-disable-next-line default-case | ||
switch (error.params.type) { | ||
case 'number': | ||
return `${dataPath} should be a ${this.getSchemaPartText(error.parentSchema, false, true)}`; | ||
{ | ||
const { | ||
parentSchema, | ||
params | ||
} = error; // eslint-disable-next-line default-case | ||
case 'integer': | ||
return `${dataPath} should be a ${this.getSchemaPartText(error.parentSchema, false, true)}`; | ||
switch ( | ||
/** @type {import("ajv").TypeParams} */ | ||
params.type) { | ||
case 'number': | ||
return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`; | ||
case 'string': | ||
return `${dataPath} should be a ${this.getSchemaPartText(error.parentSchema, false, true)}`; | ||
case 'integer': | ||
return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`; | ||
case 'boolean': | ||
return `${dataPath} should be a ${this.getSchemaPartText(error.parentSchema, false, true)}`; | ||
case 'string': | ||
return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`; | ||
case 'array': | ||
return `${dataPath} should be an array:\n${this.getSchemaPartText(error.parentSchema)}`; | ||
case 'boolean': | ||
return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`; | ||
case 'object': | ||
return `${dataPath} should be an object:\n${this.getSchemaPartText(error.parentSchema)}`; | ||
case 'array': | ||
return `${dataPath} should be an array:\n${this.getSchemaPartText(parentSchema)}`; | ||
case 'null': | ||
return `${dataPath} should be a ${this.getSchemaPartText(error.parentSchema, false, true)}`; | ||
case 'object': | ||
return `${dataPath} should be an object:\n${this.getSchemaPartText(parentSchema)}`; | ||
default: | ||
return `${dataPath} should be:\n${this.getSchemaPartText(error.parentSchema)}`; | ||
case 'null': | ||
return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`; | ||
default: | ||
return `${dataPath} should be:\n${this.getSchemaPartText(parentSchema)}`; | ||
} | ||
} | ||
case 'instanceof': | ||
return `${dataPath} should be an instance of ${this.getSchemaPartText(error.parentSchema)}.`; | ||
{ | ||
const { | ||
parentSchema | ||
} = error; | ||
return `${dataPath} should be an instance of ${this.getSchemaPartText(parentSchema)}.`; | ||
} | ||
case 'pattern': | ||
return `${dataPath} should match pattern ${JSON.stringify(error.params.pattern)}${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
pattern | ||
} = | ||
/** @type {import("ajv").PatternParams} */ | ||
params; | ||
return `${dataPath} should match pattern ${JSON.stringify(pattern)}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
case 'format': | ||
return `${dataPath} should match format ${JSON.stringify(error.params.format)}${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
format | ||
} = | ||
/** @type {import("ajv").FormatParams} */ | ||
params; | ||
return `${dataPath} should match format ${JSON.stringify(format)}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
case 'formatMinimum': | ||
case 'formatMaximum': | ||
return `${dataPath} should be ${error.params.comparison} ${JSON.stringify(error.params.limit)}${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
comparison, | ||
limit | ||
} = | ||
/** @type {import("ajv").ComparisonParams} */ | ||
params; | ||
return `${dataPath} should be ${comparison} ${JSON.stringify(limit)}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
@@ -621,25 +933,67 @@ case 'minimum': | ||
{ | ||
const hints = numberHints(error.parentSchema); | ||
const { | ||
parentSchema, | ||
params | ||
} = error; | ||
const { | ||
comparison, | ||
limit | ||
} = | ||
/** @type {import("ajv").ComparisonParams} */ | ||
params; | ||
const hints = numberHints(parentSchema); | ||
if (hints.length === 0) { | ||
hints.push(`should be ${error.params.comparison} ${error.params.limit}`); | ||
hints.push(`should be ${comparison} ${limit}`); | ||
} | ||
return `${dataPath} ${hints.join(' ')}${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
return `${dataPath} ${hints.join(' ')}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
case 'multipleOf': | ||
return `${dataPath} should be multiple of ${error.params.multipleOf}${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
multipleOf | ||
} = | ||
/** @type {import("ajv").MultipleOfParams} */ | ||
params; | ||
return `${dataPath} should be multiple of ${multipleOf}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
case 'patternRequired': | ||
return `${dataPath} should have property matching pattern ${JSON.stringify(error.params.missingPattern)}${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
missingPattern | ||
} = | ||
/** @type {import("ajv").PatternRequiredParams} */ | ||
params; | ||
return `${dataPath} should have property matching pattern ${JSON.stringify(missingPattern)}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
case 'minLength': | ||
{ | ||
if (error.params.limit === 1) { | ||
return `${dataPath} should be an non-empty string${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
limit | ||
} = | ||
/** @type {import("ajv").LimitParams} */ | ||
params; | ||
if (limit === 1) { | ||
return `${dataPath} should be an non-empty string${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
const length = error.params.limit - 1; | ||
return `${dataPath} should be longer than ${length} character${length > 1 ? 's' : ''}${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
const length = limit - 1; | ||
return `${dataPath} should be longer than ${length} character${length > 1 ? 's' : ''}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
@@ -649,7 +1003,17 @@ | ||
{ | ||
if (error.params.limit === 1) { | ||
return `${dataPath} should be an non-empty array${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
limit | ||
} = | ||
/** @type {import("ajv").LimitParams} */ | ||
params; | ||
if (limit === 1) { | ||
return `${dataPath} should be an non-empty array${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
return `${dataPath} should not have fewer than ${error.params.limit} items${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
return `${dataPath} should not have fewer than ${limit} items${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
@@ -659,41 +1023,147 @@ | ||
{ | ||
if (error.params.limit === 1) { | ||
return `${dataPath} should be an non-empty object${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
limit | ||
} = | ||
/** @type {import("ajv").LimitParams} */ | ||
params; | ||
if (limit === 1) { | ||
return `${dataPath} should be an non-empty object${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
return `${dataPath} should not have fewer than ${error.params.limit} properties${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
return `${dataPath} should not have fewer than ${limit} properties${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
case 'maxLength': | ||
return `${dataPath} should be shorter than ${error.params.limit + 1} characters${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
limit | ||
} = | ||
/** @type {import("ajv").LimitParams} */ | ||
params; | ||
return `${dataPath} should be shorter than ${limit + 1} characters${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
case 'maxItems': | ||
return `${dataPath} should not have more than ${error.params.limit} items${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
limit | ||
} = | ||
/** @type {import("ajv").LimitParams} */ | ||
params; | ||
return `${dataPath} should not have more than ${limit} items${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
case 'maxProperties': | ||
return `${dataPath} should not have more than ${error.params.limit} properties${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
limit | ||
} = | ||
/** @type {import("ajv").LimitParams} */ | ||
params; | ||
return `${dataPath} should not have more than ${limit} properties${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
case 'uniqueItems': | ||
return `${dataPath} should not contain the item '${error.data[error.params.i]}' twice${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
i | ||
} = | ||
/** @type {import("ajv").UniqueItemsParams} */ | ||
params; | ||
return `${dataPath} should not contain the item '${error.data[i]}' twice${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
case 'additionalItems': | ||
return `${dataPath} should not have more than ${error.params.limit} items${getSchemaNonTypes(error.parentSchema)}. These items are valid:\n${this.getSchemaPartText(error.parentSchema)}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
limit | ||
} = | ||
/** @type {import("ajv").LimitParams} */ | ||
params; | ||
return `${dataPath} should not have more than ${limit} items${getSchemaNonTypes(parentSchema)}. These items are valid:\n${this.getSchemaPartText(parentSchema)}`; | ||
} | ||
case 'contains': | ||
return `${dataPath} should contains at least one ${this.getSchemaPartText(error.parentSchema, ['contains'])} item${getSchemaNonTypes(error.parentSchema)}.`; | ||
{ | ||
const { | ||
parentSchema | ||
} = error; | ||
return `${dataPath} should contains at least one ${this.getSchemaPartText(parentSchema, ['contains'])} item${getSchemaNonTypes(parentSchema)}.`; | ||
} | ||
case 'required': | ||
{ | ||
const missingProperty = error.params.missingProperty.replace(/^\./, ''); | ||
const hasProperty = Boolean(error.parentSchema.properties && error.parentSchema.properties[missingProperty]); | ||
return `${dataPath} misses the property '${missingProperty}'${getSchemaNonTypes(error.parentSchema)}.${hasProperty ? ` Should be:\n${this.getSchemaPartText(error.parentSchema, ['properties', missingProperty])}` : this.getSchemaPartDescription(error.parentSchema)}`; | ||
const { | ||
parentSchema, | ||
params | ||
} = error; | ||
const missingProperty = | ||
/** @type {import("ajv").DependenciesParams} */ | ||
params.missingProperty.replace(/^\./, ''); | ||
const hasProperty = parentSchema && Boolean( | ||
/** @type {Schema} */ | ||
parentSchema.properties && | ||
/** @type {Schema} */ | ||
parentSchema.properties[missingProperty]); | ||
return `${dataPath} misses the property '${missingProperty}'${getSchemaNonTypes(parentSchema)}.${hasProperty ? ` Should be:\n${this.getSchemaPartText(parentSchema, ['properties', missingProperty])}` : this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
case 'additionalProperties': | ||
return `${dataPath} has an unknown property '${error.params.additionalProperty}'${getSchemaNonTypes(error.parentSchema)}. These properties are valid:\n${this.getSchemaPartText(error.parentSchema)}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
additionalProperty | ||
} = | ||
/** @type {import("ajv").AdditionalPropertiesParams} */ | ||
params; | ||
return `${dataPath} has an unknown property '${additionalProperty}'${getSchemaNonTypes(parentSchema)}. These properties are valid:\n${this.getSchemaPartText(parentSchema)}`; | ||
} | ||
case 'dependencies': | ||
{ | ||
const dependencies = error.params.deps.split(',').map(dep => `'${dep.trim()}'`).join(', '); | ||
return `${dataPath} should have properties ${dependencies} when property '${error.params.property}' is present${getSchemaNonTypes(error.parentSchema)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
property, | ||
deps | ||
} = | ||
/** @type {import("ajv").DependenciesParams} */ | ||
params; | ||
const dependencies = deps.split(',').map( | ||
/** | ||
* @param {string} dep | ||
* @returns {string} | ||
*/ | ||
dep => `'${dep.trim()}'`).join(', '); | ||
return `${dataPath} should have properties ${dependencies} when property '${property}' is present${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
@@ -703,3 +1173,13 @@ | ||
{ | ||
return `${dataPath} property name '${error.params.propertyName}' is invalid${getSchemaNonTypes(error.parentSchema)}. Property names should be match format ${JSON.stringify(error.schema.format)}.${this.getSchemaPartDescription(error.parentSchema)}`; | ||
const { | ||
params, | ||
parentSchema, | ||
schema | ||
} = error; | ||
const { | ||
propertyName | ||
} = | ||
/** @type {import("ajv").PropertyNamesParams} */ | ||
params; | ||
return `${dataPath} property name '${propertyName}' is invalid${getSchemaNonTypes(parentSchema)}. Property names should be match format ${JSON.stringify(schema.format)}.${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
@@ -709,14 +1189,33 @@ | ||
{ | ||
if (error.parentSchema && error.parentSchema.enum && error.parentSchema.enum.length === 1) { | ||
return `${dataPath} should be ${this.getSchemaPartText(error.parentSchema, false, true)}`; | ||
const { | ||
parentSchema | ||
} = error; | ||
if (parentSchema && | ||
/** @type {Schema} */ | ||
parentSchema.enum && | ||
/** @type {Schema} */ | ||
parentSchema.enum.length === 1) { | ||
return `${dataPath} should be ${this.getSchemaPartText(parentSchema, false, true)}`; | ||
} | ||
return `${dataPath} should be one of these:\n${this.getSchemaPartText(error.parentSchema)}`; | ||
return `${dataPath} should be one of these:\n${this.getSchemaPartText(parentSchema)}`; | ||
} | ||
case 'const': | ||
return `${dataPath} should be equal to constant ${this.getSchemaPartText(error.parentSchema)}`; | ||
{ | ||
const { | ||
parentSchema | ||
} = error; | ||
return `${dataPath} should be equal to constant ${this.getSchemaPartText(parentSchema)}`; | ||
} | ||
case 'not': | ||
return `${dataPath} should not be ${this.getSchemaPartText(error.schema)}${likeObject(error.parentSchema) ? `\n${this.getSchemaPartText(error.parentSchema)}` : ''}`; | ||
{ | ||
const { | ||
schema, | ||
parentSchema | ||
} = error; | ||
return `${dataPath} should not be ${this.getSchemaPartText(schema)}${parentSchema && likeObject(parentSchema) ? `\n${this.getSchemaPartText(parentSchema)}` : ''}`; | ||
} | ||
@@ -726,43 +1225,82 @@ case 'oneOf': | ||
{ | ||
if (error.children && error.children.length > 0) { | ||
const { | ||
parentSchema, | ||
children | ||
} = error; | ||
if (children && children.length > 0) { | ||
if (error.schema.length === 1) { | ||
const lastChild = error.children[error.children.length - 1]; | ||
const remainingChildren = error.children.slice(0, error.children.length - 1); | ||
const lastChild = children[children.length - 1]; | ||
const remainingChildren = children.slice(0, children.length - 1); | ||
return this.formatValidationError(Object.assign({}, lastChild, { | ||
children: remainingChildren, | ||
parentSchema: Object.assign({}, error.parentSchema, lastChild.parentSchema) | ||
parentSchema: Object.assign({}, parentSchema, lastChild.parentSchema) | ||
})); | ||
} | ||
let children = filterChildren(error.children); | ||
let filteredChildren = filterChildren(children); | ||
if (children.length === 1) { | ||
return this.formatValidationError(children[0]); | ||
if (filteredChildren.length === 1) { | ||
return this.formatValidationError(filteredChildren[0]); | ||
} | ||
children = groupChildrenByFirstChild(children); | ||
return `${dataPath} should be one of these:\n${this.getSchemaPartText(error.parentSchema)}\nDetails:\n${children.map(nestedError => ` * ${indent(this.formatValidationError(nestedError), ' ')}`).join('\n')}`; | ||
filteredChildren = groupChildrenByFirstChild(filteredChildren); | ||
return `${dataPath} should be one of these:\n${this.getSchemaPartText(parentSchema)}\nDetails:\n${filteredChildren.map( | ||
/** | ||
* @param {SchemaUtilErrorObject} nestedError | ||
* @returns {string} | ||
*/ | ||
nestedError => ` * ${indent(this.formatValidationError(nestedError), ' ')}`).join('\n')}`; | ||
} | ||
return `${dataPath} should be one of these:\n${this.getSchemaPartText(error.parentSchema)}`; | ||
return `${dataPath} should be one of these:\n${this.getSchemaPartText(parentSchema)}`; | ||
} | ||
case 'if': | ||
return `${dataPath} should match "${error.params.failingKeyword}" schema:\n${this.getSchemaPartText(error.parentSchema, [error.params.failingKeyword])}`; | ||
{ | ||
const { | ||
params, | ||
parentSchema | ||
} = error; | ||
const { | ||
failingKeyword | ||
} = | ||
/** @type {import("ajv").IfParams} */ | ||
params; | ||
return `${dataPath} should match "${failingKeyword}" schema:\n${this.getSchemaPartText(parentSchema, [failingKeyword])}`; | ||
} | ||
case 'absolutePath': | ||
return `${dataPath}: ${error.message}${this.getSchemaPartDescription(error.parentSchema)}`; | ||
{ | ||
const { | ||
message, | ||
parentSchema | ||
} = error; | ||
return `${dataPath}: ${message}${this.getSchemaPartDescription(parentSchema)}`; | ||
} | ||
/* istanbul ignore next */ | ||
default: | ||
// For `custom`, `false schema`, `$ref` keywords | ||
// Fallback for unknown keywords | ||
{ | ||
const { | ||
message, | ||
parentSchema | ||
} = error; | ||
const ErrorInJSON = JSON.stringify(error, null, 2); // For `custom`, `false schema`, `$ref` keywords | ||
// Fallback for unknown keywords | ||
/* istanbul ignore next */ | ||
return `${dataPath} ${error.message} (${JSON.stringify(error, null, 2)}).\n${this.getSchemaPartText(error.parentSchema)}`; | ||
return `${dataPath} ${message} (${ErrorInJSON}).\n${this.getSchemaPartText(parentSchema, false)}`; | ||
} | ||
} | ||
} | ||
/** | ||
* @param {Array<SchemaUtilErrorObject>} errors | ||
* @returns {string} | ||
*/ | ||
formatValidationErrors(errors) { | ||
return errors.map(error => { | ||
let formattedError = this.formatValidationError(error, this.schema); | ||
let formattedError = this.formatValidationError(error); | ||
@@ -769,0 +1307,0 @@ if (this.postFormatter) { |
{ | ||
"name": "schema-utils", | ||
"version": "2.5.0", | ||
"version": "2.6.0", | ||
"description": "webpack Validation Utils", | ||
@@ -10,4 +10,8 @@ "license": "MIT", | ||
"bugs": "https://github.com/webpack/schema-utils/issues", | ||
"funding": { | ||
"type": "opencollective", | ||
"url": "https://opencollective.com/webpack" | ||
}, | ||
"main": "dist/index.js", | ||
"types": "index.d.ts", | ||
"types": "declarations/index.d.ts", | ||
"engines": { | ||
@@ -18,17 +22,20 @@ "node": ">= 8.9.0" | ||
"start": "npm run build -- -w", | ||
"clean": "del-cli dist declarations", | ||
"prebuild": "npm run clean", | ||
"build": "cross-env NODE_ENV=production babel src -d dist --ignore \"src/**/*.test.js\" --copy-files", | ||
"clean": "del-cli dist", | ||
"build:types": "tsc --declaration --emitDeclarationOnly --outDir declarations && prettier \"declarations/**/*.ts\" --write", | ||
"build:code": "cross-env NODE_ENV=production babel src -d dist --copy-files", | ||
"build": "npm-run-all -p \"build:**\"", | ||
"commitlint": "commitlint --from=master", | ||
"lint:prettier": "prettier \"{**/*,*}.{js,json,md,yml,css}\" --list-different", | ||
"lint:js": "eslint --cache src test", | ||
"security": "npm audit", | ||
"lint:prettier": "prettier \"{**/*,*}.{js,json,md,yml,css,ts}\" --list-different", | ||
"lint:js": "eslint --cache .", | ||
"lint:types": "tsc --pretty --noEmit", | ||
"lint": "npm-run-all -l -p \"lint:**\"", | ||
"test:only": "cross-env NODE_ENV=test jest", | ||
"test:watch": "npm run test:only -- --watch", | ||
"test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", | ||
"pretest": "npm run lint", | ||
"test": "npm run test:coverage", | ||
"prepare": "npm run build", | ||
"release": "standard-version", | ||
"security": "npm audit", | ||
"test:only": "cross-env NODE_ENV=test jest", | ||
"test:watch": "cross-env NODE_ENV=test jest --watch", | ||
"test:coverage": "cross-env NODE_ENV=test jest --collectCoverageFrom=\"src/**/*.js\" --coverage", | ||
"pretest": "npm run lint", | ||
"test": "cross-env NODE_ENV=test npm run test:coverage", | ||
"defaults": "webpack-defaults" | ||
@@ -38,3 +45,3 @@ }, | ||
"dist", | ||
"index.d.ts" | ||
"declarations" | ||
], | ||
@@ -46,25 +53,26 @@ "dependencies": { | ||
"devDependencies": { | ||
"@babel/cli": "^7.6.2", | ||
"@babel/core": "^7.6.2", | ||
"@babel/preset-env": "^7.6.2", | ||
"@babel/cli": "^7.7.4", | ||
"@babel/core": "^7.7.4", | ||
"@babel/preset-env": "^7.7.4", | ||
"@commitlint/cli": "^8.2.0", | ||
"@commitlint/config-conventional": "^8.2.0", | ||
"@types/json-schema": "^7.0.3", | ||
"@webpack-contrib/defaults": "^5.0.2", | ||
"@webpack-contrib/defaults": "^6.2.0", | ||
"@webpack-contrib/eslint-config-webpack": "^3.0.0", | ||
"babel-jest": "^24.9.0", | ||
"commitlint-azure-pipelines-cli": "^1.0.2", | ||
"cross-env": "^6.0.0", | ||
"cross-env": "^6.0.3", | ||
"del": "^5.1.0", | ||
"del-cli": "^3.0.0", | ||
"eslint": "^6.4.0", | ||
"eslint-config-prettier": "^6.3.0", | ||
"eslint": "^6.7.1", | ||
"eslint-config-prettier": "^6.7.0", | ||
"eslint-plugin-import": "^2.18.2", | ||
"husky": "^3.0.5", | ||
"husky": "^3.1.0", | ||
"jest": "^24.9.0", | ||
"jest-junit": "^8.0.0", | ||
"lint-staged": "^9.4.0", | ||
"jest-junit": "^9.0.0", | ||
"lint-staged": "^9.5.0", | ||
"npm-run-all": "^4.1.5", | ||
"prettier": "^1.18.2", | ||
"standard-version": "^7.0.0" | ||
"prettier": "^1.19.1", | ||
"standard-version": "^7.0.1", | ||
"typescript": "^3.7.2" | ||
}, | ||
@@ -71,0 +79,0 @@ "keywords": [ |
@@ -99,2 +99,22 @@ <div align="center"> | ||
There is an alternative method to configure the `name` and`baseDataPath` options via the `title` property in the schema. | ||
For example: | ||
```json | ||
{ | ||
"title": "My Loader options", | ||
"type": "object", | ||
"properties": { | ||
"name": { | ||
"description": "This is description of option.", | ||
"type": "string" | ||
} | ||
}, | ||
"additionalProperties": false | ||
} | ||
``` | ||
The last word used for the `baseDataPath` option, other words used for the `name` option. | ||
Based on the example above the `name` option equals `My Loader`, the `baseDataPath` option equals `options`. | ||
#### `name` | ||
@@ -101,0 +121,0 @@ |
70543
14
1604
277
24