From 57dc8532094617a0576982a8d1f63121eaf5f417 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Sun, 30 Jul 2023 21:39:40 +0200 Subject: [PATCH] Add abortEarly option to abort on first error --- library/CHANGELOG.md | 1 + library/src/error/ValiError/ValiError.ts | 1 + library/src/methods/is/is.ts | 2 +- library/src/methods/parse/parse.ts | 2 +- library/src/methods/parse/parseAsync.ts | 2 +- library/src/methods/safeParse/safeParse.ts | 2 +- .../src/methods/safeParse/safeParseAsync.ts | 2 +- library/src/schemas/array/array.test.ts | 24 ++++++++++ library/src/schemas/array/array.ts | 5 +- library/src/schemas/array/arrayAsync.test.ts | 24 ++++++++++ library/src/schemas/array/arrayAsync.ts | 5 +- library/src/schemas/map/map.test.ts | 34 ++++++++++++++ library/src/schemas/map/map.ts | 10 +++- library/src/schemas/map/mapAsync.test.ts | 34 ++++++++++++++ library/src/schemas/map/mapAsync.ts | 10 +++- library/src/schemas/object/object.test.ts | 24 ++++++++++ library/src/schemas/object/object.ts | 5 +- .../src/schemas/object/objectAsync.test.ts | 24 ++++++++++ library/src/schemas/object/objectAsync.ts | 5 +- library/src/schemas/record/record.test.ts | 36 ++++++++++++++ library/src/schemas/record/record.ts | 10 +++- .../src/schemas/record/recordAsync.test.ts | 36 ++++++++++++++ library/src/schemas/record/recordAsync.ts | 10 +++- library/src/schemas/set/set.test.ts | 24 ++++++++++ library/src/schemas/set/set.ts | 5 +- library/src/schemas/set/setAsync.test.ts | 24 ++++++++++ library/src/schemas/set/setAsync.ts | 5 +- library/src/schemas/tuple/tuple.test.ts | 34 ++++++++++++++ library/src/schemas/tuple/tuple.ts | 10 +++- library/src/schemas/tuple/tupleAsync.test.ts | 34 ++++++++++++++ library/src/schemas/tuple/tupleAsync.ts | 10 +++- library/src/types.ts | 2 +- library/src/utils/executePipe/executePipe.ts | 12 +++-- .../src/utils/executePipe/executePipeAsync.ts | 8 ++-- .../(main-concepts)/parse-data/index.mdx | 47 +++++++++++++++++++ .../(main-concepts)/pipelines/index.mdx | 2 +- 36 files changed, 492 insertions(+), 33 deletions(-) diff --git a/library/CHANGELOG.md b/library/CHANGELOG.md index 3c9f79ead..830d14ab6 100644 --- a/library/CHANGELOG.md +++ b/library/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the library will be documented in this file. - Add `is` method which can be used as a type guard (pull request #13) - Throw all validation issues of a pipeline by default (issues #18) - Add `abortPipeEarly` option to abort pipe on first error (issues #18) +- Add `abortEarly` option to abort on first error ## v0.6.0 (July 30, 2023) diff --git a/library/src/error/ValiError/ValiError.ts b/library/src/error/ValiError/ValiError.ts index 7ee4323e1..aa4b4cb24 100644 --- a/library/src/error/ValiError/ValiError.ts +++ b/library/src/error/ValiError/ValiError.ts @@ -33,6 +33,7 @@ export type Issue = { input: any; path?: PathItem[]; issues?: Issues; + abortEarly?: boolean; abortPipeEarly?: boolean; }; diff --git a/library/src/methods/is/is.ts b/library/src/methods/is/is.ts index d9fde2a6d..c7bbb6562 100644 --- a/library/src/methods/is/is.ts +++ b/library/src/methods/is/is.ts @@ -14,7 +14,7 @@ export function is( input: unknown ): input is Input { try { - schema.parse(input, { abortPipeEarly: true }); + schema.parse(input, { abortEarly: true }); return true; } catch (error) { return false; diff --git a/library/src/methods/parse/parse.ts b/library/src/methods/parse/parse.ts index 56f69c135..fb1f7d904 100644 --- a/library/src/methods/parse/parse.ts +++ b/library/src/methods/parse/parse.ts @@ -12,7 +12,7 @@ import type { BaseSchema, Output, ParseInfo } from '../../types.ts'; export function parse( schema: TSchema, input: unknown, - info?: Pick + info?: Pick ): Output { return schema.parse(input, info); } diff --git a/library/src/methods/parse/parseAsync.ts b/library/src/methods/parse/parseAsync.ts index b57b7052f..46d1e624f 100644 --- a/library/src/methods/parse/parseAsync.ts +++ b/library/src/methods/parse/parseAsync.ts @@ -17,7 +17,7 @@ import type { export async function parseAsync( schema: TSchema, input: unknown, - info?: Pick + info?: Pick ): Promise> { return schema.parse(input, info); } diff --git a/library/src/methods/safeParse/safeParse.ts b/library/src/methods/safeParse/safeParse.ts index 33f0cd6f8..3263980b3 100644 --- a/library/src/methods/safeParse/safeParse.ts +++ b/library/src/methods/safeParse/safeParse.ts @@ -13,7 +13,7 @@ import type { BaseSchema, Output, ParseInfo } from '../../types.ts'; export function safeParse( schema: TSchema, input: unknown, - info?: Pick + info?: Pick ): | { success: true; data: Output } | { success: false; error: ValiError } { diff --git a/library/src/methods/safeParse/safeParseAsync.ts b/library/src/methods/safeParse/safeParseAsync.ts index 7cfdcdf06..01ccff31e 100644 --- a/library/src/methods/safeParse/safeParseAsync.ts +++ b/library/src/methods/safeParse/safeParseAsync.ts @@ -20,7 +20,7 @@ export async function safeParseAsync< >( schema: TSchema, input: unknown, - info?: Pick + info?: Pick ): Promise< | { success: true; data: Output } | { success: false; error: ValiError } diff --git a/library/src/schemas/array/array.test.ts b/library/src/schemas/array/array.test.ts index 4c28d7bcd..ea94f79a5 100644 --- a/library/src/schemas/array/array.test.ts +++ b/library/src/schemas/array/array.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parse } from '../../methods/index.ts'; import { maxLength, @@ -36,6 +37,29 @@ describe('array', () => { expect(() => parse(schema, 123)).toThrowError(error); }); + test('should throw every issue', () => { + const schema = array(number()); + const input = ['1', 2, '3']; + expect(() => parse(schema, input)).toThrowError(); + try { + parse(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(2); + } + }); + + test('should throw only first issue', () => { + const schema = array(number()); + const input = ['1', 2, '3']; + const info = { abortEarly: true }; + expect(() => parse(schema, input, info)).toThrowError(); + try { + parse(schema, input, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + } + }); + test('should execute pipe', () => { const lengthError = 'Invalid length'; const contentError = 'Invalid content'; diff --git a/library/src/schemas/array/array.ts b/library/src/schemas/array/array.ts index 0963e4750..5832bf600 100644 --- a/library/src/schemas/array/array.ts +++ b/library/src/schemas/array/array.ts @@ -122,8 +122,11 @@ export function array( }) ); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } }); diff --git a/library/src/schemas/array/arrayAsync.test.ts b/library/src/schemas/array/arrayAsync.test.ts index d065670e2..347549d2d 100644 --- a/library/src/schemas/array/arrayAsync.test.ts +++ b/library/src/schemas/array/arrayAsync.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parseAsync } from '../../methods/index.ts'; import { maxLength, @@ -36,6 +37,29 @@ describe('array', () => { await expect(parseAsync(schema, 123)).rejects.toThrowError(error); }); + test('should throw every issue', async () => { + const schema = arrayAsync(number()); + const input = ['1', 2, '3']; + await expect(parseAsync(schema, input)).rejects.toThrowError(); + try { + await parseAsync(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(2); + } + }); + + test('should throw only first issue', async () => { + const schema = arrayAsync(number()); + const input = ['1', 2, '3']; + const info = { abortEarly: true }; + await expect(parseAsync(schema, input, info)).rejects.toThrowError(); + try { + await parseAsync(schema, input, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + } + }); + test('should execute pipe', async () => { const lengthError = 'Invalid length'; const contentError = 'Invalid content'; diff --git a/library/src/schemas/array/arrayAsync.ts b/library/src/schemas/array/arrayAsync.ts index bf657db40..d1bcc3690 100644 --- a/library/src/schemas/array/arrayAsync.ts +++ b/library/src/schemas/array/arrayAsync.ts @@ -117,8 +117,11 @@ export function arrayAsync( }), }); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } }) diff --git a/library/src/schemas/map/map.test.ts b/library/src/schemas/map/map.test.ts index 16f823d37..b123c8a4c 100644 --- a/library/src/schemas/map/map.test.ts +++ b/library/src/schemas/map/map.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parse } from '../../methods/index.ts'; import { maxSize, minSize, size } from '../../validations/index.ts'; import { map } from '../map/index.ts'; @@ -34,6 +35,39 @@ describe('map', () => { expect(() => parse(schema, new Set())).toThrowError(error); }); + test('should throw every issue', () => { + const schema = map(number(), string()); + const input = new Map().set(1, 1).set(2, '2').set('3', '3'); + expect(() => parse(schema, input)).toThrowError(); + try { + parse(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(2); + } + }); + + test('should throw only first issue', () => { + const schema = map(number(), string()); + const info = { abortEarly: true }; + const input1 = new Map().set(1, 1).set(2, '2').set('3', '3'); + expect(() => parse(schema, input1, info)).toThrowError(); + try { + parse(schema, input1, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + expect((error as ValiError).issues[0].origin).toBe('value'); + } + + const input2 = new Map().set('1', 1).set(2, '2').set('3', '3'); + expect(() => parse(schema, input2, info)).toThrowError(); + try { + parse(schema, input2, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + expect((error as ValiError).issues[0].origin).toBe('key'); + } + }); + test('should execute pipe', () => { const sizeError = 'Invalid size'; diff --git a/library/src/schemas/map/map.ts b/library/src/schemas/map/map.ts index d56b2e566..344c9a814 100644 --- a/library/src/schemas/map/map.ts +++ b/library/src/schemas/map/map.ts @@ -121,8 +121,11 @@ export function map( // further down can be recognized as valid value outputKey = [key.parse(inputKey, { ...info, origin: 'key', path })]; - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } @@ -133,8 +136,11 @@ export function map( // further down can be recognized as valid value outputValue = [value.parse(inputValue, { ...info, path })]; - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } diff --git a/library/src/schemas/map/mapAsync.test.ts b/library/src/schemas/map/mapAsync.test.ts index 872db699a..f55190d9a 100644 --- a/library/src/schemas/map/mapAsync.test.ts +++ b/library/src/schemas/map/mapAsync.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parseAsync } from '../../methods/index.ts'; import { mapAsync } from '../map/index.ts'; import { string } from '../string/index.ts'; @@ -38,6 +39,39 @@ describe('mapAsync', () => { await expect(parseAsync(schema, new Set())).rejects.toThrowError(error); }); + test('should throw every issue', async () => { + const schema = mapAsync(number(), string()); + const input = new Map().set(1, 1).set(2, '2').set('3', '3'); + await expect(parseAsync(schema, input)).rejects.toThrowError(); + try { + await parseAsync(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(2); + } + }); + + test('should throw only first issue', async () => { + const schema = mapAsync(number(), string()); + const info = { abortEarly: true }; + const input1 = new Map().set(1, 1).set(2, '2').set('3', '3'); + await expect(parseAsync(schema, input1, info)).rejects.toThrowError(); + try { + await parseAsync(schema, input1, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + expect((error as ValiError).issues[0].origin).toBe('value'); + } + + const input2 = new Map().set('1', 1).set(2, '2').set('3', '3'); + await expect(parseAsync(schema, input2, info)).rejects.toThrowError(); + try { + await parseAsync(schema, input2, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + expect((error as ValiError).issues[0].origin).toBe('key'); + } + }); + test('should execute pipe', async () => { const sizeError = 'Invalid size'; diff --git a/library/src/schemas/map/mapAsync.ts b/library/src/schemas/map/mapAsync.ts index dbab0380c..a10356f68 100644 --- a/library/src/schemas/map/mapAsync.ts +++ b/library/src/schemas/map/mapAsync.ts @@ -139,8 +139,11 @@ export function mapAsync< await key.parse(inputKey, { ...info, origin: 'key', path }), ] as const; - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } })(), @@ -154,8 +157,11 @@ export function mapAsync< await value.parse(inputValue, { ...info, path }), ] as const; - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } })(), diff --git a/library/src/schemas/object/object.test.ts b/library/src/schemas/object/object.test.ts index 992ad988c..e90477808 100644 --- a/library/src/schemas/object/object.test.ts +++ b/library/src/schemas/object/object.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parse } from '../../methods/index.ts'; import { number } from '../number/index.ts'; import { string } from '../string/index.ts'; @@ -22,6 +23,29 @@ describe('object', () => { expect(() => parse(schema, 123)).toThrowError(error); }); + test('should throw every issue', () => { + const schema = object({ 1: number(), 2: number(), 3: number() }); + const input = { 1: '1', 2: 2, 3: '3' }; + expect(() => parse(schema, input)).toThrowError(); + try { + parse(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(2); + } + }); + + test('should throw only first issue', () => { + const schema = object({ 1: number(), 2: number(), 3: number() }); + const input = { 1: '1', 2: 2, 3: '3' }; + const info = { abortEarly: true }; + expect(() => parse(schema, input, info)).toThrowError(); + try { + parse(schema, input, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + } + }); + test('should execute pipe', () => { const input = { key1: '1', key2: 1 }; const transformInput = () => ({ key1: '2', key2: 2 }); diff --git a/library/src/schemas/object/object.ts b/library/src/schemas/object/object.ts index f42f9c165..e56aa965f 100644 --- a/library/src/schemas/object/object.ts +++ b/library/src/schemas/object/object.ts @@ -116,8 +116,11 @@ export function object( path: getCurrentPath(info, { schema: 'object', input, key, value }), }); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } }); diff --git a/library/src/schemas/object/objectAsync.test.ts b/library/src/schemas/object/objectAsync.test.ts index 15ad0e45c..5ae6f142c 100644 --- a/library/src/schemas/object/objectAsync.test.ts +++ b/library/src/schemas/object/objectAsync.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parseAsync } from '../../methods/index.ts'; import { number } from '../number/index.ts'; import { string, stringAsync } from '../string/index.ts'; @@ -22,6 +23,29 @@ describe('objectAsync', () => { await expect(parseAsync(schema, 123)).rejects.toThrowError(error); }); + test('should throw every issue', async () => { + const schema = objectAsync({ 1: number(), 2: number(), 3: number() }); + const input = { 1: '1', 2: 2, 3: '3' }; + await expect(parseAsync(schema, input)).rejects.toThrowError(); + try { + await parseAsync(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(2); + } + }); + + test('should throw only first issue', async () => { + const schema = objectAsync({ 1: number(), 2: number(), 3: number() }); + const input = { 1: '1', 2: 2, 3: '3' }; + const info = { abortEarly: true }; + await expect(parseAsync(schema, input, info)).rejects.toThrowError(); + try { + await parseAsync(schema, input, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + } + }); + test('should execute pipe', async () => { const input = { key1: '1', key2: 1 }; const transformInput = () => ({ key1: '2', key2: 2 }); diff --git a/library/src/schemas/object/objectAsync.ts b/library/src/schemas/object/objectAsync.ts index 58de23264..2d2a1092d 100644 --- a/library/src/schemas/object/objectAsync.ts +++ b/library/src/schemas/object/objectAsync.ts @@ -125,8 +125,11 @@ export function objectAsync( }), }); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } }) diff --git a/library/src/schemas/record/record.test.ts b/library/src/schemas/record/record.test.ts index d02906fbf..af754c0cb 100644 --- a/library/src/schemas/record/record.test.ts +++ b/library/src/schemas/record/record.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parse } from '../../methods/index.ts'; import { minLength } from '../../validations/index.ts'; import { number } from '../number/index.ts'; @@ -36,6 +37,41 @@ describe('record', () => { expect(() => parse(schema2, new Date())).toThrowError(error); }); + test('should throw every issue', () => { + const schema = record(number()); + const input = { 1: '1', 2: 2, 3: '3' }; + expect(() => parse(schema, input)).toThrowError(); + try { + parse(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(2); + } + }); + + test('should throw only first issue', () => { + const info = { abortEarly: true }; + + const schema1 = record(number()); + const input1 = { 1: '1', 2: 2, 3: '3' }; + expect(() => parse(schema1, input1, info)).toThrowError(); + try { + parse(schema1, input1, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + expect((error as ValiError).issues[0].origin).toBe('value'); + } + + const schema2 = record(string([minLength(2)]), number()); + const input2 = { '1': '1', 2: 2, 3: '3' }; + expect(() => parse(schema2, input2, info)).toThrowError(); + try { + parse(schema2, input2, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + expect((error as ValiError).issues[0].origin).toBe('key'); + } + }); + test('should execute pipe', () => { const input = { key1: 1, key2: 1 }; const transformInput = () => ({ key1: 2, key2: 2 }); diff --git a/library/src/schemas/record/record.ts b/library/src/schemas/record/record.ts index 5e8550caf..c6b9607ba 100644 --- a/library/src/schemas/record/record.ts +++ b/library/src/schemas/record/record.ts @@ -176,8 +176,11 @@ export function record< try { outputKey = key.parse(inputKey, { ...info, origin: 'key', path }); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } @@ -188,8 +191,11 @@ export function record< // down can be recognized as valid value outputValue = [value.parse(inputValue, { ...info, path })]; - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } diff --git a/library/src/schemas/record/recordAsync.test.ts b/library/src/schemas/record/recordAsync.test.ts index e29ce5f68..ddb2f39a0 100644 --- a/library/src/schemas/record/recordAsync.test.ts +++ b/library/src/schemas/record/recordAsync.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parseAsync } from '../../methods/index.ts'; import { minLength } from '../../validations/index.ts'; import { number } from '../number/index.ts'; @@ -42,6 +43,41 @@ describe('recordAsync', () => { await expect(parseAsync(schema2, new Date())).rejects.toThrowError(error); }); + test('should throw every issue', async () => { + const schema = recordAsync(number()); + const input = { 1: '1', 2: 2, 3: '3' }; + await expect(parseAsync(schema, input)).rejects.toThrowError(); + try { + await parseAsync(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(2); + } + }); + + test('should throw only first issue', async () => { + const info = { abortEarly: true }; + + const schema1 = recordAsync(number()); + const input1 = { 1: '1', 2: 2, 3: '3' }; + await expect(parseAsync(schema1, input1, info)).rejects.toThrowError(); + try { + await parseAsync(schema1, input1, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + expect((error as ValiError).issues[0].origin).toBe('value'); + } + + const schema2 = recordAsync(string([minLength(2)]), number()); + const input2 = { '1': '1', 2: 2, 3: '3' }; + await expect(parseAsync(schema2, input2, info)).rejects.toThrowError(); + try { + await parseAsync(schema2, input2, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + expect((error as ValiError).issues[0].origin).toBe('key'); + } + }); + test('should execute pipe', async () => { const input = { key1: 1, key2: 1 }; const transformInput = () => ({ key1: 2, key2: 2 }); diff --git a/library/src/schemas/record/recordAsync.ts b/library/src/schemas/record/recordAsync.ts index 1bcb60cdb..cd087ac80 100644 --- a/library/src/schemas/record/recordAsync.ts +++ b/library/src/schemas/record/recordAsync.ts @@ -191,8 +191,11 @@ export function recordAsync< path, }); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } })(), @@ -206,8 +209,11 @@ export function recordAsync< await value.parse(inputValue, { ...info, path }), ] as const; - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } })(), diff --git a/library/src/schemas/set/set.test.ts b/library/src/schemas/set/set.test.ts index aa23c62cf..c1cf2ab6d 100644 --- a/library/src/schemas/set/set.test.ts +++ b/library/src/schemas/set/set.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parse } from '../../methods/index.ts'; import { maxSize, minSize, size } from '../../validations/index.ts'; import { string } from '../string/index.ts'; @@ -37,6 +38,29 @@ describe('set', () => { expect(() => parse(schema, 'test')).toThrowError(error); }); + test('should throw every issue', () => { + const schema = set(number()); + const input = new Set().add('1').add(2).add('3'); + expect(() => parse(schema, input)).toThrowError(); + try { + parse(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(2); + } + }); + + test('should throw only first issue', () => { + const schema = set(number()); + const input = new Set().add('1').add(2).add('3'); + const info = { abortEarly: true }; + expect(() => parse(schema, input, info)).toThrowError(); + try { + parse(schema, input, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + } + }); + test('should execute pipe', () => { const sizeError = 'Invalid size'; diff --git a/library/src/schemas/set/set.ts b/library/src/schemas/set/set.ts index d16629418..a98fa5701 100644 --- a/library/src/schemas/set/set.ts +++ b/library/src/schemas/set/set.ts @@ -114,8 +114,11 @@ export function set( }) ); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } } diff --git a/library/src/schemas/set/setAsync.test.ts b/library/src/schemas/set/setAsync.test.ts index 47472802a..fcc44791c 100644 --- a/library/src/schemas/set/setAsync.test.ts +++ b/library/src/schemas/set/setAsync.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parseAsync } from '../../methods/index.ts'; import { maxSize, minSize, size } from '../../validations/index.ts'; import { string } from '../string/index.ts'; @@ -39,6 +40,29 @@ describe('setAsync', () => { await expect(parseAsync(schema, 'test')).rejects.toThrowError(error); }); + test('should throw every issue', async () => { + const schema = setAsync(number()); + const input = new Set().add('1').add(2).add('3'); + await expect(parseAsync(schema, input)).rejects.toThrowError(); + try { + await parseAsync(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(2); + } + }); + + test('should throw only first issue', async () => { + const schema = setAsync(number()); + const input = new Set().add('1').add(2).add('3'); + const info = { abortEarly: true }; + await expect(parseAsync(schema, input, info)).rejects.toThrowError(); + try { + await parseAsync(schema, input, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + } + }); + test('should execute pipe', async () => { const sizeError = 'Invalid size'; diff --git a/library/src/schemas/set/setAsync.ts b/library/src/schemas/set/setAsync.ts index a17813527..a99dd97e3 100644 --- a/library/src/schemas/set/setAsync.ts +++ b/library/src/schemas/set/setAsync.ts @@ -114,8 +114,11 @@ export function setAsync( }) ); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } }) diff --git a/library/src/schemas/tuple/tuple.test.ts b/library/src/schemas/tuple/tuple.test.ts index a2bf17615..87e7bd1c3 100644 --- a/library/src/schemas/tuple/tuple.test.ts +++ b/library/src/schemas/tuple/tuple.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parse } from '../../methods/index.ts'; import { maxLength, minLength } from '../../validations/index.ts'; import { string } from '../string/index.ts'; @@ -39,6 +40,39 @@ describe('tuple', () => { expect(() => parse(tuple([number()], error), null)).toThrowError(error); }); + test('should throw every issue', () => { + const schema = tuple([string(), string(), string()], number()); + const input = [1, '2', 3, '4', 5, '6']; + expect(() => parse(schema, input)).toThrowError(); + try { + parse(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(4); + } + }); + + test('should throw only first issue', () => { + const info = { abortEarly: true }; + + const schema1 = tuple([number(), number(), number()]); + const input1 = ['1', 2, '3']; + expect(() => parse(schema1, input1, info)).toThrowError(); + try { + parse(schema1, input1, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + } + + const schema2 = tuple([string()], number()); + const input2 = ['hello', 1, '2', 3, '4']; + expect(() => parse(schema2, input2, info)).toThrowError(); + try { + parse(schema2, input2, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + } + }); + test('should execute pipe', () => { const lengthError = 'Invalid length'; diff --git a/library/src/schemas/tuple/tuple.ts b/library/src/schemas/tuple/tuple.ts index aee1ed7cc..72477ef07 100644 --- a/library/src/schemas/tuple/tuple.ts +++ b/library/src/schemas/tuple/tuple.ts @@ -178,8 +178,11 @@ export function tuple< }), }); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } }); @@ -199,8 +202,11 @@ export function tuple< }), }); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } }); diff --git a/library/src/schemas/tuple/tupleAsync.test.ts b/library/src/schemas/tuple/tupleAsync.test.ts index f7cc48ad2..15c79d5fc 100644 --- a/library/src/schemas/tuple/tupleAsync.test.ts +++ b/library/src/schemas/tuple/tupleAsync.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { type ValiError } from '../../error/index.ts'; import { parseAsync } from '../../methods/index.ts'; import { maxLength, minLength } from '../../validations/index.ts'; import { string } from '../string/index.ts'; @@ -41,6 +42,39 @@ describe('tupleAsync', () => { ).rejects.toThrowError(error); }); + test('should throw every issue', async () => { + const schema = tupleAsync([string(), string(), string()], number()); + const input = [1, '2', 3, '4', 5, '6']; + await expect(parseAsync(schema, input)).rejects.toThrowError(); + try { + await parseAsync(schema, input); + } catch (error) { + expect((error as ValiError).issues.length).toBe(4); + } + }); + + test('should throw only first issue', async () => { + const info = { abortEarly: true }; + + const schema1 = tupleAsync([number(), number(), number()]); + const input1 = ['1', 2, '3']; + await expect(parseAsync(schema1, input1, info)).rejects.toThrowError(); + try { + await parseAsync(schema1, input1, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + } + + const schema2 = tupleAsync([string()], number()); + const input2 = ['hello', 1, '2', 3, '4']; + await expect(parseAsync(schema2, input2, info)).rejects.toThrowError(); + try { + await parseAsync(schema2, input2, info); + } catch (error) { + expect((error as ValiError).issues.length).toBe(1); + } + }); + test('should execute pipe', async () => { const lengthError = 'Invalid length'; diff --git a/library/src/schemas/tuple/tupleAsync.ts b/library/src/schemas/tuple/tupleAsync.ts index ded9e92a7..aceaa9a20 100644 --- a/library/src/schemas/tuple/tupleAsync.ts +++ b/library/src/schemas/tuple/tupleAsync.ts @@ -183,8 +183,11 @@ export function tupleAsync< }), }); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } }) @@ -206,8 +209,11 @@ export function tupleAsync< }), }); - // Fill issues in case of an error + // Throw or fill issues in case of an error } catch (error) { + if (info?.abortEarly) { + throw error; + } issues.push(...(error as ValiError).issues); } }) diff --git a/library/src/types.ts b/library/src/types.ts index 64cef1e96..bd5ea0606 100644 --- a/library/src/types.ts +++ b/library/src/types.ts @@ -12,7 +12,7 @@ import type { * Parse info type. */ export type ParseInfo = Partial< - Pick + Pick >; /** diff --git a/library/src/utils/executePipe/executePipe.ts b/library/src/utils/executePipe/executePipe.ts index 5338d906b..e39e6efc5 100644 --- a/library/src/utils/executePipe/executePipe.ts +++ b/library/src/utils/executePipe/executePipe.ts @@ -20,16 +20,18 @@ export function executePipe( const issues: Issue[] = []; // Execute any action of pipe - for (const action of pipe) { + pipe.forEach((action) => { try { output = action(output, info); + + // Throw or fill issues in case of an error } catch (error) { - issues.push(...(error as ValiError).issues); - if (info.abortPipeEarly) { - break; + if (info.abortEarly || info.abortPipeEarly) { + throw error; } + issues.push(...(error as ValiError).issues); } - } + }); // Throw error if there are issues if (issues.length) { diff --git a/library/src/utils/executePipe/executePipeAsync.ts b/library/src/utils/executePipe/executePipeAsync.ts index 343ab1d7e..71d4506fb 100644 --- a/library/src/utils/executePipe/executePipeAsync.ts +++ b/library/src/utils/executePipe/executePipeAsync.ts @@ -23,11 +23,13 @@ export async function executePipeAsync( for (const action of pipe) { try { output = await action(output, info); + + // Throw or fill issues in case of an error } catch (error) { - issues.push(...(error as ValiError).issues); - if (info.abortPipeEarly) { - break; + if (info.abortEarly || info.abortPipeEarly) { + throw error; } + issues.push(...(error as ValiError).issues); } } diff --git a/website/src/routes/guides/(main-concepts)/parse-data/index.mdx b/website/src/routes/guides/(main-concepts)/parse-data/index.mdx index a43e79de3..07bb0a6e8 100644 --- a/website/src/routes/guides/(main-concepts)/parse-data/index.mdx +++ b/website/src/routes/guides/(main-concepts)/parse-data/index.mdx @@ -63,3 +63,50 @@ if (is(EmailSchema, data)) { const email = data; // string } ``` + +## Abort early + +By default, I collect each issue during validation to give you detailed feedback on why the input does not match the schema. If this is not required in your use case, you can control this behavior with `abortEarly` or `abortPipeEarly`. + +### Abort validation + +If you set `abortEarly` to `true` I immediately abort the validation after finding a first issue. If you just want to know if data matches a schema, but you don't care about the details, this can speed up the performance. + +```ts +import { object, parse, string } from 'valibot'; + +try { + const ProfileSchema = object({ + name: string(), + bio: string(), + }); + const profile = parse( + ProfileSchema, + { name: 'Jane', bio: '' }, + { abortEarly: true } + ); + + // Handle errors if one occurs +} catch (error) { + console.log(error); +} +``` + +### Abort pipeline + +If you only set `abortPipeEarly` to `true` I only abort the validation within a pipeline after I found a first issue. For example, if you want to show only the first error of a field when validating a form, you can use this option to speed up the performance of validation. + +```ts +import { email, endsWith, parse, string } from 'valibot'; + +try { + const EmailSchema = string([email(), endsWith('@example.com')]); + const email = parse(EmailSchema, 'jane@example.com', { + abortPipeEarly: true, + }); + + // Handle errors if one occurs +} catch (error) { + console.log(error); +} +``` diff --git a/website/src/routes/guides/(main-concepts)/pipelines/index.mdx b/website/src/routes/guides/(main-concepts)/pipelines/index.mdx index 99d14650f..29e967ab4 100644 --- a/website/src/routes/guides/(main-concepts)/pipelines/index.mdx +++ b/website/src/routes/guides/(main-concepts)/pipelines/index.mdx @@ -29,7 +29,7 @@ const EmailSchema = string([toTrimmed(), email(), endsWith('@example.com')]); Validation functions examine the input and, if the input does not meet a certain condition, throw a `ValiError`. If the input is valid, it is returned and, if present, picked up by the next function in the pipeline. -> By default I run a pipeline completely, even if an error is thrown in between, to collect all issues. If you want me to abort the pipeline early after the first error is thrown, you have to set the `abortPipeEarly` option of `parse`, `parseAsync`, `safeParse` or `safeParseAsync` to `true`. +> By default I run a pipeline completely, even if an error is thrown in between, to collect all issues. If you want me to abort the pipeline early after the first error is thrown, you have to set the `abortPipeEarly` option of `parse`, `parseAsync`, `safeParse` or `safeParseAsync` to `true`. Learn more here. ### Example