From 3fc4a153b4fbbd3d5f426767eae7b970a7dc84e0 Mon Sep 17 00:00:00 2001 From: Heath C <51679588+heath-freenome@users.noreply.github.com> Date: Thu, 22 Dec 2022 10:06:22 -0800 Subject: [PATCH] fix: #3239 by providing an ErrorSchemaBuilder class in @rjsf/utils (#3307) fix: #3239 by providing a new `ErrorSchemaBuilder` class in `@rjsf/utils` - In `@rjsf/utils` added `ErrorSchemaBuilder` to facilitate building `ErrorSchema` objects without the need for fancy casting - Exported the new class as part of the main `index.js` - Added 100% unit tests - In `@rjsf/validator-ajv6` and `@rjsf/validator-ajv8` updated the `toErrorSchema()` function to use the `ErrorSchemaBuilder` to simplify the implementation - Also updated the tests to use the `ErrorSchemaBuilder` to replace the expected values that required doing `as ErrorSchema` casting - Updated the `utility-functions.md` file to document `ErrorSchemaBuilder` - Updated the `CHANGELOG.md` accordingly for this fix as well as PR #3297 --- CHANGELOG.md | 14 + docs/api-reference/utility-functions.md | 70 +++++ packages/utils/src/ErrorSchemaBuilder.ts | 118 +++++++ packages/utils/src/index.ts | 2 + .../utils/test/ErrorSchemaBuilder.test.ts | 293 ++++++++++++++++++ packages/validator-ajv6/src/validator.ts | 36 +-- .../validator-ajv6/test/validator.test.ts | 23 +- packages/validator-ajv8/src/validator.ts | 38 +-- .../validator-ajv8/test/validator.test.ts | 53 ++-- 9 files changed, 548 insertions(+), 99 deletions(-) create mode 100644 packages/utils/src/ErrorSchemaBuilder.ts create mode 100644 packages/utils/test/ErrorSchemaBuilder.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0826d50c71..f587615d1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,12 +21,26 @@ should change the heading of the (upcoming) version to include a major version b - Pass the `schema` along to the `ArrayFieldItemTemplate` as part of the fix for [#3253](https://github.com/rjsf-team/react-jsonschema-form/issues/3253) - Tweak Babel configuration to emit ES5-compatible output files, fixing [#3240](https://github.com/rjsf-team/react-jsonschema-form/issues/3240) +## @rjsf/material-ui +- Reverse the condition used in the `onChange` handler in the `RangeWidget`, fixing [#2161](https://github.com/rjsf-team/react-jsonschema-form/issues/2161) + +## @rjsf/mui +- Reverse the condition used in the `onChange` handler in the `RangeWidget`, fixing [#2161](https://github.com/rjsf-team/react-jsonschema-form/issues/2161) + ## @rjsf/utils - Update the `ArrayFieldItemTemplate` to add `schema` as part of the fix for [#3253](https://github.com/rjsf-team/react-jsonschema-form/issues/3253) - Fix improper merging of nested `allOf`s ([#3025](https://github.com/rjsf-team/react-jsonschema-form/pull/3025), [#3227](https://github.com/rjsf-team/react-jsonschema-form/pull/3227)), fixing [#2923](https://github.com/rjsf-team/react-jsonschema-form/pull/2929) +- Added a new `ErrorSchemaBuilder` class to enable building a proper `ErrorSchema` without crazy castings, fixing [#3239](https://github.com/rjsf-team/react-jsonschema-form/issues/3239) + +## @rjsf/validator-ajv6 +- Updated the validator to use the `ErrorSchemaBuilder` in the `toErrorSchema()` function to simplify the implementation + +## @rjsf/validator-ajv8 +- Updated the validator to use the `ErrorSchemaBuilder` in the `toErrorSchema()` function to simplify the implementation ## Dev / docs / playground - Fixed the documentation for `ArrayFieldItemTemplate` as part of the fix for [#3253](https://github.com/rjsf-team/react-jsonschema-form/issues/3253) +- Added documentation for `ErrorSchemaBuilder` in the `utility-functions.md`, fixing [#3239](https://github.com/rjsf-team/react-jsonschema-form/issues/3239) # 5.0.0-beta.14 diff --git a/docs/api-reference/utility-functions.md b/docs/api-reference/utility-functions.md index e57cf92626..a9c5d9969c 100644 --- a/docs/api-reference/utility-functions.md +++ b/docs/api-reference/utility-functions.md @@ -544,3 +544,73 @@ The resulting interface implementation will forward the `validator` and `rootSch #### Returns - SchemaUtilsType - An implementation of a `SchemaUtilsType` interface + +## ErrorSchema builder class + +### ErrorSchemaBuilder(initialSchema?: ErrorSchema) constructor +The `ErrorSchemaBuilder` is used to build an `ErrorSchema` since the definition of the `ErrorSchema` type is designed for reading information rather than writing it. +Use this class to add, replace or clear errors in an error schema by using either dotted path or an array of path names. +Once you are done building the `ErrorSchema`, you can get the result and/or reset all the errors back to an initial set and start again. + +#### Parameters +- [initialSchema]: ErrorSchema - The optional set of initial errors, that will be cloned into the class + +#### Returns +- ErrorSchemaBuilder - The instance of the `ErrorSchemaBuilder` class + +### ErrorSchema getter function +Returns the `ErrorSchema` that has been updated by the methods of the `ErrorSchemaBuilder` + +Usage: + +```ts +import { ErrorSchemaBuilder, ErrorSchema } from "@rjsf/utils"; + +const builder = new ErrorSchemaBuilder(); + +// Do some work using the builder +... + +const errorSchema: ErrorSchema = builder.ErrorSchema; +``` + +### resetAllErrors() +Resets all errors in the `ErrorSchemaBuilder` back to the `initialSchema` if provided, otherwise an empty set. + +#### Parameters +- [initialSchema]: ErrorSchema - The optional set of initial errors, that will be cloned into the class + +#### Returns +- ErrorSchemaBuilder - The instance of the `ErrorSchemaBuilder` class + +### addErrors() +Adds the `errorOrList` to the list of errors in the `ErrorSchema` at either the root level or the location within the schema described by the `pathOfError`. +For more information about how to specify the path see the [eslint lodash plugin docs](https://github.com/wix/eslint-plugin-lodash/blob/master/docs/rules/path-style.md). + +#### Parameters +- errorOrList: string | string[] - The error or list of errors to add into the `ErrorSchema` +- [pathOfError]: string | string[] - The optional path into the `ErrorSchema` at which to add the error(s) + +#### Returns +- ErrorSchemaBuilder - The instance of the `ErrorSchemaBuilder` class + +### setErrors() +Sets/replaces the `errorOrList` as the error(s) in the `ErrorSchema` at either the root level or the location within the schema described by the `pathOfError`. +For more information about how to specify the path see the [eslint lodash plugin docs](https://github.com/wix/eslint-plugin-lodash/blob/master/docs/rules/path-style.md). + +#### Parameters +- errorOrList: string | string[] - The error or list of errors to add into the `ErrorSchema` +- [pathOfError]: string | string[] - The optional path into the `ErrorSchema` at which to add the error(s) + +#### Returns +- ErrorSchemaBuilder - The instance of the `ErrorSchemaBuilder` class + +### clearErrors() +Clears the error(s) in the `ErrorSchema` at either the root level or the location within the schema described by the `pathOfError`. +For more information about how to specify the path see the [eslint lodash plugin docs](https://github.com/wix/eslint-plugin-lodash/blob/master/docs/rules/path-style.md). + +#### Parameters +- [pathOfError]: string | string[] - The optional path into the `ErrorSchema` at which to add the error(s) + +#### Returns +- ErrorSchemaBuilder - The instance of the `ErrorSchemaBuilder` class diff --git a/packages/utils/src/ErrorSchemaBuilder.ts b/packages/utils/src/ErrorSchemaBuilder.ts new file mode 100644 index 0000000000..67d882c1ad --- /dev/null +++ b/packages/utils/src/ErrorSchemaBuilder.ts @@ -0,0 +1,118 @@ +import cloneDeep from "lodash/cloneDeep"; +import get from "lodash/get"; +import set from "lodash/set"; + +import { ErrorSchema } from "./types"; +import { ERRORS_KEY } from "./constants"; + +/** The `ErrorSchemaBuilder` is used to build an `ErrorSchema` since the definition of the `ErrorSchema` type is + * designed for reading information rather than writing it. Use this class to add, replace or clear errors in an error + * schema by using either dotted path or an array of path names. Once you are done building the `ErrorSchema`, you can + * get the result and/or reset all the errors back to an initial set and start again. + */ +export default class ErrorSchemaBuilder { + /** The error schema being built + * + * @private + */ + private errorSchema: ErrorSchema = {}; + + /** Construct an `ErrorSchemaBuilder` with an optional initial set of errors in an `ErrorSchema`. + * + * @param [initialSchema] - The optional set of initial errors, that will be cloned into the class + */ + constructor(initialSchema?: ErrorSchema) { + this.resetAllErrors(initialSchema); + } + + /** Returns the `ErrorSchema` that has been updated by the methods of the `ErrorSchemaBuilder` + */ + get ErrorSchema() { + return this.errorSchema; + } + + /** Will get an existing `ErrorSchema` at the specified `pathOfError` or create and return one. + * + * @param [pathOfError] - The optional path into the `ErrorSchema` at which to add the error(s) + * @returns - The error block for the given `pathOfError` or the root if not provided + * @private + */ + private getOrCreateErrorBlock(pathOfError?: string | string[]) { + const hasPath = + (Array.isArray(pathOfError) && pathOfError.length > 0) || + typeof pathOfError === "string"; + let errorBlock: ErrorSchema = hasPath + ? get(this.errorSchema, pathOfError) + : this.errorSchema; + if (!errorBlock && pathOfError) { + errorBlock = {}; + set(this.errorSchema, pathOfError, errorBlock); + } + return errorBlock; + } + + /** Resets all errors in the `ErrorSchemaBuilder` back to the `initialSchema` if provided, otherwise an empty set. + * + * @param [initialSchema] - The optional set of initial errors, that will be cloned into the class + * @returns - The `ErrorSchemaBuilder` object for chaining purposes + */ + resetAllErrors(initialSchema?: ErrorSchema) { + this.errorSchema = initialSchema ? cloneDeep(initialSchema) : {}; + return this; + } + + /** Adds the `errorOrList` to the list of errors in the `ErrorSchema` at either the root level or the location within + * the schema described by the `pathOfError`. For more information about how to specify the path see the + * [eslint lodash plugin docs](https://github.com/wix/eslint-plugin-lodash/blob/master/docs/rules/path-style.md). + * + * @param errorOrList - The error or list of errors to add into the `ErrorSchema` + * @param [pathOfError] - The optional path into the `ErrorSchema` at which to add the error(s) + * @returns - The `ErrorSchemaBuilder` object for chaining purposes + */ + addErrors(errorOrList: string | string[], pathOfError?: string | string[]) { + const errorBlock: ErrorSchema = this.getOrCreateErrorBlock(pathOfError); + let errorsList = get(errorBlock, ERRORS_KEY); + if (!Array.isArray(errorsList)) { + errorsList = []; + errorBlock[ERRORS_KEY] = errorsList; + } + + if (Array.isArray(errorOrList)) { + errorsList.push(...errorOrList); + } else { + errorsList.push(errorOrList); + } + return this; + } + + /** Sets/replaces the `errorOrList` as the error(s) in the `ErrorSchema` at either the root level or the location + * within the schema described by the `pathOfError`. For more information about how to specify the path see the + * [eslint lodash plugin docs](https://github.com/wix/eslint-plugin-lodash/blob/master/docs/rules/path-style.md). + * + * @param errorOrList - The error or list of errors to set into the `ErrorSchema` + * @param [pathOfError] - The optional path into the `ErrorSchema` at which to set the error(s) + * @returns - The `ErrorSchemaBuilder` object for chaining purposes + */ + setErrors(errorOrList: string | string[], pathOfError?: string | string[]) { + const errorBlock: ErrorSchema = this.getOrCreateErrorBlock(pathOfError); + // Effectively clone the array being given to prevent accidental outside manipulation of the given list + const listToAdd = Array.isArray(errorOrList) + ? [...errorOrList] + : [errorOrList]; + set(errorBlock, ERRORS_KEY, listToAdd); + return this; + } + + /** Clears the error(s) in the `ErrorSchema` at either the root level or the location within the schema described by + * the `pathOfError`. For more information about how to specify the path see the + * [eslint lodash plugin docs](https://github.com/wix/eslint-plugin-lodash/blob/master/docs/rules/path-style.md). + * + * @param [pathOfError] - The optional path into the `ErrorSchema` at which to clear the error(s) + * @returns - The `ErrorSchemaBuilder` object for chaining purposes + */ + clearErrors(pathOfError?: string | string[]) { + const errorBlock: ErrorSchema = this.getOrCreateErrorBlock(pathOfError); + set(errorBlock, ERRORS_KEY, []); + return this; + } +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 6a2c2a1753..b4e447d423 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -4,6 +4,7 @@ import canExpand from "./canExpand"; import createSchemaUtils from "./createSchemaUtils"; import dataURItoBlob from "./dataURItoBlob"; import deepEquals from "./deepEquals"; +import ErrorSchemaBuilder from "./ErrorSchemaBuilder"; import findSchemaDefinition from "./findSchemaDefinition"; import getInputProps from "./getInputProps"; import getSchemaType from "./getSchemaType"; @@ -45,6 +46,7 @@ export { createSchemaUtils, dataURItoBlob, deepEquals, + ErrorSchemaBuilder, findSchemaDefinition, getInputProps, getSchemaType, diff --git a/packages/utils/test/ErrorSchemaBuilder.test.ts b/packages/utils/test/ErrorSchemaBuilder.test.ts new file mode 100644 index 0000000000..4d1fdfad8d --- /dev/null +++ b/packages/utils/test/ErrorSchemaBuilder.test.ts @@ -0,0 +1,293 @@ +import { ErrorSchemaBuilder, ERRORS_KEY, ErrorSchema } from "../src"; + +const AN_ERROR = "an error"; +const SOME_ERRORS = ["error1", "error2"]; +const STRING_PATH = "foo"; +const ARRAY_PATH = ["bar", "baz"]; + +const INITIAL_ROOT = "initial root"; +const INITIAL_STRING = "initial string path"; +const INITIAL_ARRAY = "initial array path"; + +// We have to cast to ErrorSchema because the type is not meant for building +const INITIAL_SCHEMA: ErrorSchema = { + [ERRORS_KEY]: [INITIAL_ROOT], + [STRING_PATH]: { + [ERRORS_KEY]: [INITIAL_STRING], + } as ErrorSchema, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [INITIAL_ARRAY], + }, + }, +} as ErrorSchema; + +describe("ErrorSchemaBuilder", () => { + describe("no initial schema", () => { + let builder: ErrorSchemaBuilder; + beforeAll(() => { + builder = new ErrorSchemaBuilder(); + }); + it("returns an empty error schema after construction", () => { + expect(builder.ErrorSchema).toEqual({}); + }); + it("adding error string without a path puts it into the root", () => { + expect(builder.addErrors(AN_ERROR).ErrorSchema).toEqual({ + [ERRORS_KEY]: [AN_ERROR], + }); + }); + it("setting error string list without a path replaces at the root", () => { + expect(builder.setErrors(SOME_ERRORS).ErrorSchema).toEqual({ + [ERRORS_KEY]: SOME_ERRORS, + }); + }); + it("clearing errors without a path clears them from the root", () => { + expect(builder.clearErrors().ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + }); + }); + it("adding error string with a string path puts it at the path", () => { + expect(builder.addErrors(AN_ERROR, STRING_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [AN_ERROR], + }, + }); + }); + it("setting error string list with a string path replaces errors at the path", () => { + expect(builder.setErrors(SOME_ERRORS, STRING_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: SOME_ERRORS, + }, + }); + }); + it("clearing errors with a string path clears them the path", () => { + expect(builder.clearErrors(STRING_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [], + }, + }); + }); + it("adding error string with a string[] path puts it at the path", () => { + expect(builder.addErrors(AN_ERROR, ARRAY_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [AN_ERROR], + }, + }, + }); + }); + it("setting error string list with a string[] path replaces errors at the path", () => { + expect(builder.setErrors(SOME_ERRORS, ARRAY_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: SOME_ERRORS, + }, + }, + }); + }); + it("setting error string with a new path set errors at the path", () => { + expect( + builder.setErrors(AN_ERROR, ["another", "path"]).ErrorSchema + ).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: SOME_ERRORS, + }, + }, + another: { + path: { + [ERRORS_KEY]: [AN_ERROR], + }, + }, + }); + }); + it("clearing errors with a string[] path clears them the path", () => { + expect(builder.clearErrors(ARRAY_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [], + }, + }, + another: { + path: { + [ERRORS_KEY]: [AN_ERROR], + }, + }, + }); + }); + it("clearing errors with a new path creates an empty block", () => { + expect(builder.clearErrors("newPath").ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [], + }, + }, + another: { + path: { + [ERRORS_KEY]: [AN_ERROR], + }, + }, + newPath: { + [ERRORS_KEY]: [], + }, + }); + }); + it("resetting error restores things back to an empty object", () => { + expect(builder.resetAllErrors().ErrorSchema).toEqual({}); + }); + }); + describe("using initial schema", () => { + let builder: ErrorSchemaBuilder; + beforeAll(() => { + builder = new ErrorSchemaBuilder(INITIAL_SCHEMA); + }); + it("returns the INITIAL_SCHEMA after construction", () => { + expect(builder.ErrorSchema).toEqual(INITIAL_SCHEMA); + }); + it("adding error array with an empty array as a path puts it into the root", () => { + expect(builder.addErrors(SOME_ERRORS, []).ErrorSchema).toEqual({ + [ERRORS_KEY]: [INITIAL_ROOT, ...SOME_ERRORS], + [STRING_PATH]: { + [ERRORS_KEY]: [INITIAL_STRING], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [INITIAL_ARRAY], + }, + }, + }); + }); + it("setting error string without a path replaces at the root", () => { + expect(builder.setErrors(AN_ERROR).ErrorSchema).toEqual({ + [ERRORS_KEY]: [AN_ERROR], + [STRING_PATH]: { + [ERRORS_KEY]: [INITIAL_STRING], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [INITIAL_ARRAY], + }, + }, + }); + }); + it("clearing errors without a path clears them from the root", () => { + expect(builder.clearErrors().ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [INITIAL_STRING], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [INITIAL_ARRAY], + }, + }, + }); + }); + it("adding error string with a string path puts it at the path", () => { + expect(builder.addErrors(AN_ERROR, STRING_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [INITIAL_STRING, AN_ERROR], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [INITIAL_ARRAY], + }, + }, + }); + }); + it("setting error string list with a string path replaces errors at the path", () => { + expect(builder.setErrors(SOME_ERRORS, STRING_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: SOME_ERRORS, + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [INITIAL_ARRAY], + }, + }, + }); + }); + it("clearing errors with a string path clears them the path", () => { + expect(builder.clearErrors(STRING_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [INITIAL_ARRAY], + }, + }, + }); + }); + it("adding error string with a string[] path puts it at the path", () => { + expect(builder.addErrors(AN_ERROR, ARRAY_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [INITIAL_ARRAY, AN_ERROR], + }, + }, + }); + }); + it("setting error string list with a string[] path replaces errors at the path", () => { + expect(builder.setErrors(SOME_ERRORS, ARRAY_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: SOME_ERRORS, + }, + }, + }); + }); + it("clearing errors with a string[] path clears them the path", () => { + expect(builder.clearErrors(ARRAY_PATH).ErrorSchema).toEqual({ + [ERRORS_KEY]: [], + [STRING_PATH]: { + [ERRORS_KEY]: [], + }, + [ARRAY_PATH[0]]: { + [ARRAY_PATH[1]]: { + [ERRORS_KEY]: [], + }, + }, + }); + }); + it("resetting error restores things back to the INITIAL_SCHEMA", () => { + expect(builder.resetAllErrors(INITIAL_SCHEMA).ErrorSchema).toEqual( + INITIAL_SCHEMA + ); + }); + }); +}); diff --git a/packages/validator-ajv6/src/validator.ts b/packages/validator-ajv6/src/validator.ts index 7e3ecb6114..43d6fa5aaa 100644 --- a/packages/validator-ajv6/src/validator.ts +++ b/packages/validator-ajv6/src/validator.ts @@ -3,6 +3,7 @@ import toPath from "lodash/toPath"; import { CustomValidator, ErrorSchema, + ErrorSchemaBuilder, ErrorTransformer, FieldValidation, FormValidation, @@ -70,42 +71,23 @@ export default class AJV6Validator * @private */ private toErrorSchema(errors: RJSFValidationError[]): ErrorSchema { - if (!errors.length) { - return {} as ErrorSchema; - } - return errors.reduce( - (errorSchema: ErrorSchema, error): ErrorSchema => { + const builder = new ErrorSchemaBuilder(); + if (errors.length) { + errors.forEach((error) => { const { property, message } = error; const path = toPath(property); - let parent: GenericObjectType = errorSchema; // If the property is at the root (.level1) then toPath creates // an empty array element at the first index. Remove it. if (path.length > 0 && path[0] === "") { path.splice(0, 1); } - - for (const segment of path.slice(0)) { - if (!(segment in parent)) { - parent[segment] = {}; - } - parent = parent[segment]; - } - - if (Array.isArray(parent.__errors)) { - // We store the list of errors for this node in a property named __errors - // to avoid name collision with a possible sub schema field named - // 'errors' (see `validate.createErrorHandler`). - parent.__errors = parent.__errors.concat(message!); - } else { - if (message) { - parent.__errors = [message]; - } + if (message) { + builder.addErrors(message, path); } - return errorSchema; - }, - {} as ErrorSchema - ); + }); + } + return builder.ErrorSchema; } /** Converts an `errorSchema` into a list of `RJSFValidationErrors` diff --git a/packages/validator-ajv6/test/validator.test.ts b/packages/validator-ajv6/test/validator.test.ts index 35078e8023..12e8727d55 100644 --- a/packages/validator-ajv6/test/validator.test.ts +++ b/packages/validator-ajv6/test/validator.test.ts @@ -1,5 +1,6 @@ import { ErrorSchema, + ErrorSchemaBuilder, FormValidation, RJSFSchema, RJSFValidationError, @@ -19,6 +20,13 @@ const metaSchemaDraft4 = require("ajv/lib/refs/json-schema-draft-04.json"); const metaSchemaDraft6 = require("ajv/lib/refs/json-schema-draft-06.json"); describe("AJV6Validator", () => { + let builder: ErrorSchemaBuilder; + beforeAll(() => { + builder = new ErrorSchemaBuilder(); + }); + afterEach(() => { + builder.resetAllErrors(); + }); describe("default options", () => { // Use the TestValidator to access the `withIdRefPrefix` function let validator: TestValidator; @@ -111,17 +119,10 @@ describe("AJV6Validator", () => { expect(validator.toErrorList()).toEqual([]); }); it("should convert an errorSchema into a flat list", () => { - const errorSchema: ErrorSchema = { - __errors: ["err1", "err2"], - a: { - b: { - __errors: ["err3", "err4"], - } as ErrorSchema, - }, - c: { - __errors: ["err5"], - } as ErrorSchema, - } as unknown as ErrorSchema; + const errorSchema = builder + .addErrors(["err1", "err2"]) + .addErrors(["err3", "err4"], "a.b") + .addErrors(["err5"], "c").ErrorSchema; expect(validator.toErrorList(errorSchema)).toEqual([ { property: ".", message: "err1", stack: ". err1" }, { property: ".", message: "err2", stack: ". err2" }, diff --git a/packages/validator-ajv8/src/validator.ts b/packages/validator-ajv8/src/validator.ts index d0f314aee9..7802a20c67 100644 --- a/packages/validator-ajv8/src/validator.ts +++ b/packages/validator-ajv8/src/validator.ts @@ -6,6 +6,7 @@ import { CustomValidator, ERRORS_KEY, ErrorSchema, + ErrorSchemaBuilder, ErrorTransformer, FieldValidation, FormValidation, @@ -87,42 +88,23 @@ export default class AJV8Validator< * @private */ private toErrorSchema(errors: RJSFValidationError[]): ErrorSchema { - if (!errors.length) { - return {} as ErrorSchema; - } - return errors.reduce( - (errorSchema: ErrorSchema, error): ErrorSchema => { + const builder = new ErrorSchemaBuilder(); + if (errors.length) { + errors.forEach((error) => { const { property, message } = error; const path = toPath(property); - let parent: GenericObjectType = errorSchema; // If the property is at the root (.level1) then toPath creates // an empty array element at the first index. Remove it. if (path.length > 0 && path[0] === "") { path.splice(0, 1); } - - for (const segment of path.slice(0)) { - if (!(segment in parent)) { - parent[segment] = {}; - } - parent = parent[segment]; - } - - if (Array.isArray(parent.__errors)) { - // We store the list of errors for this node in a property named __errors - // to avoid name collision with a possible sub schema field named - // 'errors' (see `validate.createErrorHandler`). - parent.__errors = parent.__errors.concat(message!); - } else { - if (message) { - parent.__errors = [message]; - } + if (message) { + builder.addErrors(message, path); } - return errorSchema; - }, - {} as ErrorSchema - ); + }); + } + return builder.ErrorSchema; } /** Converts an `errorSchema` into a list of `RJSFValidationErrors` @@ -137,7 +119,7 @@ export default class AJV8Validator< let errorList: RJSFValidationError[] = []; if (ERRORS_KEY in errorSchema) { errorList = errorList.concat( - errorSchema.__errors!.map((message: string) => { + errorSchema[ERRORS_KEY]!.map((message: string) => { const property = `.${fieldPath.join(".")}`; return { property, diff --git a/packages/validator-ajv8/test/validator.test.ts b/packages/validator-ajv8/test/validator.test.ts index a133d22c66..8b9a0fb2a6 100644 --- a/packages/validator-ajv8/test/validator.test.ts +++ b/packages/validator-ajv8/test/validator.test.ts @@ -2,6 +2,7 @@ import Ajv2019 from "ajv/dist/2019"; import Ajv2020 from "ajv/dist/2020"; import { ErrorSchema, + ErrorSchemaBuilder, FormValidation, RJSFSchema, RJSFValidationError, @@ -21,6 +22,13 @@ const illFormedKey = "bar`'()=+*&^%$#@!"; const metaSchemaDraft6 = require("ajv/lib/refs/json-schema-draft-06.json"); describe("AJV8Validator", () => { + let builder: ErrorSchemaBuilder; + beforeAll(() => { + builder = new ErrorSchemaBuilder(); + }); + afterEach(() => { + builder.resetAllErrors(); + }); describe("default options", () => { // Use the TestValidator to access the `withIdRefPrefix` function let validator: TestValidator; @@ -141,17 +149,10 @@ describe("AJV8Validator", () => { expect(validator.toErrorList()).toEqual([]); }); it("should convert an errorSchema into a flat list", () => { - const errorSchema: ErrorSchema = { - __errors: ["err1", "err2"], - a: { - b: { - __errors: ["err3", "err4"], - } as ErrorSchema, - }, - c: { - __errors: ["err5"], - } as ErrorSchema, - } as unknown as ErrorSchema; + const errorSchema = builder + .addErrors(["err1", "err2"]) + .addErrors(["err3", "err4"], "a.b") + .addErrors(["err5"], "c").ErrorSchema; expect(validator.toErrorList(errorSchema)).toEqual([ { property: ".", message: "err1", stack: ". err1" }, { property: ".", message: "err2", stack: ". err2" }, @@ -589,17 +590,10 @@ describe("AJV8Validator", () => { expect(validator.toErrorList()).toEqual([]); }); it("should convert an errorSchema into a flat list", () => { - const errorSchema: ErrorSchema = { - __errors: ["err1", "err2"], - a: { - b: { - __errors: ["err3", "err4"], - } as ErrorSchema, - }, - c: { - __errors: ["err5"], - } as ErrorSchema, - } as unknown as ErrorSchema; + const errorSchema = builder + .addErrors(["err1", "err2"]) + .addErrors(["err3", "err4"], "a.b") + .addErrors(["err5"], "c").ErrorSchema; expect(validator.toErrorList(errorSchema)).toEqual([ { property: ".", message: "err1", stack: ". err1" }, { property: ".", message: "err2", stack: ". err2" }, @@ -1038,17 +1032,10 @@ describe("AJV8Validator", () => { expect(validator.toErrorList()).toEqual([]); }); it("should convert an errorSchema into a flat list", () => { - const errorSchema: ErrorSchema = { - __errors: ["err1", "err2"], - a: { - b: { - __errors: ["err3", "err4"], - } as ErrorSchema, - }, - c: { - __errors: ["err5"], - } as ErrorSchema, - } as unknown as ErrorSchema; + const errorSchema = builder + .addErrors(["err1", "err2"]) + .addErrors(["err3", "err4"], "a.b") + .addErrors(["err5"], "c").ErrorSchema; expect(validator.toErrorList(errorSchema)).toEqual([ { property: ".", message: "err1", stack: ". err1" }, { property: ".", message: "err2", stack: ". err2" },