From defca2ce21dcd98fbedffde2dd693c6672ee3b13 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Sep 2020 20:18:22 +0100 Subject: [PATCH] feat: DefinedError as discriminated union with different parameter types, closes #843, closes #1090 --- lib/compile/validate/dataType.ts | 4 ++ lib/types/index.ts | 8 ++- .../applicator/additionalItems.ts | 4 ++ .../applicator/additionalProperties.ts | 4 ++ lib/vocabularies/applicator/dependencies.ts | 9 ++- lib/vocabularies/applicator/if.ts | 4 ++ lib/vocabularies/applicator/oneOf.ts | 4 ++ lib/vocabularies/applicator/propertyNames.ts | 4 ++ lib/vocabularies/core/ref.ts | 4 ++ lib/vocabularies/errors.ts | 69 +++++++++++++++++++ lib/vocabularies/format/format.ts | 4 ++ lib/vocabularies/validation/const.ts | 4 ++ lib/vocabularies/validation/enum.ts | 4 ++ lib/vocabularies/validation/limit.ts | 9 +++ lib/vocabularies/validation/multipleOf.ts | 4 ++ lib/vocabularies/validation/pattern.ts | 4 ++ lib/vocabularies/validation/required.ts | 4 ++ lib/vocabularies/validation/uniqueItems.ts | 5 ++ spec/types/async-validate.spec.ts | 3 +- spec/types/error-parameters.spec.ts | 31 +++++++++ 20 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 lib/vocabularies/errors.ts create mode 100644 spec/types/error-parameters.spec.ts diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index f45206fd0..b3354e22b 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -134,6 +134,10 @@ function assignParentData({gen, parentData, parentDataProperty}: SchemaObjCxt, e ) } +export interface TypeErrorParams { + type: string +} + const typeError: KeywordErrorDefinition = { message: ({schema}) => str`should be ${schema}`, params: ({schema, schemaValue}) => diff --git a/lib/types/index.ts b/lib/types/index.ts index 581f82dae..c07f058ba 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -3,6 +3,8 @@ import type {SchemaEnv} from "../compile" import type KeywordCxt from "../compile/context" import type Ajv from "../ajv" +export {DefinedError} from "../vocabularies/errors" + interface _SchemaObject { $id?: string $schema?: string @@ -141,11 +143,11 @@ export interface AsyncValidateFunction extends ValidateFunction { export type AnyValidateFunction = ValidateFunction | AsyncValidateFunction -export interface ErrorObject { - keyword: T +export interface ErrorObject> { + keyword: K dataPath: string schemaPath: string - params: Record // TODO add interface + params: P // Added to validation errors of "propertyNames" keyword schema propertyName?: string // Excluded if option `messages` set to false. diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index 92c7c423c..df9d5643c 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -4,6 +4,10 @@ import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_, Name, str} from "../../compile/codegen" +export interface AdditionalItemsErrorParams { + limit: number +} + const error: KeywordErrorDefinition = { message: ({params: {len}}) => str`should NOT have more than ${len} items`, params: ({params: {len}}) => _`{limit: ${len}}`, diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 7e4f55a91..e54712eeb 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -4,6 +4,10 @@ import {applySubschema, SubschemaApplication, Type} from "../../compile/subschem import {_, nil, or, Code, Name} from "../../compile/codegen" import N from "../../compile/names" +export interface AdditionalPropsErrorParams { + additionalProperty: string +} + const error: KeywordErrorDefinition = { message: "should NOT have additional properties", params: ({params}) => _`{additionalProperty: ${params.additionalProperty}}`, diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index da279861b..c5ccbe547 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -11,6 +11,13 @@ interface PropertyDependencies { type SchemaDependencies = SchemaMap +export interface DependenciesErrorParams { + property: string + missingProperty: string + depsCount: number + deps: string // TODO change to string[] +} + const error: KeywordErrorDefinition = { message: ({params: {property, depsCount, deps}}) => { const property_ies = depsCount === 1 ? "property" : "properties" @@ -20,7 +27,7 @@ const error: KeywordErrorDefinition = { _`{property: ${property}, missingProperty: ${missingProperty}, depsCount: ${depsCount}, - deps: ${deps}}`, // TODO change to reference? + deps: ${deps}}`, // TODO change to reference } const def: CodeKeywordDefinition = { diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 4bcd3654c..714e38de1 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -4,6 +4,10 @@ import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str, Name} from "../../compile/codegen" +export interface IfErrorParams { + failingKeyword: string +} + const error: KeywordErrorDefinition = { message: ({params}) => str`should match "${params.ifClause}" schema`, params: ({params}) => _`{failingKeyword: ${params.ifClause}}`, diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index cc4c74d54..37f3bfd1e 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -4,6 +4,10 @@ import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_} from "../../compile/codegen" +export interface OneOfErrorParams { + passingSchemas: [number, number] +} + const error: KeywordErrorDefinition = { message: "should match exactly one schema in oneOf", params: ({params}) => _`{passingSchemas: ${params.passing}}`, diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 72d67c4f0..5e775291d 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -4,6 +4,10 @@ import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str} from "../../compile/codegen" +export interface PropertyNamesErrorParams { + propertyName: string +} + const error: KeywordErrorDefinition = { message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? params: ({params}) => _`{propertyName: ${params.propertyName}}`, diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index ea316a0d0..af536da5b 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -7,6 +7,10 @@ import {_, str, nil, Code, Name} from "../../compile/codegen" import N from "../../compile/names" import {SchemaEnv, resolveRef} from "../../compile" +export interface RefErrorParams { + ref: string +} + const error: KeywordErrorDefinition = { message: ({schema}) => str`can't resolve reference ${schema}`, params: ({schema}) => _`{ref: ${schema}}`, diff --git a/lib/vocabularies/errors.ts b/lib/vocabularies/errors.ts new file mode 100644 index 000000000..ea36eabee --- /dev/null +++ b/lib/vocabularies/errors.ts @@ -0,0 +1,69 @@ +import type {ErrorObject} from "../types" +import type {RefErrorParams} from "./core/ref" +import type {TypeErrorParams} from "../compile/validate/dataType" +import type {AdditionalItemsErrorParams} from "./applicator/additionalItems" +import type {AdditionalPropsErrorParams} from "./applicator/additionalProperties" +import type {DependenciesErrorParams} from "./applicator/dependencies" +import type {IfErrorParams} from "./applicator/if" +import type {OneOfErrorParams} from "./applicator/oneOf" +import type {PropertyNamesErrorParams} from "./applicator/propertyNames" +import type {LimitErrorParams, LimitNumberErrorParams} from "./validation/limit" +import type {MultipleOfErrorParams} from "./validation/multipleOf" +import type {PatternErrorParams} from "./validation/pattern" +import type {RequiredErrorParams} from "./validation/required" +import type {UniqueItemsErrorParams} from "./validation/uniqueItems" +import type {ConstErrorParams} from "./validation/const" +import type {EnumErrorParams} from "./validation/enum" +import type {FormatErrorParams} from "./format/format" + +type LimitKeyword = + | "maxItems" + | "minItems" + | "minProperties" + | "maxProperties" + | "minLength" + | "maxLength" + +type LimitNumberKeyword = "maximum" | "minimum" | "exclusiveMaximum" | "exclusiveMinimum" + +type RefError = ErrorObject<"ref", RefErrorParams> +type TypeError = ErrorObject<"type", TypeErrorParams> +type ErrorWithoutParams = ErrorObject< + "anyOf" | "contains" | "not" | "false schema", + Record +> +type AdditionalItemsError = ErrorObject<"additionalItems", AdditionalItemsErrorParams> +type AdditionalPropsError = ErrorObject<"additionalProperties", AdditionalPropsErrorParams> +type DependenciesError = ErrorObject<"dependencies", DependenciesErrorParams> +type IfKeywordError = ErrorObject<"if", IfErrorParams> +type OneOfError = ErrorObject<"oneOf", OneOfErrorParams> +type PropertyNamesError = ErrorObject<"propertyNames", PropertyNamesErrorParams> +type LimitError = ErrorObject +type LimitNumberError = ErrorObject +type MultipleOfError = ErrorObject<"multipleOf", MultipleOfErrorParams> +type PatternError = ErrorObject<"pattern", PatternErrorParams> +type RequiredError = ErrorObject<"required", RequiredErrorParams> +type UniqueItemsError = ErrorObject<"uniqueItems", UniqueItemsErrorParams> +type ConstError = ErrorObject<"const", ConstErrorParams> +type EnumError = ErrorObject<"enum", EnumErrorParams> +type FormatError = ErrorObject<"format", FormatErrorParams> + +export type DefinedError = + | RefError + | TypeError + | ErrorWithoutParams + | AdditionalItemsError + | AdditionalPropsError + | DependenciesError + | IfKeywordError + | OneOfError + | PropertyNamesError + | LimitNumberError + | MultipleOfError + | LimitError + | PatternError + | RequiredError + | UniqueItemsError + | ConstError + | EnumError + | FormatError diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 437af8477..281bc47f7 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -17,6 +17,10 @@ type FormatValidate = | RegExp | string +export interface FormatErrorParams { + format: string +} + const error: KeywordErrorDefinition = { message: ({schemaCode}) => str`should match format "${schemaCode}"`, params: ({schemaCode}) => _`{format: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index f44d7b9b1..ad9352c90 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -3,6 +3,10 @@ import type KeywordCxt from "../../compile/context" import {_} from "../../compile/codegen" import equal from "fast-deep-equal" +export interface ConstErrorParams { + allowedValue: any +} + const error: KeywordErrorDefinition = { message: "should be equal to constant", params: ({schemaCode}) => _`{allowedValue: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index f2bfb302e..de041b9ca 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -3,6 +3,10 @@ import type KeywordCxt from "../../compile/context" import {_, or, Name, Code} from "../../compile/codegen" import equal from "fast-deep-equal" +export interface EnumErrorParams { + allowedValues: any[] +} + const error: KeywordErrorDefinition = { message: "should be equal to one of the allowed values", params: ({schemaCode}) => _`{allowedValues: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 22086f1eb..790894746 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -11,6 +11,15 @@ const OPS: {[index: string]: {fail: Code; ok: Code; okStr: string}} = { exclusiveMinimum: {okStr: ">", ok: ops.GT, fail: ops.LTE}, } +export interface LimitErrorParams { + limit: number +} + +export interface LimitNumberErrorParams { + limit: number + comparison: "<=" | ">=" | "<" | ">" +} + const error: KeywordErrorDefinition = { message: ({keyword, schemaCode}) => str`should be ${OPS[keyword].okStr} ${schemaCode}`, params: ({keyword, schemaCode}) => _`{comparison: ${OPS[keyword].okStr}, limit: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index d1a763e7a..11d4de480 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -2,6 +2,10 @@ import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, str} from "../../compile/codegen" +export interface MultipleOfErrorParams { + multipleOf: number +} + const error: KeywordErrorDefinition = { message: ({schemaCode}) => str`should be multiple of ${schemaCode}`, params: ({schemaCode}) => _`{multipleOf: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 0d41aab58..d51a46268 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -3,6 +3,10 @@ import type KeywordCxt from "../../compile/context" import {usePattern} from "../util" import {_, str} from "../../compile/codegen" +export interface PatternErrorParams { + pattern: string +} + const error: KeywordErrorDefinition = { message: ({schemaCode}) => str`should match pattern "${schemaCode}"`, params: ({schemaCode}) => _`{pattern: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index dc66ff15b..a14de9e77 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -4,6 +4,10 @@ import {propertyInData, noPropertyInData} from "../util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" import {_, str, nil, Name} from "../../compile/codegen" +export interface RequiredErrorParams { + missingProperty: string +} + const error: KeywordErrorDefinition = { message: ({params: {missingProperty}}) => str`should have required property '${missingProperty}'`, params: ({params: {missingProperty}}) => _`{missingProperty: ${missingProperty}}`, diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 53f065efe..f89f0371c 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -5,6 +5,11 @@ import {checkDataTypes, DataType} from "../../compile/util" import {_, str, Name} from "../../compile/codegen" import equal from "fast-deep-equal" +export interface UniqueItemsErrorParams { + i: number + j: number +} + const error: KeywordErrorDefinition = { message: ({params: {i, j}}) => str`should NOT have duplicate items (items ## ${j} and ${i} are identical)`, diff --git a/spec/types/async-validate.spec.ts b/spec/types/async-validate.spec.ts index 82fa02f4c..f8eba8d8b 100644 --- a/spec/types/async-validate.spec.ts +++ b/spec/types/async-validate.spec.ts @@ -1,6 +1,7 @@ import type {AnySchemaObject, SchemaObject, AsyncSchema} from "../../dist/types" import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() interface Foo { foo: number diff --git a/spec/types/error-parameters.spec.ts b/spec/types/error-parameters.spec.ts new file mode 100644 index 000000000..638b5fadc --- /dev/null +++ b/spec/types/error-parameters.spec.ts @@ -0,0 +1,31 @@ +import {DefinedError} from "../../dist/types" +import _Ajv from "../ajv" +import chai from "../chai" +const should = chai.should() + +describe("error object parameters type", () => { + const ajv = new _Ajv({allErrors: true}) + + it("should be determined by the keyword", () => { + const validate = ajv.compile({minimum: 0, multipleOf: 2}) + const valid = validate(-1) + valid.should.equal(false) + const errs = validate.errors + if (errs) { + errs.length.should.equal(2) + for (const err of errs as DefinedError[]) { + switch (err.keyword) { + case "minimum": + err.params.limit.should.equal(0) + err.params.comparison.should.equal(">=") + break + case "multipleOf": + err.params.multipleOf.should.equal(2) + break + default: + should.fail() + } + } + } + }) +})