├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── .prettierignore ├── .gitattributes ├── .github ├── assets │ └── logo.png └── workflows │ └── nodejs.yml ├── lint-staged.config.js ├── jest.config.js ├── declarations ├── util │ ├── hints.d.ts │ ├── memorize.d.ts │ └── Range.d.ts ├── keywords │ ├── absolutePath.d.ts │ ├── limit.d.ts │ └── undefinedAsNull.d.ts ├── index.d.ts ├── validate.d.ts └── ValidationError.d.ts ├── commitlint.config.js ├── test ├── fixtures │ ├── schema-title.json │ └── schema-title-broken.json ├── hints.test.js ├── range.test.js ├── __snapshots__ │ └── api.test.js.snap ├── api.test.js └── index.test.js ├── .editorconfig ├── eslint.config.mjs ├── tsconfig.json ├── babel.config.js ├── .gitignore ├── src ├── util │ ├── memorize.js │ ├── hints.js │ └── Range.js ├── index.js ├── keywords │ ├── undefinedAsNull.js │ ├── absolutePath.js │ └── limit.js ├── validate.js └── ValidationError.js ├── LICENSE ├── package.json ├── README.md └── CHANGELOG.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /test/fixtures 5 | CHANGELOG.md 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | yarn.lock -diff 2 | * text=auto 3 | bin/* eol=lf 4 | 5 | package-lock.json -diff -------------------------------------------------------------------------------- /.github/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/schema-utils/HEAD/.github/assets/logo.png -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*": ["prettier --write --ignore-unknown"], 3 | "*.js": ["eslint --cache --fix"], 4 | }; 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('jest').Config} */ 2 | const config = { 3 | prettierPath: require.resolve("prettier-2"), 4 | }; 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /declarations/util/hints.d.ts: -------------------------------------------------------------------------------- 1 | export function stringHints(schema: Schema, logic: boolean): string[]; 2 | export function numberHints(schema: Schema, logic: boolean): string[]; 3 | export type Schema = import("../validate").Schema; 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "header-max-length": [0], 5 | "body-max-line-length": [0], 6 | "footer-max-line-length": [0], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema-title.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "CSS Loader options", 3 | "additionalProperties": false, 4 | "properties": { 5 | "name": { 6 | "type": "boolean" 7 | } 8 | }, 9 | "type": "object" 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/schema-title-broken.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "CSSLoaderoptions", 3 | "additionalProperties": false, 4 | "properties": { 5 | "name": { 6 | "type": "boolean" 7 | } 8 | }, 9 | "type": "object" 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import configs from "eslint-config-webpack/configs.js"; 3 | 4 | export default defineConfig([ 5 | { 6 | extends: [configs["recommended-dirty"]], 7 | rules: { 8 | "n/prefer-node-protocol": "off", 9 | }, 10 | }, 11 | ]); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "strict": true, 8 | "types": ["node"], 9 | "resolveJsonModule": true, 10 | "newLine": "LF" 11 | }, 12 | "include": ["./src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const MIN_BABEL_VERSION = 7; 2 | 3 | module.exports = (api) => { 4 | api.assertVersion(MIN_BABEL_VERSION); 5 | api.cache(true); 6 | 7 | return { 8 | presets: [ 9 | [ 10 | "@babel/preset-env", 11 | { 12 | targets: { 13 | node: "10.13.0", 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | 9 | # Build 10 | 11 | .eslintcache 12 | 13 | /local 14 | /reports 15 | /coverage 16 | /node_modules 17 | 18 | # Editor & OS 19 | 20 | .idea 21 | .vscode 22 | *.sublime-project 23 | *.sublime-workspace 24 | 25 | DS_Store 26 | .DS_Store 27 | Thumbs.db 28 | *.iml 29 | 30 | #build 31 | dist 32 | /dist 33 | -------------------------------------------------------------------------------- /declarations/util/memorize.d.ts: -------------------------------------------------------------------------------- 1 | export default memoize; 2 | export type FunctionReturning = () => T; 3 | /** 4 | * @template T 5 | * @typedef {() => T} FunctionReturning 6 | */ 7 | /** 8 | * @template T 9 | * @param {FunctionReturning} fn memorized function 10 | * @returns {FunctionReturning} new function 11 | */ 12 | declare function memoize(fn: FunctionReturning): FunctionReturning; 13 | -------------------------------------------------------------------------------- /declarations/keywords/absolutePath.d.ts: -------------------------------------------------------------------------------- 1 | export default addAbsolutePathKeyword; 2 | export type Ajv = import("ajv").default; 3 | export type SchemaValidateFunction = import("ajv").SchemaValidateFunction; 4 | export type AnySchemaObject = import("ajv").AnySchemaObject; 5 | export type SchemaUtilErrorObject = import("../validate").SchemaUtilErrorObject; 6 | /** 7 | * @param {Ajv} ajv ajv 8 | * @returns {Ajv} configured ajv 9 | */ 10 | declare function addAbsolutePathKeyword(ajv: Ajv): Ajv; 11 | -------------------------------------------------------------------------------- /declarations/keywords/limit.d.ts: -------------------------------------------------------------------------------- 1 | export default addLimitKeyword; 2 | export type Ajv = import("ajv").default; 3 | export type Code = import("ajv").Code; 4 | export type Name = import("ajv").Name; 5 | export type KeywordErrorDefinition = import("ajv").KeywordErrorDefinition; 6 | /** @typedef {import("ajv").default} Ajv */ 7 | /** @typedef {import("ajv").Code} Code */ 8 | /** @typedef {import("ajv").Name} Name */ 9 | /** @typedef {import("ajv").KeywordErrorDefinition} KeywordErrorDefinition */ 10 | /** 11 | * @param {Ajv} ajv ajv 12 | * @returns {Ajv} ajv with limit keyword 13 | */ 14 | declare function addLimitKeyword(ajv: Ajv): Ajv; 15 | -------------------------------------------------------------------------------- /src/util/memorize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @template T 3 | * @typedef {() => T} FunctionReturning 4 | */ 5 | 6 | /** 7 | * @template T 8 | * @param {FunctionReturning} fn memorized function 9 | * @returns {FunctionReturning} new function 10 | */ 11 | const memoize = (fn) => { 12 | let cache = false; 13 | /** @type {T} */ 14 | let result; 15 | 16 | return () => { 17 | if (cache) { 18 | return result; 19 | } 20 | result = fn(); 21 | cache = true; 22 | // Allow to clean up memory for fn 23 | // and all dependent resources 24 | /** @type {FunctionReturning | undefined} */ 25 | (fn) = undefined; 26 | 27 | return result; 28 | }; 29 | }; 30 | 31 | export default memoize; 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("./validate").Schema} Schema */ 2 | /** @typedef {import("./validate").JSONSchema4} JSONSchema4 */ 3 | /** @typedef {import("./validate").JSONSchema6} JSONSchema6 */ 4 | /** @typedef {import("./validate").JSONSchema7} JSONSchema7 */ 5 | /** @typedef {import("./validate").ExtendedSchema} ExtendedSchema */ 6 | /** @typedef {import("./validate").ValidationErrorConfiguration} ValidationErrorConfiguration */ 7 | 8 | const { 9 | validate, 10 | ValidationError, 11 | enableValidation, 12 | disableValidation, 13 | needValidate, 14 | } = require("./validate"); 15 | 16 | module.exports = { 17 | validate, 18 | ValidationError, 19 | enableValidation, 20 | disableValidation, 21 | needValidate, 22 | }; 23 | -------------------------------------------------------------------------------- /declarations/keywords/undefinedAsNull.d.ts: -------------------------------------------------------------------------------- 1 | export default addUndefinedAsNullKeyword; 2 | export type Ajv = import("ajv").default; 3 | export type SchemaValidateFunction = import("ajv").SchemaValidateFunction; 4 | export type AnySchemaObject = import("ajv").AnySchemaObject; 5 | export type ValidateFunction = import("ajv").ValidateFunction; 6 | /** @typedef {import("ajv").default} Ajv */ 7 | /** @typedef {import("ajv").SchemaValidateFunction} SchemaValidateFunction */ 8 | /** @typedef {import("ajv").AnySchemaObject} AnySchemaObject */ 9 | /** @typedef {import("ajv").ValidateFunction} ValidateFunction */ 10 | /** 11 | * @param {Ajv} ajv ajv 12 | * @returns {Ajv} configured ajv 13 | */ 14 | declare function addUndefinedAsNullKeyword(ajv: Ajv): Ajv; 15 | -------------------------------------------------------------------------------- /declarations/index.d.ts: -------------------------------------------------------------------------------- 1 | export type Schema = import("./validate").Schema; 2 | export type JSONSchema4 = import("./validate").JSONSchema4; 3 | export type JSONSchema6 = import("./validate").JSONSchema6; 4 | export type JSONSchema7 = import("./validate").JSONSchema7; 5 | export type ExtendedSchema = import("./validate").ExtendedSchema; 6 | export type ValidationErrorConfiguration = 7 | import("./validate").ValidationErrorConfiguration; 8 | import { validate } from "./validate"; 9 | import { ValidationError } from "./validate"; 10 | import { enableValidation } from "./validate"; 11 | import { disableValidation } from "./validate"; 12 | import { needValidate } from "./validate"; 13 | export { 14 | validate, 15 | ValidationError, 16 | enableValidation, 17 | disableValidation, 18 | needValidate, 19 | }; 20 | -------------------------------------------------------------------------------- /src/keywords/undefinedAsNull.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("ajv").default} Ajv */ 2 | /** @typedef {import("ajv").SchemaValidateFunction} SchemaValidateFunction */ 3 | /** @typedef {import("ajv").AnySchemaObject} AnySchemaObject */ 4 | /** @typedef {import("ajv").ValidateFunction} ValidateFunction */ 5 | 6 | /** 7 | * @param {Ajv} ajv ajv 8 | * @returns {Ajv} configured ajv 9 | */ 10 | function addUndefinedAsNullKeyword(ajv) { 11 | ajv.addKeyword({ 12 | keyword: "undefinedAsNull", 13 | before: "enum", 14 | modifying: true, 15 | /** @type {SchemaValidateFunction} */ 16 | validate(kwVal, data, metadata, dataCxt) { 17 | if ( 18 | kwVal && 19 | dataCxt && 20 | metadata && 21 | typeof metadata.enum !== "undefined" 22 | ) { 23 | const idx = dataCxt.parentDataProperty; 24 | 25 | if (typeof dataCxt.parentData[idx] === "undefined") { 26 | dataCxt.parentData[dataCxt.parentDataProperty] = null; 27 | } 28 | } 29 | 30 | return true; 31 | }, 32 | }); 33 | 34 | return ajv; 35 | } 36 | 37 | export default addUndefinedAsNullKeyword; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/hints.test.js: -------------------------------------------------------------------------------- 1 | const { stringHints } = require("../src/util/hints"); 2 | 3 | /** 4 | * Test cases in format [0] schema, [1] hints without 'not' logic, [2] hints with 'not' logic 5 | */ 6 | const testCases = [ 7 | [ 8 | { 9 | format: "[0-9]*", 10 | minLength: 10, 11 | }, 12 | ["should be longer than 9 characters", 'should match format "[0-9]*"'], 13 | [ 14 | "should be shorter than 11 characters", 15 | 'should not match format "[0-9]*"', 16 | ], 17 | ], 18 | [ 19 | { 20 | maxLength: 10, 21 | minLength: 1, 22 | }, 23 | ["should be shorter than 11 characters"], 24 | ["should be longer than 9 characters"], 25 | ], 26 | [ 27 | { 28 | pattern: "phone", 29 | }, 30 | ['should match pattern "phone"'], 31 | ['should not match pattern "phone"'], 32 | ], 33 | [ 34 | { 35 | format: "date", 36 | formatMaximum: "01.01.2022", 37 | formatExclusiveMaximum: "01.01.2022", 38 | }, 39 | ['should match format "date"', 'should be < "01.01.2022"'], 40 | ['should not match format "date"', 'should be >= "01.01.2022"'], 41 | ], 42 | ]; 43 | 44 | describe("hints", () => { 45 | for (const testCase of testCases) { 46 | it(JSON.stringify(testCase[0]), () => { 47 | const [input, withoutNot, withNot] = testCase; 48 | const computedWithoutNot = stringHints(input, true); 49 | const computedWithNot = stringHints(input, false); 50 | 51 | expect(computedWithNot).toEqual(expect.arrayContaining(withNot)); 52 | expect(computedWithoutNot).toEqual(expect.arrayContaining(withoutNot)); 53 | }); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /declarations/validate.d.ts: -------------------------------------------------------------------------------- 1 | export { default as ValidationError } from "./ValidationError"; 2 | export type JSONSchema4 = import("json-schema").JSONSchema4; 3 | export type JSONSchema6 = import("json-schema").JSONSchema6; 4 | export type JSONSchema7 = import("json-schema").JSONSchema7; 5 | export type ErrorObject = import("ajv").ErrorObject; 6 | export type ExtendedSchema = { 7 | /** 8 | * format minimum 9 | */ 10 | formatMinimum?: (string | number) | undefined; 11 | /** 12 | * format maximum 13 | */ 14 | formatMaximum?: (string | number) | undefined; 15 | /** 16 | * format exclusive minimum 17 | */ 18 | formatExclusiveMinimum?: (string | boolean) | undefined; 19 | /** 20 | * format exclusive maximum 21 | */ 22 | formatExclusiveMaximum?: (string | boolean) | undefined; 23 | /** 24 | * link 25 | */ 26 | link?: string | undefined; 27 | /** 28 | * undefined will be resolved as null 29 | */ 30 | undefinedAsNull?: boolean | undefined; 31 | }; 32 | export type Extend = ExtendedSchema; 33 | export type Schema = (JSONSchema4 | JSONSchema6 | JSONSchema7) & ExtendedSchema; 34 | export type SchemaUtilErrorObject = ErrorObject & { 35 | children?: Array; 36 | }; 37 | export type PostFormatter = ( 38 | formattedError: string, 39 | error: SchemaUtilErrorObject, 40 | ) => string; 41 | export type ValidationErrorConfiguration = { 42 | /** 43 | * name 44 | */ 45 | name?: string | undefined; 46 | /** 47 | * base data path 48 | */ 49 | baseDataPath?: string | undefined; 50 | /** 51 | * post formatter 52 | */ 53 | postFormatter?: PostFormatter | undefined; 54 | }; 55 | /** 56 | * @param {Schema} schema schema 57 | * @param {Array | object} options options 58 | * @param {ValidationErrorConfiguration=} configuration configuration 59 | * @returns {void} 60 | */ 61 | export function validate( 62 | schema: Schema, 63 | options: Array | object, 64 | configuration?: ValidationErrorConfiguration | undefined, 65 | ): void; 66 | /** 67 | * @returns {void} 68 | */ 69 | export function enableValidation(): void; 70 | /** 71 | * @returns {void} 72 | */ 73 | export function disableValidation(): void; 74 | /** 75 | * @returns {boolean} true when need validate, otherwise false 76 | */ 77 | export function needValidate(): boolean; 78 | -------------------------------------------------------------------------------- /declarations/ValidationError.d.ts: -------------------------------------------------------------------------------- 1 | export default ValidationError; 2 | export type JSONSchema6 = import("json-schema").JSONSchema6; 3 | export type JSONSchema7 = import("json-schema").JSONSchema7; 4 | export type Schema = import("./validate").Schema; 5 | export type ValidationErrorConfiguration = 6 | import("./validate").ValidationErrorConfiguration; 7 | export type PostFormatter = import("./validate").PostFormatter; 8 | export type SchemaUtilErrorObject = import("./validate").SchemaUtilErrorObject; 9 | declare class ValidationError extends Error { 10 | /** 11 | * @param {Array} errors array of error objects 12 | * @param {Schema} schema schema 13 | * @param {ValidationErrorConfiguration} configuration configuration 14 | */ 15 | constructor( 16 | errors: Array, 17 | schema: Schema, 18 | configuration?: ValidationErrorConfiguration, 19 | ); 20 | /** @type {Array} */ 21 | errors: Array; 22 | /** @type {Schema} */ 23 | schema: Schema; 24 | /** @type {string} */ 25 | headerName: string; 26 | /** @type {string} */ 27 | baseDataPath: string; 28 | /** @type {PostFormatter | null} */ 29 | postFormatter: PostFormatter | null; 30 | /** 31 | * @param {string} path path 32 | * @returns {Schema} schema 33 | */ 34 | getSchemaPart(path: string): Schema; 35 | /** 36 | * @param {Schema} schema schema 37 | * @param {boolean} logic logic 38 | * @param {Array} prevSchemas prev schemas 39 | * @returns {string} formatted schema 40 | */ 41 | formatSchema( 42 | schema: Schema, 43 | logic?: boolean, 44 | prevSchemas?: Array, 45 | ): string; 46 | /** 47 | * @param {Schema=} schemaPart schema part 48 | * @param {(boolean | Array)=} additionalPath additional path 49 | * @param {boolean=} needDot true when need dot 50 | * @param {boolean=} logic logic 51 | * @returns {string} schema part text 52 | */ 53 | getSchemaPartText( 54 | schemaPart?: Schema | undefined, 55 | additionalPath?: (boolean | Array) | undefined, 56 | needDot?: boolean | undefined, 57 | logic?: boolean | undefined, 58 | ): string; 59 | /** 60 | * @param {Schema=} schemaPart schema part 61 | * @returns {string} schema part description 62 | */ 63 | getSchemaPartDescription(schemaPart?: Schema | undefined): string; 64 | /** 65 | * @param {SchemaUtilErrorObject} error error object 66 | * @returns {string} formatted error object 67 | */ 68 | formatValidationError(error: SchemaUtilErrorObject): string; 69 | /** 70 | * @param {Array} errors errors 71 | * @returns {string} formatted errors 72 | */ 73 | formatValidationErrors(errors: Array): string; 74 | } 75 | -------------------------------------------------------------------------------- /declarations/util/Range.d.ts: -------------------------------------------------------------------------------- 1 | export = Range; 2 | /** 3 | * @typedef {[number, boolean]} RangeValue 4 | */ 5 | /** 6 | * @callback RangeValueCallback 7 | * @param {RangeValue} rangeValue 8 | * @returns {boolean} 9 | */ 10 | declare class Range { 11 | /** 12 | * @param {"left" | "right"} side side 13 | * @param {boolean} exclusive exclusive 14 | * @returns {">" | ">=" | "<" | "<="} operator 15 | */ 16 | static getOperator( 17 | side: "left" | "right", 18 | exclusive: boolean, 19 | ): ">" | ">=" | "<" | "<="; 20 | /** 21 | * @param {number} value value 22 | * @param {boolean} logic is not logic applied 23 | * @param {boolean} exclusive is range exclusive 24 | * @returns {string} formatted right 25 | */ 26 | static formatRight(value: number, logic: boolean, exclusive: boolean): string; 27 | /** 28 | * @param {number} value value 29 | * @param {boolean} logic is not logic applied 30 | * @param {boolean} exclusive is range exclusive 31 | * @returns {string} formatted left 32 | */ 33 | static formatLeft(value: number, logic: boolean, exclusive: boolean): string; 34 | /** 35 | * @param {number} start left side value 36 | * @param {number} end right side value 37 | * @param {boolean} startExclusive is range exclusive from left side 38 | * @param {boolean} endExclusive is range exclusive from right side 39 | * @param {boolean} logic is not logic applied 40 | * @returns {string} formatted range 41 | */ 42 | static formatRange( 43 | start: number, 44 | end: number, 45 | startExclusive: boolean, 46 | endExclusive: boolean, 47 | logic: boolean, 48 | ): string; 49 | /** 50 | * @param {Array} values values 51 | * @param {boolean} logic is not logic applied 52 | * @returns {RangeValue} computed value and it's exclusive flag 53 | */ 54 | static getRangeValue(values: Array, logic: boolean): RangeValue; 55 | /** @type {Array} */ 56 | _left: Array; 57 | /** @type {Array} */ 58 | _right: Array; 59 | /** 60 | * @param {number} value value 61 | * @param {boolean=} exclusive true when exclusive, otherwise false 62 | */ 63 | left(value: number, exclusive?: boolean | undefined): void; 64 | /** 65 | * @param {number} value value 66 | * @param {boolean=} exclusive true when exclusive, otherwise false 67 | */ 68 | right(value: number, exclusive?: boolean | undefined): void; 69 | /** 70 | * @param {boolean} logic is not logic applied 71 | * @returns {string} "smart" range string representation 72 | */ 73 | format(logic?: boolean): string; 74 | } 75 | declare namespace Range { 76 | export { RangeValue, RangeValueCallback }; 77 | } 78 | type RangeValue = [number, boolean]; 79 | type RangeValueCallback = (rangeValue: RangeValue) => boolean; 80 | -------------------------------------------------------------------------------- /src/keywords/absolutePath.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("ajv").default} Ajv */ 2 | /** @typedef {import("ajv").SchemaValidateFunction} SchemaValidateFunction */ 3 | /** @typedef {import("ajv").AnySchemaObject} AnySchemaObject */ 4 | /** @typedef {import("../validate").SchemaUtilErrorObject} SchemaUtilErrorObject */ 5 | 6 | /** 7 | * @param {string} message message 8 | * @param {object} schema schema 9 | * @param {string} data data 10 | * @returns {SchemaUtilErrorObject} error object 11 | */ 12 | function errorMessage(message, schema, data) { 13 | return { 14 | dataPath: undefined, 15 | // @ts-expect-error 16 | schemaPath: undefined, 17 | keyword: "absolutePath", 18 | params: { absolutePath: data }, 19 | message, 20 | parentSchema: schema, 21 | }; 22 | } 23 | 24 | /** 25 | * @param {boolean} shouldBeAbsolute true when should be absolute path, otherwise false 26 | * @param {object} schema schema 27 | * @param {string} data data 28 | * @returns {SchemaUtilErrorObject} error object 29 | */ 30 | function getErrorFor(shouldBeAbsolute, schema, data) { 31 | const message = shouldBeAbsolute 32 | ? `The provided value ${JSON.stringify(data)} is not an absolute path!` 33 | : `A relative path is expected. However, the provided value ${JSON.stringify( 34 | data, 35 | )} is an absolute path!`; 36 | 37 | return errorMessage(message, schema, data); 38 | } 39 | 40 | /** 41 | * @param {Ajv} ajv ajv 42 | * @returns {Ajv} configured ajv 43 | */ 44 | function addAbsolutePathKeyword(ajv) { 45 | ajv.addKeyword({ 46 | keyword: "absolutePath", 47 | type: "string", 48 | errors: true, 49 | /** 50 | * @param {boolean} schema schema 51 | * @param {AnySchemaObject} parentSchema parent schema 52 | * @returns {SchemaValidateFunction} validate function 53 | */ 54 | compile(schema, parentSchema) { 55 | /** @type {SchemaValidateFunction} */ 56 | const callback = (data) => { 57 | let passes = true; 58 | const isExclamationMarkPresent = data.includes("!"); 59 | 60 | if (isExclamationMarkPresent) { 61 | callback.errors = [ 62 | errorMessage( 63 | `The provided value ${JSON.stringify( 64 | data, 65 | )} contains exclamation mark (!) which is not allowed because it's reserved for loader syntax.`, 66 | parentSchema, 67 | data, 68 | ), 69 | ]; 70 | passes = false; 71 | } 72 | 73 | // ?:[A-Za-z]:\\ - Windows absolute path 74 | // \\\\ - Windows network absolute path 75 | // \/ - Unix-like OS absolute path 76 | const isCorrectAbsolutePath = 77 | schema === /^(?:[A-Za-z]:(\\|\/)|\\\\|\/)/.test(data); 78 | 79 | if (!isCorrectAbsolutePath) { 80 | callback.errors = [getErrorFor(schema, parentSchema, data)]; 81 | passes = false; 82 | } 83 | 84 | return passes; 85 | }; 86 | 87 | callback.errors = []; 88 | 89 | return callback; 90 | }, 91 | }); 92 | 93 | return ajv; 94 | } 95 | 96 | export default addAbsolutePathKeyword; 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "schema-utils", 3 | "version": "4.3.3", 4 | "description": "webpack Validation Utils", 5 | "license": "MIT", 6 | "repository": "webpack/schema-utils", 7 | "author": "webpack Contrib (https://github.com/webpack-contrib)", 8 | "homepage": "https://github.com/webpack/schema-utils", 9 | "bugs": "https://github.com/webpack/schema-utils/issues", 10 | "funding": { 11 | "type": "opencollective", 12 | "url": "https://opencollective.com/webpack" 13 | }, 14 | "main": "dist/index.js", 15 | "types": "declarations/index.d.ts", 16 | "engines": { 17 | "node": ">= 10.13.0" 18 | }, 19 | "scripts": { 20 | "start": "npm run build -- -w", 21 | "clean": "del-cli dist declarations", 22 | "prebuild": "npm run clean", 23 | "build:types": "tsc --declaration --emitDeclarationOnly --outDir declarations && prettier \"declarations/**/*.ts\" --write", 24 | "build:code": "babel src -d dist --copy-files", 25 | "build": "npm-run-all -p \"build:**\"", 26 | "commitlint": "commitlint --from=main", 27 | "security": "npm audit --production", 28 | "fmt:check": "prettier \"{**/*,*}.{js,json,md,yml,css,ts}\" --list-different", 29 | "lint:code": "eslint --cache .", 30 | "lint:types": "tsc --pretty --noEmit", 31 | "lint": "npm-run-all lint:code lint:types fmt:check", 32 | "fmt": "npm run fmt:check -- --write", 33 | "fix:js": "npm run lint:code -- --fix", 34 | "fix": "npm-run-all fix:js fmt", 35 | "test:only": "jest", 36 | "test:watch": "npm run test:only -- --watch", 37 | "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", 38 | "pretest": "npm run lint", 39 | "test": "npm run test:coverage", 40 | "prepare": "npm run build && husky install", 41 | "release": "standard-version" 42 | }, 43 | "files": [ 44 | "dist", 45 | "declarations" 46 | ], 47 | "dependencies": { 48 | "@types/json-schema": "^7.0.9", 49 | "ajv": "^8.9.0", 50 | "ajv-formats": "^2.1.1", 51 | "ajv-keywords": "^5.1.0" 52 | }, 53 | "devDependencies": { 54 | "@eslint/js": "^9.28.0", 55 | "@eslint/markdown": "^6.5.0", 56 | "@babel/cli": "^7.17.0", 57 | "@babel/core": "^7.17.0", 58 | "@babel/preset-env": "^7.16.11", 59 | "@commitlint/cli": "^17.6.1", 60 | "@commitlint/config-conventional": "^16.0.0", 61 | "@types/node": "^22.15.19", 62 | "@stylistic/eslint-plugin": "^4.4.1", 63 | "babel-jest": "^27.4.6", 64 | "del": "^6.0.0", 65 | "del-cli": "^4.0.1", 66 | "globals": "^16.2.0", 67 | "eslint": "^9.28.0", 68 | "eslint-config-webpack": "^4.0.2", 69 | "eslint-config-prettier": "^10.1.5", 70 | "eslint-plugin-import": "^2.31.0", 71 | "eslint-plugin-jest": "^28.12.0", 72 | "eslint-plugin-jsdoc": "^50.7.1", 73 | "eslint-plugin-n": "^17.19.0", 74 | "eslint-plugin-prettier": "^5.4.1", 75 | "eslint-plugin-unicorn": "^59.0.1", 76 | "husky": "^7.0.4", 77 | "jest": "^27.4.7", 78 | "lint-staged": "^16.0.0", 79 | "npm-run-all": "^4.1.5", 80 | "prettier": "^3.5.3", 81 | "prettier-2": "npm:prettier@^2", 82 | "standard-version": "^9.3.2", 83 | "typescript": "^5.8.3", 84 | "webpack": "^5.99.8" 85 | }, 86 | "keywords": [ 87 | "webpack" 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /test/range.test.js: -------------------------------------------------------------------------------- 1 | import Range from "../src/util/Range"; 2 | 3 | describe("range", () => { 4 | it("5 <= x <= 5", () => { 5 | const range = new Range(); 6 | range.left(5); 7 | range.right(5); 8 | 9 | expect(range.format()).toBe("should be = 5"); 10 | }); 11 | 12 | it("not 5 <= x <= 5", () => { 13 | const range = new Range(); 14 | range.left(5); 15 | range.right(5); 16 | 17 | expect(range.format(false)).toBe("should be != 5"); 18 | }); 19 | 20 | it("5 < x <= 6", () => { 21 | const range = new Range(); 22 | range.left(5, true); 23 | range.right(6); 24 | 25 | expect(range.format()).toBe("should be = 6"); 26 | }); 27 | 28 | it("-1 < x < 1", () => { 29 | const range = new Range(); 30 | range.left(-1, true); 31 | range.right(1, true); 32 | 33 | expect(range.format()).toBe("should be = 0"); 34 | }); 35 | 36 | it("not -1 < x < 1", () => { 37 | const range = new Range(); 38 | range.left(-1, true); 39 | range.right(1, true); 40 | 41 | expect(range.format(false)).toBe("should be != 0"); 42 | }); 43 | 44 | it("not 0 < x <= 10", () => { 45 | const range = new Range(); 46 | range.left(0, true); 47 | range.right(10, false); 48 | 49 | expect(range.format(false)).toBe("should be <= 0 or > 10"); 50 | }); 51 | 52 | it("x > 1000", () => { 53 | const range = new Range(); 54 | range.left(10000, false); 55 | range.left(1000, true); 56 | 57 | expect(range.format(true)).toBe("should be > 1000"); 58 | }); 59 | 60 | it("x < 0", () => { 61 | const range = new Range(); 62 | range.right(-1000, true); 63 | range.right(-0, true); 64 | 65 | expect(range.format()).toBe("should be < 0"); 66 | }); 67 | 68 | it("x >= -1000", () => { 69 | const range = new Range(); 70 | range.right(-1000, true); 71 | range.right(0, false); 72 | 73 | // expect x >= -1000 since it covers bigger range. [-1000, Infinity] is greater than [0, Infinity] 74 | expect(range.format(false)).toBe("should be >= -1000"); 75 | }); 76 | 77 | it("x <= 0", () => { 78 | const range = new Range(); 79 | range.left(0, true); 80 | range.left(-100, false); 81 | 82 | // expect x <= 0 since it covers bigger range. [-Infinity, 0] is greater than [-Infinity, -100] 83 | expect(range.format(false)).toBe("should be <= 0"); 84 | }); 85 | 86 | it("empty string for infinity range", () => { 87 | const range = new Range(); 88 | 89 | expect(range.format(false)).toBe(""); 90 | }); 91 | 92 | it("0 < x < 122", () => { 93 | const range = new Range(); 94 | range.left(0, true); 95 | range.right(12, false); 96 | range.right(122, true); 97 | 98 | expect(range.format()).toBe("should be > 0 and < 122"); 99 | }); 100 | 101 | it("-1 <= x < 10", () => { 102 | const range = new Range(); 103 | range.left(-1, false); 104 | range.left(10, true); 105 | range.right(10, true); 106 | 107 | expect(range.format()).toBe("should be >= -1 and < 10"); 108 | }); 109 | 110 | it("not 10 < x < 10", () => { 111 | const range = new Range(); 112 | range.left(-1, false); 113 | range.left(10, true); 114 | range.right(10, true); 115 | 116 | // expect x <= 10 since it covers bigger range. [-Infinity, 10] is greater than [-Infinity, -1] 117 | expect(range.format(false)).toBe("should be <= 10 or >= 10"); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: schema-utils 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | branches: 10 | - main 11 | - next 12 | 13 | jobs: 14 | lint: 15 | name: Lint - ${{ matrix.os }} - Node v${{ matrix.node-version }} 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest] 21 | node-version: [lts/*] 22 | runs-on: ${{ matrix.os }} 23 | concurrency: 24 | group: lint-${{ matrix.os }}-v${{ matrix.node-version }}-${{ github.ref }} 25 | cancel-in-progress: true 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | cache: "npm" 35 | - name: Install dependencies 36 | run: npm ci 37 | - name: Lint 38 | run: npm run lint 39 | - name: Build declarations 40 | run: npm run build:types 41 | - name: Run lint declarations 42 | run: if [ -n "$(git status declarations --porcelain)" ]; then echo "Missing declarations in git"; exit 1; else echo "All declarations are valid"; fi 43 | - name: Security audit 44 | run: npm run security 45 | - name: Check commit message 46 | uses: wagoid/commitlint-github-action@v4 47 | test: 48 | name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }} 49 | strategy: 50 | matrix: 51 | os: [ubuntu-latest, windows-latest, macos-latest] 52 | node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.x, 24.x] 53 | runs-on: ${{ matrix.os }} 54 | concurrency: 55 | group: test-${{ matrix.os }}-v${{ matrix.node-version }}-${{ github.ref }} 56 | cancel-in-progress: true 57 | steps: 58 | - name: Setup Git 59 | if: matrix.os == 'windows-latest' 60 | run: git config --global core.autocrlf input 61 | - uses: actions/checkout@v4 62 | - uses: actions/github-script@v7 63 | id: calculate_architecture 64 | with: 65 | result-encoding: string 66 | script: | 67 | if ('${{ matrix.os }}' === 'macos-latest' && ('${{ matrix['node-version'] }}' === '10.x' || '${{ matrix['node-version'] }}' === '12.x' || '${{ matrix['node-version'] }}' === '14.x')) { 68 | return "x64" 69 | } else { 70 | return '' 71 | } 72 | - name: Use Node.js ${{ matrix.node-version }} 73 | uses: actions/setup-node@v4 74 | with: 75 | node-version: ${{ matrix.node-version }} 76 | architecture: ${{ steps.calculate_architecture.outputs.result }} 77 | cache: "npm" 78 | - name: Install dependencies 79 | run: | 80 | npm i -D typescript@4 del-cli@^3 81 | npm i --ignore-engines 82 | if: matrix.node-version == '10.x' || matrix.node-version == '12.x' || matrix.node-version == '14.x' || matrix.node-version == '16.x' || matrix.node-version == '18.x' 83 | - name: Install dependencies 84 | run: npm ci 85 | if: matrix.node-version == '20.x' || matrix.node-version == '22.x' || matrix.node-version == '24.x' 86 | - name: Run tests with coverage 87 | run: npm run test:coverage -- --ci 88 | - uses: codecov/codecov-action@v5 89 | with: 90 | flags: integration 91 | token: ${{ secrets.CODECOV_TOKEN }} 92 | -------------------------------------------------------------------------------- /src/util/hints.js: -------------------------------------------------------------------------------- 1 | const Range = require("./Range"); 2 | 3 | /** @typedef {import("../validate").Schema} Schema */ 4 | 5 | /** 6 | * @param {Schema} schema schema 7 | * @param {boolean} logic logic 8 | * @returns {string[]} array of hints 9 | */ 10 | module.exports.stringHints = function stringHints(schema, logic) { 11 | const hints = []; 12 | let type = "string"; 13 | const currentSchema = { ...schema }; 14 | 15 | if (!logic) { 16 | const tmpLength = currentSchema.minLength; 17 | const tmpFormat = currentSchema.formatMinimum; 18 | 19 | currentSchema.minLength = currentSchema.maxLength; 20 | currentSchema.maxLength = tmpLength; 21 | currentSchema.formatMinimum = currentSchema.formatMaximum; 22 | currentSchema.formatMaximum = tmpFormat; 23 | } 24 | 25 | if (typeof currentSchema.minLength === "number") { 26 | if (currentSchema.minLength === 1) { 27 | type = "non-empty string"; 28 | } else { 29 | const length = Math.max(currentSchema.minLength - 1, 0); 30 | hints.push( 31 | `should be longer than ${length} character${length > 1 ? "s" : ""}`, 32 | ); 33 | } 34 | } 35 | 36 | if (typeof currentSchema.maxLength === "number") { 37 | if (currentSchema.maxLength === 0) { 38 | type = "empty string"; 39 | } else { 40 | const length = currentSchema.maxLength + 1; 41 | hints.push( 42 | `should be shorter than ${length} character${length > 1 ? "s" : ""}`, 43 | ); 44 | } 45 | } 46 | 47 | if (currentSchema.pattern) { 48 | hints.push( 49 | `should${logic ? "" : " not"} match pattern ${JSON.stringify( 50 | currentSchema.pattern, 51 | )}`, 52 | ); 53 | } 54 | 55 | if (currentSchema.format) { 56 | hints.push( 57 | `should${logic ? "" : " not"} match format ${JSON.stringify( 58 | currentSchema.format, 59 | )}`, 60 | ); 61 | } 62 | 63 | if (currentSchema.formatMinimum) { 64 | hints.push( 65 | `should be ${ 66 | currentSchema.formatExclusiveMinimum ? ">" : ">=" 67 | } ${JSON.stringify(currentSchema.formatMinimum)}`, 68 | ); 69 | } 70 | 71 | if (currentSchema.formatMaximum) { 72 | hints.push( 73 | `should be ${ 74 | currentSchema.formatExclusiveMaximum ? "<" : "<=" 75 | } ${JSON.stringify(currentSchema.formatMaximum)}`, 76 | ); 77 | } 78 | 79 | return [type, ...hints]; 80 | }; 81 | 82 | /** 83 | * @param {Schema} schema schema 84 | * @param {boolean} logic logic 85 | * @returns {string[]} array of hints 86 | */ 87 | module.exports.numberHints = function numberHints(schema, logic) { 88 | const hints = [schema.type === "integer" ? "integer" : "number"]; 89 | const range = new Range(); 90 | 91 | if (typeof schema.minimum === "number") { 92 | range.left(schema.minimum); 93 | } 94 | 95 | if (typeof schema.exclusiveMinimum === "number") { 96 | range.left(schema.exclusiveMinimum, true); 97 | } 98 | 99 | if (typeof schema.maximum === "number") { 100 | range.right(schema.maximum); 101 | } 102 | 103 | if (typeof schema.exclusiveMaximum === "number") { 104 | range.right(schema.exclusiveMaximum, true); 105 | } 106 | 107 | const rangeFormat = range.format(logic); 108 | 109 | if (rangeFormat) { 110 | hints.push(rangeFormat); 111 | } 112 | 113 | if (typeof schema.multipleOf === "number") { 114 | hints.push( 115 | `should${logic ? "" : " not"} be multiple of ${schema.multipleOf}`, 116 | ); 117 | } 118 | 119 | return hints; 120 | }; 121 | -------------------------------------------------------------------------------- /test/__snapshots__/api.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`api should allow to enable validation using "process.env.SKIP_VALIDATION" #2 1`] = ` 4 | "Invalid options object. NAME has been initialized using an options object that does not match the API schema. 5 | - options has an unknown property 'foo'. These properties are valid: 6 | object { name? }" 7 | `; 8 | 9 | exports[`api should allow to enable validation using "process.env.SKIP_VALIDATION" 1`] = ` 10 | "Invalid options object. NAME has been initialized using an options object that does not match the API schema. 11 | - options has an unknown property 'foo'. These properties are valid: 12 | object { name? }" 13 | `; 14 | 15 | exports[`api should allow to enable validation using API 1`] = ` 16 | "Invalid options object. NAME has been initialized using an options object that does not match the API schema. 17 | - options has an unknown property 'foo'. These properties are valid: 18 | object { name? }" 19 | `; 20 | 21 | exports[`api should get configuration from schema 1`] = ` 22 | "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. 23 | - options has an unknown property 'foo'. These properties are valid: 24 | object { name? }" 25 | `; 26 | 27 | exports[`api should prefer configuration over "title" #1 1`] = ` 28 | "Invalid options object. NAME has been initialized using an options object that does not match the API schema. 29 | - options has an unknown property 'foo'. These properties are valid: 30 | object { name? }" 31 | `; 32 | 33 | exports[`api should prefer configuration over "title" #2 1`] = ` 34 | "Invalid BaseDataPath object. CSS Loader has been initialized using a BaseDataPath object that does not match the API schema. 35 | - BaseDataPath has an unknown property 'foo'. These properties are valid: 36 | object { name? }" 37 | `; 38 | 39 | exports[`api should prefer configuration over "title" 1`] = ` 40 | "Invalid BaseDataPath object. NAME has been initialized using a BaseDataPath object that does not match the API schema. 41 | - BaseDataPath has an unknown property 'foo'. These properties are valid: 42 | object { name? }" 43 | `; 44 | 45 | exports[`api should use default values when "title" is broken 1`] = ` 46 | "Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. 47 | - configuration has an unknown property 'foo'. These properties are valid: 48 | object { name? }" 49 | `; 50 | 51 | exports[`api should work with anyOf 1`] = ` 52 | "Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. 53 | - configuration should be one of these: 54 | object { bar, … } | object { baz, … } 55 | Details: 56 | * configuration misses the property 'bar' | should be any non-object. Should be: 57 | number 58 | * configuration misses the property 'baz' | should be any non-object. Should be: 59 | number" 60 | `; 61 | 62 | exports[`api should work with minProperties properties 1`] = ` 63 | "Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. 64 | - configuration should be a non-empty object." 65 | `; 66 | 67 | exports[`api should work with required properties #2 1`] = ` 68 | "Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. 69 | - configuration misses the property 'e'." 70 | `; 71 | 72 | exports[`api should work with required properties 1`] = ` 73 | "Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. 74 | - configuration.c misses the property 'e'. Should be: 75 | string" 76 | `; 77 | -------------------------------------------------------------------------------- /src/util/Range.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {[number, boolean]} RangeValue 3 | */ 4 | 5 | /** 6 | * @callback RangeValueCallback 7 | * @param {RangeValue} rangeValue 8 | * @returns {boolean} 9 | */ 10 | 11 | class Range { 12 | /** 13 | * @param {"left" | "right"} side side 14 | * @param {boolean} exclusive exclusive 15 | * @returns {">" | ">=" | "<" | "<="} operator 16 | */ 17 | static getOperator(side, exclusive) { 18 | if (side === "left") { 19 | return exclusive ? ">" : ">="; 20 | } 21 | 22 | return exclusive ? "<" : "<="; 23 | } 24 | 25 | /** 26 | * @param {number} value value 27 | * @param {boolean} logic is not logic applied 28 | * @param {boolean} exclusive is range exclusive 29 | * @returns {string} formatted right 30 | */ 31 | static formatRight(value, logic, exclusive) { 32 | if (logic === false) { 33 | return Range.formatLeft(value, !logic, !exclusive); 34 | } 35 | 36 | return `should be ${Range.getOperator("right", exclusive)} ${value}`; 37 | } 38 | 39 | /** 40 | * @param {number} value value 41 | * @param {boolean} logic is not logic applied 42 | * @param {boolean} exclusive is range exclusive 43 | * @returns {string} formatted left 44 | */ 45 | static formatLeft(value, logic, exclusive) { 46 | if (logic === false) { 47 | return Range.formatRight(value, !logic, !exclusive); 48 | } 49 | 50 | return `should be ${Range.getOperator("left", exclusive)} ${value}`; 51 | } 52 | 53 | /** 54 | * @param {number} start left side value 55 | * @param {number} end right side value 56 | * @param {boolean} startExclusive is range exclusive from left side 57 | * @param {boolean} endExclusive is range exclusive from right side 58 | * @param {boolean} logic is not logic applied 59 | * @returns {string} formatted range 60 | */ 61 | static formatRange(start, end, startExclusive, endExclusive, logic) { 62 | let result = "should be"; 63 | 64 | result += ` ${Range.getOperator( 65 | logic ? "left" : "right", 66 | logic ? startExclusive : !startExclusive, 67 | )} ${start} `; 68 | result += logic ? "and" : "or"; 69 | result += ` ${Range.getOperator( 70 | logic ? "right" : "left", 71 | logic ? endExclusive : !endExclusive, 72 | )} ${end}`; 73 | 74 | return result; 75 | } 76 | 77 | /** 78 | * @param {Array} values values 79 | * @param {boolean} logic is not logic applied 80 | * @returns {RangeValue} computed value and it's exclusive flag 81 | */ 82 | static getRangeValue(values, logic) { 83 | let minMax = logic ? Infinity : -Infinity; 84 | let j = -1; 85 | const predicate = logic 86 | ? /** @type {RangeValueCallback} */ 87 | ([value]) => value <= minMax 88 | : /** @type {RangeValueCallback} */ 89 | ([value]) => value >= minMax; 90 | 91 | for (let i = 0; i < values.length; i++) { 92 | if (predicate(values[i])) { 93 | [minMax] = values[i]; 94 | j = i; 95 | } 96 | } 97 | 98 | if (j > -1) { 99 | return values[j]; 100 | } 101 | 102 | return [Infinity, true]; 103 | } 104 | 105 | constructor() { 106 | /** @type {Array} */ 107 | this._left = []; 108 | /** @type {Array} */ 109 | this._right = []; 110 | } 111 | 112 | /** 113 | * @param {number} value value 114 | * @param {boolean=} exclusive true when exclusive, otherwise false 115 | */ 116 | left(value, exclusive = false) { 117 | this._left.push([value, exclusive]); 118 | } 119 | 120 | /** 121 | * @param {number} value value 122 | * @param {boolean=} exclusive true when exclusive, otherwise false 123 | */ 124 | right(value, exclusive = false) { 125 | this._right.push([value, exclusive]); 126 | } 127 | 128 | /** 129 | * @param {boolean} logic is not logic applied 130 | * @returns {string} "smart" range string representation 131 | */ 132 | format(logic = true) { 133 | const [start, leftExclusive] = Range.getRangeValue(this._left, logic); 134 | const [end, rightExclusive] = Range.getRangeValue(this._right, !logic); 135 | 136 | if (!Number.isFinite(start) && !Number.isFinite(end)) { 137 | return ""; 138 | } 139 | 140 | const realStart = leftExclusive ? start + 1 : start; 141 | const realEnd = rightExclusive ? end - 1 : end; 142 | 143 | // e.g. 5 < x < 7, 5 < x <= 6, 6 <= x <= 6 144 | if (realStart === realEnd) { 145 | return `should be ${logic ? "" : "!"}= ${realStart}`; 146 | } 147 | 148 | // e.g. 4 < x < ∞ 149 | if (Number.isFinite(start) && !Number.isFinite(end)) { 150 | return Range.formatLeft(start, logic, leftExclusive); 151 | } 152 | 153 | // e.g. ∞ < x < 4 154 | if (!Number.isFinite(start) && Number.isFinite(end)) { 155 | return Range.formatRight(end, logic, rightExclusive); 156 | } 157 | 158 | return Range.formatRange(start, end, leftExclusive, rightExclusive, logic); 159 | } 160 | } 161 | 162 | module.exports = Range; 163 | -------------------------------------------------------------------------------- /src/keywords/limit.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("ajv").default} Ajv */ 2 | /** @typedef {import("ajv").Code} Code */ 3 | /** @typedef {import("ajv").Name} Name */ 4 | /** @typedef {import("ajv").KeywordErrorDefinition} KeywordErrorDefinition */ 5 | 6 | /** 7 | * @param {Ajv} ajv ajv 8 | * @returns {Ajv} ajv with limit keyword 9 | */ 10 | function addLimitKeyword(ajv) { 11 | const { _, str, KeywordCxt, nil, Name } = require("ajv"); 12 | 13 | /** 14 | * @param {Code | Name} nameOrCode name or code 15 | * @returns {Code | Name} name or code 16 | */ 17 | function par(nameOrCode) { 18 | return nameOrCode instanceof Name ? nameOrCode : _`(${nameOrCode})`; 19 | } 20 | 21 | /** 22 | * @param {Code} op op 23 | * @returns {(xValue: Code, yValue: Code) => Code} code 24 | */ 25 | function mappend(op) { 26 | return (xValue, yValue) => 27 | xValue === nil 28 | ? yValue 29 | : yValue === nil 30 | ? xValue 31 | : _`${par(xValue)} ${op} ${par(yValue)}`; 32 | } 33 | 34 | const orCode = mappend(_`||`); 35 | 36 | // boolean OR (||) expression with the passed arguments 37 | /** 38 | * @param {...Code} args args 39 | * @returns {Code} code 40 | */ 41 | function or(...args) { 42 | return args.reduce(orCode); 43 | } 44 | 45 | /** 46 | * @param {string | number} key key 47 | * @returns {Code} property 48 | */ 49 | function getProperty(key) { 50 | return _`[${key}]`; 51 | } 52 | 53 | const keywords = { 54 | formatMaximum: { okStr: "<=", ok: _`<=`, fail: _`>` }, 55 | formatMinimum: { okStr: ">=", ok: _`>=`, fail: _`<` }, 56 | formatExclusiveMaximum: { okStr: "<", ok: _`<`, fail: _`>=` }, 57 | formatExclusiveMinimum: { okStr: ">", ok: _`>`, fail: _`<=` }, 58 | }; 59 | 60 | /** @type {KeywordErrorDefinition} */ 61 | const error = { 62 | message: ({ keyword, schemaCode }) => 63 | str`should be ${ 64 | keywords[/** @type {keyof typeof keywords} */ (keyword)].okStr 65 | } ${schemaCode}`, 66 | params: ({ keyword, schemaCode }) => 67 | _`{comparison: ${ 68 | keywords[/** @type {keyof typeof keywords} */ (keyword)].okStr 69 | }, limit: ${schemaCode}}`, 70 | }; 71 | 72 | for (const keyword of Object.keys(keywords)) { 73 | ajv.addKeyword({ 74 | keyword, 75 | type: "string", 76 | schemaType: keyword.startsWith("formatExclusive") 77 | ? ["string", "boolean"] 78 | : ["string", "number"], 79 | $data: true, 80 | error, 81 | code(cxt) { 82 | const { gen, data, schemaCode, keyword, it } = cxt; 83 | const { opts, self } = it; 84 | if (!opts.validateFormats) return; 85 | const fCxt = new KeywordCxt( 86 | it, 87 | // eslint-disable-next-line jsdoc/no-restricted-syntax 88 | /** @type {any} */ 89 | (self.RULES.all.format).definition, 90 | "format", 91 | ); 92 | 93 | /** 94 | * @param {Name} fmt fmt 95 | * @returns {Code} code 96 | */ 97 | function compareCode(fmt) { 98 | return _`${fmt}.compare(${data}, ${schemaCode}) ${ 99 | keywords[/** @type {keyof typeof keywords} */ (keyword)].fail 100 | } 0`; 101 | } 102 | 103 | /** 104 | * @returns {void} 105 | */ 106 | function validate$DataFormat() { 107 | const fmts = gen.scopeValue("formats", { 108 | ref: self.formats, 109 | code: opts.code.formats, 110 | }); 111 | const fmt = gen.const("fmt", _`${fmts}[${fCxt.schemaCode}]`); 112 | 113 | cxt.fail$data( 114 | or( 115 | _`typeof ${fmt} != "object"`, 116 | _`${fmt} instanceof RegExp`, 117 | _`typeof ${fmt}.compare != "function"`, 118 | compareCode(fmt), 119 | ), 120 | ); 121 | } 122 | 123 | /** 124 | * @returns {void} 125 | */ 126 | function validateFormat() { 127 | const format = fCxt.schema; 128 | const fmtDef = self.formats[format]; 129 | 130 | if (!fmtDef || fmtDef === true) { 131 | return; 132 | } 133 | 134 | if ( 135 | typeof fmtDef !== "object" || 136 | fmtDef instanceof RegExp || 137 | typeof fmtDef.compare !== "function" 138 | ) { 139 | throw new Error( 140 | `"${keyword}": format "${format}" does not define "compare" function`, 141 | ); 142 | } 143 | 144 | const fmt = gen.scopeValue("formats", { 145 | key: format, 146 | ref: fmtDef, 147 | code: opts.code.formats 148 | ? _`${opts.code.formats}${getProperty(format)}` 149 | : undefined, 150 | }); 151 | 152 | cxt.fail$data(compareCode(fmt)); 153 | } 154 | 155 | if (fCxt.$data) { 156 | validate$DataFormat(); 157 | } else { 158 | validateFormat(); 159 | } 160 | }, 161 | dependencies: ["format"], 162 | }); 163 | } 164 | 165 | return ajv; 166 | } 167 | 168 | export default addLimitKeyword; 169 | -------------------------------------------------------------------------------- /src/validate.js: -------------------------------------------------------------------------------- 1 | import ValidationError from "./ValidationError"; 2 | import memoize from "./util/memorize"; 3 | 4 | const getAjv = memoize(() => { 5 | // Use CommonJS require for ajv libs so TypeScript consumers aren't locked into esModuleInterop (see #110). 6 | 7 | const Ajv = require("ajv").default; 8 | 9 | const ajvKeywords = require("ajv-keywords").default; 10 | 11 | const addFormats = require("ajv-formats").default; 12 | 13 | /** 14 | * @type {Ajv} 15 | */ 16 | const ajv = new Ajv({ 17 | strict: false, 18 | allErrors: true, 19 | verbose: true, 20 | $data: true, 21 | }); 22 | 23 | ajvKeywords(ajv, ["instanceof", "patternRequired"]); 24 | // TODO set `{ keywords: true }` for the next major release and remove `keywords/limit.js` 25 | addFormats(ajv, { keywords: false }); 26 | 27 | // Custom keywords 28 | 29 | const addAbsolutePathKeyword = require("./keywords/absolutePath").default; 30 | 31 | addAbsolutePathKeyword(ajv); 32 | 33 | const addLimitKeyword = require("./keywords/limit").default; 34 | 35 | addLimitKeyword(ajv); 36 | 37 | const addUndefinedAsNullKeyword = 38 | require("./keywords/undefinedAsNull").default; 39 | 40 | addUndefinedAsNullKeyword(ajv); 41 | 42 | return ajv; 43 | }); 44 | 45 | /** @typedef {import("json-schema").JSONSchema4} JSONSchema4 */ 46 | /** @typedef {import("json-schema").JSONSchema6} JSONSchema6 */ 47 | /** @typedef {import("json-schema").JSONSchema7} JSONSchema7 */ 48 | /** @typedef {import("ajv").ErrorObject} ErrorObject */ 49 | 50 | /** 51 | * @typedef {object} ExtendedSchema 52 | * @property {(string | number)=} formatMinimum format minimum 53 | * @property {(string | number)=} formatMaximum format maximum 54 | * @property {(string | boolean)=} formatExclusiveMinimum format exclusive minimum 55 | * @property {(string | boolean)=} formatExclusiveMaximum format exclusive maximum 56 | * @property {string=} link link 57 | * @property {boolean=} undefinedAsNull undefined will be resolved as null 58 | */ 59 | 60 | // TODO remove me in the next major release 61 | /** @typedef {ExtendedSchema} Extend */ 62 | 63 | /** @typedef {(JSONSchema4 | JSONSchema6 | JSONSchema7) & ExtendedSchema} Schema */ 64 | 65 | /** @typedef {ErrorObject & { children?: Array }} SchemaUtilErrorObject */ 66 | 67 | /** 68 | * @callback PostFormatter 69 | * @param {string} formattedError 70 | * @param {SchemaUtilErrorObject} error 71 | * @returns {string} 72 | */ 73 | 74 | /** 75 | * @typedef {object} ValidationErrorConfiguration 76 | * @property {string=} name name 77 | * @property {string=} baseDataPath base data path 78 | * @property {PostFormatter=} postFormatter post formatter 79 | */ 80 | 81 | /** 82 | * @param {SchemaUtilErrorObject} error error 83 | * @param {number} idx idx 84 | * @returns {SchemaUtilErrorObject} error object with idx 85 | */ 86 | function applyPrefix(error, idx) { 87 | error.instancePath = `[${idx}]${error.instancePath}`; 88 | 89 | if (error.children) { 90 | for (const err of error.children) applyPrefix(err, idx); 91 | } 92 | 93 | return error; 94 | } 95 | 96 | let skipValidation = false; 97 | 98 | // We use `process.env.SKIP_VALIDATION` because you can have multiple `schema-utils` with different version, 99 | // so we want to disable it globally, `process.env` doesn't supported by browsers, so we have the local `skipValidation` variables 100 | 101 | // Enable validation 102 | /** 103 | * @returns {void} 104 | */ 105 | function enableValidation() { 106 | skipValidation = false; 107 | 108 | // Disable validation for any versions 109 | if (process && process.env) { 110 | process.env.SKIP_VALIDATION = "n"; 111 | } 112 | } 113 | 114 | // Disable validation 115 | /** 116 | * @returns {void} 117 | */ 118 | function disableValidation() { 119 | skipValidation = true; 120 | 121 | if (process && process.env) { 122 | process.env.SKIP_VALIDATION = "y"; 123 | } 124 | } 125 | 126 | // Check if we need to confirm 127 | /** 128 | * @returns {boolean} true when need validate, otherwise false 129 | */ 130 | function needValidate() { 131 | if (skipValidation) { 132 | return false; 133 | } 134 | 135 | if (process && process.env && process.env.SKIP_VALIDATION) { 136 | const value = process.env.SKIP_VALIDATION.trim(); 137 | 138 | if (/^(?:y|yes|true|1|on)$/i.test(value)) { 139 | return false; 140 | } 141 | 142 | if (/^(?:n|no|false|0|off)$/i.test(value)) { 143 | return true; 144 | } 145 | } 146 | 147 | return true; 148 | } 149 | 150 | /** 151 | * @param {Array} errors array of error objects 152 | * @returns {Array} filtered array of objects 153 | */ 154 | function filterErrors(errors) { 155 | /** @type {Array} */ 156 | let newErrors = []; 157 | 158 | for (const error of /** @type {Array} */ (errors)) { 159 | const { instancePath } = error; 160 | /** @type {Array} */ 161 | let children = []; 162 | 163 | newErrors = newErrors.filter((oldError) => { 164 | if (oldError.instancePath.includes(instancePath)) { 165 | if (oldError.children) { 166 | children = [...children, ...oldError.children]; 167 | } 168 | 169 | oldError.children = undefined; 170 | children.push(oldError); 171 | 172 | return false; 173 | } 174 | 175 | return true; 176 | }); 177 | 178 | if (children.length) { 179 | error.children = children; 180 | } 181 | 182 | newErrors.push(error); 183 | } 184 | 185 | return newErrors; 186 | } 187 | 188 | /** 189 | * @param {Schema} schema schema 190 | * @param {Array | object} options options 191 | * @returns {Array} array of error objects 192 | */ 193 | function validateObject(schema, options) { 194 | // Not need to cache, because `ajv@8` has built-in cache 195 | const compiledSchema = getAjv().compile(schema); 196 | const valid = compiledSchema(options); 197 | 198 | if (valid) return []; 199 | 200 | return compiledSchema.errors ? filterErrors(compiledSchema.errors) : []; 201 | } 202 | 203 | /** 204 | * @param {Schema} schema schema 205 | * @param {Array | object} options options 206 | * @param {ValidationErrorConfiguration=} configuration configuration 207 | * @returns {void} 208 | */ 209 | function validate(schema, options, configuration) { 210 | if (!needValidate()) { 211 | return; 212 | } 213 | 214 | let errors = []; 215 | 216 | if (Array.isArray(options)) { 217 | for (let i = 0; i <= options.length - 1; i++) { 218 | errors.push( 219 | ...validateObject(schema, options[i]).map((err) => applyPrefix(err, i)), 220 | ); 221 | } 222 | } else { 223 | errors = validateObject(schema, options); 224 | } 225 | 226 | if (errors.length > 0) { 227 | throw new ValidationError(errors, schema, configuration); 228 | } 229 | } 230 | 231 | export { validate, enableValidation, disableValidation, needValidate }; 232 | export { default as ValidationError } from "./ValidationError"; 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 5 | 6 | 7 | 9 | 10 |
11 | 12 | [![npm][npm]][npm-url] 13 | [![node][node]][node-url] 14 | [![tests][tests]][tests-url] 15 | [![coverage][cover]][cover-url] 16 | [![GitHub Discussions][discussion]][discussion-url] 17 | [![size][size]][size-url] 18 | 19 | # schema-utils 20 | 21 | Package for validate options in loaders and plugins. 22 | 23 | ## Getting Started 24 | 25 | To begin, you'll need to install `schema-utils`: 26 | 27 | ```console 28 | npm install schema-utils 29 | ``` 30 | 31 | ## API 32 | 33 | **schema.json** 34 | 35 | ```json 36 | { 37 | "type": "object", 38 | "properties": { 39 | "option": { 40 | "type": "boolean" 41 | } 42 | }, 43 | "additionalProperties": false 44 | } 45 | ``` 46 | 47 | ```js 48 | import schema from "./path/to/schema.json"; 49 | import { validate } from "schema-utils"; 50 | 51 | const options = { option: true }; 52 | const configuration = { name: "Loader Name/Plugin Name/Name" }; 53 | 54 | validate(schema, options, configuration); 55 | ``` 56 | 57 | ### `schema` 58 | 59 | Type: `String` 60 | 61 | JSON schema. 62 | 63 | Simple example of schema: 64 | 65 | ```json 66 | { 67 | "type": "object", 68 | "properties": { 69 | "name": { 70 | "description": "This is description of option.", 71 | "type": "string" 72 | } 73 | }, 74 | "additionalProperties": false 75 | } 76 | ``` 77 | 78 | ### `options` 79 | 80 | Type: `Object` 81 | 82 | Object with options. 83 | 84 | ```js 85 | import schema from "./path/to/schema.json"; 86 | import { validate } from "schema-utils"; 87 | 88 | const options = { foo: "bar" }; 89 | 90 | validate(schema, { name: 123 }, { name: "MyPlugin" }); 91 | ``` 92 | 93 | ### `configuration` 94 | 95 | Allow to configure validator. 96 | 97 | There is an alternative method to configure the `name` and`baseDataPath` options via the `title` property in the schema. 98 | For example: 99 | 100 | ```json 101 | { 102 | "title": "My Loader options", 103 | "type": "object", 104 | "properties": { 105 | "name": { 106 | "description": "This is description of option.", 107 | "type": "string" 108 | } 109 | }, 110 | "additionalProperties": false 111 | } 112 | ``` 113 | 114 | The last word used for the `baseDataPath` option, other words used for the `name` option. 115 | Based on the example above the `name` option equals `My Loader`, the `baseDataPath` option equals `options`. 116 | 117 | #### `name` 118 | 119 | Type: `Object` 120 | Default: `"Object"` 121 | 122 | Allow to setup name in validation errors. 123 | 124 | ```js 125 | import schema from "./path/to/schema.json"; 126 | import { validate } from "schema-utils"; 127 | 128 | const options = { foo: "bar" }; 129 | 130 | validate(schema, options, { name: "MyPlugin" }); 131 | ``` 132 | 133 | ```shell 134 | Invalid configuration object. MyPlugin has been initialised using a configuration object that does not match the API schema. 135 | - configuration.optionName should be a integer. 136 | ``` 137 | 138 | #### `baseDataPath` 139 | 140 | Type: `String` 141 | Default: `"configuration"` 142 | 143 | Allow to setup base data path in validation errors. 144 | 145 | ```js 146 | import schema from "./path/to/schema.json"; 147 | import { validate } from "schema-utils"; 148 | 149 | const options = { foo: "bar" }; 150 | 151 | validate(schema, options, { name: "MyPlugin", baseDataPath: "options" }); 152 | ``` 153 | 154 | ```shell 155 | Invalid options object. MyPlugin has been initialised using an options object that does not match the API schema. 156 | - options.optionName should be a integer. 157 | ``` 158 | 159 | #### `postFormatter` 160 | 161 | Type: `Function` 162 | Default: `undefined` 163 | 164 | Allow to reformat errors. 165 | 166 | ```js 167 | import schema from "./path/to/schema.json"; 168 | import { validate } from "schema-utils"; 169 | 170 | const options = { foo: "bar" }; 171 | 172 | validate(schema, options, { 173 | name: "MyPlugin", 174 | postFormatter: (formattedError, error) => { 175 | if (error.keyword === "type") { 176 | return `${formattedError}\nAdditional Information.`; 177 | } 178 | 179 | return formattedError; 180 | }, 181 | }); 182 | ``` 183 | 184 | ```shell 185 | Invalid options object. MyPlugin has been initialized using an options object that does not match the API schema. 186 | - options.optionName should be a integer. 187 | Additional Information. 188 | ``` 189 | 190 | ## Examples 191 | 192 | **schema.json** 193 | 194 | ```json 195 | { 196 | "type": "object", 197 | "properties": { 198 | "name": { 199 | "type": "string" 200 | }, 201 | "test": { 202 | "anyOf": [ 203 | { "type": "array" }, 204 | { "type": "string" }, 205 | { "instanceof": "RegExp" } 206 | ] 207 | }, 208 | "transform": { 209 | "instanceof": "Function" 210 | }, 211 | "sourceMap": { 212 | "type": "boolean" 213 | } 214 | }, 215 | "additionalProperties": false 216 | } 217 | ``` 218 | 219 | ### `Loader` 220 | 221 | ```js 222 | import { getOptions } from "loader-utils"; 223 | import { validate } from "schema-utils"; 224 | 225 | import schema from "path/to/schema.json"; 226 | 227 | function loader(src, map) { 228 | const options = getOptions(this); 229 | 230 | validate(schema, options, { 231 | name: "Loader Name", 232 | baseDataPath: "options", 233 | }); 234 | 235 | // Code... 236 | } 237 | 238 | export default loader; 239 | ``` 240 | 241 | ### `Plugin` 242 | 243 | ```js 244 | import { validate } from "schema-utils"; 245 | 246 | import schema from "path/to/schema.json"; 247 | 248 | class Plugin { 249 | constructor(options) { 250 | validate(schema, options, { 251 | name: "Plugin Name", 252 | baseDataPath: "options", 253 | }); 254 | 255 | this.options = options; 256 | } 257 | 258 | apply(compiler) { 259 | // Code... 260 | } 261 | } 262 | 263 | export default Plugin; 264 | ``` 265 | 266 | ### Allow to disable and enable validation (the `validate` function do nothing) 267 | 268 | This can be useful when you don't want to do validation for `production` builds. 269 | 270 | ```js 271 | import { disableValidation, enableValidation, validate } from "schema-utils"; 272 | 273 | // Disable validation 274 | disableValidation(); 275 | // Do nothing 276 | validate(schema, options); 277 | 278 | // Enable validation 279 | enableValidation(); 280 | // Will throw an error if schema is not valid 281 | validate(schema, options); 282 | 283 | // Allow to undestand do you need validation or not 284 | const need = needValidate(); 285 | 286 | console.log(need); 287 | ``` 288 | 289 | Also you can enable/disable validation using the `process.env.SKIP_VALIDATION` env variable. 290 | 291 | Supported values (case insensitive): 292 | 293 | - `yes`/`y`/`true`/`1`/`on` 294 | - `no`/`n`/`false`/`0`/`off` 295 | 296 | ## Contributing 297 | 298 | Please take a moment to read our contributing guidelines if you haven't yet done so. 299 | 300 | [CONTRIBUTING](https://github.com/webpack/schema-utils?tab=contributing-ov-file#contributing) 301 | 302 | ## License 303 | 304 | [MIT](./LICENSE) 305 | 306 | [npm]: https://img.shields.io/npm/v/schema-utils.svg 307 | [npm-url]: https://npmjs.com/package/schema-utils 308 | [node]: https://img.shields.io/node/v/schema-utils.svg 309 | [node-url]: https://nodejs.org 310 | [tests]: https://github.com/webpack/schema-utils/workflows/schema-utils/badge.svg 311 | [tests-url]: https://github.com/webpack/schema-utils/actions 312 | [cover]: https://codecov.io/gh/webpack/schema-utils/branch/main/graph/badge.svg 313 | [cover-url]: https://codecov.io/gh/webpack/schema-utils 314 | [discussion]: https://img.shields.io/github/discussions/webpack/webpack 315 | [discussion-url]: https://github.com/webpack/webpack/discussions 316 | [size]: https://packagephobia.com/badge?p=schema-utils 317 | [size-url]: https://packagephobia.com/result?p=schema-utils 318 | -------------------------------------------------------------------------------- /test/api.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | validate, 3 | ValidationError, 4 | enableValidation, 5 | disableValidation, 6 | needValidate, 7 | } from "../src/index"; 8 | 9 | import schema from "./fixtures/schema.json"; 10 | import schemaTitle from "./fixtures/schema-title.json"; 11 | import schemaTitleBrone from "./fixtures/schema-title-broken.json"; 12 | 13 | describe("api", () => { 14 | it("should export validate and ValidateError", () => { 15 | expect(typeof validate).toBe("function"); 16 | expect(typeof ValidationError).toBe("function"); 17 | }); 18 | 19 | it("should work", () => { 20 | let errored; 21 | 22 | try { 23 | validate(schema, { minimumWithTypeNumber: 5 }); 24 | } catch (err) { 25 | errored = err; 26 | } 27 | 28 | expect(errored).toBeUndefined(); 29 | }); 30 | 31 | it("should work when options will be changed", () => { 32 | expect.assertions(1); 33 | 34 | const options = { minimumWithTypeNumber: 5 }; 35 | 36 | validate(schema, options); 37 | 38 | options.minimumWithTypeNumber = 1; 39 | 40 | try { 41 | validate(schema, options); 42 | } catch (error) { 43 | expect(error).toBeDefined(); 44 | } 45 | 46 | options.minimumWithTypeNumber = 120; 47 | 48 | validate(schema, options); 49 | }); 50 | 51 | it("should get configuration from schema", () => { 52 | try { 53 | validate(schemaTitle, { foo: "bar" }); 54 | } catch (error) { 55 | if (error.name !== "ValidationError") { 56 | throw error; 57 | } 58 | 59 | expect(error.message).toMatchSnapshot(); 60 | } 61 | }); 62 | 63 | it('should prefer configuration over "title"', () => { 64 | try { 65 | validate( 66 | schemaTitle, 67 | { foo: "bar" }, 68 | { name: "NAME", baseDataPath: "BaseDataPath" }, 69 | ); 70 | } catch (error) { 71 | if (error.name !== "ValidationError") { 72 | throw error; 73 | } 74 | 75 | expect(error.message).toMatchSnapshot(); 76 | } 77 | }); 78 | 79 | it('should prefer configuration over "title" #1', () => { 80 | try { 81 | validate(schemaTitle, { foo: "bar" }, { name: "NAME" }); 82 | } catch (error) { 83 | if (error.name !== "ValidationError") { 84 | throw error; 85 | } 86 | 87 | expect(error.message).toMatchSnapshot(); 88 | } 89 | }); 90 | 91 | it('should prefer configuration over "title" #2', () => { 92 | try { 93 | validate(schemaTitle, { foo: "bar" }, { baseDataPath: "BaseDataPath" }); 94 | } catch (error) { 95 | if (error.name !== "ValidationError") { 96 | throw error; 97 | } 98 | 99 | expect(error.message).toMatchSnapshot(); 100 | } 101 | }); 102 | 103 | it('should use default values when "title" is broken', () => { 104 | try { 105 | validate(schemaTitleBrone, { foo: "bar" }); 106 | } catch (error) { 107 | if (error.name !== "ValidationError") { 108 | throw error; 109 | } 110 | 111 | expect(error.message).toMatchSnapshot(); 112 | } 113 | }); 114 | 115 | it("should work with required properties", () => { 116 | try { 117 | validate( 118 | { 119 | type: "object", 120 | properties: { 121 | c: { 122 | type: "object", 123 | properties: { 124 | d: { 125 | type: "string", 126 | }, 127 | e: { 128 | type: "string", 129 | }, 130 | }, 131 | additionalProperties: true, 132 | required: ["d", "e"], 133 | }, 134 | }, 135 | }, 136 | { c: { d: "e" } }, 137 | ); 138 | } catch (error) { 139 | if (error.name !== "ValidationError") { 140 | throw error; 141 | } 142 | 143 | expect(error.message).toMatchSnapshot(); 144 | } 145 | }); 146 | 147 | it("should work with required properties #2", () => { 148 | try { 149 | validate( 150 | { 151 | type: "object", 152 | properties: {}, 153 | required: ["d", "e"], 154 | }, 155 | {}, 156 | ); 157 | } catch (error) { 158 | if (error.name !== "ValidationError") { 159 | throw error; 160 | } 161 | 162 | expect(error.message).toMatchSnapshot(); 163 | } 164 | }); 165 | 166 | it("should work with minProperties properties", () => { 167 | try { 168 | validate( 169 | { 170 | type: "object", 171 | properties: {}, 172 | minProperties: 1, 173 | }, 174 | {}, 175 | ); 176 | } catch (error) { 177 | if (error.name !== "ValidationError") { 178 | throw error; 179 | } 180 | 181 | expect(error.message).toMatchSnapshot(); 182 | } 183 | }); 184 | 185 | it("should work with anyOf", () => { 186 | try { 187 | validate( 188 | { 189 | type: "object", 190 | properties: { foo: { type: "number" } }, 191 | unevaluatedProperties: false, 192 | anyOf: [ 193 | { 194 | required: ["bar"], 195 | properties: { bar: { type: "number" } }, 196 | }, 197 | { 198 | required: ["baz"], 199 | properties: { baz: { type: "number" } }, 200 | }, 201 | ], 202 | }, 203 | {}, 204 | ); 205 | } catch (error) { 206 | if (error.name !== "ValidationError") { 207 | throw error; 208 | } 209 | 210 | expect(error.message).toMatchSnapshot(); 211 | } 212 | }); 213 | 214 | it('should allow to disable validation using "process.env.SKIP_VALIDATION"', () => { 215 | const oldValue = process.env.SKIP_VALIDATION; 216 | 217 | let errored; 218 | 219 | process.env.SKIP_VALIDATION = "y"; 220 | 221 | try { 222 | validate(schemaTitle, { foo: "bar" }, { name: "NAME" }); 223 | } catch (error) { 224 | errored = error; 225 | } 226 | 227 | expect(errored).toBeUndefined(); 228 | 229 | process.env.SKIP_VALIDATION = oldValue; 230 | }); 231 | 232 | it('should allow to disable validation using "process.env.SKIP_VALIDATION" #2', () => { 233 | const oldValue = process.env.SKIP_VALIDATION; 234 | 235 | let errored; 236 | 237 | process.env.SKIP_VALIDATION = "YeS"; 238 | 239 | try { 240 | validate(schemaTitle, { foo: "bar" }, { name: "NAME" }); 241 | } catch (error) { 242 | errored = error; 243 | } 244 | 245 | expect(errored).toBeUndefined(); 246 | 247 | process.env.SKIP_VALIDATION = oldValue; 248 | }); 249 | 250 | it('should allow to enable validation using "process.env.SKIP_VALIDATION"', () => { 251 | const oldValue = process.env.SKIP_VALIDATION; 252 | 253 | process.env.SKIP_VALIDATION = "n"; 254 | 255 | try { 256 | validate(schemaTitle, { foo: "bar" }, { name: "NAME" }); 257 | } catch (error) { 258 | if (error.name !== "ValidationError") { 259 | throw error; 260 | } 261 | 262 | expect(error.message).toMatchSnapshot(); 263 | } 264 | 265 | process.env.SKIP_VALIDATION = oldValue; 266 | }); 267 | 268 | it('should allow to enable validation using "process.env.SKIP_VALIDATION" #2', () => { 269 | const oldValue = process.env.SKIP_VALIDATION; 270 | 271 | process.env.SKIP_VALIDATION = " FaLse "; 272 | 273 | try { 274 | validate(schemaTitle, { foo: "bar" }, { name: "NAME" }); 275 | } catch (error) { 276 | if (error.name !== "ValidationError") { 277 | throw error; 278 | } 279 | 280 | expect(error.message).toMatchSnapshot(); 281 | } 282 | 283 | process.env.SKIP_VALIDATION = oldValue; 284 | }); 285 | 286 | it("should allow to disable validation using API", () => { 287 | let errored; 288 | 289 | disableValidation(); 290 | 291 | try { 292 | validate(schemaTitle, { foo: "bar" }, { name: "NAME" }); 293 | } catch (error) { 294 | errored = error; 295 | } 296 | 297 | expect(errored).toBeUndefined(); 298 | 299 | enableValidation(); 300 | }); 301 | 302 | it("should allow to enable validation using API", () => { 303 | disableValidation(); 304 | enableValidation(); 305 | 306 | try { 307 | validate(schemaTitle, { foo: "bar" }, { name: "NAME" }); 308 | } catch (error) { 309 | if (error.name !== "ValidationError") { 310 | throw error; 311 | } 312 | 313 | expect(error.message).toMatchSnapshot(); 314 | } 315 | }); 316 | 317 | it("should allow to enable and disable validation using API", () => { 318 | process.env.SKIP_VALIDATION = "unknown"; 319 | expect(needValidate()).toBe(true); 320 | 321 | process.env.SKIP_VALIDATION = "no"; 322 | expect(needValidate()).toBe(true); 323 | 324 | process.env.SKIP_VALIDATION = "yes"; 325 | expect(needValidate()).toBe(false); 326 | 327 | enableValidation(); 328 | expect(process.env.SKIP_VALIDATION).toBe("n"); 329 | 330 | process.env.SKIP_VALIDATION = "undefined"; 331 | 332 | enableValidation(); 333 | expect(needValidate()).toBe(true); 334 | 335 | disableValidation(); 336 | expect(needValidate()).toBe(false); 337 | enableValidation(); 338 | 339 | enableValidation(); 340 | enableValidation(); 341 | expect(needValidate()).toBe(true); 342 | 343 | enableValidation(); 344 | disableValidation(); 345 | expect(needValidate()).toBe(false); 346 | enableValidation(); 347 | 348 | enableValidation(); 349 | expect(process.env.SKIP_VALIDATION).toBe("n"); 350 | 351 | disableValidation(); 352 | expect(process.env.SKIP_VALIDATION).toBe("y"); 353 | enableValidation(); 354 | expect(process.env.SKIP_VALIDATION).toBe("n"); 355 | }); 356 | }); 357 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [4.3.3](https://github.com/webpack/schema-utils/compare/v4.3.2...v4.3.3) (2025-10-02) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * reexport `ValidationErrorConfiguration` type ([#204](https://github.com/webpack/schema-utils/issues/204)) ([49af922](https://github.com/webpack/schema-utils/commit/49af9226eb84c6b4d7d4d49d113a6f436dc214ed)) 11 | 12 | ### [4.3.2](https://github.com/webpack/schema-utils/compare/v4.3.1...v4.3.2) (2025-04-22) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * compatibility with old types ([#198](https://github.com/webpack/schema-utils/issues/198)) ([8e31ef3](https://github.com/webpack/schema-utils/commit/8e31ef311484ad1c0d122266fe520d1f1cb90fad)) 18 | 19 | ### [4.3.1](https://github.com/webpack/schema-utils/compare/v4.3.0...v4.3.1) (2025-04-22) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * export `Schema` and additional schemas ([#197](https://github.com/webpack/schema-utils/issues/197)) ([f72cd60](https://github.com/webpack/schema-utils/commit/f72cd6063dc8af6e191540a44333c0624b3fbcab)) 25 | 26 | ## [4.3.0](https://github.com/webpack/schema-utils/compare/v4.2.0...v4.3.0) (2024-12-11) 27 | 28 | 29 | ### Features 30 | 31 | * backport old logic from v3 ([2e2ba9d](https://github.com/webpack/schema-utils/commit/2e2ba9dd84575d11326d6fff3d795df2e33db935)) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * disallow arrays as the object type ([#194](https://github.com/webpack/schema-utils/issues/194)) ([4b8acf2](https://github.com/webpack/schema-utils/commit/4b8acf2b1a2bc787af24546c7039bd6102a8f038)) 37 | 38 | ## [4.2.0](https://github.com/webpack/schema-utils/compare/v4.1.0...v4.2.0) (2023-06-14) 39 | 40 | 41 | ### Features 42 | 43 | * added API to disable and enable validation ([#180](https://github.com/webpack/schema-utils/issues/180)) ([d6b9c9e](https://github.com/webpack/schema-utils/commit/d6b9c9e14bc5304f28f713b3bbf0497461bb117e)) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * lazy loading some modules ([#178](https://github.com/webpack/schema-utils/issues/178)) ([3806c65](https://github.com/webpack/schema-utils/commit/3806c65167f880051a331b8b728ea03ad0632840)) 49 | 50 | ## [4.1.0](https://github.com/webpack/schema-utils/compare/v4.0.1...v4.1.0) (2023-06-07) 51 | 52 | 53 | ### Features 54 | 55 | * implement `undefinedAsNull` keyword for `enum` type ([#175](https://github.com/webpack/schema-utils/issues/175)) ([1265eac](https://github.com/webpack/schema-utils/commit/1265eac8621cddf2f4dad342efc2d76a169912f7)) 56 | 57 | ### [4.0.1](https://github.com/webpack/schema-utils/compare/v4.0.0...v4.0.1) (2023-04-15) 58 | 59 | ### Bug Fixes 60 | 61 | * (perf) improved initial start time 62 | 63 | ## [4.0.0](https://github.com/webpack/schema-utils/compare/v3.1.1...v4.0.0) (2021-11-16) 64 | 65 | 66 | ### ⚠ BREAKING CHANGES 67 | 68 | * minimum supported `Node.js` version is `>= 12.13.0` 69 | * update `ajv` to `8.8.0` version, please read [internal changes](https://github.com/ajv-validator/ajv/releases), `postFormatter` require attention due some properties were changed 70 | * logic for `formatExclusiveMaximum` and `formatExclusiveMinimum` was changed (due usage `ajv-formats` package) 71 | 72 | ### [3.1.1](https://github.com/webpack/schema-utils/compare/v3.1.0...v3.1.1) (2021-07-19) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * update error message for `integer` ([#136](https://github.com/webpack/schema-utils/issues/136)) ([2daa97e](https://github.com/webpack/schema-utils/commit/2daa97eae87e6790b92711746a6a527b859ac13b)) 78 | 79 | ## [3.1.0](https://github.com/webpack/schema-utils/compare/v3.0.0...v3.1.0) (2021-06-30) 80 | 81 | 82 | ### Features 83 | 84 | * added the `link` property in validation error ([589aa59](https://github.com/webpack/schema-utils/commit/589aa5993424a8bc45ec22b67dff55be92c456a9)) 85 | 86 | 87 | ### Bug Fixes 88 | 89 | * non-empty validation error message ([#116](https://github.com/webpack/schema-utils/issues/116)) ([c51abef](https://github.com/webpack/schema-utils/commit/c51abefa4d4d62e1346b3a105182d36675595077)) 90 | 91 | ## [3.0.0](https://github.com/webpack/schema-utils/compare/v2.7.1...v3.0.0) (2020-10-05) 92 | 93 | 94 | ### ⚠ BREAKING CHANGES 95 | 96 | * minimum supported `Node.js` version is `10.13.0`, 97 | * the packages exports was changed, please use `const { validate } = require('schema-utils');` 98 | * the `ValidateError` export was removed in favor the `ValidationError` export, please use `const { ValidationError } = require('schema-utils');` 99 | 100 | ### [2.7.1](https://github.com/webpack/schema-utils/compare/v2.7.0...v2.7.1) (2020-08-31) 101 | 102 | 103 | ### Bug Fixes 104 | 105 | * remove esModuleInterop from tsconfig ([#110](https://github.com/webpack/schema-utils/issues/110)) ([#111](https://github.com/webpack/schema-utils/issues/111)) ([2f40154](https://github.com/webpack/schema-utils/commit/2f40154b91e45b393258ae9dd8f10cc3b8590b7d)) 106 | 107 | ## [2.7.0](https://github.com/webpack/schema-utils/compare/v2.6.6...v2.7.0) (2020-05-29) 108 | 109 | 110 | ### Features 111 | 112 | * improve hints ([a36e535](https://github.com/webpack/schema-utils/commit/a36e535faca1b01e27c3bfa3c8bee9227c3f836c)) 113 | * smart not case ([#101](https://github.com/webpack/schema-utils/issues/101)) ([698d8b0](https://github.com/webpack/schema-utils/commit/698d8b05462d86aadb217e25a45c7b953a79a52e)) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * move @types/json-schema from devDependencies to dependencies ([#97](https://github.com/webpack/schema-utils/issues/97)) ([#98](https://github.com/webpack/schema-utils/issues/98)) ([945e67d](https://github.com/webpack/schema-utils/commit/945e67db5e19baf7ec7df72813b0739dd56f950d)) 119 | 120 | ### [2.6.6](https://github.com/webpack/schema-utils/compare/v2.6.5...v2.6.6) (2020-04-17) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * improve perf 126 | 127 | ### [2.6.5](https://github.com/webpack/schema-utils/compare/v2.6.4...v2.6.5) (2020-03-11) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * correct dots at end of sentence ([7284beb](https://github.com/webpack/schema-utils/commit/7284bebe00cd570f1bef2c15951a07b9794038e6)) 133 | 134 | ### [2.6.4](https://github.com/webpack/schema-utils/compare/v2.6.3...v2.6.4) (2020-01-17) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * change `initialised` to `initialized` ([#87](https://github.com/webpack/schema-utils/issues/87)) ([70f12d3](https://github.com/webpack/schema-utils/commit/70f12d33a8eaa27249bc9c1a27f886724cf91ea7)) 140 | 141 | ### [2.6.3](https://github.com/webpack/schema-utils/compare/v2.6.2...v2.6.3) (2020-01-17) 142 | 143 | 144 | ### Bug Fixes 145 | 146 | * prefer the `baseDataPath` option from arguments ([#86](https://github.com/webpack/schema-utils/issues/86)) ([e236859](https://github.com/webpack/schema-utils/commit/e236859e85b28e35e1294f86fc1ff596a5031cea)) 147 | 148 | ### [2.6.2](https://github.com/webpack/schema-utils/compare/v2.6.1...v2.6.2) (2020-01-14) 149 | 150 | 151 | ### Bug Fixes 152 | 153 | * better handle Windows absolute paths ([#85](https://github.com/webpack/schema-utils/issues/85)) ([1fa2930](https://github.com/webpack/schema-utils/commit/1fa2930a161e907b9fc53a7233d605910afdb883)) 154 | 155 | ### [2.6.1](https://github.com/webpack/schema-utils/compare/v2.6.0...v2.6.1) (2019-11-28) 156 | 157 | 158 | ### Bug Fixes 159 | 160 | * typescript declarations ([#84](https://github.com/webpack/schema-utils/issues/84)) ([89d55a9](https://github.com/webpack/schema-utils/commit/89d55a9a8edfa6a8ac8b112f226bb3154e260319)) 161 | 162 | ## [2.6.0](https://github.com/webpack/schema-utils/compare/v2.5.0...v2.6.0) (2019-11-27) 163 | 164 | 165 | ### Features 166 | 167 | * support configuration via title ([#81](https://github.com/webpack/schema-utils/issues/81)) ([afddc10](https://github.com/webpack/schema-utils/commit/afddc109f6891cd37a9f1835d50862d119a072bf)) 168 | 169 | 170 | ### Bug Fixes 171 | 172 | * typescript definitions ([#70](https://github.com/webpack/schema-utils/issues/70)) ([f38158d](https://github.com/webpack/schema-utils/commit/f38158d6d040e2c701622778ae8122fb26a4f990)) 173 | 174 | ## [2.5.0](https://github.com/webpack/schema-utils/compare/v2.4.1...v2.5.0) (2019-10-15) 175 | 176 | 177 | ### Bug Fixes 178 | 179 | * rework format for maxLength, minLength ([#67](https://github.com/webpack/schema-utils/issues/67)) ([0d12259](https://github.com/webpack/schema-utils/commit/0d12259)) 180 | * support all cases with one number in range ([#64](https://github.com/webpack/schema-utils/issues/64)) ([7fc8069](https://github.com/webpack/schema-utils/commit/7fc8069)) 181 | * typescript definition and export naming ([#69](https://github.com/webpack/schema-utils/issues/69)) ([a435b79](https://github.com/webpack/schema-utils/commit/a435b79)) 182 | 183 | 184 | ### Features 185 | 186 | * "smart" numbers range ([62fb107](https://github.com/webpack/schema-utils/commit/62fb107)) 187 | 188 | ### [2.4.1](https://github.com/webpack/schema-utils/compare/v2.4.0...v2.4.1) (2019-09-27) 189 | 190 | 191 | ### Bug Fixes 192 | 193 | * publish definitions ([#58](https://github.com/webpack/schema-utils/issues/58)) ([1885faa](https://github.com/webpack/schema-utils/commit/1885faa)) 194 | 195 | ## [2.4.0](https://github.com/webpack/schema-utils/compare/v2.3.0...v2.4.0) (2019-09-26) 196 | 197 | 198 | ### Features 199 | 200 | * better errors when the `type` keyword doesn't exist ([0988be2](https://github.com/webpack/schema-utils/commit/0988be2)) 201 | * support $data reference ([#56](https://github.com/webpack/schema-utils/issues/56)) ([d2f11d6](https://github.com/webpack/schema-utils/commit/d2f11d6)) 202 | * types definitions ([#52](https://github.com/webpack/schema-utils/issues/52)) ([facb431](https://github.com/webpack/schema-utils/commit/facb431)) 203 | 204 | ## [2.3.0](https://github.com/webpack/schema-utils/compare/v2.2.0...v2.3.0) (2019-09-26) 205 | 206 | 207 | ### Features 208 | 209 | * support `not` keyword ([#53](https://github.com/webpack/schema-utils/issues/53)) ([765f458](https://github.com/webpack/schema-utils/commit/765f458)) 210 | 211 | ## [2.2.0](https://github.com/webpack/schema-utils/compare/v2.1.0...v2.2.0) (2019-09-02) 212 | 213 | 214 | ### Features 215 | 216 | * better error output for `oneOf` and `anyOf` ([#48](https://github.com/webpack/schema-utils/issues/48)) ([#50](https://github.com/webpack/schema-utils/issues/50)) ([332242f](https://github.com/webpack/schema-utils/commit/332242f)) 217 | 218 | ## [2.1.0](https://github.com/webpack-contrib/schema-utils/compare/v2.0.1...v2.1.0) (2019-08-07) 219 | 220 | 221 | ### Bug Fixes 222 | 223 | * throw error on sparse arrays ([#47](https://github.com/webpack-contrib/schema-utils/issues/47)) ([b85ac38](https://github.com/webpack-contrib/schema-utils/commit/b85ac38)) 224 | 225 | 226 | ### Features 227 | 228 | * export `ValidateError` ([#46](https://github.com/webpack-contrib/schema-utils/issues/46)) ([ff781d7](https://github.com/webpack-contrib/schema-utils/commit/ff781d7)) 229 | 230 | 231 | 232 | ### [2.0.1](https://github.com/webpack-contrib/schema-utils/compare/v2.0.0...v2.0.1) (2019-07-18) 233 | 234 | 235 | ### Bug Fixes 236 | 237 | * error message for empty object ([#44](https://github.com/webpack-contrib/schema-utils/issues/44)) ([0b4b4a2](https://github.com/webpack-contrib/schema-utils/commit/0b4b4a2)) 238 | 239 | 240 | 241 | ### [2.0.0](https://github.com/webpack-contrib/schema-utils/compare/v1.0.0...v2.0.0) (2019-07-17) 242 | 243 | 244 | ### BREAKING CHANGES 245 | 246 | * drop support for Node.js < 8.9.0 247 | * drop support `errorMessage`, please use `description` for links. 248 | * api was changed, please look documentation. 249 | * error messages was fully rewritten. 250 | 251 | 252 | 253 | # [1.0.0](https://github.com/webpack-contrib/schema-utils/compare/v0.4.7...v1.0.0) (2018-08-07) 254 | 255 | 256 | ### Features 257 | 258 | * **src:** add support for custom error messages ([#33](https://github.com/webpack-contrib/schema-utils/issues/33)) ([1cbe4ef](https://github.com/webpack-contrib/schema-utils/commit/1cbe4ef)) 259 | 260 | 261 | 262 | 263 | ## [0.4.7](https://github.com/webpack-contrib/schema-utils/compare/v0.4.6...v0.4.7) (2018-08-07) 264 | 265 | 266 | ### Bug Fixes 267 | 268 | * **src:** `node >= v4.0.0` support ([#32](https://github.com/webpack-contrib/schema-utils/issues/32)) ([cb13dd4](https://github.com/webpack-contrib/schema-utils/commit/cb13dd4)) 269 | 270 | 271 | 272 | 273 | ## [0.4.6](https://github.com/webpack-contrib/schema-utils/compare/v0.4.5...v0.4.6) (2018-08-06) 274 | 275 | 276 | ### Bug Fixes 277 | 278 | * **package:** remove lockfile ([#28](https://github.com/webpack-contrib/schema-utils/issues/28)) ([69f1a81](https://github.com/webpack-contrib/schema-utils/commit/69f1a81)) 279 | * **package:** remove unnecessary `webpack` dependency ([#26](https://github.com/webpack-contrib/schema-utils/issues/26)) ([532eaa5](https://github.com/webpack-contrib/schema-utils/commit/532eaa5)) 280 | 281 | 282 | 283 | 284 | ## [0.4.5](https://github.com/webpack-contrib/schema-utils/compare/v0.4.4...v0.4.5) (2018-02-13) 285 | 286 | 287 | ### Bug Fixes 288 | 289 | * **CHANGELOG:** update broken links ([4483b9f](https://github.com/webpack-contrib/schema-utils/commit/4483b9f)) 290 | * **package:** update broken links ([f2494ba](https://github.com/webpack-contrib/schema-utils/commit/f2494ba)) 291 | 292 | 293 | 294 | 295 | ## [0.4.4](https://github.com/webpack-contrib/schema-utils/compare/v0.4.3...v0.4.4) (2018-02-13) 296 | 297 | 298 | ### Bug Fixes 299 | 300 | * **package:** update `dependencies` ([#22](https://github.com/webpack-contrib/schema-utils/issues/22)) ([3aecac6](https://github.com/webpack-contrib/schema-utils/commit/3aecac6)) 301 | 302 | 303 | 304 | 305 | ## [0.4.3](https://github.com/webpack-contrib/schema-utils/compare/v0.4.2...v0.4.3) (2017-12-14) 306 | 307 | 308 | ### Bug Fixes 309 | 310 | * **validateOptions:** throw `err` instead of `process.exit(1)` ([#17](https://github.com/webpack-contrib/schema-utils/issues/17)) ([c595eda](https://github.com/webpack-contrib/schema-utils/commit/c595eda)) 311 | * **ValidationError:** never return `this` in the ctor ([#16](https://github.com/webpack-contrib/schema-utils/issues/16)) ([c723791](https://github.com/webpack-contrib/schema-utils/commit/c723791)) 312 | 313 | 314 | 315 | 316 | ## [0.4.2](https://github.com/webpack-contrib/schema-utils/compare/v0.4.1...v0.4.2) (2017-11-09) 317 | 318 | 319 | ### Bug Fixes 320 | 321 | * **validateOptions:** catch `ValidationError` and handle it internally ([#15](https://github.com/webpack-contrib/schema-utils/issues/15)) ([9c5ef5e](https://github.com/webpack-contrib/schema-utils/commit/9c5ef5e)) 322 | 323 | 324 | 325 | 326 | ## [0.4.1](https://github.com/webpack-contrib/schema-utils/compare/v0.4.0...v0.4.1) (2017-11-03) 327 | 328 | 329 | ### Bug Fixes 330 | 331 | * **ValidationError:** use `Error.captureStackTrace` for `err.stack` handling ([#14](https://github.com/webpack-contrib/schema-utils/issues/14)) ([a6fb974](https://github.com/webpack-contrib/schema-utils/commit/a6fb974)) 332 | 333 | 334 | 335 | 336 | # [0.4.0](https://github.com/webpack-contrib/schema-utils/compare/v0.3.0...v0.4.0) (2017-10-28) 337 | 338 | 339 | ### Features 340 | 341 | * add support for `typeof`, `instanceof` (`{Function\|RegExp}`) ([#10](https://github.com/webpack-contrib/schema-utils/issues/10)) ([9f01816](https://github.com/webpack-contrib/schema-utils/commit/9f01816)) 342 | 343 | 344 | 345 | 346 | # [0.3.0](https://github.com/webpack-contrib/schema-utils/compare/v0.2.1...v0.3.0) (2017-04-29) 347 | 348 | 349 | ### Features 350 | 351 | * add ValidationError ([#8](https://github.com/webpack-contrib/schema-utils/issues/8)) ([d48f0fb](https://github.com/webpack-contrib/schema-utils/commit/d48f0fb)) 352 | 353 | 354 | 355 | 356 | ## [0.2.1](https://github.com/webpack-contrib/schema-utils/compare/v0.2.0...v0.2.1) (2017-03-13) 357 | 358 | 359 | ### Bug Fixes 360 | 361 | * Include .babelrc to `files` ([28f0363](https://github.com/webpack-contrib/schema-utils/commit/28f0363)) 362 | * Include source to `files` ([43b0f2f](https://github.com/webpack-contrib/schema-utils/commit/43b0f2f)) 363 | 364 | 365 | 366 | 367 | # [0.2.0](https://github.com/webpack-contrib/schema-utils/compare/v0.1.0...v0.2.0) (2017-03-12) 368 | 369 | 370 | # 0.1.0 (2017-03-07) 371 | 372 | 373 | ### Features 374 | 375 | * **validations:** add validateOptions module ([ae9b47b](https://github.com/webpack-contrib/schema-utils/commit/ae9b47b)) 376 | 377 | 378 | 379 | # Change Log 380 | 381 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 382 | -------------------------------------------------------------------------------- /src/ValidationError.js: -------------------------------------------------------------------------------- 1 | import memoize from "./util/memorize"; 2 | 3 | /** @typedef {import("json-schema").JSONSchema6} JSONSchema6 */ 4 | /** @typedef {import("json-schema").JSONSchema7} JSONSchema7 */ 5 | 6 | /** @typedef {import("./validate").Schema} Schema */ 7 | /** @typedef {import("./validate").ValidationErrorConfiguration} ValidationErrorConfiguration */ 8 | /** @typedef {import("./validate").PostFormatter} PostFormatter */ 9 | /** @typedef {import("./validate").SchemaUtilErrorObject} SchemaUtilErrorObject */ 10 | 11 | /** @enum {number} */ 12 | const SPECIFICITY = { 13 | type: 1, 14 | not: 1, 15 | oneOf: 1, 16 | anyOf: 1, 17 | if: 1, 18 | enum: 1, 19 | const: 1, 20 | instanceof: 1, 21 | required: 2, 22 | pattern: 2, 23 | patternRequired: 2, 24 | format: 2, 25 | formatMinimum: 2, 26 | formatMaximum: 2, 27 | minimum: 2, 28 | exclusiveMinimum: 2, 29 | maximum: 2, 30 | exclusiveMaximum: 2, 31 | multipleOf: 2, 32 | uniqueItems: 2, 33 | contains: 2, 34 | minLength: 2, 35 | maxLength: 2, 36 | minItems: 2, 37 | maxItems: 2, 38 | minProperties: 2, 39 | maxProperties: 2, 40 | dependencies: 2, 41 | propertyNames: 2, 42 | additionalItems: 2, 43 | additionalProperties: 2, 44 | absolutePath: 2, 45 | }; 46 | 47 | /** 48 | * @param {string} value value 49 | * @returns {value is number} true when is number, otherwise false 50 | */ 51 | function isNumeric(value) { 52 | return /^-?\d+$/.test(value); 53 | } 54 | 55 | /** 56 | * @param {Array} array array of error objects 57 | * @param {(item: SchemaUtilErrorObject) => number} fn function 58 | * @returns {Array} filtered max 59 | */ 60 | function filterMax(array, fn) { 61 | const evaluatedMax = array.reduce((max, item) => Math.max(max, fn(item)), 0); 62 | 63 | return array.filter((item) => fn(item) === evaluatedMax); 64 | } 65 | 66 | /** 67 | * @param {Array} children children 68 | * @returns {Array} filtered children 69 | */ 70 | function filterChildren(children) { 71 | let newChildren = children; 72 | 73 | newChildren = filterMax( 74 | newChildren, 75 | /** 76 | * @param {SchemaUtilErrorObject} error error object 77 | * @returns {number} result 78 | */ 79 | (error) => (error.instancePath ? error.instancePath.length : 0), 80 | ); 81 | newChildren = filterMax( 82 | newChildren, 83 | /** 84 | * @param {SchemaUtilErrorObject} error error object 85 | * @returns {number} result 86 | */ 87 | (error) => 88 | SPECIFICITY[/** @type {keyof typeof SPECIFICITY} */ (error.keyword)] || 2, 89 | ); 90 | 91 | return newChildren; 92 | } 93 | 94 | /** 95 | * Extracts all refs from schema 96 | * @param {SchemaUtilErrorObject} error error object 97 | * @returns {Array} extracted refs 98 | */ 99 | function extractRefs(error) { 100 | const { schema } = error; 101 | 102 | if (!Array.isArray(schema)) { 103 | return []; 104 | } 105 | 106 | return schema.map(({ $ref }) => $ref).filter(Boolean); 107 | } 108 | 109 | /** 110 | * Find all children errors 111 | * @param {Array} children children 112 | * @param {Array} schemaPaths schema paths 113 | * @returns {number} returns index of first child 114 | */ 115 | function findAllChildren(children, schemaPaths) { 116 | let i = children.length - 1; 117 | const predicate = 118 | /** 119 | * @param {string} schemaPath schema path 120 | * @returns {boolean} predicate 121 | */ 122 | (schemaPath) => children[i].schemaPath.indexOf(schemaPath) !== 0; 123 | 124 | while (i > -1 && !schemaPaths.every(predicate)) { 125 | if (children[i].keyword === "anyOf" || children[i].keyword === "oneOf") { 126 | const refs = extractRefs(children[i]); 127 | const childrenStart = findAllChildren(children.slice(0, i), [ 128 | ...refs, 129 | children[i].schemaPath, 130 | ]); 131 | 132 | i = childrenStart - 1; 133 | } else { 134 | i -= 1; 135 | } 136 | } 137 | 138 | return i + 1; 139 | } 140 | 141 | /** 142 | * Groups children by their first level parent (assuming that error is root) 143 | * @param {Array} children children 144 | * @returns {Array} grouped children 145 | */ 146 | function groupChildrenByFirstChild(children) { 147 | const result = []; 148 | let i = children.length - 1; 149 | 150 | while (i > 0) { 151 | const child = children[i]; 152 | 153 | if (child.keyword === "anyOf" || child.keyword === "oneOf") { 154 | const refs = extractRefs(child); 155 | const childrenStart = findAllChildren(children.slice(0, i), [ 156 | ...refs, 157 | child.schemaPath, 158 | ]); 159 | 160 | if (childrenStart !== i) { 161 | result.push({ ...child, children: children.slice(childrenStart, i) }); 162 | i = childrenStart; 163 | } else { 164 | result.push(child); 165 | } 166 | } else { 167 | result.push(child); 168 | } 169 | 170 | i -= 1; 171 | } 172 | 173 | if (i === 0) { 174 | result.push(children[i]); 175 | } 176 | 177 | return result.reverse(); 178 | } 179 | 180 | /** 181 | * @param {string} str string 182 | * @param {string} prefix prefix 183 | * @returns {string} string with indent and prefix 184 | */ 185 | function indent(str, prefix) { 186 | return str.replace(/\n(?!$)/g, `\n${prefix}`); 187 | } 188 | 189 | /** 190 | * @param {Schema} schema schema 191 | * @returns {schema is (Schema & {not: Schema})} true when `not` in schema, otherwise false 192 | */ 193 | function hasNotInSchema(schema) { 194 | return Boolean(schema.not); 195 | } 196 | 197 | /** 198 | * @param {Schema} schema schema 199 | * @returns {Schema} first typed schema 200 | */ 201 | function findFirstTypedSchema(schema) { 202 | if (hasNotInSchema(schema)) { 203 | return findFirstTypedSchema(schema.not); 204 | } 205 | 206 | return schema; 207 | } 208 | 209 | /** 210 | * @param {Schema} schema schema 211 | * @returns {boolean} true when schema type is number, otherwise false 212 | */ 213 | function likeNumber(schema) { 214 | return ( 215 | schema.type === "number" || 216 | typeof schema.minimum !== "undefined" || 217 | typeof schema.exclusiveMinimum !== "undefined" || 218 | typeof schema.maximum !== "undefined" || 219 | typeof schema.exclusiveMaximum !== "undefined" || 220 | typeof schema.multipleOf !== "undefined" 221 | ); 222 | } 223 | 224 | /** 225 | * @param {Schema} schema schema 226 | * @returns {boolean} true when schema type is integer, otherwise false 227 | */ 228 | function likeInteger(schema) { 229 | return ( 230 | schema.type === "integer" || 231 | typeof schema.minimum !== "undefined" || 232 | typeof schema.exclusiveMinimum !== "undefined" || 233 | typeof schema.maximum !== "undefined" || 234 | typeof schema.exclusiveMaximum !== "undefined" || 235 | typeof schema.multipleOf !== "undefined" 236 | ); 237 | } 238 | 239 | /** 240 | * @param {Schema} schema schema 241 | * @returns {boolean} true when schema type is string, otherwise false 242 | */ 243 | function likeString(schema) { 244 | return ( 245 | schema.type === "string" || 246 | typeof schema.minLength !== "undefined" || 247 | typeof schema.maxLength !== "undefined" || 248 | typeof schema.pattern !== "undefined" || 249 | typeof schema.format !== "undefined" || 250 | typeof schema.formatMinimum !== "undefined" || 251 | typeof schema.formatMaximum !== "undefined" 252 | ); 253 | } 254 | 255 | /** 256 | * @param {Schema} schema schema 257 | * @returns {boolean} true when null, otherwise false 258 | */ 259 | function likeNull(schema) { 260 | return schema.type === "null"; 261 | } 262 | 263 | /** 264 | * @param {Schema} schema schema 265 | * @returns {boolean} true when schema type is boolean, otherwise false 266 | */ 267 | function likeBoolean(schema) { 268 | return schema.type === "boolean"; 269 | } 270 | 271 | /** 272 | * @param {Schema} schema schema 273 | * @returns {boolean} true when can apply not, otherwise false 274 | */ 275 | function canApplyNot(schema) { 276 | const typedSchema = findFirstTypedSchema(schema); 277 | 278 | return ( 279 | likeNumber(typedSchema) || 280 | likeInteger(typedSchema) || 281 | likeString(typedSchema) || 282 | likeNull(typedSchema) || 283 | likeBoolean(typedSchema) 284 | ); 285 | } 286 | 287 | // eslint-disable-next-line jsdoc/no-restricted-syntax 288 | /** 289 | * @param {any} maybeObj maybe obj 290 | * @returns {boolean} true when value is object, otherwise false 291 | */ 292 | function isObject(maybeObj) { 293 | return ( 294 | typeof maybeObj === "object" && 295 | !Array.isArray(maybeObj) && 296 | maybeObj !== null 297 | ); 298 | } 299 | 300 | /** 301 | * @param {Schema} schema schema 302 | * @returns {boolean} true when schema type is array, otherwise false 303 | */ 304 | function likeArray(schema) { 305 | return ( 306 | schema.type === "array" || 307 | typeof schema.minItems === "number" || 308 | typeof schema.maxItems === "number" || 309 | typeof schema.uniqueItems !== "undefined" || 310 | typeof schema.items !== "undefined" || 311 | typeof schema.additionalItems !== "undefined" || 312 | typeof schema.contains !== "undefined" 313 | ); 314 | } 315 | 316 | /** 317 | * @param {Schema & {patternRequired?: Array}} schema schema 318 | * @returns {boolean} true when schema type is object, otherwise false 319 | */ 320 | function likeObject(schema) { 321 | return ( 322 | schema.type === "object" || 323 | typeof schema.minProperties !== "undefined" || 324 | typeof schema.maxProperties !== "undefined" || 325 | typeof schema.required !== "undefined" || 326 | typeof schema.properties !== "undefined" || 327 | typeof schema.patternProperties !== "undefined" || 328 | typeof schema.additionalProperties !== "undefined" || 329 | typeof schema.dependencies !== "undefined" || 330 | typeof schema.propertyNames !== "undefined" || 331 | typeof schema.patternRequired !== "undefined" 332 | ); 333 | } 334 | 335 | /** 336 | * @param {string} type type 337 | * @returns {string} article 338 | */ 339 | function getArticle(type) { 340 | if (/^[aeiou]/i.test(type)) { 341 | return "an"; 342 | } 343 | 344 | return "a"; 345 | } 346 | 347 | /** 348 | * @param {Schema=} schema schema 349 | * @returns {string} schema non types 350 | */ 351 | function getSchemaNonTypes(schema) { 352 | if (!schema) { 353 | return ""; 354 | } 355 | 356 | if (!schema.type) { 357 | if (likeNumber(schema) || likeInteger(schema)) { 358 | return " | should be any non-number"; 359 | } 360 | 361 | if (likeString(schema)) { 362 | return " | should be any non-string"; 363 | } 364 | 365 | if (likeArray(schema)) { 366 | return " | should be any non-array"; 367 | } 368 | 369 | if (likeObject(schema)) { 370 | return " | should be any non-object"; 371 | } 372 | } 373 | 374 | return ""; 375 | } 376 | 377 | /** 378 | * @param {Array} hints hints 379 | * @returns {string} formatted hints 380 | */ 381 | function formatHints(hints) { 382 | return hints.length > 0 ? `(${hints.join(", ")})` : ""; 383 | } 384 | 385 | const getUtilHints = memoize(() => require("./util/hints")); 386 | 387 | /** 388 | * @param {Schema} schema schema 389 | * @param {boolean} logic logic 390 | * @returns {string[]} array of hints 391 | */ 392 | function getHints(schema, logic) { 393 | if (likeNumber(schema) || likeInteger(schema)) { 394 | const util = getUtilHints(); 395 | 396 | return util.numberHints(schema, logic); 397 | } else if (likeString(schema)) { 398 | const util = getUtilHints(); 399 | 400 | return util.stringHints(schema, logic); 401 | } 402 | 403 | return []; 404 | } 405 | 406 | class ValidationError extends Error { 407 | /** 408 | * @param {Array} errors array of error objects 409 | * @param {Schema} schema schema 410 | * @param {ValidationErrorConfiguration} configuration configuration 411 | */ 412 | constructor(errors, schema, configuration = {}) { 413 | super(); 414 | 415 | /** @type {string} */ 416 | this.name = "ValidationError"; 417 | /** @type {Array} */ 418 | this.errors = errors; 419 | /** @type {Schema} */ 420 | this.schema = schema; 421 | 422 | let headerNameFromSchema; 423 | let baseDataPathFromSchema; 424 | 425 | if (schema.title && (!configuration.name || !configuration.baseDataPath)) { 426 | const splittedTitleFromSchema = schema.title.match(/^(.+) (.+)$/); 427 | 428 | if (splittedTitleFromSchema) { 429 | if (!configuration.name) { 430 | [, headerNameFromSchema] = splittedTitleFromSchema; 431 | } 432 | 433 | if (!configuration.baseDataPath) { 434 | [, , baseDataPathFromSchema] = splittedTitleFromSchema; 435 | } 436 | } 437 | } 438 | 439 | /** @type {string} */ 440 | this.headerName = configuration.name || headerNameFromSchema || "Object"; 441 | /** @type {string} */ 442 | this.baseDataPath = 443 | configuration.baseDataPath || baseDataPathFromSchema || "configuration"; 444 | 445 | /** @type {PostFormatter | null} */ 446 | this.postFormatter = configuration.postFormatter || null; 447 | 448 | const header = `Invalid ${this.baseDataPath} object. ${ 449 | this.headerName 450 | } has been initialized using ${getArticle(this.baseDataPath)} ${ 451 | this.baseDataPath 452 | } object that does not match the API schema.\n`; 453 | 454 | /** @type {string} */ 455 | this.message = `${header}${this.formatValidationErrors(errors)}`; 456 | 457 | Error.captureStackTrace(this, this.constructor); 458 | } 459 | 460 | /** 461 | * @param {string} path path 462 | * @returns {Schema} schema 463 | */ 464 | getSchemaPart(path) { 465 | const newPath = path.split("/"); 466 | 467 | let schemaPart = this.schema; 468 | 469 | for (let i = 1; i < newPath.length; i++) { 470 | const inner = schemaPart[/** @type {keyof Schema} */ (newPath[i])]; 471 | 472 | if (!inner) { 473 | break; 474 | } 475 | 476 | schemaPart = inner; 477 | } 478 | 479 | return schemaPart; 480 | } 481 | 482 | /** 483 | * @param {Schema} schema schema 484 | * @param {boolean} logic logic 485 | * @param {Array} prevSchemas prev schemas 486 | * @returns {string} formatted schema 487 | */ 488 | formatSchema(schema, logic = true, prevSchemas = []) { 489 | let newLogic = logic; 490 | const formatInnerSchema = 491 | /** 492 | * @param {Schema} innerSchema inner schema 493 | * @param {boolean=} addSelf true when need to add self 494 | * @returns {string} formatted schema 495 | */ 496 | (innerSchema, addSelf) => { 497 | if (!addSelf) { 498 | return this.formatSchema(innerSchema, newLogic, prevSchemas); 499 | } 500 | 501 | if (prevSchemas.includes(innerSchema)) { 502 | return "(recursive)"; 503 | } 504 | 505 | return this.formatSchema(innerSchema, newLogic, [ 506 | ...prevSchemas, 507 | schema, 508 | ]); 509 | }; 510 | 511 | if (hasNotInSchema(schema) && !likeObject(schema)) { 512 | if (canApplyNot(schema.not)) { 513 | newLogic = !logic; 514 | 515 | return formatInnerSchema(schema.not); 516 | } 517 | 518 | const needApplyLogicHere = !schema.not.not; 519 | const prefix = logic ? "" : "non "; 520 | newLogic = !logic; 521 | 522 | return needApplyLogicHere 523 | ? prefix + formatInnerSchema(schema.not) 524 | : formatInnerSchema(schema.not); 525 | } 526 | 527 | if ( 528 | /** @type {Schema & {instanceof: string | Array}} */ 529 | (schema).instanceof 530 | ) { 531 | const { instanceof: value } = 532 | /** @type {Schema & {instanceof: string | Array}} */ (schema); 533 | 534 | const values = !Array.isArray(value) ? [value] : value; 535 | 536 | return values 537 | .map( 538 | /** 539 | * @param {string} item item 540 | * @returns {string} result 541 | */ 542 | (item) => (item === "Function" ? "function" : item), 543 | ) 544 | .join(" | "); 545 | } 546 | 547 | if (schema.enum) { 548 | // eslint-disable-next-line jsdoc/no-restricted-syntax 549 | const enumValues = /** @type {Array} */ (schema.enum) 550 | .map((item) => { 551 | if (item === null && schema.undefinedAsNull) { 552 | return `${JSON.stringify(item)} | undefined`; 553 | } 554 | 555 | return JSON.stringify(item); 556 | }) 557 | .join(" | "); 558 | 559 | return `${enumValues}`; 560 | } 561 | 562 | if (typeof schema.const !== "undefined") { 563 | return JSON.stringify(schema.const); 564 | } 565 | 566 | if (schema.oneOf) { 567 | return /** @type {Array} */ (schema.oneOf) 568 | .map((item) => formatInnerSchema(item, true)) 569 | .join(" | "); 570 | } 571 | 572 | if (schema.anyOf) { 573 | return /** @type {Array} */ (schema.anyOf) 574 | .map((item) => formatInnerSchema(item, true)) 575 | .join(" | "); 576 | } 577 | 578 | if (schema.allOf) { 579 | return /** @type {Array} */ (schema.allOf) 580 | .map((item) => formatInnerSchema(item, true)) 581 | .join(" & "); 582 | } 583 | 584 | if (/** @type {JSONSchema7} */ (schema).if) { 585 | const { 586 | if: ifValue, 587 | then: thenValue, 588 | else: elseValue, 589 | } = /** @type {JSONSchema7} */ (schema); 590 | 591 | return `${ifValue ? `if ${ifValue === true ? "true" : formatInnerSchema(ifValue)}` : ""}${ 592 | thenValue 593 | ? ` then ${thenValue === true ? "true" : formatInnerSchema(thenValue)}` 594 | : "" 595 | }${elseValue ? ` else ${elseValue === true ? "true" : formatInnerSchema(elseValue)}` : ""}`; 596 | } 597 | 598 | if (schema.$ref) { 599 | return formatInnerSchema(this.getSchemaPart(schema.$ref), true); 600 | } 601 | 602 | if (likeNumber(schema) || likeInteger(schema)) { 603 | const [type, ...hints] = getHints(schema, logic); 604 | const str = `${type}${hints.length > 0 ? ` ${formatHints(hints)}` : ""}`; 605 | 606 | return logic 607 | ? str 608 | : hints.length > 0 609 | ? `non-${type} | ${str}` 610 | : `non-${type}`; 611 | } 612 | 613 | if (likeString(schema)) { 614 | const [type, ...hints] = getHints(schema, logic); 615 | const str = `${type}${hints.length > 0 ? ` ${formatHints(hints)}` : ""}`; 616 | 617 | return logic 618 | ? str 619 | : str === "string" 620 | ? "non-string" 621 | : `non-string | ${str}`; 622 | } 623 | 624 | if (likeBoolean(schema)) { 625 | return `${logic ? "" : "non-"}boolean`; 626 | } 627 | 628 | if (likeArray(schema)) { 629 | // not logic already applied in formatValidationError 630 | newLogic = true; 631 | const hints = []; 632 | 633 | if (typeof schema.minItems === "number") { 634 | hints.push( 635 | `should not have fewer than ${schema.minItems} item${ 636 | schema.minItems > 1 ? "s" : "" 637 | }`, 638 | ); 639 | } 640 | 641 | if (typeof schema.maxItems === "number") { 642 | hints.push( 643 | `should not have more than ${schema.maxItems} item${ 644 | schema.maxItems > 1 ? "s" : "" 645 | }`, 646 | ); 647 | } 648 | 649 | if (schema.uniqueItems) { 650 | hints.push("should not have duplicate items"); 651 | } 652 | 653 | const hasAdditionalItems = 654 | typeof schema.additionalItems === "undefined" || 655 | Boolean(schema.additionalItems); 656 | let items = ""; 657 | 658 | if (schema.items) { 659 | if (Array.isArray(schema.items) && schema.items.length > 0) { 660 | items = `${ 661 | /** @type {Array} */ (schema.items) 662 | .map((item) => formatInnerSchema(item)) 663 | .join(", ") 664 | }`; 665 | 666 | if ( 667 | hasAdditionalItems && 668 | schema.additionalItems && 669 | isObject(schema.additionalItems) && 670 | Object.keys(schema.additionalItems).length > 0 671 | ) { 672 | hints.push( 673 | `additional items should be ${ 674 | schema.additionalItems === true 675 | ? "added" 676 | : formatInnerSchema(schema.additionalItems) 677 | }`, 678 | ); 679 | } 680 | } else if ( 681 | schema.items && 682 | Object.keys(schema.items).length > 0 && 683 | schema.items !== true 684 | ) { 685 | // "additionalItems" is ignored 686 | items = `${formatInnerSchema(schema.items)}`; 687 | } else { 688 | // Fallback for empty `items` value 689 | items = "any"; 690 | } 691 | } else { 692 | // "additionalItems" is ignored 693 | items = "any"; 694 | } 695 | 696 | if (schema.contains && Object.keys(schema.contains).length > 0) { 697 | hints.push( 698 | `should contains at least one ${this.formatSchema( 699 | schema.contains, 700 | )} item`, 701 | ); 702 | } 703 | 704 | return `[${items}${hasAdditionalItems ? ", ..." : ""}]${ 705 | hints.length > 0 ? ` (${hints.join(", ")})` : "" 706 | }`; 707 | } 708 | 709 | if (likeObject(schema)) { 710 | // not logic already applied in formatValidationError 711 | newLogic = true; 712 | const hints = []; 713 | 714 | if (typeof schema.minProperties === "number") { 715 | hints.push( 716 | `should not have fewer than ${schema.minProperties} ${ 717 | schema.minProperties > 1 ? "properties" : "property" 718 | }`, 719 | ); 720 | } 721 | 722 | if (typeof schema.maxProperties === "number") { 723 | hints.push( 724 | `should not have more than ${schema.maxProperties} ${ 725 | schema.minProperties && schema.minProperties > 1 726 | ? "properties" 727 | : "property" 728 | }`, 729 | ); 730 | } 731 | 732 | if ( 733 | schema.patternProperties && 734 | Object.keys(schema.patternProperties).length > 0 735 | ) { 736 | const patternProperties = Object.keys(schema.patternProperties); 737 | 738 | hints.push( 739 | `additional property names should match pattern${ 740 | patternProperties.length > 1 ? "s" : "" 741 | } ${patternProperties 742 | .map((pattern) => JSON.stringify(pattern)) 743 | .join(" | ")}`, 744 | ); 745 | } 746 | 747 | const properties = schema.properties 748 | ? Object.keys(schema.properties) 749 | : []; 750 | const required = 751 | /** @type {string[]} */ 752 | (schema.required ? schema.required : []); 753 | const allProperties = [ 754 | ...new Set(/** @type {Array} */ ([...required, ...properties])), 755 | ]; 756 | 757 | const objectStructure = [ 758 | ...allProperties.map((property) => { 759 | const isRequired = required.includes(property); 760 | 761 | // Some properties need quotes, maybe we should add check 762 | // Maybe we should output type of property (`foo: string`), but it is looks very unreadable 763 | return `${property}${isRequired ? "" : "?"}`; 764 | }), 765 | ...(typeof schema.additionalProperties === "undefined" || 766 | Boolean(schema.additionalProperties) 767 | ? schema.additionalProperties && 768 | isObject(schema.additionalProperties) && 769 | schema.additionalProperties !== true 770 | ? [`: ${formatInnerSchema(schema.additionalProperties)}`] 771 | : ["…"] 772 | : []), 773 | ].join(", "); 774 | 775 | const { dependencies, propertyNames, patternRequired } = 776 | /** @type {Schema & {patternRequired?: Array;}} */ (schema); 777 | 778 | if (dependencies) { 779 | for (const dependencyName of Object.keys(dependencies)) { 780 | const dependency = dependencies[dependencyName]; 781 | 782 | if (Array.isArray(dependency)) { 783 | hints.push( 784 | `should have ${ 785 | dependency.length > 1 ? "properties" : "property" 786 | } ${dependency 787 | .map((dep) => `'${dep}'`) 788 | .join(", ")} when property '${dependencyName}' is present`, 789 | ); 790 | } else { 791 | hints.push( 792 | `should be valid according to the schema ${ 793 | typeof dependency === "boolean" 794 | ? `${dependency}` 795 | : formatInnerSchema(dependency) 796 | } when property '${dependencyName}' is present`, 797 | ); 798 | } 799 | } 800 | } 801 | 802 | if (propertyNames && Object.keys(propertyNames).length > 0) { 803 | hints.push( 804 | `each property name should match format ${JSON.stringify( 805 | schema.propertyNames.format, 806 | )}`, 807 | ); 808 | } 809 | 810 | if (patternRequired && patternRequired.length > 0) { 811 | hints.push( 812 | `should have property matching pattern ${patternRequired.map( 813 | /** 814 | * @param {string} item item 815 | * @returns {string} stringified item 816 | */ 817 | (item) => JSON.stringify(item), 818 | )}`, 819 | ); 820 | } 821 | 822 | return `object {${objectStructure ? ` ${objectStructure} ` : ""}}${ 823 | hints.length > 0 ? ` (${hints.join(", ")})` : "" 824 | }`; 825 | } 826 | 827 | if (likeNull(schema)) { 828 | return `${logic ? "" : "non-"}null`; 829 | } 830 | 831 | if (Array.isArray(schema.type)) { 832 | // not logic already applied in formatValidationError 833 | return `${schema.type.join(" | ")}`; 834 | } 835 | 836 | // Fallback for unknown keywords 837 | // not logic already applied in formatValidationError 838 | /* istanbul ignore next */ 839 | return JSON.stringify(schema, null, 2); 840 | } 841 | 842 | /** 843 | * @param {Schema=} schemaPart schema part 844 | * @param {(boolean | Array)=} additionalPath additional path 845 | * @param {boolean=} needDot true when need dot 846 | * @param {boolean=} logic logic 847 | * @returns {string} schema part text 848 | */ 849 | getSchemaPartText(schemaPart, additionalPath, needDot = false, logic = true) { 850 | if (!schemaPart) { 851 | return ""; 852 | } 853 | 854 | if (Array.isArray(additionalPath)) { 855 | for (let i = 0; i < additionalPath.length; i++) { 856 | /** @type {Schema | undefined} */ 857 | const inner = 858 | schemaPart[/** @type {keyof Schema} */ (additionalPath[i])]; 859 | 860 | if (inner) { 861 | schemaPart = inner; 862 | } else { 863 | break; 864 | } 865 | } 866 | } 867 | 868 | while (schemaPart.$ref) { 869 | schemaPart = this.getSchemaPart(schemaPart.$ref); 870 | } 871 | 872 | let schemaText = `${this.formatSchema(schemaPart, logic)}${ 873 | needDot ? "." : "" 874 | }`; 875 | 876 | if (schemaPart.description) { 877 | schemaText += `\n-> ${schemaPart.description}`; 878 | } 879 | 880 | if (schemaPart.link) { 881 | schemaText += `\n-> Read more at ${schemaPart.link}`; 882 | } 883 | 884 | return schemaText; 885 | } 886 | 887 | /** 888 | * @param {Schema=} schemaPart schema part 889 | * @returns {string} schema part description 890 | */ 891 | getSchemaPartDescription(schemaPart) { 892 | if (!schemaPart) { 893 | return ""; 894 | } 895 | 896 | while (schemaPart.$ref) { 897 | schemaPart = this.getSchemaPart(schemaPart.$ref); 898 | } 899 | 900 | let schemaText = ""; 901 | 902 | if (schemaPart.description) { 903 | schemaText += `\n-> ${schemaPart.description}`; 904 | } 905 | 906 | if (schemaPart.link) { 907 | schemaText += `\n-> Read more at ${schemaPart.link}`; 908 | } 909 | 910 | return schemaText; 911 | } 912 | 913 | /** 914 | * @param {SchemaUtilErrorObject} error error object 915 | * @returns {string} formatted error object 916 | */ 917 | formatValidationError(error) { 918 | const { keyword, instancePath: errorInstancePath } = error; 919 | 920 | const splittedInstancePath = errorInstancePath.split("/"); 921 | /** 922 | * @type {Array} 923 | */ 924 | const defaultValue = []; 925 | const prettyInstancePath = splittedInstancePath 926 | .reduce((acc, val) => { 927 | if (val.length > 0) { 928 | if (isNumeric(val)) { 929 | acc.push(`[${val}]`); 930 | } else if (/^\[/.test(val)) { 931 | acc.push(val); 932 | } else { 933 | acc.push(`.${val}`); 934 | } 935 | } 936 | 937 | return acc; 938 | }, defaultValue) 939 | .join(""); 940 | const instancePath = `${this.baseDataPath}${prettyInstancePath}`; 941 | 942 | // const { keyword, instancePath: errorInstancePath } = error; 943 | // const instancePath = `${this.baseDataPath}${errorInstancePath.replace(/\//g, '.')}`; 944 | 945 | switch (keyword) { 946 | case "type": { 947 | const { parentSchema, params } = error; 948 | 949 | switch (params.type) { 950 | case "number": 951 | return `${instancePath} should be a ${this.getSchemaPartText( 952 | parentSchema, 953 | false, 954 | true, 955 | )}`; 956 | case "integer": 957 | return `${instancePath} should be an ${this.getSchemaPartText( 958 | parentSchema, 959 | false, 960 | true, 961 | )}`; 962 | case "string": 963 | return `${instancePath} should be a ${this.getSchemaPartText( 964 | parentSchema, 965 | false, 966 | true, 967 | )}`; 968 | case "boolean": 969 | return `${instancePath} should be a ${this.getSchemaPartText( 970 | parentSchema, 971 | false, 972 | true, 973 | )}`; 974 | case "array": 975 | return `${instancePath} should be an array:\n${this.getSchemaPartText( 976 | parentSchema, 977 | )}`; 978 | case "object": 979 | return `${instancePath} should be an object:\n${this.getSchemaPartText( 980 | parentSchema, 981 | )}`; 982 | case "null": 983 | return `${instancePath} should be a ${this.getSchemaPartText( 984 | parentSchema, 985 | false, 986 | true, 987 | )}`; 988 | default: 989 | return `${instancePath} should be:\n${this.getSchemaPartText( 990 | parentSchema, 991 | )}`; 992 | } 993 | } 994 | case "instanceof": { 995 | const { parentSchema } = error; 996 | 997 | return `${instancePath} should be an instance of ${this.getSchemaPartText( 998 | parentSchema, 999 | false, 1000 | true, 1001 | )}`; 1002 | } 1003 | case "pattern": { 1004 | const { params, parentSchema } = error; 1005 | const { pattern } = params; 1006 | 1007 | return `${instancePath} should match pattern ${JSON.stringify( 1008 | pattern, 1009 | )}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription( 1010 | parentSchema, 1011 | )}`; 1012 | } 1013 | case "format": { 1014 | const { params, parentSchema } = error; 1015 | const { format } = params; 1016 | 1017 | return `${instancePath} should match format ${JSON.stringify( 1018 | format, 1019 | )}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription( 1020 | parentSchema, 1021 | )}`; 1022 | } 1023 | case "formatMinimum": 1024 | case "formatExclusiveMinimum": 1025 | case "formatMaximum": 1026 | case "formatExclusiveMaximum": { 1027 | const { params, parentSchema } = error; 1028 | const { comparison, limit } = params; 1029 | 1030 | return `${instancePath} should be ${comparison} ${JSON.stringify( 1031 | limit, 1032 | )}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription( 1033 | parentSchema, 1034 | )}`; 1035 | } 1036 | case "minimum": 1037 | case "maximum": 1038 | case "exclusiveMinimum": 1039 | case "exclusiveMaximum": { 1040 | const { parentSchema, params } = error; 1041 | const { comparison, limit } = params; 1042 | const [, ...hints] = getHints( 1043 | /** @type {Schema} */ (parentSchema), 1044 | true, 1045 | ); 1046 | 1047 | if (hints.length === 0) { 1048 | hints.push(`should be ${comparison} ${limit}`); 1049 | } 1050 | 1051 | return `${instancePath} ${hints.join(" ")}${getSchemaNonTypes( 1052 | parentSchema, 1053 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1054 | } 1055 | case "multipleOf": { 1056 | const { params, parentSchema } = error; 1057 | const { multipleOf } = params; 1058 | 1059 | return `${instancePath} should be multiple of ${multipleOf}${getSchemaNonTypes( 1060 | parentSchema, 1061 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1062 | } 1063 | case "patternRequired": { 1064 | const { params, parentSchema } = error; 1065 | const { missingPattern } = params; 1066 | 1067 | return `${instancePath} should have property matching pattern ${JSON.stringify( 1068 | missingPattern, 1069 | )}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription( 1070 | parentSchema, 1071 | )}`; 1072 | } 1073 | case "minLength": { 1074 | const { params, parentSchema } = error; 1075 | const { limit } = params; 1076 | 1077 | if (limit === 1) { 1078 | return `${instancePath} should be a non-empty string${getSchemaNonTypes( 1079 | parentSchema, 1080 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1081 | } 1082 | 1083 | const length = limit - 1; 1084 | 1085 | return `${instancePath} should be longer than ${length} character${ 1086 | length > 1 ? "s" : "" 1087 | }${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription( 1088 | parentSchema, 1089 | )}`; 1090 | } 1091 | case "minItems": { 1092 | const { params, parentSchema } = error; 1093 | const { limit } = params; 1094 | 1095 | if (limit === 1) { 1096 | return `${instancePath} should be a non-empty array${getSchemaNonTypes( 1097 | parentSchema, 1098 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1099 | } 1100 | 1101 | return `${instancePath} should not have fewer than ${limit} items${getSchemaNonTypes( 1102 | parentSchema, 1103 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1104 | } 1105 | case "minProperties": { 1106 | const { params, parentSchema } = error; 1107 | const { limit } = params; 1108 | 1109 | if (limit === 1) { 1110 | return `${instancePath} should be a non-empty object${getSchemaNonTypes( 1111 | parentSchema, 1112 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1113 | } 1114 | 1115 | return `${instancePath} should not have fewer than ${limit} properties${getSchemaNonTypes( 1116 | parentSchema, 1117 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1118 | } 1119 | 1120 | case "maxLength": { 1121 | const { params, parentSchema } = error; 1122 | const { limit } = params; 1123 | const max = limit + 1; 1124 | 1125 | return `${instancePath} should be shorter than ${max} character${ 1126 | max > 1 ? "s" : "" 1127 | }${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription( 1128 | parentSchema, 1129 | )}`; 1130 | } 1131 | case "maxItems": { 1132 | const { params, parentSchema } = error; 1133 | const { limit } = params; 1134 | 1135 | return `${instancePath} should not have more than ${limit} items${getSchemaNonTypes( 1136 | parentSchema, 1137 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1138 | } 1139 | case "maxProperties": { 1140 | const { params, parentSchema } = error; 1141 | const { limit } = params; 1142 | 1143 | return `${instancePath} should not have more than ${limit} properties${getSchemaNonTypes( 1144 | parentSchema, 1145 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1146 | } 1147 | case "uniqueItems": { 1148 | const { params, parentSchema } = error; 1149 | const { i } = params; 1150 | 1151 | return `${instancePath} should not contain the item '${ 1152 | // eslint-disable-next-line jsdoc/no-restricted-syntax 1153 | /** @type {{ data: Array }} * */ 1154 | (error).data[i] 1155 | }' twice${getSchemaNonTypes( 1156 | parentSchema, 1157 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1158 | } 1159 | case "additionalItems": { 1160 | const { params, parentSchema } = error; 1161 | const { limit } = params; 1162 | 1163 | return `${instancePath} should not have more than ${limit} items${getSchemaNonTypes( 1164 | parentSchema, 1165 | )}. These items are valid:\n${this.getSchemaPartText(parentSchema)}`; 1166 | } 1167 | case "contains": { 1168 | const { parentSchema } = error; 1169 | 1170 | return `${instancePath} should contains at least one ${this.getSchemaPartText( 1171 | parentSchema, 1172 | ["contains"], 1173 | )} item${getSchemaNonTypes(parentSchema)}.`; 1174 | } 1175 | case "required": { 1176 | const { parentSchema, params } = error; 1177 | const missingProperty = params.missingProperty.replace(/^\./, ""); 1178 | const hasProperty = 1179 | parentSchema && 1180 | Boolean( 1181 | /** @type {Schema} */ 1182 | (parentSchema).properties && 1183 | /** @type {Schema} */ 1184 | (parentSchema).properties[missingProperty], 1185 | ); 1186 | 1187 | return `${instancePath} misses the property '${missingProperty}'${getSchemaNonTypes( 1188 | parentSchema, 1189 | )}.${ 1190 | hasProperty 1191 | ? ` Should be:\n${this.getSchemaPartText(parentSchema, [ 1192 | "properties", 1193 | missingProperty, 1194 | ])}` 1195 | : this.getSchemaPartDescription(parentSchema) 1196 | }`; 1197 | } 1198 | case "additionalProperties": { 1199 | const { params, parentSchema } = error; 1200 | const { additionalProperty } = params; 1201 | 1202 | return `${instancePath} has an unknown property '${additionalProperty}'${getSchemaNonTypes( 1203 | parentSchema, 1204 | )}. These properties are valid:\n${this.getSchemaPartText( 1205 | parentSchema, 1206 | )}`; 1207 | } 1208 | case "dependencies": { 1209 | const { params, parentSchema } = error; 1210 | const { property, deps } = params; 1211 | const dependencies = deps 1212 | .split(",") 1213 | .map( 1214 | /** 1215 | * @param {string} dep dependency 1216 | * @returns {string} normalized dependency 1217 | */ 1218 | (dep) => `'${dep.trim()}'`, 1219 | ) 1220 | .join(", "); 1221 | 1222 | return `${instancePath} should have properties ${dependencies} when property '${property}' is present${getSchemaNonTypes( 1223 | parentSchema, 1224 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1225 | } 1226 | case "propertyNames": { 1227 | const { params, parentSchema, schema } = error; 1228 | const { propertyName } = params; 1229 | 1230 | return `${instancePath} property name '${propertyName}' is invalid${getSchemaNonTypes( 1231 | parentSchema, 1232 | )}. Property names should be match format ${JSON.stringify( 1233 | schema.format, 1234 | )}.${this.getSchemaPartDescription(parentSchema)}`; 1235 | } 1236 | case "enum": { 1237 | const { parentSchema } = error; 1238 | 1239 | if ( 1240 | parentSchema && 1241 | /** @type {Schema} */ 1242 | (parentSchema).enum && 1243 | /** @type {Schema} */ 1244 | (parentSchema).enum.length === 1 1245 | ) { 1246 | return `${instancePath} should be ${this.getSchemaPartText( 1247 | parentSchema, 1248 | false, 1249 | true, 1250 | )}`; 1251 | } 1252 | 1253 | return `${instancePath} should be one of these:\n${this.getSchemaPartText( 1254 | parentSchema, 1255 | )}`; 1256 | } 1257 | case "const": { 1258 | const { parentSchema } = error; 1259 | 1260 | return `${instancePath} should be equal to constant ${this.getSchemaPartText( 1261 | parentSchema, 1262 | false, 1263 | true, 1264 | )}`; 1265 | } 1266 | case "not": { 1267 | const postfix = likeObject(/** @type {Schema} */ (error.parentSchema)) 1268 | ? `\n${this.getSchemaPartText(error.parentSchema)}` 1269 | : ""; 1270 | const schemaOutput = this.getSchemaPartText( 1271 | error.schema, 1272 | false, 1273 | false, 1274 | false, 1275 | ); 1276 | 1277 | if (canApplyNot(error.schema)) { 1278 | return `${instancePath} should be any ${schemaOutput}${postfix}.`; 1279 | } 1280 | 1281 | const { schema, parentSchema } = error; 1282 | 1283 | return `${instancePath} should not be ${this.getSchemaPartText( 1284 | schema, 1285 | false, 1286 | true, 1287 | )}${ 1288 | parentSchema && likeObject(parentSchema) 1289 | ? `\n${this.getSchemaPartText(parentSchema)}` 1290 | : "" 1291 | }`; 1292 | } 1293 | case "oneOf": 1294 | case "anyOf": { 1295 | const { parentSchema, children } = error; 1296 | 1297 | if (children && children.length > 0) { 1298 | if (error.schema.length === 1) { 1299 | const lastChild = children[children.length - 1]; 1300 | const remainingChildren = children.slice(0, -1); 1301 | 1302 | return this.formatValidationError({ 1303 | ...lastChild, 1304 | children: remainingChildren, 1305 | parentSchema: { 1306 | ...parentSchema, 1307 | ...lastChild.parentSchema, 1308 | }, 1309 | }); 1310 | } 1311 | 1312 | let filteredChildren = filterChildren(children); 1313 | 1314 | if (filteredChildren.length === 1) { 1315 | return this.formatValidationError(filteredChildren[0]); 1316 | } 1317 | 1318 | filteredChildren = groupChildrenByFirstChild(filteredChildren); 1319 | 1320 | return `${instancePath} should be one of these:\n${this.getSchemaPartText( 1321 | parentSchema, 1322 | )}\nDetails:\n${filteredChildren 1323 | .map( 1324 | /** 1325 | * @param {SchemaUtilErrorObject} nestedError nested error 1326 | * @returns {string} formatted errors 1327 | */ 1328 | (nestedError) => 1329 | ` * ${indent(this.formatValidationError(nestedError), " ")}`, 1330 | ) 1331 | .join("\n")}`; 1332 | } 1333 | 1334 | return `${instancePath} should be one of these:\n${this.getSchemaPartText( 1335 | parentSchema, 1336 | )}`; 1337 | } 1338 | case "if": { 1339 | const { params, parentSchema } = error; 1340 | const { failingKeyword } = params; 1341 | 1342 | return `${instancePath} should match "${failingKeyword}" schema:\n${this.getSchemaPartText( 1343 | parentSchema, 1344 | [failingKeyword], 1345 | )}`; 1346 | } 1347 | case "absolutePath": { 1348 | const { message, parentSchema } = error; 1349 | 1350 | return `${instancePath}: ${message}${this.getSchemaPartDescription( 1351 | parentSchema, 1352 | )}`; 1353 | } 1354 | /* istanbul ignore next */ 1355 | default: { 1356 | const { message, parentSchema } = error; 1357 | const ErrorInJSON = JSON.stringify(error, null, 2); 1358 | 1359 | // For `custom`, `false schema`, `$ref` keywords 1360 | // Fallback for unknown keywords 1361 | return `${instancePath} ${message} (${ErrorInJSON}).\n${this.getSchemaPartText( 1362 | parentSchema, 1363 | false, 1364 | )}`; 1365 | } 1366 | } 1367 | } 1368 | 1369 | /** 1370 | * @param {Array} errors errors 1371 | * @returns {string} formatted errors 1372 | */ 1373 | formatValidationErrors(errors) { 1374 | return errors 1375 | .map((error) => { 1376 | let formattedError = this.formatValidationError(error); 1377 | 1378 | if (this.postFormatter) { 1379 | formattedError = this.postFormatter(formattedError, error); 1380 | } 1381 | 1382 | return ` - ${indent(formattedError, " ")}`; 1383 | }) 1384 | .join("\n"); 1385 | } 1386 | } 1387 | 1388 | export default ValidationError; 1389 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import webpackSchema from "webpack/schemas/WebpackOptions.json"; 2 | 3 | import { validate } from "../src"; 4 | 5 | import schema from "./fixtures/schema.json"; 6 | 7 | /* eslint-disable jest/no-standalone-expect */ 8 | 9 | describe("validation", () => { 10 | // eslint-disable-next-line jsdoc/no-restricted-syntax 11 | /** 12 | * @param {string} name name 13 | * @param {Record} config config 14 | * @param {({ name: string })=} options options 15 | * @param {Record} testSchema test schema 16 | */ 17 | function createSuccessTestCase( 18 | name, 19 | config, 20 | options = {}, 21 | testSchema = schema, 22 | ) { 23 | it(`should pass validation for ${name}`, () => { 24 | let error; 25 | 26 | try { 27 | validate(testSchema, config, options.name); 28 | } catch (err) { 29 | if (err.name !== "ValidationError") { 30 | throw err; 31 | } 32 | 33 | error = err; 34 | } 35 | 36 | expect(error).toBeUndefined(); 37 | }); 38 | } 39 | 40 | // eslint-disable-next-line jsdoc/no-restricted-syntax 41 | /** 42 | * @param {string} name name 43 | * @param {Record} config config 44 | * @param {(message: string) => void} fn fn 45 | * @param {{ baseDataPath?: string }} configuration configuration 46 | */ 47 | function createFailedTestCase(name, config, fn, configuration = {}) { 48 | it(`should fail validation for ${name}`, () => { 49 | try { 50 | validate(schema, config, configuration); 51 | } catch (error) { 52 | if (error.name !== "ValidationError") { 53 | throw error; 54 | } 55 | 56 | expect(error.message).toMatch( 57 | new RegExp( 58 | `^Invalid ${configuration.baseDataPath || "configuration"} object`, 59 | ), 60 | ); 61 | 62 | fn(error.message); 63 | 64 | return; 65 | } 66 | 67 | throw new Error("Validation didn't fail"); 68 | }); 69 | } 70 | 71 | createSuccessTestCase("empty array", { 72 | arrayType: [], 73 | }); 74 | 75 | createSuccessTestCase("non empty array", { 76 | arrayType2: ["1", 2, true], 77 | }); 78 | 79 | createSuccessTestCase("only additional items", { 80 | onlyAdditionalItems: ["1", 2, true], 81 | }); 82 | 83 | createSuccessTestCase("boolean type", { 84 | booleanType: true, 85 | }); 86 | 87 | createSuccessTestCase("number keywords without number type", { 88 | numberWithoutType: true, 89 | }); 90 | 91 | createSuccessTestCase("number keywords without number type #2", { 92 | numberWithoutType: 25, 93 | }); 94 | 95 | createSuccessTestCase("number keywords without number type #3", { 96 | numberWithoutType2: true, 97 | }); 98 | 99 | createSuccessTestCase("number keywords without number type #4", { 100 | numberWithoutType2: 20, 101 | }); 102 | 103 | createSuccessTestCase("string keywords without string type", { 104 | stringWithoutType: true, 105 | }); 106 | 107 | createSuccessTestCase("string keywords without string type", { 108 | stringWithoutType: 127, 109 | }); 110 | 111 | createSuccessTestCase("string keywords without string type #2", { 112 | stringWithoutType: "127.0.0.1", 113 | }); 114 | 115 | createSuccessTestCase("array keywords without array type", { 116 | arrayWithoutType: true, 117 | }); 118 | 119 | createSuccessTestCase("array keywords without array type #2", { 120 | arrayWithoutType: [1, "test", true, false], 121 | }); 122 | 123 | createSuccessTestCase("array keywords without array type #3", { 124 | arrayWithoutType: "string", 125 | }); 126 | 127 | createSuccessTestCase("object with required and properties", { 128 | justAnObject: { foo: "test", bar: 4, qwerty: "qwerty" }, 129 | }); 130 | 131 | createSuccessTestCase("object with patternRequired", { 132 | objectTest4: { fao: true, fbo: true, c: true }, 133 | }); 134 | 135 | createSuccessTestCase("array with items with true", { 136 | itemsTrue: [1, 2, 3, "test"], 137 | }); 138 | 139 | createSuccessTestCase("empty const", { 140 | emptyConst: "", 141 | }); 142 | 143 | createSuccessTestCase("ref inside object inside allOf", { 144 | refAndAnyOf: { foo: [/test/] }, 145 | }); 146 | 147 | createSuccessTestCase("additionalProperties inside oneOf", { 148 | additionalPropertiesInsideOneOf: { foo: 100 }, 149 | }); 150 | 151 | createSuccessTestCase("additionalProperties inside oneOf #2", { 152 | additionalPropertiesInsideOneOf2: { foo: 100, bar: "baz" }, 153 | }); 154 | 155 | createSuccessTestCase("single item in contains", { 156 | singleContainsItems: [1, "test", true], 157 | }); 158 | 159 | createSuccessTestCase("array with contains", { 160 | arrayKeyword17: [/test/, 1, "test", true], 161 | }); 162 | 163 | createSuccessTestCase("const", { 164 | constKeyword: "foo", 165 | }); 166 | 167 | createSuccessTestCase("const #2", { 168 | constKeyword2: ["foo", "bar"], 169 | }); 170 | 171 | createSuccessTestCase("const with array notation", { 172 | constWithArrayNotation: [1, 2, 3], 173 | }); 174 | 175 | createSuccessTestCase("const with object notation", { 176 | constWithObjectNotation: { foo: "bar", baz: 123 }, 177 | }); 178 | 179 | createSuccessTestCase("items and additionalItems", { 180 | additionalItemsWithoutType: [1, 2, 3, 4, 5], 181 | }); 182 | 183 | createSuccessTestCase("items and additionalItems #2", { 184 | additionalItemsWithoutType2: [ 185 | 1, 186 | true, 187 | /test/, 188 | "string", 189 | [1, 2, 3], 190 | { foo: "bar" }, 191 | null, 192 | ], 193 | }); 194 | 195 | createSuccessTestCase("items and additionalItems #3", { 196 | additionalItemsWithoutType3: ["string", "other-string", 1, true, 2, false], 197 | }); 198 | 199 | createSuccessTestCase("contains and additionalItems", { 200 | containsAndAdditionalItems: [/test/, true, "string"], 201 | }); 202 | 203 | createSuccessTestCase("contains and additionalItems", { 204 | containsAndAdditionalItems: [/test/, 1, "string"], 205 | }); 206 | 207 | createSuccessTestCase("contains inside items", { 208 | containsInsideItem: [1, "test", true, /test/], 209 | }); 210 | 211 | createSuccessTestCase("contains inside items #2", { 212 | containsInsideItem: ["test", "test", "test"], 213 | }); 214 | 215 | createSuccessTestCase("contains inside items #3", { 216 | containsInsideItem: [["test", 1], "1", /test/], 217 | }); 218 | 219 | createSuccessTestCase("contains inside items #3", { 220 | containsInsideItem: [[1, "test"], "1", /test/], 221 | }); 222 | 223 | createSuccessTestCase("object without properties", { 224 | emptyObject: {}, 225 | }); 226 | 227 | createSuccessTestCase("non empty object", { 228 | nonEmptyObject: { bar: 123 }, 229 | }); 230 | 231 | createSuccessTestCase("non empty object #2", { 232 | nonEmptyObject: {}, 233 | }); 234 | 235 | createSuccessTestCase("non empty object #3", { 236 | nonEmptyObject2: { foo: "test" }, 237 | }); 238 | 239 | createSuccessTestCase("oneOf", { 240 | optimization: { 241 | runtimeChunk: { 242 | name: "fef", 243 | }, 244 | }, 245 | }); 246 | 247 | createSuccessTestCase("not array", { 248 | notArray: 1, 249 | }); 250 | 251 | createSuccessTestCase("no type like number with minimum", { 252 | noTypeLikeNumberMinimum: 6, 253 | }); 254 | 255 | createSuccessTestCase("no type like number with minimum", { 256 | noTypeLikeNumberMinimum: true, 257 | }); 258 | 259 | createSuccessTestCase("no type like number with maximum", { 260 | noTypeLikeNumberMaximum: 4, 261 | }); 262 | 263 | createSuccessTestCase("no type like number with maximum", { 264 | noTypeLikeNumberMaximum: true, 265 | }); 266 | 267 | createSuccessTestCase("no type like number with minimum", { 268 | noTypeLikeNumberExclusiveMinimum: 6, 269 | }); 270 | 271 | createSuccessTestCase("no type like number with minimum", { 272 | noTypeLikeNumberExclusiveMinimum: true, 273 | }); 274 | 275 | createSuccessTestCase("no type like number with maximum", { 276 | noTypeLikeNumberExclusiveMaximum: 4, 277 | }); 278 | 279 | createSuccessTestCase("no type like number with maximum", { 280 | noTypeLikeNumberExclusiveMaximum: true, 281 | }); 282 | 283 | createSuccessTestCase("no type like number with multipleOf", { 284 | noTypeLikeNumberMultipleOf: true, 285 | }); 286 | 287 | createSuccessTestCase("no type like string with pattern", { 288 | noTypeLikeStringPattern: 1, 289 | }); 290 | 291 | createSuccessTestCase("no type like string with pattern #2", { 292 | noTypeLikeStringPattern: "a", 293 | }); 294 | 295 | createSuccessTestCase("no type like string with MinLength equals 1", { 296 | noTypeLikeStringMinLength1: 1, 297 | }); 298 | 299 | createSuccessTestCase("no type like array with additionalItems", { 300 | noTypeLikeArrayAdditionalItems: true, 301 | }); 302 | 303 | createSuccessTestCase("absolutePath", { 304 | testAbsolutePath: "/directory/deep/tree", 305 | }); 306 | 307 | createSuccessTestCase("absolutePath #1", { 308 | testAbsolutePath: "c:\\directory\\deep\\tree", 309 | }); 310 | 311 | createSuccessTestCase("absolutePath #2", { 312 | testAbsolutePath: "C:\\directory\\deep\\tree", 313 | }); 314 | 315 | createSuccessTestCase("absolutePath #3", { 316 | testAbsolutePath: "C:/directory/deep/tree", 317 | }); 318 | 319 | createSuccessTestCase("absolutePath #4", { 320 | testAbsolutePath: "\\\\server\\directory\\deep\\tree", 321 | }); 322 | 323 | createSuccessTestCase("absolutePath #5", { 324 | testAbsolutePath: "//server/directory/deep/tree", 325 | }); 326 | 327 | createSuccessTestCase("$data", { 328 | dollarData: { 329 | smaller: 5, 330 | larger: 7, 331 | }, 332 | }); 333 | 334 | createSuccessTestCase("enum with undefinedAsNull", { 335 | enumKeywordAndUndefined: undefined, 336 | }); 337 | 338 | createSuccessTestCase("enum with undefinedAsNull #2", { 339 | enumKeywordAndUndefined: 0, 340 | }); 341 | 342 | createSuccessTestCase("array with enum and undefinedAsNull", { 343 | arrayStringAndEnum: ["a", "b", "c"], 344 | }); 345 | 346 | createSuccessTestCase("array with enum and undefinedAsNull #2", { 347 | arrayStringAndEnum: [undefined, false, undefined, 0, "test", undefined], 348 | }); 349 | 350 | createSuccessTestCase("array with enum and undefinedAsNull #3", { 351 | arrayStringAndEnum: [undefined, null, false, 0, ""], 352 | }); 353 | 354 | createSuccessTestCase("string and undefinedAsNull #3", { 355 | stringTypeAndUndefinedAsNull: "test", 356 | }); 357 | 358 | // The "name" option 359 | createFailedTestCase( 360 | "webpack name", 361 | { 362 | entry: "", 363 | }, 364 | (msg) => expect(msg).toMatchSnapshot(), 365 | { 366 | name: "Webpack", 367 | }, 368 | ); 369 | 370 | createFailedTestCase( 371 | "css-loader name", 372 | { 373 | entry: "", 374 | }, 375 | (msg) => expect(msg).toMatchSnapshot(), 376 | { 377 | name: "CSS Loader", 378 | }, 379 | ); 380 | 381 | createFailedTestCase( 382 | "terser-webpack-plugin name", 383 | { 384 | entry: "", 385 | }, 386 | (msg) => expect(msg).toMatchSnapshot(), 387 | { 388 | name: "Terser Plugin", 389 | }, 390 | ); 391 | 392 | // The "dataPath" option 393 | createFailedTestCase( 394 | "configuration dataPath", 395 | { 396 | entry: "", 397 | }, 398 | (msg) => expect(msg).toMatchSnapshot(), 399 | { 400 | name: "Webpack", 401 | baseDataPath: "configuration", 402 | }, 403 | ); 404 | 405 | createFailedTestCase( 406 | "configuration dataPath #1", 407 | { 408 | entry: "", 409 | }, 410 | (msg) => expect(msg).toMatchSnapshot(), 411 | { 412 | name: "MyPlugin", 413 | baseDataPath: "options", 414 | }, 415 | ); 416 | 417 | // The "postFormatter" option 418 | createFailedTestCase( 419 | "postFormatter", 420 | { 421 | debug: true, 422 | }, 423 | (msg) => expect(msg).toMatchSnapshot(), 424 | { 425 | name: "Webpack", 426 | baseDataPath: "configuration", 427 | postFormatter: (formattedError, error) => { 428 | if ( 429 | error.keyword === "additionalProperties" && 430 | !error.dataPath && 431 | error.params.additionalProperty === "debug" 432 | ) { 433 | return ( 434 | `${formattedError}\n` + 435 | "The 'debug' property was removed in webpack 2.0.0.\n" + 436 | "Loaders should be updated to allow passing this option via loader options in module.rules.\n" + 437 | "Until loaders are updated one can use the LoaderOptionsPlugin to switch loaders into debug mode:\n" + 438 | "plugins: [\n" + 439 | " new webpack.LoaderOptionsPlugin({\n" + 440 | " debug: true\n" + 441 | " })\n" + 442 | "]" 443 | ); 444 | } 445 | 446 | return formattedError; 447 | }, 448 | }, 449 | ); 450 | 451 | createFailedTestCase( 452 | "postFormatter #1", 453 | { 454 | minify: true, 455 | }, 456 | (msg) => expect(msg).toMatchSnapshot(), 457 | { 458 | name: "Webpack", 459 | baseDataPath: "configuration", 460 | postFormatter: (formattedError, error) => { 461 | if ( 462 | error.keyword === "additionalProperties" && 463 | !error.dataPath && 464 | error.params.additionalProperty 465 | ) { 466 | return ( 467 | `${formattedError}\n` + 468 | "For typos: please correct them.\n" + 469 | "For loader options: webpack >= v2.0.0 no longer allows custom properties in configuration.\n" + 470 | " Loaders should be updated to allow passing options via loader options in module.rules.\n" + 471 | " Until loaders are updated one can use the LoaderOptionsPlugin to pass these options to the loader:\n" + 472 | " plugins: [\n" + 473 | " new webpack.LoaderOptionsPlugin({\n" + 474 | " // test: /\\.xxx$/, // may apply this only for some modules\n" + 475 | " options: {\n" + 476 | ` ${error.params.additionalProperty}: …\n` + 477 | " }\n" + 478 | " })\n" + 479 | " ]" 480 | ); 481 | } 482 | 483 | return formattedError; 484 | }, 485 | }, 486 | ); 487 | 488 | createFailedTestCase( 489 | "postFormatter #2", 490 | { 491 | entry: "foo.js", 492 | output: { 493 | filename: "/bar", 494 | }, 495 | }, 496 | (msg) => expect(msg).toMatchSnapshot(), 497 | { 498 | name: "Webpack", 499 | baseDataPath: "configuration", 500 | postFormatter: (formattedError, error) => { 501 | if ( 502 | error.children && 503 | error.children.some( 504 | (child) => 505 | child.keyword === "absolutePath" && 506 | child.instancePath === "/output/filename", 507 | ) 508 | ) { 509 | return ( 510 | `${formattedError}\n` + 511 | "Please use output.path to specify absolute path and output.filename for the file name." 512 | ); 513 | } 514 | 515 | return formattedError; 516 | }, 517 | }, 518 | ); 519 | 520 | createFailedTestCase("undefined configuration", undefined, (msg) => 521 | expect(msg).toMatchSnapshot(), 522 | ); 523 | 524 | createFailedTestCase("null configuration", null, (msg) => 525 | expect(msg).toMatchSnapshot(), 526 | ); 527 | 528 | createFailedTestCase( 529 | "empty entry string", 530 | { 531 | entry: "", 532 | }, 533 | (msg) => expect(msg).toMatchSnapshot(), 534 | ); 535 | 536 | createFailedTestCase( 537 | "empty entry bundle array", 538 | { 539 | entry: { 540 | bundle: [], 541 | }, 542 | }, 543 | (msg) => expect(msg).toMatchSnapshot(), 544 | ); 545 | 546 | createFailedTestCase( 547 | "invalid instanceof", 548 | { 549 | entry: "a", 550 | module: { 551 | wrappedContextRegExp: 1337, 552 | }, 553 | }, 554 | (msg) => expect(msg).toMatchSnapshot(), 555 | ); 556 | 557 | createFailedTestCase( 558 | "invalid minimum", 559 | { 560 | entry: "a", 561 | parallelism: 0, 562 | }, 563 | (msg) => expect(msg).toMatchSnapshot(), 564 | ); 565 | 566 | createFailedTestCase( 567 | "repeated value", 568 | { 569 | entry: ["abc", "def", "abc"], 570 | }, 571 | (msg) => expect(msg).toMatchSnapshot(), 572 | ); 573 | 574 | createFailedTestCase( 575 | "multiple errors", 576 | { 577 | entry: [/a/], 578 | output: { 579 | filename: /a/, 580 | }, 581 | }, 582 | (msg) => expect(msg).toMatchSnapshot(), 583 | ); 584 | 585 | createFailedTestCase( 586 | "multiple configurations", 587 | [ 588 | { 589 | entry: [/a/], 590 | }, 591 | { 592 | entry: "a", 593 | output: { 594 | filename: /a/, 595 | }, 596 | }, 597 | ], 598 | (msg) => expect(msg).toMatchSnapshot(), 599 | ); 600 | 601 | createFailedTestCase( 602 | "deep error", 603 | { 604 | entry: "a", 605 | module: { 606 | rules: [ 607 | { 608 | oneOf: [ 609 | { 610 | test: "/a", 611 | passer: { 612 | amd: false, 613 | }, 614 | }, 615 | ], 616 | }, 617 | ], 618 | }, 619 | }, 620 | (msg) => expect(msg).toMatchSnapshot(), 621 | ); 622 | 623 | createFailedTestCase( 624 | "additional key on root", 625 | { 626 | entry: "a", 627 | postcss: () => {}, 628 | }, 629 | (msg) => expect(msg).toMatchSnapshot(), 630 | ); 631 | 632 | createFailedTestCase( 633 | "enum", 634 | { 635 | entry: "a", 636 | devtool: true, 637 | }, 638 | (msg) => expect(msg).toMatchSnapshot(), 639 | ); 640 | 641 | // Require for integration with webpack 642 | createFailedTestCase( 643 | "! in path", 644 | { 645 | entry: "foo.js", 646 | output: { 647 | path: "/somepath/!test", 648 | filename: "bar", 649 | }, 650 | }, 651 | (msg) => expect(msg).toMatchSnapshot(), 652 | ); 653 | 654 | createFailedTestCase( 655 | "relative path", 656 | { 657 | entry: "foo.js", 658 | output: { 659 | filename: "/bar", 660 | }, 661 | }, 662 | (msg) => expect(msg).toMatchSnapshot(), 663 | ); 664 | 665 | createFailedTestCase( 666 | "absolute path", 667 | { 668 | entry: "foo.js", 669 | output: { 670 | filename: "bar", 671 | }, 672 | context: "baz", 673 | }, 674 | (msg) => expect(msg).toMatchSnapshot(), 675 | ); 676 | 677 | createFailedTestCase( 678 | "missing stats option", 679 | { 680 | entry: "foo.js", 681 | stats: { 682 | foobar: true, 683 | }, 684 | }, 685 | (msg) => { 686 | expect( 687 | msg 688 | .replace(/object \{ .* \}/g, "object {...}") 689 | .replace(/"none" \| .+/g, '"none" | ...'), 690 | ).toMatchSnapshot(); 691 | }, 692 | ); 693 | 694 | createFailedTestCase( 695 | "invalid plugin provided: bool", 696 | { 697 | entry: "foo.js", 698 | plugins: [false], 699 | }, 700 | (msg) => expect(msg).toMatchSnapshot(), 701 | ); 702 | 703 | createFailedTestCase( 704 | "invalid plugin provided: array", 705 | { 706 | entry: "foo.js", 707 | plugins: [[]], 708 | }, 709 | (msg) => expect(msg).toMatchSnapshot(), 710 | ); 711 | 712 | createFailedTestCase( 713 | "invalid plugin provided: string", 714 | { 715 | entry: "foo.js", 716 | plugins: ["abc123"], 717 | }, 718 | (msg) => expect(msg).toMatchSnapshot(), 719 | ); 720 | 721 | createFailedTestCase( 722 | "Invalid plugin provided: int", 723 | { 724 | entry: "foo.js", 725 | plugins: [12], 726 | }, 727 | (msg) => expect(msg).toMatchSnapshot(), 728 | ); 729 | 730 | createFailedTestCase( 731 | "Invalid plugin provided: object without apply function", 732 | { 733 | entry: "foo.js", 734 | plugins: [{}], 735 | }, 736 | (msg) => expect(msg).toMatchSnapshot(), 737 | ); 738 | 739 | createFailedTestCase( 740 | "invalid mode", 741 | { 742 | mode: "protuction", 743 | }, 744 | (msg) => expect(msg).toMatchSnapshot(), 745 | ); 746 | 747 | createFailedTestCase( 748 | "min length", 749 | { 750 | minLengthTwo: "1", 751 | }, 752 | (msg) => expect(msg).toMatchSnapshot(), 753 | ); 754 | 755 | createFailedTestCase( 756 | "min properties", 757 | { 758 | entry: {}, 759 | }, 760 | (msg) => expect(msg).toMatchSnapshot(), 761 | ); 762 | 763 | createFailedTestCase( 764 | "integer type", 765 | { 766 | integerType: "type", 767 | }, 768 | (msg) => expect(msg).toMatchSnapshot(), 769 | ); 770 | 771 | createFailedTestCase( 772 | "null type", 773 | { 774 | nullType: "type", 775 | }, 776 | (msg) => expect(msg).toMatchSnapshot(), 777 | ); 778 | 779 | createFailedTestCase( 780 | "boolean type", 781 | { 782 | bail: "true", 783 | }, 784 | (msg) => expect(msg).toMatchSnapshot(), 785 | ); 786 | 787 | createFailedTestCase( 788 | "object type", 789 | { 790 | devServer: [], 791 | }, 792 | (msg) => expect(msg).toMatchSnapshot(), 793 | ); 794 | 795 | createFailedTestCase( 796 | "array type", 797 | { 798 | dependencies: {}, 799 | }, 800 | (msg) => expect(msg).toMatchSnapshot(), 801 | ); 802 | 803 | createFailedTestCase( 804 | "number type", 805 | { 806 | parallelism: "1", 807 | }, 808 | (msg) => expect(msg).toMatchSnapshot(), 809 | ); 810 | 811 | createFailedTestCase( 812 | "object in object with anyOf", 813 | { 814 | allOfRef: { alias: 123 }, 815 | }, 816 | (msg) => expect(msg).toMatchSnapshot(), 817 | ); 818 | 819 | createFailedTestCase( 820 | "non empty string in object with anyOf", 821 | { 822 | allOfRef: { aliasFields: 123 }, 823 | }, 824 | (msg) => expect(msg).toMatchSnapshot(), 825 | ); 826 | 827 | createFailedTestCase( 828 | "boolean and object in object with anyOf", 829 | { 830 | allOfRef: { unsafeCache: [] }, 831 | }, 832 | (msg) => expect(msg).toMatchSnapshot(), 833 | ); 834 | 835 | createFailedTestCase( 836 | "boolean and number in object with anyOf", 837 | { 838 | watchOptions: { poll: "1" }, 839 | }, 840 | (msg) => expect(msg).toMatchSnapshot(), 841 | ); 842 | 843 | createFailedTestCase( 844 | "integer and null in object with anyOf", 845 | { 846 | customObject: { anyOfKeyword: "1" }, 847 | }, 848 | (msg) => expect(msg).toMatchSnapshot(), 849 | ); 850 | 851 | createFailedTestCase( 852 | "maxLength", 853 | { 854 | customObject: { maxLength: "11111" }, 855 | }, 856 | (msg) => expect(msg).toMatchSnapshot(), 857 | ); 858 | 859 | createFailedTestCase( 860 | "maxItems", 861 | { 862 | customObject: { maxItems: ["1", "2", "3", "4"] }, 863 | }, 864 | (msg) => expect(msg).toMatchSnapshot(), 865 | ); 866 | 867 | createFailedTestCase( 868 | "maxProperties", 869 | { 870 | customObject: { 871 | maxProperties: { one: "one", two: "two", three: "three", four: "four" }, 872 | }, 873 | }, 874 | (msg) => expect(msg).toMatchSnapshot(), 875 | ); 876 | 877 | createFailedTestCase( 878 | "minimum", 879 | { 880 | customObject: { minimumKeyword: 1 }, 881 | }, 882 | (msg) => expect(msg).toMatchSnapshot(), 883 | ); 884 | 885 | createFailedTestCase( 886 | "maximum", 887 | { 888 | customObject: { maximumKeyword: 11111 }, 889 | }, 890 | (msg) => expect(msg).toMatchSnapshot(), 891 | ); 892 | 893 | createFailedTestCase( 894 | "multipleOf", 895 | { 896 | customObject: { multipleOfKeyword: 11111 }, 897 | }, 898 | (msg) => expect(msg).toMatchSnapshot(), 899 | ); 900 | 901 | createFailedTestCase( 902 | "pattern", 903 | { 904 | customObject: { patternKeyword: "def" }, 905 | }, 906 | (msg) => expect(msg).toMatchSnapshot(), 907 | ); 908 | 909 | createFailedTestCase( 910 | "format", 911 | { 912 | customObject: { formatKeyword: "def" }, 913 | }, 914 | (msg) => expect(msg).toMatchSnapshot(), 915 | ); 916 | 917 | createFailedTestCase( 918 | "contains", 919 | { 920 | customObject: { containsKeyword: ["def"] }, 921 | }, 922 | (msg) => expect(msg).toMatchSnapshot(), 923 | ); 924 | 925 | createFailedTestCase( 926 | "contains #1", 927 | { 928 | multipleContains2: [/test/], 929 | }, 930 | (msg) => expect(msg).toMatchSnapshot(), 931 | ); 932 | 933 | createFailedTestCase( 934 | "oneOf #1", 935 | { 936 | entry: { foo: () => [] }, 937 | }, 938 | (msg) => expect(msg).toMatchSnapshot(), 939 | ); 940 | 941 | createFailedTestCase( 942 | "oneOf #2", 943 | { 944 | optimization: { 945 | runtimeChunk: { 946 | name: /fef/, 947 | }, 948 | }, 949 | }, 950 | (msg) => expect(msg).toMatchSnapshot(), 951 | ); 952 | 953 | createFailedTestCase( 954 | "oneOf #3", 955 | { 956 | optimization: { 957 | runtimeChunk: (name) => name, 958 | }, 959 | }, 960 | (msg) => expect(msg).toMatchSnapshot(), 961 | ); 962 | 963 | createFailedTestCase( 964 | "allOf", 965 | { 966 | objectType: { objectProperty: 1 }, 967 | }, 968 | (msg) => expect(msg).toMatchSnapshot(), 969 | ); 970 | 971 | createFailedTestCase( 972 | "anyOf", 973 | { 974 | anyOfKeyword: true, 975 | }, 976 | (msg) => expect(msg).toMatchSnapshot(), 977 | ); 978 | 979 | createFailedTestCase( 980 | "array without items", 981 | { 982 | nestedArrayWithoutItems: true, 983 | }, 984 | (msg) => expect(msg).toMatchSnapshot(), 985 | ); 986 | 987 | createFailedTestCase( 988 | "object without items", 989 | { 990 | nestedObjectWithoutItems: true, 991 | }, 992 | (msg) => expect(msg).toMatchSnapshot(), 993 | ); 994 | 995 | createFailedTestCase( 996 | "multiple types in array", 997 | { 998 | arrayType2: ["1", 2, true, /test/], 999 | }, 1000 | (msg) => expect(msg).toMatchSnapshot(), 1001 | ); 1002 | 1003 | createFailedTestCase( 1004 | "multiple types", 1005 | { 1006 | multipleTypes: /test/, 1007 | }, 1008 | (msg) => expect(msg).toMatchSnapshot(), 1009 | ); 1010 | 1011 | createFailedTestCase( 1012 | "zero max items", 1013 | { 1014 | zeroMaxItems: [1], 1015 | }, 1016 | (msg) => expect(msg).toMatchSnapshot(), 1017 | ); 1018 | 1019 | createFailedTestCase( 1020 | "multiple types in contains", 1021 | { 1022 | multipleContains: [/test/], 1023 | }, 1024 | (msg) => expect(msg).toMatchSnapshot(), 1025 | ); 1026 | 1027 | createFailedTestCase( 1028 | "exclusiveMinimum", 1029 | { 1030 | exclusiveMinimumKeyword: 4.5, 1031 | }, 1032 | (msg) => expect(msg).toMatchSnapshot(), 1033 | ); 1034 | 1035 | createFailedTestCase( 1036 | "exclusiveMaximum", 1037 | { 1038 | exclusiveMaximumKeyword: 5.5, 1039 | }, 1040 | (msg) => expect(msg).toMatchSnapshot(), 1041 | ); 1042 | 1043 | createFailedTestCase( 1044 | "uniqueItems", 1045 | { 1046 | uniqueItemsKeyword: [1, 2, 1], 1047 | }, 1048 | (msg) => expect(msg).toMatchSnapshot(), 1049 | ); 1050 | 1051 | createFailedTestCase( 1052 | "minProperties", 1053 | { 1054 | minPropertiesKeyword: { foo: "bar" }, 1055 | }, 1056 | (msg) => expect(msg).toMatchSnapshot(), 1057 | ); 1058 | 1059 | createFailedTestCase( 1060 | "maxProperties", 1061 | { 1062 | maxPropertiesKeyword: { foo: "bar", bar: "foo", foobaz: "foobaz" }, 1063 | }, 1064 | (msg) => expect(msg).toMatchSnapshot(), 1065 | ); 1066 | 1067 | createFailedTestCase( 1068 | "required", 1069 | { 1070 | requiredKeyword: {}, 1071 | }, 1072 | (msg) => expect(msg).toMatchSnapshot(), 1073 | ); 1074 | 1075 | createFailedTestCase( 1076 | "required #2", 1077 | { 1078 | requiredKeyword: { b: 1 }, 1079 | }, 1080 | (msg) => expect(msg).toMatchSnapshot(), 1081 | ); 1082 | 1083 | createFailedTestCase( 1084 | "required with additionalProperties", 1085 | { 1086 | requiredKeywordWithAdditionalProperties: { b: 1 }, 1087 | }, 1088 | (msg) => expect(msg).toMatchSnapshot(), 1089 | ); 1090 | 1091 | createFailedTestCase( 1092 | "enum", 1093 | { 1094 | enumKeyword: 1, 1095 | }, 1096 | (msg) => expect(msg).toMatchSnapshot(), 1097 | ); 1098 | 1099 | createFailedTestCase( 1100 | "formatMinimum", 1101 | { 1102 | formatMinimumKeyword: "2016-02-05", 1103 | }, 1104 | (msg) => expect(msg).toMatchSnapshot(), 1105 | ); 1106 | 1107 | createFailedTestCase( 1108 | "formatMaximum", 1109 | { 1110 | formatMaximumKeyword: "2016-02-07", 1111 | }, 1112 | (msg) => expect(msg).toMatchSnapshot(), 1113 | ); 1114 | 1115 | createFailedTestCase( 1116 | "formatExclusiveMinimum", 1117 | { 1118 | formatExclusiveMinimumKeyword: "2016-02-06", 1119 | }, 1120 | (msg) => expect(msg).toMatchSnapshot(), 1121 | ); 1122 | 1123 | createFailedTestCase( 1124 | "formatExclusiveMinimumKeyword #2", 1125 | { 1126 | formatExclusiveMinimumKeyword: "2016-02-05", 1127 | }, 1128 | (msg) => expect(msg).toMatchSnapshot(), 1129 | ); 1130 | 1131 | createFailedTestCase( 1132 | "formatExclusiveMinimumKeyword #3", 1133 | { 1134 | formatExclusiveMinimumKeyword: "2016-02-06", 1135 | }, 1136 | (msg) => expect(msg).toMatchSnapshot(), 1137 | ); 1138 | 1139 | createFailedTestCase( 1140 | "formatExclusiveMaximum", 1141 | { 1142 | formatExclusiveMaximumKeyword: "2016-02-06", 1143 | }, 1144 | (msg) => expect(msg).toMatchSnapshot(), 1145 | ); 1146 | 1147 | createFailedTestCase( 1148 | "formatExclusiveMaximum #2", 1149 | { 1150 | formatExclusiveMaximumKeyword: "2016-02-06", 1151 | }, 1152 | (msg) => expect(msg).toMatchSnapshot(), 1153 | ); 1154 | 1155 | createFailedTestCase( 1156 | "formatExclusiveMaximum #3", 1157 | { 1158 | formatExclusiveMaximumKeyword: "2016-02-07", 1159 | }, 1160 | (msg) => expect(msg).toMatchSnapshot(), 1161 | ); 1162 | 1163 | createFailedTestCase( 1164 | "format and formatMinimum and formatMaximum and formatExclusiveMaximum", 1165 | { 1166 | formatMinMaxExclusiveMinKeyword: "2016-02-05", 1167 | }, 1168 | (msg) => expect(msg).toMatchSnapshot(), 1169 | ); 1170 | 1171 | createFailedTestCase( 1172 | "format and formatMinimum and formatMaximum and formatExclusiveMaximum", 1173 | { 1174 | formatMinMaxExclusiveMaxKeyword: "2016-02-05", 1175 | }, 1176 | (msg) => expect(msg).toMatchSnapshot(), 1177 | ); 1178 | 1179 | createFailedTestCase( 1180 | "minItems Keyword", 1181 | { 1182 | minItemsKeyword: ["1"], 1183 | }, 1184 | (msg) => expect(msg).toMatchSnapshot(), 1185 | ); 1186 | 1187 | createFailedTestCase( 1188 | "maxItems Keyword", 1189 | { 1190 | maxItemsKeyword: ["1", "2", "3", "4"], 1191 | }, 1192 | (msg) => expect(msg).toMatchSnapshot(), 1193 | ); 1194 | 1195 | createFailedTestCase( 1196 | "items", 1197 | { 1198 | itemsKeyword: ["1"], 1199 | }, 1200 | (msg) => expect(msg).toMatchSnapshot(), 1201 | ); 1202 | 1203 | createFailedTestCase( 1204 | "items", 1205 | { 1206 | itemsKeyword2: [true], 1207 | }, 1208 | (msg) => expect(msg).toMatchSnapshot(), 1209 | ); 1210 | 1211 | createFailedTestCase( 1212 | "additionalItems", 1213 | { 1214 | additionalItemsKeyword: [1, "abc"], 1215 | }, 1216 | (msg) => expect(msg).toMatchSnapshot(), 1217 | ); 1218 | 1219 | createFailedTestCase( 1220 | "additionalItems #2", 1221 | { 1222 | additionalItemsKeyword2: ["abc"], 1223 | }, 1224 | (msg) => expect(msg).toMatchSnapshot(), 1225 | ); 1226 | 1227 | createFailedTestCase( 1228 | "additionalItems #3", 1229 | { 1230 | additionalItemsKeyword3: ["abc"], 1231 | }, 1232 | (msg) => expect(msg).toMatchSnapshot(), 1233 | ); 1234 | 1235 | createFailedTestCase( 1236 | "additionalItems #4", 1237 | { 1238 | additionalItemsKeyword4: [1, 1, "foo"], 1239 | }, 1240 | (msg) => expect(msg).toMatchSnapshot(), 1241 | ); 1242 | 1243 | createFailedTestCase( 1244 | "properties", 1245 | { 1246 | propertiesKeyword: { foo: 1 }, 1247 | }, 1248 | (msg) => expect(msg).toMatchSnapshot(), 1249 | ); 1250 | 1251 | createFailedTestCase( 1252 | "patternProperties", 1253 | { 1254 | patternPropertiesKeyword: { foo: 1 }, 1255 | }, 1256 | (msg) => expect(msg).toMatchSnapshot(), 1257 | ); 1258 | 1259 | createFailedTestCase( 1260 | "patternProperties #1", 1261 | { 1262 | patternPropertiesKeyword2: { b: "t" }, 1263 | }, 1264 | (msg) => expect(msg).toMatchSnapshot(), 1265 | ); 1266 | 1267 | createFailedTestCase( 1268 | "array with only number", 1269 | { 1270 | arrayWithOnlyNumber: ["foo"], 1271 | }, 1272 | (msg) => expect(msg).toMatchSnapshot(), 1273 | ); 1274 | 1275 | createFailedTestCase( 1276 | "only required", 1277 | { 1278 | onlyRequired: {}, 1279 | }, 1280 | (msg) => expect(msg).toMatchSnapshot(), 1281 | ); 1282 | 1283 | createFailedTestCase( 1284 | "dependencies", 1285 | { 1286 | dependenciesKeyword: { foo: 1, bar: 2 }, 1287 | }, 1288 | (msg) => expect(msg).toMatchSnapshot(), 1289 | ); 1290 | 1291 | createFailedTestCase( 1292 | "dependencies #2", 1293 | { 1294 | dependenciesKeyword2: { foo: 1, bar: "2" }, 1295 | }, 1296 | (msg) => expect(msg).toMatchSnapshot(), 1297 | ); 1298 | 1299 | createFailedTestCase( 1300 | "patternRequired", 1301 | { 1302 | patternRequiredKeyword: { bar: 2 }, 1303 | }, 1304 | (msg) => expect(msg).toMatchSnapshot(), 1305 | ); 1306 | 1307 | createFailedTestCase( 1308 | "patternRequired #2", 1309 | { 1310 | patternRequiredKeyword2: { foo: 1 }, 1311 | }, 1312 | (msg) => expect(msg).toMatchSnapshot(), 1313 | ); 1314 | 1315 | createFailedTestCase( 1316 | "only properties", 1317 | { 1318 | onlyProperties: { foo: 1 }, 1319 | }, 1320 | (msg) => expect(msg).toMatchSnapshot(), 1321 | ); 1322 | 1323 | createFailedTestCase( 1324 | "only properties #2", 1325 | { 1326 | onlyProperties2: { foo: "a", bar: 2, break: "test" }, 1327 | }, 1328 | (msg) => expect(msg).toMatchSnapshot(), 1329 | ); 1330 | 1331 | createFailedTestCase( 1332 | "only items", 1333 | { 1334 | onlyItems: [1, "abc"], 1335 | }, 1336 | (msg) => expect(msg).toMatchSnapshot(), 1337 | ); 1338 | 1339 | createFailedTestCase( 1340 | "only items #2", 1341 | { 1342 | onlyItems2: ["abc", 1], 1343 | }, 1344 | (msg) => expect(msg).toMatchSnapshot(), 1345 | ); 1346 | 1347 | createFailedTestCase( 1348 | "additionalProperties", 1349 | { 1350 | additionalPropertiesKeyword: { a: 3 }, 1351 | }, 1352 | (msg) => expect(msg).toMatchSnapshot(), 1353 | ); 1354 | 1355 | createFailedTestCase( 1356 | "additionalProperties #2", 1357 | { 1358 | additionalPropertiesKeyword: { foo: 1, baz: 3 }, 1359 | }, 1360 | (msg) => expect(msg).toMatchSnapshot(), 1361 | ); 1362 | 1363 | createFailedTestCase( 1364 | "additionalProperties #3", 1365 | { 1366 | additionalPropertiesKeyword: { foo: "1", baz: 3 }, 1367 | }, 1368 | (msg) => expect(msg).toMatchSnapshot(), 1369 | ); 1370 | 1371 | createFailedTestCase( 1372 | "additionalProperties #4", 1373 | { 1374 | additionalPropertiesKeyword2: { a: 3 }, 1375 | }, 1376 | (msg) => expect(msg).toMatchSnapshot(), 1377 | ); 1378 | 1379 | createFailedTestCase( 1380 | "additionalProperties #5", 1381 | { 1382 | additionalPropertiesKeyword2: { foo: 1, baz: 3 }, 1383 | }, 1384 | (msg) => expect(msg).toMatchSnapshot(), 1385 | ); 1386 | 1387 | createFailedTestCase( 1388 | "additionalProperties #6", 1389 | { 1390 | additionalPropertiesKeyword2: { foo: 1, bar: "3" }, 1391 | }, 1392 | (msg) => expect(msg).toMatchSnapshot(), 1393 | ); 1394 | 1395 | createFailedTestCase( 1396 | "propertyNames", 1397 | { 1398 | propertyNamesKeyword: { foo: "any value" }, 1399 | }, 1400 | (msg) => expect(msg).toMatchSnapshot(), 1401 | ); 1402 | 1403 | createFailedTestCase( 1404 | "constKeyword", 1405 | { 1406 | constKeyword: "bar", 1407 | }, 1408 | (msg) => expect(msg).toMatchSnapshot(), 1409 | ); 1410 | 1411 | createFailedTestCase( 1412 | "constKeyword #2", 1413 | { 1414 | constKeyword2: "baz", 1415 | }, 1416 | (msg) => expect(msg).toMatchSnapshot(), 1417 | ); 1418 | 1419 | createFailedTestCase( 1420 | "if/then/else", 1421 | { 1422 | ifThenElseKeyword: { power: 10000 }, 1423 | }, 1424 | (msg) => expect(msg).toMatchSnapshot(), 1425 | ); 1426 | 1427 | createFailedTestCase( 1428 | "if/then/else #2", 1429 | { 1430 | ifThenElseKeyword: { power: 10000, confidence: true }, 1431 | }, 1432 | (msg) => expect(msg).toMatchSnapshot(), 1433 | ); 1434 | 1435 | createFailedTestCase( 1436 | "if/then/else #3", 1437 | { 1438 | ifThenElseKeyword: { power: 1000 }, 1439 | }, 1440 | (msg) => expect(msg).toMatchSnapshot(), 1441 | ); 1442 | 1443 | createFailedTestCase( 1444 | "if/then/else #4", 1445 | { 1446 | ifThenElseKeyword2: 11, 1447 | }, 1448 | (msg) => expect(msg).toMatchSnapshot(), 1449 | ); 1450 | 1451 | createFailedTestCase( 1452 | "if/then/else #5", 1453 | { 1454 | ifThenElseKeyword2: 2000, 1455 | }, 1456 | (msg) => expect(msg).toMatchSnapshot(), 1457 | ); 1458 | 1459 | createFailedTestCase( 1460 | "if/then/else #6", 1461 | { 1462 | ifThenElseKeyword2: 0, 1463 | }, 1464 | (msg) => expect(msg).toMatchSnapshot(), 1465 | ); 1466 | 1467 | createFailedTestCase( 1468 | "string", 1469 | { 1470 | stringKeyword: "2016-02-06", 1471 | }, 1472 | (msg) => expect(msg).toMatchSnapshot(), 1473 | ); 1474 | 1475 | createFailedTestCase( 1476 | "string #1", 1477 | { 1478 | stringKeyword: "abc", 1479 | }, 1480 | (msg) => expect(msg).toMatchSnapshot(), 1481 | ); 1482 | 1483 | createFailedTestCase( 1484 | "string with link", 1485 | { 1486 | stringKeywordWithLink: "abc", 1487 | }, 1488 | (msg) => expect(msg).toMatchSnapshot(), 1489 | ); 1490 | 1491 | createFailedTestCase( 1492 | "array", 1493 | { 1494 | arrayKeyword: "abc", 1495 | }, 1496 | (msg) => expect(msg).toMatchSnapshot(), 1497 | ); 1498 | 1499 | createFailedTestCase( 1500 | "array #1", 1501 | { 1502 | arrayKeyword: ["abc"], 1503 | }, 1504 | (msg) => expect(msg).toMatchSnapshot(), 1505 | ); 1506 | 1507 | createFailedTestCase( 1508 | "array #2", 1509 | { 1510 | arrayKeyword: [1, "string", 1], 1511 | }, 1512 | (msg) => expect(msg).toMatchSnapshot(), 1513 | ); 1514 | 1515 | createFailedTestCase( 1516 | "array #3", 1517 | { 1518 | arrayKeyword: [1, "string", "1", "other", "foo"], 1519 | }, 1520 | (msg) => expect(msg).toMatchSnapshot(), 1521 | ); 1522 | 1523 | createFailedTestCase( 1524 | "array #4", 1525 | { 1526 | arrayKeyword2: [1, "1", 1], 1527 | }, 1528 | (msg) => expect(msg).toMatchSnapshot(), 1529 | ); 1530 | 1531 | createFailedTestCase( 1532 | "array #5", 1533 | { 1534 | arrayKeyword2: [1, 2, true, false], 1535 | }, 1536 | (msg) => expect(msg).toMatchSnapshot(), 1537 | ); 1538 | 1539 | createFailedTestCase( 1540 | "array #4", 1541 | { 1542 | arrayKeyword3: ["1", "2", "3"], 1543 | }, 1544 | (msg) => expect(msg).toMatchSnapshot(), 1545 | ); 1546 | 1547 | createFailedTestCase( 1548 | "array #5", 1549 | { 1550 | arrayKeyword3: [1, "1", 1], 1551 | }, 1552 | (msg) => expect(msg).toMatchSnapshot(), 1553 | ); 1554 | 1555 | createFailedTestCase( 1556 | "array #6", 1557 | { 1558 | arrayKeyword3: [1, 2, true, false], 1559 | }, 1560 | (msg) => expect(msg).toMatchSnapshot(), 1561 | ); 1562 | 1563 | createFailedTestCase( 1564 | "array #7", 1565 | { 1566 | arrayKeyword4: true, 1567 | }, 1568 | (msg) => expect(msg).toMatchSnapshot(), 1569 | ); 1570 | 1571 | createFailedTestCase( 1572 | "array #8", 1573 | { 1574 | arrayKeyword5: [1], 1575 | }, 1576 | (msg) => expect(msg).toMatchSnapshot(), 1577 | ); 1578 | 1579 | createFailedTestCase( 1580 | "array #9", 1581 | { 1582 | arrayKeyword6: [1], 1583 | }, 1584 | (msg) => expect(msg).toMatchSnapshot(), 1585 | ); 1586 | 1587 | createFailedTestCase( 1588 | "array #10", 1589 | { 1590 | arrayKeyword6: ["true", "1"], 1591 | }, 1592 | (msg) => expect(msg).toMatchSnapshot(), 1593 | ); 1594 | 1595 | createFailedTestCase( 1596 | "array #11", 1597 | { 1598 | arrayKeyword6: [true, "1"], 1599 | }, 1600 | (msg) => expect(msg).toMatchSnapshot(), 1601 | ); 1602 | 1603 | createFailedTestCase( 1604 | "array #12", 1605 | { 1606 | arrayKeyword7: ["test", 1, /test/], 1607 | }, 1608 | (msg) => expect(msg).toMatchSnapshot(), 1609 | ); 1610 | 1611 | createFailedTestCase( 1612 | "array #13", 1613 | { 1614 | arrayKeyword8: ["test", 1, "test", true], 1615 | }, 1616 | (msg) => expect(msg).toMatchSnapshot(), 1617 | ); 1618 | 1619 | createFailedTestCase( 1620 | "array #14", 1621 | { 1622 | arrayKeyword8: true, 1623 | }, 1624 | (msg) => expect(msg).toMatchSnapshot(), 1625 | ); 1626 | 1627 | createFailedTestCase( 1628 | "array #15", 1629 | { 1630 | arrayKeyword9: true, 1631 | }, 1632 | (msg) => expect(msg).toMatchSnapshot(), 1633 | ); 1634 | 1635 | createFailedTestCase( 1636 | "array #16", 1637 | { 1638 | arrayKeyword10: true, 1639 | }, 1640 | (msg) => expect(msg).toMatchSnapshot(), 1641 | ); 1642 | 1643 | createFailedTestCase( 1644 | "array #17", 1645 | { 1646 | arrayKeyword11: true, 1647 | }, 1648 | (msg) => expect(msg).toMatchSnapshot(), 1649 | ); 1650 | 1651 | createFailedTestCase( 1652 | "array #18", 1653 | { 1654 | arrayKeyword12: true, 1655 | }, 1656 | (msg) => expect(msg).toMatchSnapshot(), 1657 | ); 1658 | 1659 | createFailedTestCase( 1660 | "array #19", 1661 | { 1662 | arrayKeyword13: true, 1663 | }, 1664 | (msg) => expect(msg).toMatchSnapshot(), 1665 | ); 1666 | 1667 | createFailedTestCase( 1668 | "array #20", 1669 | { 1670 | arrayKeyword14: true, 1671 | }, 1672 | (msg) => expect(msg).toMatchSnapshot(), 1673 | ); 1674 | 1675 | createFailedTestCase( 1676 | "array #21", 1677 | { 1678 | arrayKeyword15: true, 1679 | }, 1680 | (msg) => expect(msg).toMatchSnapshot(), 1681 | ); 1682 | 1683 | createFailedTestCase( 1684 | "array #22", 1685 | { 1686 | arrayKeyword16: true, 1687 | }, 1688 | (msg) => expect(msg).toMatchSnapshot(), 1689 | ); 1690 | 1691 | createFailedTestCase( 1692 | "array #23", 1693 | { 1694 | arrayKeyword17: true, 1695 | }, 1696 | (msg) => expect(msg).toMatchSnapshot(), 1697 | ); 1698 | 1699 | createFailedTestCase( 1700 | "array #24", 1701 | { 1702 | arrayKeyword17: [1, /test/, () => {}], 1703 | }, 1704 | (msg) => expect(msg).toMatchSnapshot(), 1705 | ); 1706 | 1707 | createFailedTestCase( 1708 | "array #25", 1709 | { 1710 | arrayKeyword18: [1, 2, 3], 1711 | }, 1712 | (msg) => expect(msg).toMatchSnapshot(), 1713 | ); 1714 | 1715 | createFailedTestCase( 1716 | "array #26", 1717 | { 1718 | arrayKeyword19: true, 1719 | }, 1720 | (msg) => expect(msg).toMatchSnapshot(), 1721 | ); 1722 | 1723 | createFailedTestCase( 1724 | "recursion", 1725 | { 1726 | recursion: { person: { name: "Foo", children: {} } }, 1727 | }, 1728 | (msg) => expect(msg).toMatchSnapshot(), 1729 | ); 1730 | 1731 | createFailedTestCase( 1732 | "extending", 1733 | { 1734 | extending: { 1735 | // eslint-disable-next-line camelcase 1736 | shipping_address: { 1737 | // eslint-disable-next-line camelcase 1738 | street_address: "1600 Pennsylvania Avenue NW", 1739 | city: "Washington", 1740 | state: "DC", 1741 | }, 1742 | }, 1743 | }, 1744 | (msg) => expect(msg).toMatchSnapshot(), 1745 | ); 1746 | 1747 | createFailedTestCase( 1748 | "module", 1749 | { 1750 | module: { rules: [{ compiler: true }] }, 1751 | }, 1752 | (msg) => expect(msg).toMatchSnapshot(), 1753 | ); 1754 | 1755 | createFailedTestCase( 1756 | "array with string items and minLength", 1757 | { 1758 | longString: true, 1759 | }, 1760 | (msg) => expect(msg).toMatchSnapshot(), 1761 | ); 1762 | 1763 | createFailedTestCase( 1764 | "integer equals to 5", 1765 | { 1766 | integerEqualsTo5: 6, 1767 | }, 1768 | (msg) => expect(msg).toMatchSnapshot(), 1769 | ); 1770 | 1771 | createFailedTestCase( 1772 | "integer with minimum", 1773 | { 1774 | integerWithMinimum: true, 1775 | }, 1776 | (msg) => expect(msg).toMatchSnapshot(), 1777 | ); 1778 | 1779 | createFailedTestCase( 1780 | "integer with minimum and maximum", 1781 | { 1782 | integerWithMinimum: 1, 1783 | }, 1784 | (msg) => expect(msg).toMatchSnapshot(), 1785 | ); 1786 | 1787 | createFailedTestCase( 1788 | "integer with exclusive minimum", 1789 | { 1790 | integerWithExclusiveMinimum: true, 1791 | }, 1792 | (msg) => expect(msg).toMatchSnapshot(), 1793 | ); 1794 | 1795 | createFailedTestCase( 1796 | "integer with exclusive minimum", 1797 | { 1798 | integerWithExclusiveMinimum: 1, 1799 | }, 1800 | (msg) => expect(msg).toMatchSnapshot(), 1801 | ); 1802 | 1803 | createFailedTestCase( 1804 | "integer with exclusive maximum", 1805 | { 1806 | integerWithExclusiveMaximum: true, 1807 | }, 1808 | (msg) => expect(msg).toMatchSnapshot(), 1809 | ); 1810 | 1811 | createFailedTestCase( 1812 | "integer with exclusive maximum", 1813 | { 1814 | integerWithExclusiveMaximum: 1, 1815 | }, 1816 | (msg) => expect(msg).toMatchSnapshot(), 1817 | ); 1818 | 1819 | createFailedTestCase( 1820 | "number with minimum and maximum", 1821 | { 1822 | numberWithMinimum: true, 1823 | }, 1824 | (msg) => expect(msg).toMatchSnapshot(), 1825 | ); 1826 | 1827 | createFailedTestCase( 1828 | "multipleOf with minimum and maximum", 1829 | { 1830 | multipleOfProp: true, 1831 | }, 1832 | (msg) => expect(msg).toMatchSnapshot(), 1833 | ); 1834 | 1835 | createFailedTestCase( 1836 | "string with minLength, maxLength and pattern", 1837 | { 1838 | stringWithMinAndMaxLength: true, 1839 | }, 1840 | (msg) => expect(msg).toMatchSnapshot(), 1841 | ); 1842 | 1843 | createFailedTestCase( 1844 | "string with minLength and maxLength", 1845 | { 1846 | stringWithMinAndMaxLength: "def", 1847 | }, 1848 | (msg) => expect(msg).toMatchSnapshot(), 1849 | ); 1850 | 1851 | createFailedTestCase( 1852 | "format, formatMaximum and formatExclusiveMaximum", 1853 | { 1854 | strictFormat: true, 1855 | }, 1856 | (msg) => expect(msg).toMatchSnapshot(), 1857 | ); 1858 | 1859 | createFailedTestCase( 1860 | "format, formatMaximum and formatExclusiveMaximum #2", 1861 | { 1862 | strictFormat: "2016-02-07", 1863 | }, 1864 | (msg) => expect(msg).toMatchSnapshot(), 1865 | ); 1866 | 1867 | createFailedTestCase( 1868 | "format, formatMinimum and formatExclusiveMinimum", 1869 | { 1870 | strictFormat2: true, 1871 | }, 1872 | (msg) => expect(msg).toMatchSnapshot(), 1873 | ); 1874 | 1875 | createFailedTestCase( 1876 | "format, formatMinimum and formatExclusiveMinimum #2", 1877 | { 1878 | strictFormat2: "2016-02-06", 1879 | }, 1880 | (msg) => expect(msg).toMatchSnapshot(), 1881 | ); 1882 | 1883 | createFailedTestCase( 1884 | "uniqueItems", 1885 | { 1886 | uniqueItemsProp: true, 1887 | }, 1888 | (msg) => expect(msg).toMatchSnapshot(), 1889 | ); 1890 | 1891 | createFailedTestCase( 1892 | "uniqueItems #2", 1893 | { 1894 | uniqueItemsProp: [1, 1], 1895 | }, 1896 | (msg) => expect(msg).toMatchSnapshot(), 1897 | ); 1898 | 1899 | createFailedTestCase( 1900 | "maxProperties and minProperties", 1901 | { 1902 | maxPropertiesAndMinProperties: true, 1903 | }, 1904 | (msg) => expect(msg).toMatchSnapshot(), 1905 | ); 1906 | 1907 | createFailedTestCase( 1908 | "maxProperties and minProperties #2", 1909 | { 1910 | maxPropertiesAndMinProperties: {}, 1911 | }, 1912 | (msg) => expect(msg).toMatchSnapshot(), 1913 | ); 1914 | 1915 | createFailedTestCase( 1916 | "maxProperties and minProperties #3", 1917 | { 1918 | maxPropertiesAndMinProperties: { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }, 1919 | }, 1920 | (msg) => expect(msg).toMatchSnapshot(), 1921 | ); 1922 | 1923 | createFailedTestCase( 1924 | "object", 1925 | { 1926 | objectTest: true, 1927 | }, 1928 | (msg) => expect(msg).toMatchSnapshot(), 1929 | ); 1930 | 1931 | createFailedTestCase( 1932 | "object #2", 1933 | { 1934 | objectTest2: true, 1935 | }, 1936 | (msg) => expect(msg).toMatchSnapshot(), 1937 | ); 1938 | 1939 | createFailedTestCase( 1940 | "object #3", 1941 | { 1942 | objectTest3: true, 1943 | }, 1944 | (msg) => expect(msg).toMatchSnapshot(), 1945 | ); 1946 | 1947 | createFailedTestCase( 1948 | "object #4", 1949 | { 1950 | objectTest4: true, 1951 | }, 1952 | (msg) => expect(msg).toMatchSnapshot(), 1953 | ); 1954 | 1955 | createFailedTestCase( 1956 | "object #5", 1957 | { 1958 | objectTest4: { foo: "test", b: "test" }, 1959 | }, 1960 | (msg) => expect(msg).toMatchSnapshot(), 1961 | ); 1962 | 1963 | createFailedTestCase( 1964 | "object #6", 1965 | { 1966 | objectTest5: { 1967 | "foo@gmail.com.com": 1, 1968 | "foo1@bar.com": 1, 1969 | "foo@bar.com": 1, 1970 | }, 1971 | }, 1972 | (msg) => expect(msg).toMatchSnapshot(), 1973 | ); 1974 | 1975 | createFailedTestCase( 1976 | "object #7", 1977 | { 1978 | objectTest5: true, 1979 | }, 1980 | (msg) => expect(msg).toMatchSnapshot(), 1981 | ); 1982 | 1983 | createFailedTestCase( 1984 | "object #8", 1985 | { 1986 | objectTest6: true, 1987 | }, 1988 | (msg) => expect(msg).toMatchSnapshot(), 1989 | ); 1990 | 1991 | createFailedTestCase( 1992 | "object #9", 1993 | { 1994 | objectTest7: true, 1995 | }, 1996 | (msg) => expect(msg).toMatchSnapshot(), 1997 | ); 1998 | 1999 | createFailedTestCase( 2000 | "object #9", 2001 | { 2002 | objectTest8: true, 2003 | }, 2004 | (msg) => expect(msg).toMatchSnapshot(), 2005 | ); 2006 | 2007 | createFailedTestCase( 2008 | "object #10", 2009 | { 2010 | objectTest7: { baz: "test" }, 2011 | }, 2012 | (msg) => expect(msg).toMatchSnapshot(), 2013 | ); 2014 | 2015 | createFailedTestCase( 2016 | "object #11", 2017 | { 2018 | objectTest9: true, 2019 | }, 2020 | (msg) => expect(msg).toMatchSnapshot(), 2021 | ); 2022 | 2023 | createFailedTestCase( 2024 | "object #12", 2025 | { 2026 | objectTest9: { foo: "test", baz: "test", bar: 1 }, 2027 | }, 2028 | (msg) => expect(msg).toMatchSnapshot(), 2029 | ); 2030 | 2031 | createFailedTestCase( 2032 | "string with empty pattern", 2033 | { 2034 | stringWithEmptyPattern: true, 2035 | }, 2036 | (msg) => expect(msg).toMatchSnapshot(), 2037 | ); 2038 | 2039 | createFailedTestCase( 2040 | "array keywords without array type", 2041 | { 2042 | likeArray: ["test"], 2043 | }, 2044 | (msg) => expect(msg).toMatchSnapshot(), 2045 | ); 2046 | 2047 | createFailedTestCase( 2048 | "array with empty items, empty additionalItems, empty contains", 2049 | { 2050 | arrayWithEmptyItemsAndEmptyAdditionalItemsAndEmptyContains: true, 2051 | }, 2052 | (msg) => expect(msg).toMatchSnapshot(), 2053 | ); 2054 | 2055 | createFailedTestCase( 2056 | "additionalItems with false", 2057 | { 2058 | additionalItemsFalse: [1, 1, "foo"], 2059 | }, 2060 | (msg) => expect(msg).toMatchSnapshot(), 2061 | ); 2062 | 2063 | createFailedTestCase( 2064 | "required without object type", 2065 | { 2066 | requiredWithoutType: { b: "test" }, 2067 | }, 2068 | (msg) => expect(msg).toMatchSnapshot(), 2069 | ); 2070 | 2071 | createFailedTestCase( 2072 | "dependencies without object type", 2073 | { 2074 | dependenciesWithoutType: { foo: "test" }, 2075 | }, 2076 | (msg) => expect(msg).toMatchSnapshot(), 2077 | ); 2078 | 2079 | createFailedTestCase( 2080 | "propertyNames without object type", 2081 | { 2082 | propertyNamesWithoutType: { foo: "test" }, 2083 | }, 2084 | (msg) => expect(msg).toMatchSnapshot(), 2085 | ); 2086 | 2087 | createFailedTestCase( 2088 | "patternRequired without object type", 2089 | { 2090 | patternRequiredWithoutType: { boo: "test" }, 2091 | }, 2092 | (msg) => expect(msg).toMatchSnapshot(), 2093 | ); 2094 | 2095 | createFailedTestCase( 2096 | "additionalProperties without object type", 2097 | { 2098 | additionalPropertiesWithoutType: { boo: "test" }, 2099 | }, 2100 | (msg) => expect(msg).toMatchSnapshot(), 2101 | ); 2102 | 2103 | createFailedTestCase( 2104 | "maxProperties without object type", 2105 | { 2106 | maxPropertiesWithoutType: { boo: "test" }, 2107 | }, 2108 | (msg) => expect(msg).toMatchSnapshot(), 2109 | ); 2110 | 2111 | createFailedTestCase( 2112 | "object with required and properties", 2113 | { 2114 | justAnObject: true, 2115 | }, 2116 | (msg) => expect(msg).toMatchSnapshot(), 2117 | ); 2118 | 2119 | createFailedTestCase( 2120 | "array with absolutePath item", 2121 | { 2122 | arrayWithAbsolutePath: true, 2123 | }, 2124 | (msg) => expect(msg).toMatchSnapshot(), 2125 | ); 2126 | 2127 | createFailedTestCase( 2128 | "allOf", 2129 | { 2130 | allOfKeyword: true, 2131 | }, 2132 | (msg) => expect(msg).toMatchSnapshot(), 2133 | ); 2134 | 2135 | createFailedTestCase( 2136 | "allOf #1", 2137 | { 2138 | allOfKeyword: 4.5, 2139 | }, 2140 | (msg) => expect(msg).toMatchSnapshot(), 2141 | ); 2142 | 2143 | createFailedTestCase( 2144 | "allOf #2", 2145 | { 2146 | allOfKeyword: 5, 2147 | }, 2148 | (msg) => expect(msg).toMatchSnapshot(), 2149 | ); 2150 | 2151 | createFailedTestCase( 2152 | "oneOf with description", 2153 | { 2154 | oneOfnumberAndDescriptionAndArray: "2016-02-06", 2155 | }, 2156 | (msg) => expect(msg).toMatchSnapshot(), 2157 | ); 2158 | 2159 | createFailedTestCase( 2160 | "number and description", 2161 | { 2162 | numberAndDescription: "2016-02-06", 2163 | }, 2164 | (msg) => expect(msg).toMatchSnapshot(), 2165 | ); 2166 | 2167 | createFailedTestCase( 2168 | "const with description", 2169 | { 2170 | constWithDescription: "2016-02-06", 2171 | }, 2172 | (msg) => expect(msg).toMatchSnapshot(), 2173 | ); 2174 | 2175 | createFailedTestCase( 2176 | "array with items with true", 2177 | { 2178 | itemsTrue: true, 2179 | }, 2180 | (msg) => expect(msg).toMatchSnapshot(), 2181 | ); 2182 | 2183 | createFailedTestCase( 2184 | "empty const", 2185 | { 2186 | emptyConst: true, 2187 | }, 2188 | (msg) => expect(msg).toMatchSnapshot(), 2189 | ); 2190 | 2191 | createFailedTestCase( 2192 | "one const", 2193 | { 2194 | oneConst: true, 2195 | }, 2196 | (msg) => expect(msg).toMatchSnapshot(), 2197 | ); 2198 | 2199 | createFailedTestCase( 2200 | "const with empty string", 2201 | { 2202 | constWithEmptyString: true, 2203 | }, 2204 | (msg) => expect(msg).toMatchSnapshot(), 2205 | ); 2206 | 2207 | createFailedTestCase( 2208 | "ref inside object inside allOf", 2209 | { 2210 | refAndAnyOf: {}, 2211 | }, 2212 | (msg) => expect(msg).toMatchSnapshot(), 2213 | ); 2214 | 2215 | createFailedTestCase( 2216 | "additionalProperties inside oneOf", 2217 | { 2218 | additionalPropertiesInsideOneOf: true, 2219 | }, 2220 | (msg) => expect(msg).toMatchSnapshot(), 2221 | ); 2222 | 2223 | createFailedTestCase( 2224 | "additionalProperties inside oneOf #2", 2225 | { 2226 | additionalPropertiesInsideOneOf: { foo: 100, bar: "test" }, 2227 | }, 2228 | (msg) => expect(msg).toMatchSnapshot(), 2229 | ); 2230 | 2231 | createFailedTestCase( 2232 | "additionalProperties inside oneOf #3", 2233 | { 2234 | additionalPropertiesInsideOneOf2: true, 2235 | }, 2236 | (msg) => expect(msg).toMatchSnapshot(), 2237 | ); 2238 | 2239 | createFailedTestCase( 2240 | "single item in contains", 2241 | { 2242 | singleContainsItems: true, 2243 | }, 2244 | (msg) => expect(msg).toMatchSnapshot(), 2245 | ); 2246 | 2247 | createFailedTestCase( 2248 | "object with dependencies", 2249 | { 2250 | objectWithPropertyDependency: true, 2251 | }, 2252 | (msg) => expect(msg).toMatchSnapshot(), 2253 | ); 2254 | 2255 | createFailedTestCase( 2256 | "object with dependencies #2", 2257 | { 2258 | objectWithPropertyDependency2: true, 2259 | }, 2260 | (msg) => expect(msg).toMatchSnapshot(), 2261 | ); 2262 | 2263 | createFailedTestCase( 2264 | "object with dependencies #3", 2265 | { 2266 | objectWithPropertyDependency3: true, 2267 | }, 2268 | (msg) => expect(msg).toMatchSnapshot(), 2269 | ); 2270 | 2271 | createFailedTestCase( 2272 | "object with dependencies #4", 2273 | { 2274 | objectWithPropertyDependency4: true, 2275 | }, 2276 | (msg) => expect(msg).toMatchSnapshot(), 2277 | ); 2278 | 2279 | createFailedTestCase( 2280 | "oneOf with if", 2281 | { 2282 | oneOfWithIf: true, 2283 | }, 2284 | (msg) => expect(msg).toMatchSnapshot(), 2285 | ); 2286 | 2287 | createFailedTestCase( 2288 | "const with array notation", 2289 | { 2290 | constWithArrayNotation: true, 2291 | }, 2292 | (msg) => expect(msg).toMatchSnapshot(), 2293 | ); 2294 | 2295 | createFailedTestCase( 2296 | "const with object notation", 2297 | { 2298 | constWithObjectNotation: true, 2299 | }, 2300 | (msg) => expect(msg).toMatchSnapshot(), 2301 | ); 2302 | 2303 | createFailedTestCase( 2304 | "items and additionalItems", 2305 | { 2306 | additionalItemsWithoutType: true, 2307 | }, 2308 | (msg) => expect(msg).toMatchSnapshot(), 2309 | ); 2310 | 2311 | createFailedTestCase( 2312 | "items and additionalItems #2", 2313 | { 2314 | additionalItemsWithoutType2: true, 2315 | }, 2316 | (msg) => expect(msg).toMatchSnapshot(), 2317 | ); 2318 | 2319 | createFailedTestCase( 2320 | "items and additionalItems #3", 2321 | { 2322 | additionalItemsWithoutType3: true, 2323 | }, 2324 | (msg) => expect(msg).toMatchSnapshot(), 2325 | ); 2326 | 2327 | createFailedTestCase( 2328 | "contains and additionalItems", 2329 | { 2330 | containsAndAdditionalItems: true, 2331 | }, 2332 | (msg) => expect(msg).toMatchSnapshot(), 2333 | ); 2334 | 2335 | createFailedTestCase( 2336 | "contains and additionalItems #2", 2337 | { 2338 | containsAndAdditionalItems: [/test/, "string"], 2339 | }, 2340 | (msg) => expect(msg).toMatchSnapshot(), 2341 | ); 2342 | 2343 | createFailedTestCase( 2344 | "contains inside items", 2345 | { 2346 | containsInsideItem: true, 2347 | }, 2348 | (msg) => expect(msg).toMatchSnapshot(), 2349 | ); 2350 | 2351 | createFailedTestCase( 2352 | "contains inside items #2", 2353 | { 2354 | containsInsideItem: [["test"], "1", /test/], 2355 | }, 2356 | (msg) => expect(msg).toMatchSnapshot(), 2357 | ); 2358 | 2359 | createFailedTestCase( 2360 | "empty object", 2361 | { 2362 | emptyObject: true, 2363 | }, 2364 | (msg) => expect(msg).toMatchSnapshot(), 2365 | ); 2366 | 2367 | createFailedTestCase( 2368 | "empty object #2", 2369 | { 2370 | emptyObject: { a: "test" }, 2371 | }, 2372 | (msg) => expect(msg).toMatchSnapshot(), 2373 | ); 2374 | 2375 | createFailedTestCase( 2376 | "non empty object", 2377 | { 2378 | nonEmptyObject: true, 2379 | }, 2380 | (msg) => expect(msg).toMatchSnapshot(), 2381 | ); 2382 | 2383 | createFailedTestCase( 2384 | "non empty object #2", 2385 | { 2386 | nonEmptyObject2: true, 2387 | }, 2388 | (msg) => expect(msg).toMatchSnapshot(), 2389 | ); 2390 | 2391 | createFailedTestCase( 2392 | "holey array", 2393 | 2394 | [ 2395 | { 2396 | mode: "production", 2397 | }, // eslint-disable-next-line no-sparse-arrays 2398 | , 2399 | { 2400 | mode: "development", 2401 | }, 2402 | ], 2403 | (msg) => expect(msg).toMatchSnapshot(), 2404 | ); 2405 | 2406 | createFailedTestCase( 2407 | "missing cache group name", 2408 | { 2409 | optimization: { 2410 | splitChunks: { 2411 | cacheGroups: { 2412 | test: /abc/, 2413 | }, 2414 | }, 2415 | }, 2416 | }, 2417 | (msg) => expect(msg).toMatchSnapshot(), 2418 | ); 2419 | 2420 | createFailedTestCase( 2421 | "not enum", 2422 | { 2423 | notEnum: 2, 2424 | }, 2425 | (msg) => expect(msg).toMatchSnapshot(), 2426 | ); 2427 | 2428 | createFailedTestCase( 2429 | "not const", 2430 | { 2431 | notConst: "foo", 2432 | }, 2433 | (msg) => expect(msg).toMatchSnapshot(), 2434 | ); 2435 | 2436 | createFailedTestCase( 2437 | "not number", 2438 | { 2439 | notNumber: 1, 2440 | }, 2441 | (msg) => expect(msg).toMatchSnapshot(), 2442 | ); 2443 | 2444 | createFailedTestCase( 2445 | "not integer", 2446 | { 2447 | notNumber: 1, 2448 | }, 2449 | (msg) => expect(msg).toMatchSnapshot(), 2450 | ); 2451 | 2452 | createFailedTestCase( 2453 | "not string", 2454 | { 2455 | notString: "test", 2456 | }, 2457 | (msg) => expect(msg).toMatchSnapshot(), 2458 | ); 2459 | 2460 | createFailedTestCase( 2461 | "not boolean", 2462 | { 2463 | notBoolean: true, 2464 | }, 2465 | (msg) => expect(msg).toMatchSnapshot(), 2466 | ); 2467 | 2468 | createFailedTestCase( 2469 | "not array", 2470 | { 2471 | notArray: [1, 2, 3], 2472 | }, 2473 | (msg) => expect(msg).toMatchSnapshot(), 2474 | ); 2475 | 2476 | createFailedTestCase( 2477 | "not object", 2478 | { 2479 | notObject: { foo: "test" }, 2480 | }, 2481 | (msg) => expect(msg).toMatchSnapshot(), 2482 | ); 2483 | 2484 | createFailedTestCase( 2485 | "not null", 2486 | { 2487 | notNull: null, 2488 | }, 2489 | (msg) => expect(msg).toMatchSnapshot(), 2490 | ); 2491 | 2492 | createFailedTestCase( 2493 | "not not null", 2494 | { 2495 | notNotNull: 1, 2496 | }, 2497 | (msg) => expect(msg).toMatchSnapshot(), 2498 | ); 2499 | 2500 | createFailedTestCase( 2501 | "not not not null", 2502 | { 2503 | NotNotNotNull: null, 2504 | }, 2505 | (msg) => expect(msg).toMatchSnapshot(), 2506 | ); 2507 | 2508 | createFailedTestCase( 2509 | "not not not null", 2510 | { 2511 | notMultipleTypes: 1, 2512 | }, 2513 | (msg) => expect(msg).toMatchSnapshot(), 2514 | ); 2515 | 2516 | createFailedTestCase( 2517 | "not array less than 3 items", 2518 | { 2519 | notMaxItemsArray: [1, 2], 2520 | }, 2521 | (msg) => expect(msg).toMatchSnapshot(), 2522 | ); 2523 | 2524 | createFailedTestCase( 2525 | "no type like number with minimum", 2526 | { 2527 | noTypeLikeNumberMinimum: 4, 2528 | }, 2529 | (msg) => expect(msg).toMatchSnapshot(), 2530 | ); 2531 | 2532 | createFailedTestCase( 2533 | "no type like number with maximum", 2534 | { 2535 | noTypeLikeNumberMaximum: 6, 2536 | }, 2537 | (msg) => expect(msg).toMatchSnapshot(), 2538 | ); 2539 | 2540 | createFailedTestCase( 2541 | "no type like number with exclusive minimum", 2542 | { 2543 | noTypeLikeNumberExclusiveMinimum: 4, 2544 | }, 2545 | (msg) => expect(msg).toMatchSnapshot(), 2546 | ); 2547 | 2548 | createFailedTestCase( 2549 | "no type like number with exclusive maximum", 2550 | { 2551 | noTypeLikeNumberExclusiveMaximum: 6, 2552 | }, 2553 | (msg) => expect(msg).toMatchSnapshot(), 2554 | ); 2555 | 2556 | createFailedTestCase( 2557 | "minimum with type number", 2558 | { 2559 | minimumWithTypeNumber: 4, 2560 | }, 2561 | (msg) => expect(msg).toMatchSnapshot(), 2562 | ); 2563 | 2564 | createFailedTestCase( 2565 | "maximum with type number", 2566 | { 2567 | maximumWithTypeNumber: 6, 2568 | }, 2569 | (msg) => expect(msg).toMatchSnapshot(), 2570 | ); 2571 | 2572 | createFailedTestCase( 2573 | "exclusive minimum with type number", 2574 | { 2575 | exclusiveMinimumWithTypeNumber: 4, 2576 | }, 2577 | (msg) => expect(msg).toMatchSnapshot(), 2578 | ); 2579 | 2580 | createFailedTestCase( 2581 | "exclusive maximum with type number", 2582 | { 2583 | exclusiveMaximumWithTypeNumber: 6, 2584 | }, 2585 | (msg) => expect(msg).toMatchSnapshot(), 2586 | ); 2587 | 2588 | createFailedTestCase( 2589 | "no type like number with multipleOf", 2590 | { 2591 | noTypeLikeNumberMultipleOf: 1, 2592 | }, 2593 | (msg) => expect(msg).toMatchSnapshot(), 2594 | ); 2595 | 2596 | createFailedTestCase( 2597 | "multipleOf with type number", 2598 | { 2599 | multipleOfWithNumberType: 1, 2600 | }, 2601 | (msg) => expect(msg).toMatchSnapshot(), 2602 | ); 2603 | 2604 | createFailedTestCase( 2605 | "no type like string with minLength", 2606 | { 2607 | noTypeLikeStringMinLength: "a", 2608 | }, 2609 | (msg) => expect(msg).toMatchSnapshot(), 2610 | ); 2611 | 2612 | createFailedTestCase( 2613 | "no type like string with maxLength", 2614 | { 2615 | noTypeLikeStringMaxLength: "aaa", 2616 | }, 2617 | (msg) => expect(msg).toMatchSnapshot(), 2618 | ); 2619 | 2620 | createFailedTestCase( 2621 | "minLength with type string", 2622 | { 2623 | stringWithMinLength: "a", 2624 | }, 2625 | (msg) => expect(msg).toMatchSnapshot(), 2626 | ); 2627 | 2628 | createFailedTestCase( 2629 | "maxLength with type string", 2630 | { 2631 | stringWithMaxLength: "aaa", 2632 | }, 2633 | (msg) => expect(msg).toMatchSnapshot(), 2634 | ); 2635 | 2636 | createFailedTestCase( 2637 | "no type like string with pattern", 2638 | { 2639 | noTypeLikeStringPattern: "def", 2640 | }, 2641 | (msg) => expect(msg).toMatchSnapshot(), 2642 | ); 2643 | 2644 | createFailedTestCase( 2645 | "pattern with type string", 2646 | { 2647 | patternWithStringType: "def", 2648 | }, 2649 | (msg) => expect(msg).toMatchSnapshot(), 2650 | ); 2651 | 2652 | createFailedTestCase( 2653 | "no type like string with format", 2654 | { 2655 | noTypeLikeStringFormat: "abc", 2656 | }, 2657 | (msg) => expect(msg).toMatchSnapshot(), 2658 | ); 2659 | 2660 | createFailedTestCase( 2661 | "format with type string", 2662 | { 2663 | stringWithFormat: "abc", 2664 | }, 2665 | (msg) => expect(msg).toMatchSnapshot(), 2666 | ); 2667 | 2668 | createFailedTestCase( 2669 | "no type like string with formatMaximum", 2670 | { 2671 | noTypeLikeStringFormatMaximum: "2016-02-06", 2672 | }, 2673 | (msg) => expect(msg).toMatchSnapshot(), 2674 | ); 2675 | 2676 | createFailedTestCase( 2677 | "formatMaximum with type string", 2678 | { 2679 | stringWithFormatMaximum: "2016-02-06", 2680 | }, 2681 | (msg) => expect(msg).toMatchSnapshot(), 2682 | ); 2683 | 2684 | createFailedTestCase( 2685 | "multiple instanceof ", 2686 | { 2687 | multipleInstanceof: "test", 2688 | }, 2689 | (msg) => expect(msg).toMatchSnapshot(), 2690 | ); 2691 | 2692 | createFailedTestCase( 2693 | "no type like object with patternRequired", 2694 | { 2695 | noTypeLikeObjectPatternRequired: { bar: 2 }, 2696 | }, 2697 | (msg) => expect(msg).toMatchSnapshot(), 2698 | ); 2699 | 2700 | createFailedTestCase( 2701 | "patternRequired with type object", 2702 | { 2703 | objectWithPatternRequired: { bar: 2 }, 2704 | }, 2705 | (msg) => expect(msg).toMatchSnapshot(), 2706 | ); 2707 | 2708 | createFailedTestCase( 2709 | "no type like string with minLength equals 1", 2710 | { 2711 | noTypeLikeStringMinLength1: "", 2712 | }, 2713 | (msg) => expect(msg).toMatchSnapshot(), 2714 | ); 2715 | 2716 | createFailedTestCase( 2717 | "no type like array with minItems equals 1", 2718 | { 2719 | noTypeLikeArrayMinItems1: [], 2720 | }, 2721 | (msg) => expect(msg).toMatchSnapshot(), 2722 | ); 2723 | 2724 | createFailedTestCase( 2725 | "no type like array with minItems", 2726 | { 2727 | noTypeLikeArrayMinItems: [], 2728 | }, 2729 | (msg) => expect(msg).toMatchSnapshot(), 2730 | ); 2731 | 2732 | createFailedTestCase( 2733 | "array with minItems", 2734 | { 2735 | arrayWithMinItems: [], 2736 | }, 2737 | (msg) => expect(msg).toMatchSnapshot(), 2738 | ); 2739 | 2740 | createFailedTestCase( 2741 | "no type like array with minItems", 2742 | { 2743 | noTypeMinProperties: {}, 2744 | }, 2745 | (msg) => expect(msg).toMatchSnapshot(), 2746 | ); 2747 | 2748 | createFailedTestCase( 2749 | "no type like array with minItems", 2750 | { 2751 | noTypeMinProperties1: {}, 2752 | }, 2753 | (msg) => expect(msg).toMatchSnapshot(), 2754 | ); 2755 | 2756 | createFailedTestCase( 2757 | "no type like array with minItems", 2758 | { 2759 | objectMinProperties: {}, 2760 | }, 2761 | (msg) => expect(msg).toMatchSnapshot(), 2762 | ); 2763 | 2764 | createFailedTestCase( 2765 | "no type like array with maxItems", 2766 | { 2767 | noTypeLikeArrayMaxItems: [1, 2, 3, 4], 2768 | }, 2769 | (msg) => expect(msg).toMatchSnapshot(), 2770 | ); 2771 | 2772 | createFailedTestCase( 2773 | "array with maxItems", 2774 | { 2775 | arrayMaxItems: [1, 2, 3, 4], 2776 | }, 2777 | (msg) => expect(msg).toMatchSnapshot(), 2778 | ); 2779 | 2780 | createFailedTestCase( 2781 | "no type like object with maxProperties", 2782 | { 2783 | noTypeLikeObjectMaxProperties: { a: 1, b: 2, c: 3 }, 2784 | }, 2785 | (msg) => expect(msg).toMatchSnapshot(), 2786 | ); 2787 | 2788 | createFailedTestCase( 2789 | "object with maxProperties", 2790 | { 2791 | objectMaxProperties: { a: 1, b: 2, c: 3 }, 2792 | }, 2793 | (msg) => expect(msg).toMatchSnapshot(), 2794 | ); 2795 | 2796 | createFailedTestCase( 2797 | "no type like object with maxProperties", 2798 | { 2799 | noTypeLikeArrayUniqueItems: [1, 2, 1], 2800 | }, 2801 | (msg) => expect(msg).toMatchSnapshot(), 2802 | ); 2803 | 2804 | createFailedTestCase( 2805 | "object with maxProperties", 2806 | { 2807 | arrayWithUniqueItems: [1, 2, 1], 2808 | }, 2809 | (msg) => expect(msg).toMatchSnapshot(), 2810 | ); 2811 | 2812 | createFailedTestCase( 2813 | "no type like array with additionalItems", 2814 | { 2815 | noTypeLikeArrayAdditionalItems: [1, 1, "foo"], 2816 | }, 2817 | (msg) => expect(msg).toMatchSnapshot(), 2818 | ); 2819 | 2820 | createFailedTestCase( 2821 | "array with additionalItems", 2822 | { 2823 | arrayWithAdditionalItems: [1, 1, "foo"], 2824 | }, 2825 | (msg) => expect(msg).toMatchSnapshot(), 2826 | ); 2827 | 2828 | createFailedTestCase( 2829 | "no type like array with contains", 2830 | { 2831 | noTypeLikeArrayContains: ["foo", "bar"], 2832 | }, 2833 | (msg) => expect(msg).toMatchSnapshot(), 2834 | ); 2835 | 2836 | createFailedTestCase( 2837 | "array with contains", 2838 | { 2839 | arrayWithContains: ["foo", "bar"], 2840 | }, 2841 | (msg) => expect(msg).toMatchSnapshot(), 2842 | ); 2843 | 2844 | createFailedTestCase( 2845 | "anyOf with item without type", 2846 | { 2847 | anyOfNoTypeInItem: 4.5, 2848 | }, 2849 | (msg) => expect(msg).toMatchSnapshot(), 2850 | ); 2851 | 2852 | createFailedTestCase( 2853 | "oneOf with item without type", 2854 | { 2855 | oneOfNoTypeInItem: 4.5, 2856 | }, 2857 | (msg) => expect(msg).toMatchSnapshot(), 2858 | ); 2859 | 2860 | createFailedTestCase( 2861 | "no type like object propertyNames", 2862 | { 2863 | noTypeLikeObjectPropertyNames: { foo: "any value" }, 2864 | }, 2865 | (msg) => expect(msg).toMatchSnapshot(), 2866 | ); 2867 | 2868 | createFailedTestCase( 2869 | "object dependencies", 2870 | { 2871 | objectPropertyNames: { foo: "any value" }, 2872 | }, 2873 | (msg) => expect(msg).toMatchSnapshot(), 2874 | ); 2875 | 2876 | createFailedTestCase( 2877 | "no type like object dependencies", 2878 | { 2879 | noTypeLikeObjectDependencies: { foo: 1, baz: 3 }, 2880 | }, 2881 | (msg) => expect(msg).toMatchSnapshot(), 2882 | ); 2883 | 2884 | createFailedTestCase( 2885 | "object dependencies", 2886 | { 2887 | objectWithDependencies: { foo: 1, baz: 3 }, 2888 | }, 2889 | (msg) => expect(msg).toMatchSnapshot(), 2890 | ); 2891 | 2892 | createFailedTestCase( 2893 | "no type like object additionalProperties", 2894 | { 2895 | noTypeLikeObjectAdditionalProperties: { foo: 1, baz: 3 }, 2896 | }, 2897 | (msg) => expect(msg).toMatchSnapshot(), 2898 | ); 2899 | 2900 | createFailedTestCase( 2901 | "no type like object required", 2902 | { 2903 | noTypeLikeObjectRequired: {}, 2904 | }, 2905 | (msg) => expect(msg).toMatchSnapshot(), 2906 | ); 2907 | 2908 | createFailedTestCase( 2909 | "$data", 2910 | { 2911 | dollarData: { 2912 | smaller: 5, 2913 | larger: 4, 2914 | }, 2915 | }, 2916 | (msg) => expect(msg).toMatchSnapshot(), 2917 | ); 2918 | 2919 | createFailedTestCase( 2920 | "$data #2", 2921 | { 2922 | dollarData2: { 2923 | "date-time": "1", 2924 | email: "joe.bloggs@example.com", 2925 | }, 2926 | }, 2927 | (msg) => expect(msg).toMatchSnapshot(), 2928 | ); 2929 | 2930 | createFailedTestCase( 2931 | "enum nested", 2932 | { 2933 | enumNested: "string", 2934 | }, 2935 | (msg) => expect(msg).toMatchSnapshot(), 2936 | ); 2937 | 2938 | createFailedTestCase( 2939 | "absolute path", 2940 | { 2941 | testAbsolutePath: "bar", 2942 | }, 2943 | (msg) => expect(msg).toMatchSnapshot(), 2944 | ); 2945 | 2946 | createFailedTestCase( 2947 | "absolute path #1", 2948 | { 2949 | testAbsolutePath: "bar\\\\baz", 2950 | }, 2951 | (msg) => expect(msg).toMatchSnapshot(), 2952 | ); 2953 | 2954 | createFailedTestCase( 2955 | "absolute path #2", 2956 | { 2957 | testAbsolutePath: "bar/baz", 2958 | }, 2959 | (msg) => expect(msg).toMatchSnapshot(), 2960 | ); 2961 | 2962 | createFailedTestCase( 2963 | "absolute path #3", 2964 | { 2965 | testAbsolutePath: ".", 2966 | }, 2967 | (msg) => expect(msg).toMatchSnapshot(), 2968 | ); 2969 | 2970 | createFailedTestCase( 2971 | "absolute path #4", 2972 | { 2973 | testAbsolutePath: "..", 2974 | }, 2975 | (msg) => expect(msg).toMatchSnapshot(), 2976 | ); 2977 | 2978 | createFailedTestCase( 2979 | "not empty string #1", 2980 | { 2981 | notEmptyString: "", 2982 | }, 2983 | (msg) => expect(msg).toMatchSnapshot(), 2984 | ); 2985 | 2986 | createFailedTestCase( 2987 | "not empty string #2", 2988 | { 2989 | notEmptyString2: "", 2990 | }, 2991 | (msg) => expect(msg).toMatchSnapshot(), 2992 | ); 2993 | 2994 | createFailedTestCase( 2995 | "empty string #1", 2996 | { 2997 | emptyString: "1", 2998 | }, 2999 | (msg) => expect(msg).toMatchSnapshot(), 3000 | ); 3001 | 3002 | createFailedTestCase( 3003 | "empty string #2", 3004 | { 3005 | emptyString2: "1", 3006 | }, 3007 | (msg) => expect(msg).toMatchSnapshot(), 3008 | ); 3009 | 3010 | createFailedTestCase( 3011 | "integer with not minimum and maximum", 3012 | { 3013 | integerWithNotMinMax: 10, 3014 | }, 3015 | (msg) => expect(msg).toMatchSnapshot(), 3016 | ); 3017 | 3018 | createFailedTestCase( 3019 | "integer with not minimum", 3020 | { 3021 | integerNotWithMinimum: 5, 3022 | }, 3023 | (msg) => expect(msg).toMatchSnapshot(), 3024 | ); 3025 | 3026 | createFailedTestCase( 3027 | "integer zero", 3028 | { 3029 | integerZero: 1, 3030 | }, 3031 | (msg) => expect(msg).toMatchSnapshot(), 3032 | ); 3033 | 3034 | createFailedTestCase( 3035 | "integer not zero", 3036 | { 3037 | integerNotZero: 0, 3038 | }, 3039 | (msg) => expect(msg).toMatchSnapshot(), 3040 | ); 3041 | 3042 | createFailedTestCase( 3043 | "several not in number type", 3044 | { 3045 | notMultipleOf: 5, 3046 | }, 3047 | (msg) => expect(msg).toMatchSnapshot(), 3048 | ); 3049 | 3050 | createSuccessTestCase( 3051 | "webpack schema", 3052 | { mode: "development" }, 3053 | {}, 3054 | webpackSchema, 3055 | ); 3056 | 3057 | createFailedTestCase( 3058 | "formatExclusiveMaximum #1", 3059 | { 3060 | formatExclusiveMaximum: "2016-02-05", 3061 | }, 3062 | (msg) => expect(msg).toMatchSnapshot(), 3063 | ); 3064 | 3065 | createFailedTestCase( 3066 | "formatExclusiveMaximum #2", 3067 | { 3068 | formatExclusiveMaximum: "2016-12-27", 3069 | }, 3070 | (msg) => expect(msg).toMatchSnapshot(), 3071 | ); 3072 | 3073 | createFailedTestCase( 3074 | "enum and undefinedAsNull", 3075 | { 3076 | enumKeywordAndUndefined: "foo", 3077 | }, 3078 | (msg) => expect(msg).toMatchSnapshot(), 3079 | ); 3080 | 3081 | createFailedTestCase( 3082 | "array with enum and undefinedAsNull", 3083 | { 3084 | arrayStringAndEnum: ["foo", "bar", 1], 3085 | }, 3086 | (msg) => expect(msg).toMatchSnapshot(), 3087 | ); 3088 | 3089 | createFailedTestCase( 3090 | "array with enum and undefinedAsNull #2", 3091 | { 3092 | arrayStringAndEnum: ["foo", "bar", undefined, 1], 3093 | }, 3094 | (msg) => expect(msg).toMatchSnapshot(), 3095 | ); 3096 | 3097 | createFailedTestCase( 3098 | "array with enum and undefinedAsNull #3", 3099 | { 3100 | arrayStringAndEnumAndNoUndefined: ["foo", "bar", undefined], 3101 | }, 3102 | (msg) => expect(msg).toMatchSnapshot(), 3103 | ); 3104 | 3105 | createFailedTestCase( 3106 | "string and undefinedAsNull", 3107 | { 3108 | stringTypeAndUndefinedAsNull: 1, 3109 | }, 3110 | (msg) => expect(msg).toMatchSnapshot(), 3111 | ); 3112 | }); 3113 | --------------------------------------------------------------------------------