From 4dba8c3a1e10a5613ec5e8b91102f8e01c000a65 Mon Sep 17 00:00:00 2001 From: Nikita Zolotykh Date: Mon, 19 May 2025 12:28:37 +0200 Subject: [PATCH] test(unstable): add service field utils tests --- .../core/SchemaRendererServiceField/types.ts | 112 +- .../core/SchemaRendererServiceField/utils.ts | 450 -- .../__tests__/common.test.ts} | 8 +- .../utils/__tests__/get-ajv-validate.test.ts | 178 + .../utils/__tests__/process-ajv-error.test.ts | 6088 +++++++++++++++++ .../process-entity-parameters-error.test.ts | 240 + .../__tests__/process-error-items.test.ts | 241 + .../__tests__/process-errors-state.test.ts | 128 + .../utils/common.ts | 118 + .../utils/get-ajv-validate.ts | 64 + .../utils/get-validate.ts | 80 + .../SchemaRendererServiceField/utils/index.ts | 1 + .../utils/process-ajv-error.ts | 60 + .../utils/process-ajv-validate-errors.ts | 77 + .../utils/process-entity-parameters-error.ts | 57 + .../utils/process-error-items.ts | 71 + .../utils/process-errors-state.ts | 31 + 17 files changed, 7440 insertions(+), 564 deletions(-) delete mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils.ts rename src/lib/unstable/core/SchemaRendererServiceField/{__tests__/utils.test.ts => utils/__tests__/common.test.ts} (98%) create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/get-ajv-validate.test.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-ajv-error.test.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-entity-parameters-error.test.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-error-items.test.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-errors-state.test.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/common.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/get-ajv-validate.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/get-validate.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/index.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/process-ajv-error.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/process-ajv-validate-errors.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/process-entity-parameters-error.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/process-error-items.ts create mode 100644 src/lib/unstable/core/SchemaRendererServiceField/utils/process-errors-state.ts diff --git a/src/lib/unstable/core/SchemaRendererServiceField/types.ts b/src/lib/unstable/core/SchemaRendererServiceField/types.ts index 41979be7..88710de8 100644 --- a/src/lib/unstable/core/SchemaRendererServiceField/types.ts +++ b/src/lib/unstable/core/SchemaRendererServiceField/types.ts @@ -1,25 +1,6 @@ -import type {ErrorObject, ValidateFunction} from 'ajv'; -import type {FieldValidator} from 'final-form'; +import type {ErrorObject} from 'ajv'; -import type {ARRAY_AND_OBJECT_ERRORS} from '../constants'; -import type { - ErrorsState, - SetValidationCacheMutator, - SetValidationWaitersMutator, - ValidationState, - ValidationWaiter, -} from '../mutators'; -import type { - AsyncValidateError, - ErrorMessages, - FieldValue, - JsonSchema, - ObjectValue, - SchemaRendererConfig, - SchemaToValueType, - SyncValidateError, - Validator, -} from '../types'; +import type {FieldValue, JsonSchema, SyncValidateError, Validator} from '../types'; export type EntityParametersError = ErrorObject< 'entityParameters', @@ -30,96 +11,7 @@ export type EntityParametersError = ErrorObject< } >; -export type GetAjvValidateParams = { - config: SchemaRendererConfig; - mainSchema: JsonSchema; -}; - -export interface GetAjvValidateReturn extends ValidateFunction { - errors?: (ErrorObject | EntityParametersError)[]; -} - -export interface GetAjvErrorMessageParams { - ajvErrorMessage?: string; - errorMessages: ErrorMessages | undefined; - instancePath: string; - keyword: string; - mainSchema: JsonSchema; - schemaPath: string; -} - export interface ValidateErrorItem { error: SyncValidateError; path: string[]; } - -export interface ProcessEntityParametersErrorParams { - allValues: ObjectValue; - error: EntityParametersError; - headName: string; - onAsyncError: (waiter: { - instancePath: string; - params: EntityParametersError['params']; - promise: AsyncValidateError; - }) => void; - onError: (error: ValidateErrorItem) => void; - validationState: ValidationState | undefined; -} - -export interface ProcessAjvErrorParams { - error: ErrorObject; - errorMessages: ErrorMessages | undefined; - headName: string; - mainSchema: Schema; - onError: (error: ValidateErrorItem) => void; -} - -export interface ProcessAjvValidateErrorsParams { - ajvValidateErrors: (ErrorObject | EntityParametersError)[]; - allValues: ObjectValue; - errorMessages: ErrorMessages | undefined; - headName: string; - mainSchema: Schema; - serviceFieldName: string; - setValidationCache: SetValidationCacheMutator; - validationState: ValidationState | undefined; -} - -export interface ProcessAjvValidateErrorsReturn { - ajvErrorItems: ValidateErrorItem[]; - entityParametersErrorItems: ValidateErrorItem[]; - waiters: Record; -} - -export interface ProcessErrorsStateParams { - errorsState: ErrorsState | undefined; -} - -export interface ProcessErrorsStateReturn { - externalPriorityErrorItems: ValidateErrorItem[]; - externalRegularErrorItems: ValidateErrorItem[]; -} - -export interface ProcessErrorItemsParams { - errorItems: ValidateErrorItem[]; - headName: string; - mainSchema: Schema; -} - -export type ProcessErrorItemsReturn = { - [ARRAY_AND_OBJECT_ERRORS]: {[key: string]: boolean | string | undefined}; -} & {[key: string]: SyncValidateError}; - -export type GetValidateParams = { - config: SchemaRendererConfig; - errorMessages?: ErrorMessages; - headName: string; - mainSchema: Schema; - serviceFieldName: string; - setValidationCache: SetValidationCacheMutator; - setValidationWaiters: SetValidationWaitersMutator; -}; - -export type GetValidateReturn = FieldValidator< - SchemaToValueType ->; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils.ts deleted file mode 100644 index 4851804e..00000000 --- a/src/lib/unstable/core/SchemaRendererServiceField/utils.ts +++ /dev/null @@ -1,450 +0,0 @@ -import type {ErrorObject, FuncKeywordDefinition, SchemaValidateFunction} from 'ajv'; -import Ajv from 'ajv'; -import get from 'lodash/get'; -import isBoolean from 'lodash/isBoolean'; -import isEqual from 'lodash/isEqual'; -import isObjectLike from 'lodash/isObjectLike'; -import isString from 'lodash/isString'; -import isUndefined from 'lodash/isUndefined'; -import mapValues from 'lodash/mapValues'; -import omit from 'lodash/omit'; -import set from 'lodash/set'; - -import {ARRAY_AND_OBJECT_ERRORS, EMPTY_OBJECT, JsonSchemaType} from '../constants'; -import type {ErrorsState, ValidationState, ValidationWaiter} from '../mutators'; -import type {FieldValue, JsonSchema, ObjectValue, SyncValidateError, Validator} from '../types'; -import {getSchemaByFinalFormPath, parseFinalFormPath} from '../utils'; - -import type { - EntityParametersError, - GetAjvErrorMessageParams, - GetAjvValidateParams, - GetAjvValidateReturn, - GetValidateParams, - GetValidateReturn, - ProcessAjvErrorParams, - ProcessAjvValidateErrorsParams, - ProcessAjvValidateErrorsReturn, - ProcessEntityParametersErrorParams, - ProcessErrorItemsParams, - ProcessErrorItemsReturn, - ProcessErrorsStateParams, - ProcessErrorsStateReturn, - ValidateErrorItem, -} from './types'; - -export const getAjvValidate = ({ - config, - mainSchema, -}: GetAjvValidateParams): GetAjvValidateReturn => { - function entityParametersValidate(_: unknown, value: FieldValue, schema?: JsonSchema) { - if (schema) { - const validatorType: string | undefined = get(schema, 'entityParameters.validatorType'); - const validator: Validator | undefined = get( - config, - `${schema.type}.validators.${validatorType}`, - ); - - if (validator) { - const error: Partial = { - keyword: 'entityParameters', - message: '', - params: {validator, value, schema}, - }; - - (entityParametersValidate as SchemaValidateFunction).errors = [error]; - - return false; - } - } - - return true; - } - - const ajv = new Ajv({ - allErrors: true, - allowMatchingProperties: true, - keywords: [ - { - errors: true, - keyword: 'entityParameters', - validate: entityParametersValidate as FuncKeywordDefinition['validate'], - }, - ], - }); - const ajvValidate = ajv.compile(mainSchema) as GetAjvValidateReturn; - - return ajvValidate; -}; - -/** - * Extracts the path to the property from a JSON Schema validation error. - * Assumes that the last segment in the `schemaPath` is the keyword - * that triggered the validation error (e.g., "minLength"). - * - * @param schemaPath - A JSON Pointer string representing the schema path, - * e.g., "#/properties/name/minLength" - * @returns An array of path segments leading to the property, excluding the keyword, - * e.g., ['properties', 'name'] - */ -export const parseSchemaPath = (schemaPath: string): string[] => { - return decodeURIComponent(schemaPath) - .slice('#/'.length) - .split('/') - .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~')) - .slice(0, -1); -}; - -export const parseInstancePath = (instancePath: string): string[] => { - if (!instancePath.length) { - return []; - } - - return instancePath - .slice('/'.length) - .split('/') - .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~')); -}; - -/** - * Retrieves the sub-schema from the main schema based on the given schema path. - * Assumes that the last segment in the `schemaPath` is a validation keyword - * (e.g., "minLength") and not part of the property path. - * - * @param schemaPath - A JSON Pointer-style string representing the schema path, - * e.g., "#/properties/name/minLength". - * @param mainSchema - The root JSON schema object. - * - * @example - * const nameSchema = { - * type: JsonSchemaType.String, - * minLength: 5, - * }; - * const objectSchema = { - * type: JsonSchemaType.Object, - * properties: { - * name: nameSchema, - * }, - * }; - * getSchemaFromPath("#/properties/name/minLength", objectSchema); // returns nameSchema - * - * @returns The sub-schema object corresponding to the property path. - */ -export const getSchemaBySchemaPath = ( - schemaPath: string, - mainSchema: JsonSchema, -): JsonSchema | undefined => { - const pathArr = parseSchemaPath(schemaPath); - - if (!pathArr.length) { - return mainSchema; - } - - return get(mainSchema, pathArr); -}; - -export const getSchemaByInstancePath = ( - instancePath: string, - mainSchema: JsonSchema, -): JsonSchema | undefined => { - if (instancePath.length) { - return parseInstancePath(instancePath).reduce((acc: JsonSchema | undefined, segment) => { - const type = get(acc, 'type'); - - if (type === JsonSchemaType.Object) { - return get(acc, `properties.${segment}`); - } else if (type === JsonSchemaType.Array) { - const items = get(acc, 'items'); - - if (Array.isArray(items)) { - return get(items, `[${segment}]`); - } - - return items; - } - - return undefined; - }, mainSchema); - } - - return mainSchema; -}; - -export const getValuePaths = (value: unknown, path: string[] = []) => { - const result: string[][] = []; - - const isObject = (v: unknown): v is Record => - v !== null && typeof v === 'object' && !Array.isArray(v); - - if (Array.isArray(value)) { - value.forEach((_, index) => { - result.push(...getValuePaths(value[index], [...path, `${index}`])); - }); - } else if (isObject(value)) { - Object.keys(value).forEach((key) => { - result.push(...getValuePaths(get(value, key), [...path, key])); - }); - } else if (path.length) { - result.push(path); - } - - return result; -}; - -export const processEntityParametersError = ({ - allValues, - error, - headName, - onAsyncError, - onError, - validationState, -}: ProcessEntityParametersErrorParams) => { - const waiter = validationState?.waiters?.[error.instancePath]; - const cache = validationState?.cache?.[error.instancePath]; - const cacheItem = cache?.find((item) => isEqual(error.params, omit(item, 'result'))); - - if (cacheItem?.result) { - onError({ - error: cacheItem.result, - path: [...parseFinalFormPath(headName), ...parseInstancePath(error.instancePath)], - }); - } else if (!waiter || !isEqual(error.params, waiter)) { - const errorOrPromise = error.params.validator(error.params.value, allValues as ObjectValue); - - if (errorOrPromise instanceof Promise) { - onAsyncError({ - instancePath: error.instancePath, - params: error.params, - promise: errorOrPromise, - }); - } else { - onError({ - error: errorOrPromise, - path: [...parseFinalFormPath(headName), ...parseInstancePath(error.instancePath)], - }); - } - } -}; - -export const getAjvErrorMessage = ({ - ajvErrorMessage, - errorMessages = EMPTY_OBJECT, - instancePath, - keyword, - mainSchema, - schemaPath, -}: GetAjvErrorMessageParams): SyncValidateError => { - const propertyName = instancePath.split('/').pop() as string; - - const getErrorMessageBySchema = (schema: JsonSchema | undefined) => { - const errorOrMap: Record | string | undefined = get( - schema, - `entityParameters.errorMessages.${keyword}`, - ); - const message: string | undefined = isString(errorOrMap) - ? errorOrMap - : get(errorOrMap, propertyName); - - return message; - }; - - return ( - getErrorMessageBySchema(getSchemaBySchemaPath(schemaPath, mainSchema)) || - getErrorMessageBySchema(getSchemaByInstancePath(instancePath, mainSchema)) || - errorMessages[keyword as keyof typeof errorMessages] || - ajvErrorMessage - ); -}; - -export const processAjvError = ({ - error, - errorMessages, - headName, - mainSchema, - onError, -}: ProcessAjvErrorParams) => { - let instancePath = error.instancePath; - let keyword = error.keyword; - let schemaPath = error.schemaPath; - - if (keyword === 'required' || keyword === 'dependencies') { - instancePath += `/${error.params.missingProperty}`; - } else if (keyword === 'if') { - keyword = error.params.failingKeyword; - schemaPath = schemaPath.slice(0, -'if'.length) + error.params.failingKeyword; - } - - onError({ - path: [...parseFinalFormPath(headName), ...parseInstancePath(instancePath)], - error: getAjvErrorMessage({ - ajvErrorMessage: error.message, - errorMessages, - instancePath, - keyword, - mainSchema, - schemaPath, - }), - }); -}; - -const processAjvValidateErrors = ({ - ajvValidateErrors, - allValues, - errorMessages, - headName, - mainSchema, - serviceFieldName, - setValidationCache, - validationState, -}: ProcessAjvValidateErrorsParams): ProcessAjvValidateErrorsReturn => { - const waiters: Record = {}; - const ajvErrorItems: ValidateErrorItem[] = []; - const entityParametersErrorItems: ValidateErrorItem[] = []; - - ajvValidateErrors.forEach((ajvOrEntityParametersError) => { - if (ajvOrEntityParametersError.keyword === 'entityParameters') { - processEntityParametersError({ - allValues: allValues, - error: ajvOrEntityParametersError as EntityParametersError, - headName, - onAsyncError: (w) => { - waiters[w.instancePath] = w.params; - - w.promise.then((result) => { - setValidationCache({ - cache: { - [w.instancePath]: { - ...w.params, - result, - }, - }, - serviceFieldName, - }); - }); - }, - onError: (err) => entityParametersErrorItems.push(err), - validationState, - }); - } else { - processAjvError({ - error: ajvOrEntityParametersError as ErrorObject, - errorMessages, - headName, - mainSchema, - onError: (err) => ajvErrorItems.push(err), - }); - } - }); - - return {ajvErrorItems, entityParametersErrorItems, waiters}; -}; - -const processErrorsState = ({errorsState}: ProcessErrorsStateParams): ProcessErrorsStateReturn => { - const getErrorItems = (errors: ErrorsState['priorityErrors'] | ErrorsState['regularErrors']) => - Object.values( - mapValues(errors, (value, key) => ({ - path: parseFinalFormPath(key), - error: value, - })), - ); - - return { - externalPriorityErrorItems: getErrorItems(errorsState?.priorityErrors), - externalRegularErrorItems: getErrorItems(errorsState?.regularErrors), - }; -}; - -const processErrorItems = ({ - errorItems, - headName, - mainSchema, -}: ProcessErrorItemsParams): ProcessErrorItemsReturn => { - const result: ProcessErrorItemsReturn = { - [ARRAY_AND_OBJECT_ERRORS]: {}, - }; - - const setError = (path: string[], error: boolean | string | undefined) => { - const itemSchema = getSchemaByFinalFormPath(path, headName, mainSchema); - - if (itemSchema) { - const arrayOrObjectSchema = - itemSchema.type === JsonSchemaType.Array || - itemSchema.type === JsonSchemaType.Object; - - if (arrayOrObjectSchema) { - result[ARRAY_AND_OBJECT_ERRORS][path.join('.')] = error; - } else { - set(result, path, error); - } - } - }; - - errorItems.forEach((item) => { - if (!item.error) { - return; - } - - if (isObjectLike(item.error)) { - getValuePaths(item.error).forEach((path) => { - setError([...item.path, ...path], get(item.error, path)); - }); - - return; - } - - if (isBoolean(item.error) || isString(item.error) || isUndefined(item.error)) { - setError(item.path, item.error); - - return; - } - }); - - return result; -}; - -export const getValidate = ({ - config, - errorMessages, - headName, - mainSchema, - serviceFieldName, - setValidationCache, - setValidationWaiters, -}: GetValidateParams): GetValidateReturn => { - const ajvValidate = getAjvValidate({config, mainSchema}); - - return (_value, allValues, meta) => { - ajvValidate(get(allValues, headName)); - - const {ajvErrorItems, entityParametersErrorItems, waiters} = processAjvValidateErrors({ - ajvValidateErrors: ajvValidate.errors || [], - allValues: allValues as ObjectValue, - errorMessages, - headName, - mainSchema, - serviceFieldName, - setValidationCache, - validationState: meta?.data as ValidationState | undefined, - }); - const {externalPriorityErrorItems, externalRegularErrorItems} = processErrorsState({ - errorsState: meta?.data as ErrorsState | undefined, - }); - - if (Object.keys(waiters).length) { - setValidationWaiters({serviceFieldName, waiters}); - } - - const result = processErrorItems({ - errorItems: [ - ...externalRegularErrorItems, - ...ajvErrorItems, - ...entityParametersErrorItems, - ...externalPriorityErrorItems, - ], - headName, - mainSchema, - }); - - return result; - }; -}; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/__tests__/utils.test.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/common.test.ts similarity index 98% rename from src/lib/unstable/core/SchemaRendererServiceField/__tests__/utils.test.ts rename to src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/common.test.ts index b7633124..f455c9cd 100644 --- a/src/lib/unstable/core/SchemaRendererServiceField/__tests__/utils.test.ts +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/common.test.ts @@ -1,17 +1,17 @@ -import {JsonSchemaType} from '../../constants'; +import {JsonSchemaType} from '../../../constants'; import type { JsonSchemaArray, JsonSchemaNumber, JsonSchemaObject, JsonSchemaString, -} from '../../types'; +} from '../../../types'; import { getSchemaByInstancePath, getSchemaBySchemaPath, getValuePaths, parseInstancePath, parseSchemaPath, -} from '../utils'; +} from '../common'; const nameSchema: JsonSchemaString = {type: JsonSchemaType.String}; const streetSchema: JsonSchemaString = {type: JsonSchemaType.String}; @@ -40,7 +40,7 @@ const mainSchema: JsonSchemaObject = { }, }; -describe('SchemaRendererServiceField/utils', () => { +describe('SchemaRendererServiceField/utils/common', () => { describe('parseSchemaPath', () => { it('should parse a simple schema path', () => { const result = parseSchemaPath('#/properties/name/minLength'); diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/get-ajv-validate.test.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/get-ajv-validate.test.ts new file mode 100644 index 00000000..6556e13d --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/get-ajv-validate.test.ts @@ -0,0 +1,178 @@ +import {JsonSchemaType} from '../../../constants'; +import type { + JsonSchemaNumber, + JsonSchemaObject, + JsonSchemaString, + SchemaRendererConfig, + Validator, +} from '../../../types'; +import {getAjvValidate} from '../get-ajv-validate'; + +describe('SchemaRendererServiceField/utils/get-ajv-validate', () => { + const emailValidator: Validator = (value) => { + if (typeof value !== 'string' || !value.includes('@')) { + return 'Invalid email format'; + } + + return false; + }; + const emailSchema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + validatorType: 'emailValidator', + }, + }; + const emailValue = 'email'; + const emailError = { + instancePath: '/email', + keyword: 'entityParameters', + message: '', + params: {validator: emailValidator, value: emailValue, schema: emailSchema}, + schemaPath: '#/properties/email/entityParameters', + }; + + const ageValidator: Validator = (value) => { + if (typeof value !== 'number' || value < 18 || value > 100) { + return 'Age must be between 18 and 100'; + } + + return false; + }; + const ageSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + validatorType: 'ageValidator', + }, + }; + const ageValue = 10; + const ageError = { + instancePath: '/age', + keyword: 'entityParameters', + message: '', + params: {validator: ageValidator, value: ageValue, schema: ageSchema}, + schemaPath: '#/properties/age/entityParameters', + }; + + it('should validate with entityParameters validator', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + properties: {email: emailSchema}, + }; + + const config: SchemaRendererConfig = { + [JsonSchemaType.Array]: { + views: {}, + wrappers: {}, + validators: {}, + }, + [JsonSchemaType.Boolean]: { + views: {}, + wrappers: {}, + validators: {}, + }, + [JsonSchemaType.Number]: { + views: {}, + wrappers: {}, + validators: {}, + }, + [JsonSchemaType.Object]: { + views: {}, + wrappers: {}, + validators: {}, + }, + [JsonSchemaType.String]: { + views: {}, + wrappers: {}, + validators: {emailValidator}, + }, + }; + + const ajvValidate = getAjvValidate({config, mainSchema}); + + ajvValidate({email: emailValue}); + + expect(ajvValidate.errors).toEqual([emailError]); + }); + + it('should handle multiple validators for different fields', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + properties: {email: emailSchema, age: ageSchema}, + }; + + const config: SchemaRendererConfig = { + [JsonSchemaType.Array]: { + views: {}, + wrappers: {}, + validators: {}, + }, + [JsonSchemaType.Boolean]: { + views: {}, + wrappers: {}, + validators: {}, + }, + [JsonSchemaType.Number]: { + views: {}, + wrappers: {}, + validators: {ageValidator}, + }, + [JsonSchemaType.Object]: { + views: {}, + wrappers: {}, + validators: {}, + }, + [JsonSchemaType.String]: { + views: {}, + wrappers: {}, + validators: {emailValidator}, + }, + }; + + const ajvValidate = getAjvValidate({config, mainSchema}); + + ajvValidate({email: emailValue, age: ageValue}); + + expect(ajvValidate.errors).toEqual([emailError, ageError]); + }); + + it('should handle empty validator type', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + properties: {email: emailSchema, age: ageSchema}, + }; + + const config: SchemaRendererConfig = { + [JsonSchemaType.Array]: { + views: {}, + wrappers: {}, + validators: {}, + }, + [JsonSchemaType.Boolean]: { + views: {}, + wrappers: {}, + validators: {}, + }, + [JsonSchemaType.Number]: { + views: {}, + wrappers: {}, + validators: {}, + }, + [JsonSchemaType.Object]: { + views: {}, + wrappers: {}, + validators: {}, + }, + [JsonSchemaType.String]: { + views: {}, + wrappers: {}, + validators: {emailValidator}, + }, + }; + + const ajvValidate = getAjvValidate({config, mainSchema}); + + ajvValidate({email: emailValue, age: ageValue}); + + expect(ajvValidate.errors).toEqual([emailError]); + }); +}); diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-ajv-error.test.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-ajv-error.test.ts new file mode 100644 index 00000000..ac29e5a3 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-ajv-error.test.ts @@ -0,0 +1,6088 @@ +import Ajv from 'ajv'; + +import {JsonSchemaType} from '../../../constants'; +import type { + ArrayValue, + JsonSchemaArray, + JsonSchemaBoolean, + JsonSchemaNumber, + JsonSchemaObject, + JsonSchemaString, + ObjectValue, +} from '../../../types'; +import {processAjvError} from '../process-ajv-error'; + +const ajv = new Ajv({ + allErrors: true, + allowMatchingProperties: true, +}); + +describe('SchemaRendererServiceField/utils/process-ajv-error', () => { + describe('with type array schema', () => { + const mainSchema: JsonSchemaArray = {type: JsonSchemaType.Array}; + const errorMessages = {type: 'type error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/type', + keyword: 'type', + params: { + type: 'array', + }, + message: 'must be array', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 'not array'; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.type, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const typeErrorMessage = 'type error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + type: typeErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: typeErrorMessage, + }); + }); + }); + + describe('with boolean contains array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + contains: true, + }; + const errorMessages = {contains: 'boolean contains error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/contains', + keyword: 'contains', + params: { + minContains: 1, + }, + message: 'must contain at least 1 valid item(s)', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = []; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.contains, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const containsErrorMessage = 'boolean contains error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + contains: containsErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: containsErrorMessage, + }); + }); + }); + + describe('with schema contains array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + contains: {type: JsonSchemaType.Number}, + }; + const errorMessages = {contains: 'schema contains error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/contains', + keyword: 'contains', + params: { + minContains: 1, + }, + message: 'must contain at least 1 valid item(s)', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = []; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.contains, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const containsErrorMessage = 'schema contains error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + contains: containsErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: containsErrorMessage, + }); + }); + }); + + describe('with max items array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + maxItems: 0, + }; + const errorMessages = {maxItems: 'max items error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/maxItems', + keyword: 'maxItems', + params: { + limit: 0, + }, + message: 'must NOT have more than 0 items', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = ['']; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.maxItems, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const maxItemsErrorMessage = 'max items error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + maxItems: maxItemsErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: maxItemsErrorMessage, + }); + }); + }); + + describe('with min items array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + minItems: 1, + }; + const errorMessages = {minItems: 'min items error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/minItems', + keyword: 'minItems', + params: { + limit: 1, + }, + message: 'must NOT have fewer than 1 items', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = []; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.minItems, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const minItemsErrorMessage = 'min items error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + minItems: minItemsErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: minItemsErrorMessage, + }); + }); + }); + + describe('with unique items array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + uniqueItems: true, + }; + const errorMessages = {uniqueItems: 'unique items error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/uniqueItems', + keyword: 'uniqueItems', + params: { + i: 1, + j: 0, + }, + message: 'must NOT have duplicate items (items ## 0 and 1 are identical)', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = ['', '']; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.uniqueItems, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const uniqueItemsErrorMessage = 'unique items error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + uniqueItems: uniqueItemsErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: uniqueItemsErrorMessage, + }); + }); + }); + + describe('with all of array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + allOf: [{type: JsonSchemaType.Array, const: ['value']}], + }; + const errorMessages = {const: 'const(by all of) error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/allOf/0/const', + keyword: 'const', + params: { + allowedValue: ['value'], + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = []; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters by schema path', () => { + const allOfConstErrorMessage = + 'const(by all of, by schema path) error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + allOf: [ + { + type: JsonSchemaType.Array, + const: ['value'], + entityParameters: { + errorMessages: { + const: allOfConstErrorMessage, + }, + }, + }, + ], + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: allOfConstErrorMessage, + }); + }); + + it('should call onError with message from schema entity parameters by instance path', () => { + const allOfConstErrorMessage = + 'const(by all of, by instance path) error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + const: allOfConstErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: allOfConstErrorMessage, + }); + }); + }); + + describe('with any of array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + anyOf: [{type: JsonSchemaType.Array, const: ['value']}], + }; + const errorMessages = {anyOf: 'any of error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/anyOf', + keyword: 'anyOf', + params: {}, + message: 'must match a schema in anyOf', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/anyOf/0/const', + keyword: 'const', + params: { + allowedValue: ['value'], + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = []; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.anyOf, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const anyOfErrorMessage = 'any of error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + anyOf: anyOfErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: anyOfErrorMessage, + }); + }); + }); + + describe('with const array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + const: ['value'], + }; + const errorMessages = {const: 'const error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/const', + keyword: 'const', + params: { + allowedValue: ['value'], + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = []; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const constErrorMessage = 'const error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + const: constErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: constErrorMessage, + }); + }); + }); + + describe('with if then array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + if: {type: JsonSchemaType.Array, const: ['']}, + then: {type: JsonSchemaType.Array, const: ['value']}, + }; + const errorMessages = {then: 'if then error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/if', + keyword: 'if', + params: { + failingKeyword: 'then', + }, + message: 'must match "then" schema', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/then/const', + keyword: 'const', + params: { + allowedValue: ['value'], + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = ['']; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.then, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const ifThenErrorMessage = 'if then error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + then: ifThenErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: ifThenErrorMessage, + }); + }); + }); + + describe('with if else array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + if: {type: JsonSchemaType.Array, const: ['value']}, + else: {type: JsonSchemaType.Array, const: ['value']}, + }; + const errorMessages = {else: 'if else error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/if', + keyword: 'if', + params: { + failingKeyword: 'else', + }, + message: 'must match "else" schema', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/else/const', + keyword: 'const', + params: { + allowedValue: ['value'], + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = ['']; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.else, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const ifElseErrorMessage = 'if else error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + else: ifElseErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: ifElseErrorMessage, + }); + }); + }); + + describe('with enum array schema', () => { + const mainSchema: JsonSchemaArray = {type: JsonSchemaType.Array, enum: [['value']]}; + const errorMessages = {enum: 'enum error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/enum', + keyword: 'enum', + params: { + allowedValues: [['value']], + }, + message: 'must be equal to one of the allowed values', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = ['']; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.enum, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const enumErrorMessage = 'enum error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + enum: enumErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: enumErrorMessage, + }); + }); + }); + + describe('with not array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + not: {type: JsonSchemaType.Array, const: ['value']}, + }; + const errorMessages = {not: 'not error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/not', + keyword: 'not', + params: {}, + message: 'must NOT be valid', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = ['value']; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.not, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const notErrorMessage = 'not error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + not: notErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: notErrorMessage, + }); + }); + }); + + describe('with one of array schema', () => { + const mainSchema: JsonSchemaArray = { + type: JsonSchemaType.Array, + oneOf: [{type: JsonSchemaType.Array, const: ['value']}], + }; + const errorMessages = {oneOf: 'one of error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/oneOf', + keyword: 'oneOf', + params: { + passingSchemas: null, + }, + message: 'must match exactly one schema in oneOf', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/oneOf/0/const', + keyword: 'const', + params: { + allowedValue: ['value'], + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ArrayValue = ['']; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.oneOf, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const oneOfErrorMessage = 'one of error from schema entity parameters'; + const schema: JsonSchemaArray = { + type: JsonSchemaType.Array, + entityParameters: { + errorMessages: { + oneOf: oneOfErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: oneOfErrorMessage, + }); + }); + }); + + describe('with type boolean schema', () => { + const mainSchema: JsonSchemaBoolean = {type: JsonSchemaType.Boolean}; + const errorMessages = {type: 'type error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/type', + keyword: 'type', + params: { + type: 'boolean', + }, + message: 'must be boolean', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 'not boolean'; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.type, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const typeErrorMessage = 'type error from schema entity parameters'; + const schema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + entityParameters: { + errorMessages: { + type: typeErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: typeErrorMessage, + }); + }); + }); + + describe('with all of boolean schema', () => { + const mainSchema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + allOf: [{type: JsonSchemaType.Boolean, const: true}], + }; + const errorMessages = {const: 'const(by all of) error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/allOf/0/const', + keyword: 'const', + params: { + allowedValue: true, + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = false; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters by schema path', () => { + const allOfConstErrorMessage = + 'const(by all of, by schema path) error from schema entity parameters'; + const schema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + allOf: [ + { + type: JsonSchemaType.Boolean, + const: true, + entityParameters: { + errorMessages: { + const: allOfConstErrorMessage, + }, + }, + }, + ], + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: allOfConstErrorMessage, + }); + }); + + it('should call onError with message from schema entity parameters by instance path', () => { + const allOfConstErrorMessage = + 'const(by all of, by instance path) error from schema entity parameters'; + const schema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + entityParameters: { + errorMessages: { + const: allOfConstErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: allOfConstErrorMessage, + }); + }); + }); + + describe('with any of boolean schema', () => { + const mainSchema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + anyOf: [{type: JsonSchemaType.Boolean, const: true}], + }; + const errorMessages = {anyOf: 'any of error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/anyOf', + keyword: 'anyOf', + params: {}, + message: 'must match a schema in anyOf', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/anyOf/0/const', + keyword: 'const', + params: { + allowedValue: true, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = false; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.anyOf, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const anyOfErrorMessage = 'any of error from schema entity parameters'; + const schema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + entityParameters: { + errorMessages: { + anyOf: anyOfErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: anyOfErrorMessage, + }); + }); + }); + + describe('with const boolean schema', () => { + const mainSchema: JsonSchemaBoolean = {type: JsonSchemaType.Boolean, const: true}; + const errorMessages = {const: 'const error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/const', + keyword: 'const', + params: { + allowedValue: true, + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = false; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const constErrorMessage = 'const error from schema entity parameters'; + const schema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + entityParameters: { + errorMessages: { + const: constErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: constErrorMessage, + }); + }); + }); + + describe('with if then boolean schema', () => { + const mainSchema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + if: {type: JsonSchemaType.Boolean, const: false}, + then: {type: JsonSchemaType.Boolean, const: true}, + }; + const errorMessages = {then: 'if then error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/if', + keyword: 'if', + params: { + failingKeyword: 'then', + }, + message: 'must match "then" schema', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/then/const', + keyword: 'const', + params: { + allowedValue: true, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = false; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.then, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const ifThenErrorMessage = 'if then error from schema entity parameters'; + const schema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + entityParameters: { + errorMessages: { + then: ifThenErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: ifThenErrorMessage, + }); + }); + }); + + describe('with if else boolean schema', () => { + const mainSchema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + if: {type: JsonSchemaType.Boolean, const: true}, + else: {type: JsonSchemaType.Boolean, const: true}, + }; + const errorMessages = {else: 'if else error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/if', + keyword: 'if', + params: { + failingKeyword: 'else', + }, + message: 'must match "else" schema', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/else/const', + keyword: 'const', + params: { + allowedValue: true, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = false; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.else, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const ifElseErrorMessage = 'if else error from schema entity parameters'; + const schema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + entityParameters: { + errorMessages: { + else: ifElseErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: ifElseErrorMessage, + }); + }); + }); + + describe('with enum boolean schema', () => { + const mainSchema: JsonSchemaBoolean = {type: JsonSchemaType.Boolean, enum: [true]}; + const errorMessages = {enum: 'enum error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/enum', + keyword: 'enum', + params: { + allowedValues: [true], + }, + message: 'must be equal to one of the allowed values', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = false; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.enum, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const enumErrorMessage = 'enum error from schema entity parameters'; + const schema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + entityParameters: { + errorMessages: { + enum: enumErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: enumErrorMessage, + }); + }); + }); + + describe('with not boolean schema', () => { + const mainSchema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + not: {type: JsonSchemaType.Boolean, const: false}, + }; + const errorMessages = {not: 'not error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/not', + keyword: 'not', + params: {}, + message: 'must NOT be valid', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = false; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.not, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const notErrorMessage = 'not error from schema entity parameters'; + const schema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + entityParameters: { + errorMessages: { + not: notErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: notErrorMessage, + }); + }); + }); + + describe('with one of boolean schema', () => { + const mainSchema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + oneOf: [{type: JsonSchemaType.Boolean, const: true}], + }; + const errorMessages = {oneOf: 'one of error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/oneOf', + keyword: 'oneOf', + params: { + passingSchemas: null, + }, + message: 'must match exactly one schema in oneOf', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/oneOf/0/const', + keyword: 'const', + params: { + allowedValue: true, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = false; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.oneOf, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const oneOfErrorMessage = 'one of error from schema entity parameters'; + const schema: JsonSchemaBoolean = { + type: JsonSchemaType.Boolean, + entityParameters: { + errorMessages: { + oneOf: oneOfErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: oneOfErrorMessage, + }); + }); + }); + + describe('with type number schema', () => { + const mainSchema: JsonSchemaNumber = {type: JsonSchemaType.Number}; + const errorMessages = {type: 'type error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/type', + keyword: 'type', + params: { + type: 'number', + }, + message: 'must be number', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 'not number'; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.type, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const typeErrorMessage = 'type error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + type: typeErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: typeErrorMessage, + }); + }); + }); + + describe('with exclusive maximum number schema', () => { + const mainSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + exclusiveMaximum: 1, + }; + const errorMessages = {exclusiveMaximum: 'exclusive maximum error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/exclusiveMaximum', + keyword: 'exclusiveMaximum', + params: { + comparison: '<', + limit: 1, + }, + message: 'must be < 1', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 1; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.exclusiveMaximum, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const exclusiveMaximumErrorMessage = + 'exclusive maximum error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + exclusiveMaximum: exclusiveMaximumErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: exclusiveMaximumErrorMessage, + }); + }); + }); + + describe('with exclusive minimum number schema', () => { + const mainSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + exclusiveMinimum: 1, + }; + const errorMessages = {exclusiveMinimum: 'exclusive minimum error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/exclusiveMinimum', + keyword: 'exclusiveMinimum', + params: { + comparison: '>', + limit: 1, + }, + message: 'must be > 1', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 1; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.exclusiveMinimum, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const exclusiveMinimumErrorMessage = + 'exclusive minimum error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + exclusiveMinimum: exclusiveMinimumErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: exclusiveMinimumErrorMessage, + }); + }); + }); + + describe('with maximum number schema', () => { + const mainSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + maximum: 1, + }; + const errorMessages = {maximum: 'maximum error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/maximum', + keyword: 'maximum', + params: { + comparison: '<=', + limit: 1, + }, + message: 'must be <= 1', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 2; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.maximum, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const maximumErrorMessage = 'maximum error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + maximum: maximumErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: maximumErrorMessage, + }); + }); + }); + + describe('with minimum number schema', () => { + const mainSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + minimum: 1, + }; + const errorMessages = {minimum: 'minimum error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/minimum', + keyword: 'minimum', + params: { + comparison: '>=', + limit: 1, + }, + message: 'must be >= 1', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 0; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.minimum, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const minimumErrorMessage = 'minimum error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + minimum: minimumErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: minimumErrorMessage, + }); + }); + }); + + describe('with multiple of number schema', () => { + const mainSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + multipleOf: 2, + }; + const errorMessages = {multipleOf: 'multiple of error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/multipleOf', + keyword: 'multipleOf', + params: { + multipleOf: 2, + }, + message: 'must be multiple of 2', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 1; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.multipleOf, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const multipleOfErrorMessage = 'multiple of error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + multipleOf: multipleOfErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: multipleOfErrorMessage, + }); + }); + }); + + describe('with all of number schema', () => { + const mainSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + allOf: [{type: JsonSchemaType.Number, const: 1}], + }; + const errorMessages = {const: 'const(by all of) error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/allOf/0/const', + keyword: 'const', + params: { + allowedValue: 1, + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 0; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters by schema path', () => { + const allOfConstErrorMessage = + 'const(by all of, by schema path) error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + allOf: [ + { + type: JsonSchemaType.Number, + const: 1, + entityParameters: { + errorMessages: { + const: allOfConstErrorMessage, + }, + }, + }, + ], + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: allOfConstErrorMessage, + }); + }); + + it('should call onError with message from schema entity parameters by instance path', () => { + const allOfConstErrorMessage = + 'const(by all of, by instance path) error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + const: allOfConstErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: allOfConstErrorMessage, + }); + }); + }); + + describe('with any of number schema', () => { + const mainSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + anyOf: [{type: JsonSchemaType.Number, const: 1}], + }; + const errorMessages = {anyOf: 'any of error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/anyOf', + keyword: 'anyOf', + params: {}, + message: 'must match a schema in anyOf', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/anyOf/0/const', + keyword: 'const', + params: { + allowedValue: 1, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 0; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.anyOf, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const anyOfErrorMessage = 'any of error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + anyOf: anyOfErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: anyOfErrorMessage, + }); + }); + }); + + describe('with const number schema', () => { + const mainSchema: JsonSchemaNumber = {type: JsonSchemaType.Number, const: 1}; + const errorMessages = {const: 'const error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/const', + keyword: 'const', + params: { + allowedValue: 1, + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 0; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const constErrorMessage = 'const error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + const: constErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: constErrorMessage, + }); + }); + }); + + describe('with if then number schema', () => { + const mainSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + if: {type: JsonSchemaType.Number, const: 1}, + then: {type: JsonSchemaType.Number, const: 2}, + }; + const errorMessages = {then: 'if then error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/if', + keyword: 'if', + params: { + failingKeyword: 'then', + }, + message: 'must match "then" schema', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/then/const', + keyword: 'const', + params: { + allowedValue: 2, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 1; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.then, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const ifThenErrorMessage = 'if then error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + then: ifThenErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: ifThenErrorMessage, + }); + }); + }); + + describe('with if else number schema', () => { + const mainSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + if: {type: JsonSchemaType.Number, const: 1}, + else: {type: JsonSchemaType.Number, const: 2}, + }; + const errorMessages = {else: 'if else error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/if', + keyword: 'if', + params: { + failingKeyword: 'else', + }, + message: 'must match "else" schema', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/else/const', + keyword: 'const', + params: { + allowedValue: 2, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 0; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.else, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const ifElseErrorMessage = 'if else error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + else: ifElseErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: ifElseErrorMessage, + }); + }); + }); + + describe('with enum number schema', () => { + const mainSchema: JsonSchemaNumber = {type: JsonSchemaType.Number, enum: [1]}; + const errorMessages = {enum: 'enum error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/enum', + keyword: 'enum', + params: { + allowedValues: [1], + }, + message: 'must be equal to one of the allowed values', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 0; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.enum, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const enumErrorMessage = 'enum error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + enum: enumErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: enumErrorMessage, + }); + }); + }); + + describe('with not number schema', () => { + const mainSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + not: {type: JsonSchemaType.Number, const: 1}, + }; + const errorMessages = {not: 'not error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/not', + keyword: 'not', + params: {}, + message: 'must NOT be valid', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 1; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.not, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const notErrorMessage = 'not error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + not: notErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: notErrorMessage, + }); + }); + }); + + describe('with one of number schema', () => { + const mainSchema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + oneOf: [{type: JsonSchemaType.Number, const: 1}], + }; + const errorMessages = {oneOf: 'one of error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/oneOf', + keyword: 'oneOf', + params: { + passingSchemas: null, + }, + message: 'must match exactly one schema in oneOf', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/oneOf/0/const', + keyword: 'const', + params: { + allowedValue: 1, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 0; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.oneOf, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const oneOfErrorMessage = 'one of error from schema entity parameters'; + const schema: JsonSchemaNumber = { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + oneOf: oneOfErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: oneOfErrorMessage, + }); + }); + }); + + describe('with type object schema', () => { + const mainSchema: JsonSchemaObject = {type: JsonSchemaType.Object}; + const errorMessages = {type: 'type error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/type', + keyword: 'type', + params: { + type: 'object', + }, + message: 'must be object', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 'not object'; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.type, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const typeErrorMessage = 'type error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + type: typeErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: typeErrorMessage, + }); + }); + }); + + describe('with boolean additional properties object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + additionalProperties: false, + }; + const errorMessages = { + additionalProperties: 'boolean additional properties error from error messages', + }; + const error = { + instancePath: '', + schemaPath: '#/additionalProperties', + keyword: 'additionalProperties', + params: { + additionalProperty: 'additional', + }, + message: 'must NOT have additional properties', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {additional: 'value'}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.additionalProperties, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const additionalPropertiesErrorMessage = + 'boolean additional properties error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + additionalProperties: additionalPropertiesErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: additionalPropertiesErrorMessage, + }); + }); + }); + + describe('with schema additional properties object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + additionalProperties: {type: JsonSchemaType.Number}, + }; + const errorMessages = { + type: 'type(by schema additional properties) error from error messages', + }; + const error = { + instancePath: '/additional', + schemaPath: '#/additionalProperties/type', + keyword: 'type', + params: { + type: 'number', + }, + message: 'must be number', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {additional: 'value'}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['additional'], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['additional'], + error: errorMessages.type, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const additionalPropertiesTypeErrorMessage = + 'type(by schema additional properties) error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + additionalProperties: { + type: JsonSchemaType.Number, + entityParameters: { + errorMessages: { + type: additionalPropertiesTypeErrorMessage, + }, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['additional'], + error: additionalPropertiesTypeErrorMessage, + }); + }); + }); + + describe('with string dependencies properties object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + properties: { + first: {type: JsonSchemaType.String}, + second: {type: JsonSchemaType.String}, + }, + dependencies: { + second: ['first'], + }, + }; + const errorMessages = { + dependencies: 'string dependencies error from error messages', + }; + const error = { + instancePath: '', + schemaPath: '#/dependencies', + keyword: 'dependencies', + params: { + property: 'second', + missingProperty: 'first', + depsCount: 1, + deps: 'first', + }, + message: 'must have property first when property second is present', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {second: ''}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: errorMessages.dependencies, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const dependenciesErrorMessage = + 'string dependencies error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + dependencies: dependenciesErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: dependenciesErrorMessage, + }); + }); + }); + + describe('with schema dependencies properties object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + properties: { + first: {type: JsonSchemaType.String}, + second: {type: JsonSchemaType.String}, + }, + dependencies: { + second: { + type: JsonSchemaType.Object, + properties: { + first: { + type: JsonSchemaType.String, + const: 'value', + }, + }, + }, + }, + }; + const errorMessages = { + const: 'const(by schema dependencies) error from error messages', + }; + const error = { + instancePath: '/first', + schemaPath: '#/dependencies/second/properties/first/const', + keyword: 'const', + params: { + allowedValue: 'value', + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {first: '', second: ''}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters by instance path', () => { + const constErrorMessage = 'const error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + properties: { + first: { + type: JsonSchemaType.String, + entityParameters: {errorMessages: {const: constErrorMessage}}, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: constErrorMessage, + }); + }); + + it('should call onError with message from schema entity parameters by schema path', () => { + const dependenciesConstErrorMessage = + 'const(by schema dependencies) error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + dependencies: { + second: { + type: JsonSchemaType.Object, + properties: { + first: { + type: JsonSchemaType.String, + const: 'value', + entityParameters: { + errorMessages: { + const: dependenciesConstErrorMessage, + }, + }, + }, + }, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: dependenciesConstErrorMessage, + }); + }); + }); + + describe('with max properties object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + maxProperties: 0, + }; + const errorMessages = {maxProperties: 'max properties error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/maxProperties', + keyword: 'maxProperties', + params: { + limit: 0, + }, + message: 'must NOT have more than 0 properties', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {first: ''}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.maxProperties, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const maxPropertiesErrorMessage = 'max properties error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + maxProperties: maxPropertiesErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: maxPropertiesErrorMessage, + }); + }); + }); + + describe('with min properties object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + minProperties: 1, + }; + const errorMessages = {minProperties: 'min properties error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/minProperties', + keyword: 'minProperties', + params: { + limit: 1, + }, + message: 'must NOT have fewer than 1 properties', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.minProperties, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const minPropertiesErrorMessage = 'min properties error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + minProperties: minPropertiesErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: minPropertiesErrorMessage, + }); + }); + }); + + describe('with pattern properties object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + patternProperties: { + '^first': { + type: JsonSchemaType.String, + const: 'value', + }, + }, + }; + const errorMessages = {const: 'const(by pattern properties) error from error messages'}; + const error = { + instancePath: '/first', + schemaPath: '#/patternProperties/%5Efirst/const', + keyword: 'const', + params: { + allowedValue: 'value', + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {first: ''}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const patternPropertiesConstErrorMessage = + 'const(by pattern properties) error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + patternProperties: { + '^first': { + type: JsonSchemaType.String, + const: 'value', + entityParameters: { + errorMessages: { + const: patternPropertiesConstErrorMessage, + }, + }, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: patternPropertiesConstErrorMessage, + }); + }); + }); + + describe('with property names object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + propertyNames: { + type: JsonSchemaType.String, + maxLength: 1, + }, + }; + const errorMessages = {propertyNames: 'property names error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/propertyNames', + keyword: 'propertyNames', + params: { + propertyName: 'first', + }, + message: 'property name must be valid', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/propertyNames/maxLength', + keyword: 'maxLength', + params: { + limit: 1, + }, + message: 'must NOT have more than 1 characters', + propertyName: 'first', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {first: ''}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.propertyNames, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const propertyNamesErrorMessage = 'property names error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + propertyNames: propertyNamesErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: propertyNamesErrorMessage, + }); + }); + }); + + describe('with required object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + required: ['first'], + }; + const errorMessages = {required: 'required error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/required', + keyword: 'required', + params: { + missingProperty: 'first', + }, + message: "must have required property 'first'", + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: errorMessages.required, + }); + }); + + it('should call onError with message from schema entity parameters by schema path', () => { + const requiredErrorMessage = + 'required(by schema path) error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + required: requiredErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: requiredErrorMessage, + }); + }); + + it('should call onError with message from schema entity parameters by instance path', () => { + const requiredErrorMessage = + 'required(by instance path) error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + properties: { + first: { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + required: requiredErrorMessage, + }, + }, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: ['first'], + error: requiredErrorMessage, + }); + }); + }); + + describe('with all of object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + allOf: [{type: JsonSchemaType.Object, const: {first: 'value'}}], + }; + const errorMessages = {const: 'const(by all of) error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/allOf/0/const', + keyword: 'const', + params: { + allowedValue: { + first: 'value', + }, + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters by schema path', () => { + const allOfConstErrorMessage = + 'const(by all of, by schema path) error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + allOf: [ + { + type: JsonSchemaType.Object, + const: {first: 'value'}, + entityParameters: { + errorMessages: { + const: allOfConstErrorMessage, + }, + }, + }, + ], + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: allOfConstErrorMessage, + }); + }); + + it('should call onError with message from schema entity parameters by instance path', () => { + const allOfConstErrorMessage = + 'const(by all of, by instance path) error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + const: allOfConstErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: allOfConstErrorMessage, + }); + }); + }); + + describe('with any of object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + anyOf: [{type: JsonSchemaType.Object, const: {first: 'value'}}], + }; + const errorMessages = {anyOf: 'any of error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/anyOf', + keyword: 'anyOf', + params: {}, + message: 'must match a schema in anyOf', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/anyOf/0/const', + keyword: 'const', + params: { + allowedValue: { + first: 'value', + }, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.anyOf, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const anyOfErrorMessage = 'any of error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + anyOf: anyOfErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: anyOfErrorMessage, + }); + }); + }); + + describe('with const object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + const: {first: 'value'}, + }; + const errorMessages = {const: 'const error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/const', + keyword: 'const', + params: { + allowedValue: { + first: 'value', + }, + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const constErrorMessage = 'const error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + const: constErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: constErrorMessage, + }); + }); + }); + + describe('with if then object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + if: {type: JsonSchemaType.Object, const: {}}, + then: {type: JsonSchemaType.Object, const: {first: 'value'}}, + }; + const errorMessages = {then: 'if then error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/if', + keyword: 'if', + params: { + failingKeyword: 'then', + }, + message: 'must match "then" schema', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/then/const', + keyword: 'const', + params: { + allowedValue: { + first: 'value', + }, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.then, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const ifThenErrorMessage = 'if then error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + then: ifThenErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: ifThenErrorMessage, + }); + }); + }); + + describe('with if else object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + if: {type: JsonSchemaType.Object, const: {first: 'value'}}, + else: {type: JsonSchemaType.Object, const: {first: 'value'}}, + }; + const errorMessages = {else: 'if else error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/if', + keyword: 'if', + params: { + failingKeyword: 'else', + }, + message: 'must match "else" schema', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/else/const', + keyword: 'const', + params: { + allowedValue: { + first: 'value', + }, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.else, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const ifElseErrorMessage = 'if else error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + else: ifElseErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: ifElseErrorMessage, + }); + }); + }); + + describe('with enum object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + enum: [{first: 'value'}], + }; + const errorMessages = {enum: 'enum error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/enum', + keyword: 'enum', + params: { + allowedValues: [{first: 'value'}], + }, + message: 'must be equal to one of the allowed values', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.enum, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const enumErrorMessage = 'enum error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + enum: enumErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: enumErrorMessage, + }); + }); + }); + + describe('with not object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + not: {type: JsonSchemaType.Object, const: {}}, + }; + const errorMessages = {not: 'not error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/not', + keyword: 'not', + params: {}, + message: 'must NOT be valid', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.not, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const notErrorMessage = 'not error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + not: notErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: notErrorMessage, + }); + }); + }); + + describe('with one of object schema', () => { + const mainSchema: JsonSchemaObject = { + type: JsonSchemaType.Object, + oneOf: [{type: JsonSchemaType.Object, const: {first: 'value'}}], + }; + const errorMessages = {oneOf: 'one of error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/oneOf', + keyword: 'oneOf', + params: { + passingSchemas: null, + }, + message: 'must match exactly one schema in oneOf', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/oneOf/0/const', + keyword: 'const', + params: { + allowedValue: { + first: 'value', + }, + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value: ObjectValue = {}; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.oneOf, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const oneOfErrorMessage = 'one of error from schema entity parameters'; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + entityParameters: { + errorMessages: { + oneOf: oneOfErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: oneOfErrorMessage, + }); + }); + }); + + describe('with type string schema', () => { + const mainSchema: JsonSchemaString = {type: JsonSchemaType.String}; + const errorMessages = {type: 'type error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/type', + keyword: 'type', + params: { + type: 'string', + }, + message: 'must be string', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 0; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.type, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const typeErrorMessage = 'type error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + type: typeErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: typeErrorMessage, + }); + }); + }); + + describe('with max length string schema', () => { + const mainSchema: JsonSchemaString = {type: JsonSchemaType.String, maxLength: 1}; + const errorMessages = {maxLength: 'max length error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/maxLength', + keyword: 'maxLength', + params: { + limit: 1, + }, + message: 'must NOT have more than 1 characters', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 'value'; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.maxLength, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const maxLengthErrorMessage = 'max length error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + maxLength: maxLengthErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: maxLengthErrorMessage, + }); + }); + }); + + describe('with min length string schema', () => { + const mainSchema: JsonSchemaString = {type: JsonSchemaType.String, minLength: 1}; + const errorMessages = {minLength: 'min length error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/minLength', + keyword: 'minLength', + params: { + limit: 1, + }, + message: 'must NOT have fewer than 1 characters', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = ''; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.minLength, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const minLengthErrorMessage = 'min length error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + minLength: minLengthErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: minLengthErrorMessage, + }); + }); + }); + + describe('with pattern string schema', () => { + const mainSchema: JsonSchemaString = { + type: JsonSchemaType.String, + pattern: '^[0-9]', + }; + const errorMessages = {pattern: 'pattern error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/pattern', + keyword: 'pattern', + params: { + pattern: '^[0-9]', + }, + message: 'must match pattern "^[0-9]"', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 'value'; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.pattern, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const patternErrorMessage = 'pattern error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + pattern: patternErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: patternErrorMessage, + }); + }); + }); + + describe('with all of string schema', () => { + const mainSchema: JsonSchemaString = { + type: JsonSchemaType.String, + allOf: [{type: JsonSchemaType.String, const: 'value'}], + }; + const errorMessages = {const: 'const(by all of) error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/allOf/0/const', + keyword: 'const', + params: { + allowedValue: 'value', + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = ''; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters by schema path', () => { + const allOfConstErrorMessage = + 'const(by all of, by schema path) error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + allOf: [ + { + type: JsonSchemaType.String, + const: 'value', + entityParameters: { + errorMessages: { + const: allOfConstErrorMessage, + }, + }, + }, + ], + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: allOfConstErrorMessage, + }); + }); + + it('should call onError with message from schema entity parameters by instance path', () => { + const allOfConstErrorMessage = + 'const(by all of, by instance path) error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + const: allOfConstErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: allOfConstErrorMessage, + }); + }); + }); + + describe('with any of string schema', () => { + const mainSchema: JsonSchemaString = { + type: JsonSchemaType.String, + anyOf: [{type: JsonSchemaType.String, const: 'value'}], + }; + const errorMessages = {anyOf: 'any of error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/anyOf', + keyword: 'anyOf', + params: {}, + message: 'must match a schema in anyOf', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/anyOf/0/const', + keyword: 'const', + params: { + allowedValue: 'value', + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = ''; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.anyOf, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const anyOfErrorMessage = 'any of error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + anyOf: anyOfErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: anyOfErrorMessage, + }); + }); + }); + + describe('with const string schema', () => { + const mainSchema: JsonSchemaString = {type: JsonSchemaType.String, const: 'value'}; + const errorMessages = {const: 'const error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/const', + keyword: 'const', + params: { + allowedValue: 'value', + }, + message: 'must be equal to constant', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = ''; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.const, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const constErrorMessage = 'const error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + const: constErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: constErrorMessage, + }); + }); + }); + + describe('with if then string schema', () => { + const mainSchema: JsonSchemaString = { + type: JsonSchemaType.String, + if: {type: JsonSchemaType.String, const: ''}, + then: {type: JsonSchemaType.String, const: 'value'}, + }; + const errorMessages = {then: 'if then error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/if', + keyword: 'if', + params: { + failingKeyword: 'then', + }, + message: 'must match "then" schema', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/then/const', + keyword: 'const', + params: { + allowedValue: 'value', + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = ''; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.then, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const ifThenErrorMessage = 'if then error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + then: ifThenErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: ifThenErrorMessage, + }); + }); + }); + + describe('with if else string schema', () => { + const mainSchema: JsonSchemaString = { + type: JsonSchemaType.String, + if: {type: JsonSchemaType.String, const: 'value'}, + else: {type: JsonSchemaType.String, const: 'value'}, + }; + const errorMessages = {else: 'if else error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/if', + keyword: 'if', + params: { + failingKeyword: 'else', + }, + message: 'must match "else" schema', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/else/const', + keyword: 'const', + params: { + allowedValue: 'value', + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = ''; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.else, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const ifElseErrorMessage = 'if else error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + else: ifElseErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: ifElseErrorMessage, + }); + }); + }); + + describe('with enum string schema', () => { + const mainSchema: JsonSchemaString = {type: JsonSchemaType.String, enum: ['value']}; + const errorMessages = {enum: 'enum error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/enum', + keyword: 'enum', + params: { + allowedValues: ['value'], + }, + message: 'must be equal to one of the allowed values', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = ''; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.enum, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const enumErrorMessage = 'enum error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + enum: enumErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: enumErrorMessage, + }); + }); + }); + + describe('with not string schema', () => { + const mainSchema: JsonSchemaString = { + type: JsonSchemaType.String, + not: {type: JsonSchemaType.String, const: 'value'}, + }; + const errorMessages = {not: 'not error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/not', + keyword: 'not', + params: {}, + message: 'must NOT be valid', + }; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = 'value'; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual([error]); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.not, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const notErrorMessage = 'not error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + not: notErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: notErrorMessage, + }); + }); + }); + + describe('with one of string schema', () => { + const mainSchema: JsonSchemaString = { + type: JsonSchemaType.String, + oneOf: [{type: JsonSchemaType.String, const: 'value'}], + }; + const errorMessages = {oneOf: 'one of error from error messages'}; + const error = { + instancePath: '', + schemaPath: '#/oneOf', + keyword: 'oneOf', + params: { + passingSchemas: null, + }, + message: 'must match exactly one schema in oneOf', + }; + const errors = [ + { + instancePath: '', + schemaPath: '#/oneOf/0/const', + keyword: 'const', + params: { + allowedValue: 'value', + }, + message: 'must be equal to constant', + }, + error, + ]; + const headName = ''; + const onError = jest.fn(); + + test('errors must match', () => { + const value = ''; + const validate = ajv.compile(mainSchema); + + validate(value); + + expect(validate.errors).toEqual(errors); + }); + + it('should call onError with default ajv error message', () => { + processAjvError({ + error, + errorMessages: undefined, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: error.message, + }); + }); + + it('should call onError with message from error messages map', () => { + processAjvError({ + error, + errorMessages, + headName, + mainSchema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: errorMessages.oneOf, + }); + }); + + it('should call onError with message from schema entity parameters', () => { + const oneOfErrorMessage = 'one of error from schema entity parameters'; + const schema: JsonSchemaString = { + type: JsonSchemaType.String, + entityParameters: { + errorMessages: { + oneOf: oneOfErrorMessage, + }, + }, + }; + + processAjvError({ + error, + errorMessages, + headName, + mainSchema: schema, + onError, + }); + + expect(onError).toHaveBeenCalledWith({ + path: [], + error: oneOfErrorMessage, + }); + }); + }); +}); diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-entity-parameters-error.test.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-entity-parameters-error.test.ts new file mode 100644 index 00000000..6a1e04fd --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-entity-parameters-error.test.ts @@ -0,0 +1,240 @@ +import {JsonSchemaType} from '../../../constants'; +import type {ValidationState} from '../../../mutators'; +import type {JsonSchemaString, ObjectValue} from '../../../types'; +import type {EntityParametersError} from '../../types'; +import {processEntityParametersError} from '../process-entity-parameters-error'; + +describe('SchemaRendererServiceField/utils/process-entity-parameters-error', () => { + const fieldName = 'field'; + const fieldInstancePath = `/${fieldName}`; + const fieldValue = 'field value'; + const fieldValidateResult = 'field validate result'; + const fieldSchema: JsonSchemaString = {type: JsonSchemaType.String}; + const fieldValidator = jest.fn(); + const fieldEntityParametersError: EntityParametersError = { + instancePath: fieldInstancePath, + keyword: 'entityParameters', + schemaPath: '#/entityParameters', + params: { + schema: fieldSchema, + validator: fieldValidator, + value: fieldValue, + }, + }; + + const headName = 'headName'; + const allValues: ObjectValue = {[headName]: {[fieldName]: fieldValue}}; + const onError = jest.fn(); + const onAsyncError = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('with cache hit', () => { + const validationState: ValidationState = { + cache: { + [fieldInstancePath]: [ + { + schema: fieldSchema, + validator: fieldValidator, + value: fieldValue, + result: fieldValidateResult, + }, + ], + }, + }; + + it('should call onError with cached result', () => { + processEntityParametersError({ + allValues, + error: fieldEntityParametersError, + headName, + onAsyncError, + onError, + validationState, + }); + + expect(onError).toHaveBeenCalledWith({ + error: fieldValidateResult, + path: [headName, fieldName], + }); + expect(fieldValidator).not.toHaveBeenCalled(); + expect(onAsyncError).not.toHaveBeenCalled(); + }); + }); + + describe('with no cache and no waiter', () => { + const validationState: ValidationState = {}; + + it('should call validator and onError with synchronous result', () => { + fieldValidator.mockReturnValue(fieldValidateResult); + + processEntityParametersError({ + allValues, + error: fieldEntityParametersError, + headName, + onAsyncError, + onError, + validationState, + }); + + expect(fieldValidator).toHaveBeenCalledWith(fieldValue, allValues); + expect(onError).toHaveBeenCalledWith({ + error: fieldValidateResult, + path: [headName, fieldName], + }); + expect(onAsyncError).not.toHaveBeenCalled(); + }); + + it('should call validator and onAsyncError with asynchronous result', () => { + const promise = Promise.resolve(fieldValidateResult); + + fieldValidator.mockReturnValue(promise); + + processEntityParametersError({ + allValues, + error: fieldEntityParametersError, + headName, + onAsyncError, + onError, + validationState, + }); + + expect(fieldValidator).toHaveBeenCalledWith(fieldValue, allValues); + expect(onAsyncError).toHaveBeenCalledWith({ + instancePath: fieldInstancePath, + params: fieldEntityParametersError.params, + promise, + }); + expect(onError).not.toHaveBeenCalled(); + }); + }); + + describe('with waiter that does not match error params', () => { + const validationState: ValidationState = { + waiters: { + [fieldInstancePath]: { + schema: fieldSchema, + validator: jest.fn(), + value: 'different value', + }, + }, + }; + + it('should call validator and onError with synchronous result', () => { + fieldValidator.mockReturnValue(fieldValidateResult); + + processEntityParametersError({ + allValues, + error: fieldEntityParametersError, + headName, + onAsyncError, + onError, + validationState, + }); + + expect(fieldValidator).toHaveBeenCalledWith(fieldValue, allValues); + expect(onError).toHaveBeenCalledWith({ + error: fieldValidateResult, + path: [headName, fieldName], + }); + expect(onAsyncError).not.toHaveBeenCalled(); + }); + + it('should call validator and onAsyncError with asynchronous result', () => { + const promise = Promise.resolve(fieldValidateResult); + + fieldValidator.mockReturnValue(promise); + + processEntityParametersError({ + allValues, + error: fieldEntityParametersError, + headName, + onAsyncError, + onError, + validationState, + }); + + expect(fieldValidator).toHaveBeenCalledWith(fieldValue, allValues); + expect(onAsyncError).toHaveBeenCalledWith({ + instancePath: fieldInstancePath, + params: fieldEntityParametersError.params, + promise, + }); + expect(onError).not.toHaveBeenCalled(); + }); + }); + + describe('with matching waiter', () => { + const validationState: ValidationState = { + waiters: { + [fieldInstancePath]: { + schema: fieldSchema, + validator: fieldValidator, + value: fieldValue, + }, + }, + }; + + it('should not call validator, onError, or onAsyncError', () => { + processEntityParametersError({ + allValues, + error: fieldEntityParametersError, + headName, + onAsyncError, + onError, + validationState, + }); + + expect(fieldValidator).not.toHaveBeenCalled(); + expect(onError).not.toHaveBeenCalled(); + expect(onAsyncError).not.toHaveBeenCalled(); + }); + }); + + describe('with undefined validationState', () => { + it('should call validator and onError with synchronous result', () => { + fieldValidator.mockReturnValue(fieldValidateResult); + + processEntityParametersError({ + allValues, + error: fieldEntityParametersError, + headName, + onAsyncError, + onError, + validationState: undefined, + }); + + expect(fieldValidator).toHaveBeenCalledWith(fieldValue, allValues); + expect(onError).toHaveBeenCalledWith({ + error: fieldValidateResult, + path: [headName, fieldName], + }); + expect(onAsyncError).not.toHaveBeenCalled(); + }); + + it('should call validator and onAsyncError with asynchronous result', () => { + const promise = Promise.resolve(fieldValidateResult); + + fieldValidator.mockReturnValue(promise); + + processEntityParametersError({ + allValues, + error: fieldEntityParametersError, + headName, + onAsyncError, + onError, + validationState: undefined, + }); + + expect(fieldValidator).toHaveBeenCalledWith(fieldValue, allValues); + expect(onAsyncError).toHaveBeenCalledWith({ + instancePath: fieldInstancePath, + params: fieldEntityParametersError.params, + promise, + }); + expect(onError).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-error-items.test.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-error-items.test.ts new file mode 100644 index 00000000..ebcfd91e --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-error-items.test.ts @@ -0,0 +1,241 @@ +import {ARRAY_AND_OBJECT_ERRORS, JsonSchemaType} from '../../../constants'; +import type {ValidateErrorItem} from '../../types'; +import {processErrorItems} from '../process-error-items'; + +describe('processErrorItems', () => { + const headName = 'headName'; + const fieldName1 = 'fieldName1'; + const fieldName2 = 'fieldName2'; + const fieldName3 = 'fieldName3'; + const fieldName4 = 'fieldName4'; + + it('should return an object with ARRAY_AND_OBJECT_ERRORS property', () => { + const result = processErrorItems({ + errorItems: [], + headName, + mainSchema: {type: JsonSchemaType.Object}, + }); + + expect(result).toEqual({[ARRAY_AND_OBJECT_ERRORS]: {}}); + }); + + it('should skip error items with falsy error property', () => { + const errorItems: ValidateErrorItem[] = [ + {error: undefined, path: [headName, fieldName1]}, + {error: false, path: [headName, fieldName2]}, + ]; + + const result = processErrorItems({ + errorItems, + headName, + mainSchema: { + type: JsonSchemaType.Object, + properties: { + [fieldName1]: {type: JsonSchemaType.String}, + [fieldName2]: {type: JsonSchemaType.String}, + }, + }, + }); + + expect(result).toEqual({[ARRAY_AND_OBJECT_ERRORS]: {}}); + }); + + it('should handle boolean error values for non-array/object schemas', () => { + const error = true; + const errorItems: ValidateErrorItem[] = [{error, path: [headName, fieldName1]}]; + + const result = processErrorItems({ + errorItems, + headName, + mainSchema: { + type: JsonSchemaType.Object, + properties: { + [fieldName1]: {type: JsonSchemaType.String}, + }, + }, + }); + + expect(result).toEqual({[ARRAY_AND_OBJECT_ERRORS]: {}, [headName]: {[fieldName1]: error}}); + }); + + it('should handle string error values for non-array/object schemas', () => { + const error = 'error message'; + const errorItems: ValidateErrorItem[] = [{error, path: [headName, fieldName1]}]; + + const result = processErrorItems({ + errorItems, + headName, + mainSchema: { + type: JsonSchemaType.Object, + properties: { + [fieldName1]: {type: JsonSchemaType.String}, + }, + }, + }); + + expect(result).toEqual({[ARRAY_AND_OBJECT_ERRORS]: {}, [headName]: {[fieldName1]: error}}); + }); + + it('should handle undefined error values for non-array/object schemas', () => { + const errorItems: ValidateErrorItem[] = [{error: undefined, path: [headName, fieldName1]}]; + + const result = processErrorItems({ + errorItems, + headName, + mainSchema: { + type: JsonSchemaType.Object, + properties: { + [fieldName1]: {type: JsonSchemaType.String}, + }, + }, + }); + + expect(result).toEqual({[ARRAY_AND_OBJECT_ERRORS]: {}}); + }); + + it('should handle array schema types by adding to ARRAY_AND_OBJECT_ERRORS', () => { + const error = 'array error'; + const errorItems: ValidateErrorItem[] = [{error, path: [headName, fieldName1]}]; + + const result = processErrorItems({ + errorItems, + headName, + mainSchema: { + type: JsonSchemaType.Object, + properties: { + [fieldName1]: {type: JsonSchemaType.Array}, + }, + }, + }); + + expect(result).toEqual({[ARRAY_AND_OBJECT_ERRORS]: {[`${headName}.${fieldName1}`]: error}}); + }); + + it('should handle object schema types by adding to ARRAY_AND_OBJECT_ERRORS', () => { + const error = 'object error'; + const errorItems: ValidateErrorItem[] = [{error, path: [headName, fieldName1]}]; + + const result = processErrorItems({ + errorItems, + headName, + mainSchema: { + type: JsonSchemaType.Object, + properties: { + [fieldName1]: {type: JsonSchemaType.Object}, + }, + }, + }); + + expect(result).toEqual({[ARRAY_AND_OBJECT_ERRORS]: {[`${headName}.${fieldName1}`]: error}}); + }); + + it('should handle object-like error values by processing nested paths', () => { + const errorObject = {[fieldName1]: {[fieldName2]: 'nested error'}}; + const errorItems: ValidateErrorItem[] = [{error: errorObject, path: [headName]}]; + + const result = processErrorItems({ + errorItems, + headName, + mainSchema: { + type: JsonSchemaType.Object, + properties: { + [fieldName1]: { + type: JsonSchemaType.Object, + properties: { + [fieldName2]: {type: JsonSchemaType.String}, + }, + }, + }, + }, + }); + + expect(result).toEqual({[ARRAY_AND_OBJECT_ERRORS]: {}, [headName]: errorObject}); + }); + + it('should handle object-like error values with array/object nested schemas', () => { + const error2 = 'array error'; + const error3 = 'object error'; + const errorObject = { + [fieldName1]: { + [fieldName2]: error2, + [fieldName3]: error3, + }, + }; + + const errorItems: ValidateErrorItem[] = [{error: errorObject, path: [headName]}]; + + const result = processErrorItems({ + errorItems, + headName, + mainSchema: { + type: JsonSchemaType.Object, + properties: { + [fieldName1]: { + type: JsonSchemaType.Object, + properties: { + [fieldName2]: {type: JsonSchemaType.Array}, + [fieldName3]: {type: JsonSchemaType.Object}, + }, + }, + }, + }, + }); + + expect(result).toEqual({ + [ARRAY_AND_OBJECT_ERRORS]: { + [`${headName}.${fieldName1}.${fieldName2}`]: error2, + [`${headName}.${fieldName1}.${fieldName3}`]: error3, + }, + }); + }); + + it('should handle multiple error items', () => { + const error1 = 'string error'; + const error4 = 'nested error'; + const errorItems: ValidateErrorItem[] = [ + {error: error1, path: [headName, fieldName1]}, + {error: true, path: [headName, fieldName2]}, + {error: {[fieldName4]: error4}, path: [headName, fieldName3]}, + ]; + + const result = processErrorItems({ + errorItems, + headName, + mainSchema: { + type: JsonSchemaType.Object, + properties: { + [fieldName1]: {type: JsonSchemaType.String}, + [fieldName2]: {type: JsonSchemaType.Boolean}, + [fieldName3]: { + type: JsonSchemaType.Object, + properties: {[fieldName4]: {type: JsonSchemaType.Array}}, + }, + }, + }, + }); + + expect(result).toEqual({ + [ARRAY_AND_OBJECT_ERRORS]: { + [`${headName}.${fieldName3}.${fieldName4}`]: error4, + }, + [headName]: {[fieldName1]: error1, [fieldName2]: true}, + }); + }); + + it('should do nothing if schema is not found', () => { + const errorItems: ValidateErrorItem[] = [ + { + error: 'error message', + path: [headName, 'nonExistentField'], + }, + ]; + + const result = processErrorItems({ + errorItems, + headName, + mainSchema: {type: JsonSchemaType.Object}, + }); + + expect(result).toEqual({[ARRAY_AND_OBJECT_ERRORS]: {}}); + }); +}); diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-errors-state.test.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-errors-state.test.ts new file mode 100644 index 00000000..82b8fef3 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/__tests__/process-errors-state.test.ts @@ -0,0 +1,128 @@ +import {processErrorsState} from '../process-errors-state'; + +describe('processErrorsState', () => { + it('should return empty arrays when errorsState is undefined', () => { + const result = processErrorsState({ + errorsState: undefined, + }); + + expect(result.externalPriorityErrorItems).toEqual([]); + expect(result.externalRegularErrorItems).toEqual([]); + }); + + it('should process priorityErrors correctly', () => { + const result = processErrorsState({ + errorsState: { + priorityErrors: { + field1: 'Error 1', + field2: true, + }, + regularErrors: {}, + }, + }); + + expect(result.externalPriorityErrorItems).toEqual([ + {path: ['field1'], error: 'Error 1'}, + {path: ['field2'], error: true}, + ]); + expect(result.externalRegularErrorItems).toEqual([]); + }); + + it('should process regularErrors correctly', () => { + const result = processErrorsState({ + errorsState: { + priorityErrors: {}, + regularErrors: { + field3: 'Error 3', + field4: { + nested: 'Nested error', + }, + }, + }, + }); + + expect(result.externalPriorityErrorItems).toEqual([]); + expect(result.externalRegularErrorItems).toEqual([ + {path: ['field3'], error: 'Error 3'}, + {path: ['field4'], error: {nested: 'Nested error'}}, + ]); + }); + + it('should process both priorityErrors and regularErrors correctly', () => { + const result = processErrorsState({ + errorsState: { + priorityErrors: { + field1: 'Error 1', + field2: true, + }, + regularErrors: { + field3: 'Error 3', + field4: { + nested: 'Nested error', + }, + }, + }, + }); + + expect(result.externalPriorityErrorItems).toEqual([ + {path: ['field1'], error: 'Error 1'}, + {path: ['field2'], error: true}, + ]); + expect(result.externalRegularErrorItems).toEqual([ + {path: ['field3'], error: 'Error 3'}, + {path: ['field4'], error: {nested: 'Nested error'}}, + ]); + }); + + it('should handle complex paths correctly', () => { + const result = processErrorsState({ + errorsState: { + priorityErrors: { + 'parent.child': 'Nested error', + }, + regularErrors: { + 'array[0]': 'Array error', + }, + }, + }); + + expect(result.externalPriorityErrorItems).toEqual([ + {path: ['parent', 'child'], error: 'Nested error'}, + ]); + expect(result.externalRegularErrorItems).toEqual([ + {path: ['array', '0'], error: 'Array error'}, + ]); + }); + + it('should handle undefined priorityErrors and regularErrors', () => { + const result = processErrorsState({ + errorsState: {}, + }); + + expect(result.externalPriorityErrorItems).toEqual([]); + expect(result.externalRegularErrorItems).toEqual([]); + }); + + it('should handle various error types correctly', () => { + const result = processErrorsState({ + errorsState: { + priorityErrors: { + string: 'String error', + boolean: true, + object: {key: 'value'}, + array: ['item1', 'item2'], + undefined: undefined, + }, + regularErrors: {}, + }, + }); + + expect(result.externalPriorityErrorItems).toEqual([ + {path: ['string'], error: 'String error'}, + {path: ['boolean'], error: true}, + {path: ['object'], error: {key: 'value'}}, + {path: ['array'], error: ['item1', 'item2']}, + {path: ['undefined'], error: undefined}, + ]); + }); +}); diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/common.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/common.ts new file mode 100644 index 00000000..e5c89125 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/common.ts @@ -0,0 +1,118 @@ +import get from 'lodash/get'; + +import {JsonSchemaType} from '../../constants'; +import type {JsonSchema} from '../../types'; + +/** + * Extracts the path to the property from a JSON Schema validation error. + * Assumes that the last segment in the `schemaPath` is the keyword + * that triggered the validation error (e.g., "minLength"). + * + * @param schemaPath - A JSON Pointer string representing the schema path, + * e.g., "#/properties/name/minLength" + * @returns An array of path segments leading to the property, excluding the keyword, + * e.g., ['properties', 'name'] + */ +export const parseSchemaPath = (schemaPath: string): string[] => { + return decodeURIComponent(schemaPath) + .slice('#/'.length) + .split('/') + .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~')) + .slice(0, -1); +}; + +export const parseInstancePath = (instancePath: string): string[] => { + if (!instancePath.length) { + return []; + } + + return instancePath + .slice('/'.length) + .split('/') + .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~')); +}; + +/** + * Retrieves the sub-schema from the main schema based on the given schema path. + * Assumes that the last segment in the `schemaPath` is a validation keyword + * (e.g., "minLength") and not part of the property path. + * + * @param schemaPath - A JSON Pointer-style string representing the schema path, + * e.g., "#/properties/name/minLength". + * @param mainSchema - The root JSON schema object. + * + * @example + * const nameSchema = { + * type: JsonSchemaType.String, + * minLength: 5, + * }; + * const objectSchema = { + * type: JsonSchemaType.Object, + * properties: { + * name: nameSchema, + * }, + * }; + * getSchemaFromPath("#/properties/name/minLength", objectSchema); // returns nameSchema + * + * @returns The sub-schema object corresponding to the property path. + */ +export const getSchemaBySchemaPath = ( + schemaPath: string, + mainSchema: JsonSchema, +): JsonSchema | undefined => { + const pathArr = parseSchemaPath(schemaPath); + + if (!pathArr.length) { + return mainSchema; + } + + return get(mainSchema, pathArr); +}; + +export const getSchemaByInstancePath = ( + instancePath: string, + mainSchema: JsonSchema, +): JsonSchema | undefined => { + if (instancePath.length) { + return parseInstancePath(instancePath).reduce((acc: JsonSchema | undefined, segment) => { + const type = get(acc, 'type'); + + if (type === JsonSchemaType.Object) { + return get(acc, `properties.${segment}`); + } else if (type === JsonSchemaType.Array) { + const items = get(acc, 'items'); + + if (Array.isArray(items)) { + return get(items, `[${segment}]`); + } + + return items; + } + + return undefined; + }, mainSchema); + } + + return mainSchema; +}; + +export const getValuePaths = (value: unknown, path: string[] = []) => { + const result: string[][] = []; + + const isObject = (v: unknown): v is Record => + v !== null && typeof v === 'object' && !Array.isArray(v); + + if (Array.isArray(value)) { + value.forEach((_, index) => { + result.push(...getValuePaths(value[index], [...path, `${index}`])); + }); + } else if (isObject(value)) { + Object.keys(value).forEach((key) => { + result.push(...getValuePaths(get(value, key), [...path, key])); + }); + } else if (path.length) { + result.push(path); + } + + return result; +}; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/get-ajv-validate.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/get-ajv-validate.ts new file mode 100644 index 00000000..33311f89 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/get-ajv-validate.ts @@ -0,0 +1,64 @@ +import Ajv from 'ajv'; +import type { + ErrorObject, + FuncKeywordDefinition, + SchemaValidateFunction, + ValidateFunction, +} from 'ajv'; +import get from 'lodash/get'; + +import type {FieldValue, JsonSchema, SchemaRendererConfig, Validator} from '../../types'; +import type {EntityParametersError} from '../types'; + +interface GetAjvValidateParams { + config: SchemaRendererConfig; + mainSchema: JsonSchema; +} + +interface GetAjvValidateReturn extends ValidateFunction { + errors?: (ErrorObject | EntityParametersError)[]; +} + +export const getAjvValidate = ({ + config, + mainSchema, +}: GetAjvValidateParams): GetAjvValidateReturn => { + function entityParametersValidate(_: unknown, value: FieldValue, schema?: JsonSchema) { + if (schema) { + const validatorType: string | undefined = get(schema, 'entityParameters.validatorType'); + const validator: Validator | undefined = get( + config, + `${schema.type}.validators.${validatorType}`, + ); + + if (validator) { + const error: Partial = { + keyword: 'entityParameters', + message: '', + params: {validator, value, schema}, + }; + + (entityParametersValidate as SchemaValidateFunction).errors = [error]; + + return false; + } + } + + return true; + } + + const ajv = new Ajv({ + allErrors: true, + allowMatchingProperties: true, + keywords: [ + { + errors: true, + keyword: 'entityParameters', + validate: entityParametersValidate as FuncKeywordDefinition['validate'], + }, + ], + }); + const ajvValidate = ajv.compile(mainSchema) as GetAjvValidateReturn; + + return ajvValidate; +}; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/get-validate.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/get-validate.ts new file mode 100644 index 00000000..2de44868 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/get-validate.ts @@ -0,0 +1,80 @@ +import type {FieldValidator} from 'final-form'; +import get from 'lodash/get'; + +import type { + ErrorsState, + SetValidationCacheMutator, + SetValidationWaitersMutator, + ValidationState, +} from '../../mutators'; +import type { + ErrorMessages, + JsonSchema, + ObjectValue, + SchemaRendererConfig, + SchemaToValueType, +} from '../../types'; + +import {getAjvValidate} from './get-ajv-validate'; +import {processAjvValidateErrors} from './process-ajv-validate-errors'; +import {processErrorItems} from './process-error-items'; +import {processErrorsState} from './process-errors-state'; + +type GetValidateParams = { + config: SchemaRendererConfig; + errorMessages?: ErrorMessages; + headName: string; + mainSchema: Schema; + serviceFieldName: string; + setValidationCache: SetValidationCacheMutator; + setValidationWaiters: SetValidationWaitersMutator; +}; + +type GetValidateReturn = FieldValidator>; + +export const getValidate = ({ + config, + errorMessages, + headName, + mainSchema, + serviceFieldName, + setValidationCache, + setValidationWaiters, +}: GetValidateParams): GetValidateReturn => { + const ajvValidate = getAjvValidate({config, mainSchema}); + + return (_value, allValues, meta) => { + ajvValidate(get(allValues, headName)); + + const {ajvErrorItems, entityParametersErrorItems, waiters} = processAjvValidateErrors({ + ajvValidateErrors: ajvValidate.errors || [], + allValues: allValues as ObjectValue, + errorMessages, + headName, + mainSchema, + serviceFieldName, + setValidationCache, + validationState: meta?.data as ValidationState | undefined, + }); + const {externalPriorityErrorItems, externalRegularErrorItems} = processErrorsState({ + errorsState: meta?.data as ErrorsState | undefined, + }); + + if (Object.keys(waiters).length) { + setValidationWaiters({serviceFieldName, waiters}); + } + + const result = processErrorItems({ + errorItems: [ + ...externalRegularErrorItems, + ...ajvErrorItems, + ...entityParametersErrorItems, + ...externalPriorityErrorItems, + ], + headName, + mainSchema, + }); + + return result; + }; +}; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/index.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/index.ts new file mode 100644 index 00000000..13283897 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/index.ts @@ -0,0 +1 @@ +export * from './get-validate'; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/process-ajv-error.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/process-ajv-error.ts new file mode 100644 index 00000000..8c90ba38 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/process-ajv-error.ts @@ -0,0 +1,60 @@ +import type {ErrorObject} from 'ajv'; +import get from 'lodash/get'; +import isString from 'lodash/isString'; + +import {EMPTY_OBJECT} from '../../constants'; +import type {ErrorMessages, JsonSchema} from '../../types'; +import {parseFinalFormPath} from '../../utils'; +import type {ValidateErrorItem} from '../types'; + +import {getSchemaByInstancePath, getSchemaBySchemaPath, parseInstancePath} from './common'; + +interface ProcessAjvErrorParams { + error: ErrorObject; + errorMessages: ErrorMessages | undefined; + headName: string; + mainSchema: Schema; + onError: (error: ValidateErrorItem) => void; +} + +export const processAjvError = ({ + error, + errorMessages = EMPTY_OBJECT, + headName, + mainSchema, + onError, +}: ProcessAjvErrorParams) => { + let instancePath = error.instancePath; + let keyword = error.keyword; + let schemaPath = error.schemaPath; + + if (keyword === 'required' || keyword === 'dependencies') { + instancePath += `/${error.params.missingProperty}`; + } else if (keyword === 'if') { + keyword = error.params.failingKeyword; + schemaPath = schemaPath.slice(0, -'if'.length) + error.params.failingKeyword; + } + + const propertyName = instancePath.split('/').pop() as string; + + const getErrorMessageBySchema = (schema: JsonSchema | undefined) => { + const errorOrMap: Record | string | undefined = get( + schema, + `entityParameters.errorMessages.${keyword}`, + ); + const message: string | undefined = isString(errorOrMap) + ? errorOrMap + : get(errorOrMap, propertyName); + + return message; + }; + + onError({ + path: [...parseFinalFormPath(headName), ...parseInstancePath(instancePath)], + error: + getErrorMessageBySchema(getSchemaBySchemaPath(schemaPath, mainSchema)) || + getErrorMessageBySchema(getSchemaByInstancePath(instancePath, mainSchema)) || + errorMessages[keyword as keyof typeof errorMessages] || + error.message, + }); +}; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/process-ajv-validate-errors.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/process-ajv-validate-errors.ts new file mode 100644 index 00000000..ae29ac66 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/process-ajv-validate-errors.ts @@ -0,0 +1,77 @@ +import type {ErrorObject} from 'ajv'; + +import type {SetValidationCacheMutator, ValidationState, ValidationWaiter} from '../../mutators'; +import type {ErrorMessages, JsonSchema, ObjectValue} from '../../types'; +import type {EntityParametersError, ValidateErrorItem} from '../types'; + +import {processAjvError} from './process-ajv-error'; +import {processEntityParametersError} from './process-entity-parameters-error'; + +interface ProcessAjvValidateErrorsParams { + ajvValidateErrors: (ErrorObject | EntityParametersError)[]; + allValues: ObjectValue; + errorMessages: ErrorMessages | undefined; + headName: string; + mainSchema: Schema; + serviceFieldName: string; + setValidationCache: SetValidationCacheMutator; + validationState: ValidationState | undefined; +} + +interface ProcessAjvValidateErrorsReturn { + ajvErrorItems: ValidateErrorItem[]; + entityParametersErrorItems: ValidateErrorItem[]; + waiters: Record; +} + +export const processAjvValidateErrors = ({ + ajvValidateErrors, + allValues, + errorMessages, + headName, + mainSchema, + serviceFieldName, + setValidationCache, + validationState, +}: ProcessAjvValidateErrorsParams): ProcessAjvValidateErrorsReturn => { + const waiters: Record = {}; + const ajvErrorItems: ValidateErrorItem[] = []; + const entityParametersErrorItems: ValidateErrorItem[] = []; + + ajvValidateErrors.forEach((ajvOrEntityParametersError) => { + if (ajvOrEntityParametersError.keyword === 'entityParameters') { + processEntityParametersError({ + allValues: allValues, + error: ajvOrEntityParametersError as EntityParametersError, + headName, + onAsyncError: (w) => { + waiters[w.instancePath] = w.params; + + w.promise.then((result) => { + setValidationCache({ + cache: { + [w.instancePath]: { + ...w.params, + result, + }, + }, + serviceFieldName, + }); + }); + }, + onError: (err) => entityParametersErrorItems.push(err), + validationState, + }); + } else { + processAjvError({ + error: ajvOrEntityParametersError as ErrorObject, + errorMessages, + headName, + mainSchema, + onError: (err) => ajvErrorItems.push(err), + }); + } + }); + + return {ajvErrorItems, entityParametersErrorItems, waiters}; +}; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/process-entity-parameters-error.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/process-entity-parameters-error.ts new file mode 100644 index 00000000..7de625e1 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/process-entity-parameters-error.ts @@ -0,0 +1,57 @@ +import isEqual from 'lodash/isEqual'; +import omit from 'lodash/omit'; + +import type {ValidationState} from '../../mutators'; +import type {AsyncValidateError, ObjectValue} from '../../types'; +import {parseFinalFormPath} from '../../utils'; +import type {EntityParametersError, ValidateErrorItem} from '../types'; + +import {parseInstancePath} from './common'; + +interface ProcessEntityParametersErrorParams { + allValues: ObjectValue; + error: EntityParametersError; + headName: string; + onAsyncError: (waiter: { + instancePath: string; + params: EntityParametersError['params']; + promise: AsyncValidateError; + }) => void; + onError: (error: ValidateErrorItem) => void; + validationState: ValidationState | undefined; +} + +export const processEntityParametersError = ({ + allValues, + error, + headName, + onAsyncError, + onError, + validationState, +}: ProcessEntityParametersErrorParams) => { + const waiter = validationState?.waiters?.[error.instancePath]; + const cache = validationState?.cache?.[error.instancePath]; + const cacheItem = cache?.find((item) => isEqual(error.params, omit(item, 'result'))); + + if (cacheItem?.result) { + onError({ + error: cacheItem.result, + path: [...parseFinalFormPath(headName), ...parseInstancePath(error.instancePath)], + }); + } else if (!waiter || !isEqual(error.params, waiter)) { + const errorOrPromise = error.params.validator(error.params.value, allValues as ObjectValue); + + if (errorOrPromise instanceof Promise) { + onAsyncError({ + instancePath: error.instancePath, + params: error.params, + promise: errorOrPromise, + }); + } else { + onError({ + error: errorOrPromise, + path: [...parseFinalFormPath(headName), ...parseInstancePath(error.instancePath)], + }); + } + } +}; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/process-error-items.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/process-error-items.ts new file mode 100644 index 00000000..1f303a49 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/process-error-items.ts @@ -0,0 +1,71 @@ +import get from 'lodash/get'; +import isBoolean from 'lodash/isBoolean'; +import isObjectLike from 'lodash/isObjectLike'; +import isString from 'lodash/isString'; +import isUndefined from 'lodash/isUndefined'; +import set from 'lodash/set'; + +import {ARRAY_AND_OBJECT_ERRORS, JsonSchemaType} from '../../constants'; +import type {JsonSchema, SyncValidateError} from '../../types'; +import {getSchemaByFinalFormPath} from '../../utils'; +import type {ValidateErrorItem} from '../types'; + +import {getValuePaths} from './common'; + +interface ProcessErrorItemsParams { + errorItems: ValidateErrorItem[]; + headName: string; + mainSchema: Schema; +} + +type ProcessErrorItemsReturn = { + [ARRAY_AND_OBJECT_ERRORS]: {[key: string]: boolean | string | undefined}; +} & {[key: string]: SyncValidateError}; + +export const processErrorItems = ({ + errorItems, + headName, + mainSchema, +}: ProcessErrorItemsParams): ProcessErrorItemsReturn => { + const result: ProcessErrorItemsReturn = { + [ARRAY_AND_OBJECT_ERRORS]: {}, + }; + + const setError = (path: string[], error: boolean | string | undefined) => { + const itemSchema = getSchemaByFinalFormPath(path, headName, mainSchema); + + if (itemSchema) { + const arrayOrObjectSchema = + itemSchema.type === JsonSchemaType.Array || + itemSchema.type === JsonSchemaType.Object; + + if (arrayOrObjectSchema) { + result[ARRAY_AND_OBJECT_ERRORS][path.join('.')] = error; + } else { + set(result, path, error); + } + } + }; + + errorItems.forEach((item) => { + if (!item.error) { + return; + } + + if (isObjectLike(item.error)) { + getValuePaths(item.error).forEach((path) => { + setError([...item.path, ...path], get(item.error, path)); + }); + + return; + } + + if (isBoolean(item.error) || isString(item.error) || isUndefined(item.error)) { + setError(item.path, item.error); + + return; + } + }); + + return result; +}; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils/process-errors-state.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils/process-errors-state.ts new file mode 100644 index 00000000..1e17fac3 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils/process-errors-state.ts @@ -0,0 +1,31 @@ +import mapValues from 'lodash/mapValues'; + +import type {ErrorsState} from '../../mutators'; +import {parseFinalFormPath} from '../../utils'; +import type {ValidateErrorItem} from '../types'; + +interface ProcessErrorsStateParams { + errorsState: ErrorsState | undefined; +} + +interface ProcessErrorsStateReturn { + externalPriorityErrorItems: ValidateErrorItem[]; + externalRegularErrorItems: ValidateErrorItem[]; +} + +export const processErrorsState = ({ + errorsState, +}: ProcessErrorsStateParams): ProcessErrorsStateReturn => { + const getErrorItems = (errors: ErrorsState['priorityErrors'] | ErrorsState['regularErrors']) => + Object.values( + mapValues(errors, (value, key) => ({ + path: parseFinalFormPath(key), + error: value, + })), + ); + + return { + externalPriorityErrorItems: getErrorItems(errorsState?.priorityErrors), + externalRegularErrorItems: getErrorItems(errorsState?.regularErrors), + }; +};