From 01611967114ed7d631fec21f2feaa638451b17f6 Mon Sep 17 00:00:00 2001 From: Adrian Tedeschi Date: Fri, 24 Nov 2023 09:59:09 -0300 Subject: [PATCH] #2043 add a ValidationErrorNoStack class to be used when no stack trace is required --- src/BasicValidationError.ts | 69 ++++++++++++++++++++++++++++++++++++ src/ValidationError.ts | 15 +++----- src/util/createValidation.ts | 28 +++++++++++---- test/array.ts | 14 ++++++++ test/mixed.ts | 13 +++++++ 5 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 src/BasicValidationError.ts diff --git a/src/BasicValidationError.ts b/src/BasicValidationError.ts new file mode 100644 index 000000000..4ac7b066f --- /dev/null +++ b/src/BasicValidationError.ts @@ -0,0 +1,69 @@ +import ValidationError from './ValidationError'; +import printValue from './util/printValue'; +import toArray from './util/toArray'; + +let strReg = /\$\{\s*(\w+)\s*\}/g; + +type Params = Record; + +export default class ValidationErrorNoStack implements ValidationError { + name: string; + message: string; + stack?: string | undefined; + value: any; + path?: string; + type?: string; + errors: string[]; + + params?: Params; + + inner: ValidationErrorNoStack[]; + + static formatError( + message: string | ((params: Params) => string) | unknown, + params: Params, + ) { + const path = params.label || params.path || 'this'; + if (path !== params.path) params = { ...params, path }; + + if (typeof message === 'string') + return message.replace(strReg, (_, key) => printValue(params[key])); + if (typeof message === 'function') return message(params); + + return message; + } + + constructor( + errorOrErrors: + | string + | ValidationErrorNoStack + | readonly ValidationErrorNoStack[], + value?: any, + field?: string, + type?: string, + ) { + this.name = 'ValidationError'; + this.value = value; + this.path = field; + this.type = type; + + this.errors = []; + this.inner = []; + + toArray(errorOrErrors).forEach((err) => { + if (ValidationError.isError(err)) { + this.errors.push(...err.errors); + const innerErrors = err.inner.length ? err.inner : [err]; + this.inner.push(...innerErrors); + } else { + this.errors.push(err); + } + }); + + this.message = + this.errors.length > 1 + ? `${this.errors.length} errors occurred` + : this.errors[0]; + } + [Symbol.toStringTag] = 'Error'; +} diff --git a/src/ValidationError.ts b/src/ValidationError.ts index e84c56f5d..f02879ff8 100644 --- a/src/ValidationError.ts +++ b/src/ValidationError.ts @@ -5,10 +5,7 @@ let strReg = /\$\{\s*(\w+)\s*\}/g; type Params = Record; -export default class ValidationError implements Error { - name: string; - message: string; - stack?: string | undefined; +export default class ValidationError extends Error { value: any; path?: string; type?: string; @@ -41,8 +38,9 @@ export default class ValidationError implements Error { value?: any, field?: string, type?: string, - disableStack?: boolean, ) { + super(); + this.name = 'ValidationError'; this.value = value; this.path = field; @@ -54,8 +52,7 @@ export default class ValidationError implements Error { toArray(errorOrErrors).forEach((err) => { if (ValidationError.isError(err)) { this.errors.push(...err.errors); - const innerErrors = err.inner.length ? err.inner : [err]; - this.inner.push(...innerErrors); + this.inner = this.inner.concat(err.inner.length ? err.inner : err); } else { this.errors.push(err); } @@ -66,8 +63,6 @@ export default class ValidationError implements Error { ? `${this.errors.length} errors occurred` : this.errors[0]; - if (!disableStack && Error.captureStackTrace) - Error.captureStackTrace(this, ValidationError); + if (Error.captureStackTrace) Error.captureStackTrace(this, ValidationError); } - [Symbol.toStringTag] = 'Error'; } diff --git a/src/util/createValidation.ts b/src/util/createValidation.ts index ba543b9c8..83d698892 100644 --- a/src/util/createValidation.ts +++ b/src/util/createValidation.ts @@ -10,6 +10,7 @@ import { import Reference from '../Reference'; import type { AnySchema } from '../schema'; import isAbsent from './isAbsent'; +import ValidationErrorNoStack from '../BasicValidationError'; export type PanicCallback = (err: Error) => void; @@ -98,6 +99,7 @@ export default function createValidation(config: { label: schema.spec.label, path: overrides.path || path, spec: schema.spec, + disableStackTrace: overrides.disableStackTrace || disableStackTrace, ...params, ...overrides.params, }; @@ -106,13 +108,25 @@ export default function createValidation(config: { for (const key of Object.keys(nextParams) as Keys) nextParams[key] = resolve(nextParams[key]); - const error = new ValidationError( - ValidationError.formatError(overrides.message || message, nextParams), - value, - nextParams.path, - overrides.type || name, - overrides.disableStackTrace ?? disableStackTrace, - ); + const error = nextParams.disableStackTrace + ? new ValidationErrorNoStack( + ValidationErrorNoStack.formatError( + overrides.message || message, + nextParams, + ), + value, + nextParams.path, + overrides.type || name, + ) + : new ValidationError( + ValidationError.formatError( + overrides.message || message, + nextParams, + ), + value, + nextParams.path, + overrides.type || name, + ); error.params = nextParams; return error; } diff --git a/test/array.ts b/test/array.ts index b01b5b1d5..c1caab59d 100644 --- a/test/array.ts +++ b/test/array.ts @@ -7,6 +7,7 @@ import { AnySchema, ValidationError, } from '../src'; +import ValidationErrorNoStack from '../src/BasicValidationError'; describe('Array types', () => { describe('casting', () => { @@ -158,6 +159,19 @@ describe('Array types', () => { ); }); + it('should respect disableStackTrace', async () => { + let inst = array().of(object({ str: string().required() })); + + const data = [{ str: undefined }, { str: undefined }]; + return Promise.all([ + expect(inst.strict().validate(data)).rejects.toThrow(ValidationError), + + expect( + inst.strict().validate(data, { disableStackTrace: true }), + ).rejects.toThrow(ValidationErrorNoStack), + ]); + }); + it('should compact arrays', () => { let arr = ['', 1, 0, 4, false, null], inst = array(); diff --git a/test/mixed.ts b/test/mixed.ts index f1848e50f..321167046 100644 --- a/test/mixed.ts +++ b/test/mixed.ts @@ -12,6 +12,7 @@ import { tuple, ValidationError, } from '../src'; +import ValidationErrorNoStack from '../src/BasicValidationError'; import ObjectSchema from '../src/object'; import { ISchema } from '../src/types'; import { ensureSync, validateAll } from './helpers'; @@ -338,6 +339,18 @@ describe('Mixed Types ', () => { ]); }); + it('should respect disableStackTrace', () => { + let inst = string().trim(); + + return Promise.all([ + expect(inst.strict().validate(' hi ')).rejects.toThrow(ValidationError), + + expect( + inst.strict().validate(' hi ', { disableStackTrace: true }), + ).rejects.toThrow(ValidationErrorNoStack), + ]); + }); + it('should overload test()', () => { let inst = mixed().test('test', noop);