diff --git a/src/vine/validator.ts b/src/vine/validator.ts index 2b1013e..7f14d97 100644 --- a/src/vine/validator.ts +++ b/src/vine/validator.ts @@ -12,6 +12,7 @@ import type { MessagesProviderContact, Refs, RootNode } from '@vinejs/compiler/t import { messages } from '../defaults.js' import { ITYPE, OTYPE, PARSE } from '../symbols.js' +import { ValidationError } from '../errors/validation_error.js' import type { Infer, SchemaTypes, @@ -158,6 +159,28 @@ export class VineValidator< } } + /** + * Performs validation without throwing the validation + * exception. Instead, the validation errors are + * returned as the first argument. + */ + async tryValidate( + data: any, + ...[options]: [undefined] extends MetaData + ? [options?: ValidationOptions | undefined] + : [options: ValidationOptions] + ): Promise<[ValidationError, null] | [null, Infer]> { + try { + const result = await this.validate(data, options!) + return [null, result] + } catch (error) { + if (error instanceof ValidationError) { + return [error, null] + } + throw error + } + } + /** * Returns the compiled schema and refs. */ diff --git a/tests/integration/validator.spec.ts b/tests/integration/validator.spec.ts index d989f55..f2b7912 100644 --- a/tests/integration/validator.spec.ts +++ b/tests/integration/validator.spec.ts @@ -20,6 +20,8 @@ import vine, { VineLiteral, VineBoolean, } from '../../index.js' +import { Infer } from '../../src/types.js' +import { ValidationError } from '../../src/errors/validation_error.js' test.group('Validator | metadata', () => { test('pass metadata to the validation pipeline', async ({ assert }) => { @@ -314,3 +316,79 @@ test.group('Validator | toJSON', () => { `) }) }) + +test.group('Validator | tryValidator', () => { + test('return validation errors without throwing an exception', async ({ + assert, + expectTypeOf, + }) => { + const author = vine.object({ + name: vine.string(), + email: vine.string().email(), + }) + + const validator = vine.compile(author) + const [error, result] = await validator.tryValidate({}) + assert.instanceOf(error, ValidationError) + assert.isNull(result) + + if (error) { + expectTypeOf(result).toMatchTypeOf(null) + expectTypeOf(error).toMatchTypeOf() + } + if (result) { + expectTypeOf(error).toMatchTypeOf(null) + expectTypeOf(result).toMatchTypeOf>() + } + }) + + test('rethrow non ValidationError errors', async () => { + const author = vine.object({ + name: vine.string(), + email: vine.string().email(), + }) + + const validator = vine + .withMetaData<{ choices: string[] }>(() => { + throw new Error('Invalid metadata') + }) + .compile(author) + + await validator.tryValidate( + {}, + { + meta: { + choices: [], + }, + } + ) + }).throws('Invalid metadata') + + test('return validated data', async ({ assert, expectTypeOf }) => { + const author = vine.object({ + name: vine.string(), + email: vine.string().email(), + }) + + const validator = vine.compile(author) + const [error, result] = await validator.tryValidate({ + name: 'virk', + email: 'foo@bar.com', + }) + + assert.isNull(error) + assert.deepEqual(result, { + name: 'virk', + email: 'foo@bar.com', + }) + + if (error) { + expectTypeOf(result).toMatchTypeOf(null) + expectTypeOf(error).toMatchTypeOf() + } + if (result) { + expectTypeOf(error).toMatchTypeOf(null) + expectTypeOf(result).toMatchTypeOf>() + } + }) +})