From 77312ff09cb75396b5290ee2f96738c634147efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Miszczyszyn?= Date: Sat, 1 May 2021 19:47:37 +0200 Subject: [PATCH] feat(errors): Add information about nested fields with errors (#61) * feat(errors): Add information about nested fields with errors * v0.7.4-0 * wip * Working implementation * Coverage * Final touch --- __tests__/errors.spec.ts | 97 ++++++++++++++++++++++++++++++++ __tests__/property-tests.spec.ts | 2 +- __tests__/refinements.spec.ts | 18 ++++-- __tests__/refinements.test-d.ts | 19 ++++--- __tests__/unit-tests.spec.ts | 11 ++-- package.json | 2 +- src/errors.ts | 45 +++++++++++---- src/modifiers/minArrayLength.ts | 4 +- src/modifiers/minStringLength.ts | 7 ++- src/modifiers/nil.ts | 2 +- src/modifiers/nullable.ts | 3 +- src/modifiers/optional.ts | 3 +- src/refine.ts | 37 +++++++++--- src/stringify.ts | 15 +++-- src/types.ts | 4 +- src/validators/__validate.ts | 3 +- src/validators/array.ts | 8 +-- src/validators/boolean.ts | 16 ++---- src/validators/date.ts | 18 +++--- src/validators/number.ts | 18 +++--- src/validators/object.ts | 28 +++++---- src/validators/oneOf.ts | 4 +- src/validators/string.ts | 18 +++--- src/validators/tuple.ts | 64 ++++++++++----------- src/validators/unknown.ts | 10 +--- 25 files changed, 302 insertions(+), 154 deletions(-) create mode 100644 __tests__/errors.spec.ts diff --git a/__tests__/errors.spec.ts b/__tests__/errors.spec.ts new file mode 100644 index 0000000..12141e1 --- /dev/null +++ b/__tests__/errors.spec.ts @@ -0,0 +1,97 @@ +import { number, object, string, validate, date, pipe, ValidationError, refine } from '../src'; +import { modifierToString } from '../src/refine'; + +const expectToMatchError = (fn: () => any, obj: Record) => { + try { + fn(); + } catch (err) { + if (err instanceof ValidationError) { + return expect(err.getDetails()).toStrictEqual(obj); + } + fail(err); + } + fail(); +}; + +describe('errors', () => { + it('should list fields in object which are incorrect', () => { + const validator = pipe( + object({ + validString: string(), + invalidNumber: number(), + validNumber: number(), + invalidString: string(), + validDate: date(), + invalidDate: date(), + }), + validate, + ); + + expectToMatchError( + () => + validator({ + validString: 'aaa', + invalidNumber: 'vvvv', + validNumber: 123, + invalidString: 1333, + validDate: new Date('2020'), + invalidDate: 'no siema eniu', + }), + { + invalidNumber: { expected: 'number', got: 'vvvv' }, + invalidString: { expected: 'string', got: 1333 }, + invalidDate: { expected: 'Date', got: 'no siema eniu' }, + }, + ); + }); + + it('should list nested objects which are undefined', () => { + const validator = pipe( + object({ + nested: object({ + invalid: string(), + })(), + }), + validate, + ); + + expectToMatchError(() => validator({}), { + nested: { expected: '{ invalid: string }', got: undefined }, + }); + }); + + it('should list nested objects which are undefined', () => { + const validator = pipe( + object({ + a: number(), + nested: object({ + invalid: string(), + })(), + }), + validate, + ); + + expectToMatchError(() => validator({ a: 123 }), { + nested: { expected: '{ invalid: string }', got: undefined }, + }); + }); + + it('should use custom refinement to string', () => { + const email = refine( + (value: string, t) => (value.includes('@') ? t.nextValid(value) : t.left(value)), + modifierToString('email'), + ); + + const validator = pipe( + object({ + // shouldBeEmail: string(email()), + shouldBeEmail: string(email()), + }), + validate, + ); + + expectToMatchError(() => validator({ shouldBeEmail: 123 }), { + shouldBeEmail: { expected: 'email(string)', got: 123 }, + }); + }); +}); diff --git a/__tests__/property-tests.spec.ts b/__tests__/property-tests.spec.ts index b08a0b1..72884eb 100644 --- a/__tests__/property-tests.spec.ts +++ b/__tests__/property-tests.spec.ts @@ -38,7 +38,7 @@ const throws = ( if (ErrorClass) { const isValid = error instanceof ErrorClass && (!message || error.message === message); if (!isValid) { - console.log(error); + // console.log(error); } return isValid; } diff --git a/__tests__/refinements.spec.ts b/__tests__/refinements.spec.ts index 2373cb0..312c768 100644 --- a/__tests__/refinements.spec.ts +++ b/__tests__/refinements.spec.ts @@ -11,21 +11,27 @@ import { validate, λ, } from '../src'; +import { modifierToString } from '../src/refine'; describe('refinements', () => { - const even = refine((value: number, t) => (value % 2 === 0 ? t.nextValid(value) : t.left(value))); + const even = refine( + (value: number, t) => (value % 2 === 0 ? t.nextValid(value) : t.left(value)), + modifierToString('even'), + ); const noDuplicateItems = refine((arr: ReadonlyArray, t) => { const allUnique = arr.every((item, index) => index === arr.indexOf(item)); return allUnique ? t.nextValid(arr) : t.left(arr); - }); + }, modifierToString('noDuplicateItems')); - const allowTimestamps = refine((value, t) => - typeof value === 'number' ? t.nextValid(new Date(value)) : t.nextValid(value), + const allowTimestamps = refine( + (value, t) => (typeof value === 'number' ? t.nextValid(new Date(value)) : t.nextValid(value)), + modifierToString('allowTimestamps'), ); - const presentOrFuture = refine((value: Date, t) => - value.getTime() >= Date.now() ? t.nextValid(value) : t.left(value), + const presentOrFuture = refine( + (value: Date, t) => (value.getTime() >= Date.now() ? t.nextValid(value) : t.left(value)), + modifierToString('presentOrFuture'), ); it('nullable', () => { diff --git a/__tests__/refinements.test-d.ts b/__tests__/refinements.test-d.ts index 2aba259..4d72eed 100644 --- a/__tests__/refinements.test-d.ts +++ b/__tests__/refinements.test-d.ts @@ -12,7 +12,7 @@ import { minArrayLength, minStringLength, } from '../src'; -import { refine } from '../src/refine'; +import { modifierToString, refine } from '../src/refine'; const nullR = pipe(string, nullable, validate)(''); expectType(nullR); @@ -20,29 +20,34 @@ expectType(nullR); const optionalR = pipe(string, optional, validate)(''); expectType(optionalR); -const even = refine((value: number, t) => (value % 2 === 0 ? t.nextValid(value) : t.left(value))); +const even = refine( + (value: number, t) => (value % 2 === 0 ? t.nextValid(value) : t.left(value)), + modifierToString('even'), +); const evenR = pipe(number, even, validate)(''); expectType(evenR); const noDuplicateItems = refine((arr: ReadonlyArray, t) => { const allUnique = arr.every((item, index) => index === arr.indexOf(item)); return allUnique ? t.nextValid(arr) : t.left(arr); -}); +}, modifierToString('noDuplicateItems')); const noDuplicateItemsR = pipe(array(string()), noDuplicateItems, validate)(''); expect(noDuplicateItemsR); const noDuplicateItemsAnyR = pipe(array(number()), noDuplicateItems, validate)(''); expect(noDuplicateItemsAnyR); -const allowTimestamps = refine((value, t) => - typeof value === 'number' ? t.nextValid(new Date(value)) : t.nextValid(value), +const allowTimestamps = refine( + (value, t) => (typeof value === 'number' ? t.nextValid(new Date(value)) : t.nextValid(value)), + modifierToString('allowTimestamps'), ); const allowDateTimestamps = pipe(date, allowTimestamps); const allowDateTimestampsR = pipe(allowDateTimestamps, validate)(''); expectType(allowDateTimestampsR); -const presentOrFuture = refine((value: Date, t) => - value.getTime() >= Date.now() ? t.nextValid(value) : t.left(value), +const presentOrFuture = refine( + (value: Date, t) => (value.getTime() >= Date.now() ? t.nextValid(value) : t.left(value)), + modifierToString('presentOrFuture'), ); const allowDateTimestampsR2 = pipe(presentOrFuture, date, allowTimestamps, validate)(''); expectType(allowDateTimestampsR2); diff --git a/__tests__/unit-tests.spec.ts b/__tests__/unit-tests.spec.ts index ed57679..cc3ac2d 100644 --- a/__tests__/unit-tests.spec.ts +++ b/__tests__/unit-tests.spec.ts @@ -448,10 +448,13 @@ describe('@typeofweb/schema unit tests', () => { it('should exit early if one of the validators in oneOf returns right', () => { const spy = jest.fn(); - const shouldNotBeCalled = refine((value) => { - spy(value); - throw new Error(String(value)); - }); + const shouldNotBeCalled = refine( + (value) => { + spy(value); + throw new Error(String(value)); + }, + () => [''], + ); const validator = pipe(oneOf([string(), nullable(number()), shouldNotBeCalled()]), validate); expect(validator(null)).toEqual(null); expect(spy).toHaveBeenCalledTimes(0); diff --git a/package.json b/package.json index 3ca791e..2a994ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@typeofweb/schema", - "version": "0.7.3", + "version": "0.7.4-0", "main": "dist/index.common.js", "module": "dist/index.esm.js", "browser": "dist/index.umd.js", diff --git a/src/errors.ts b/src/errors.ts index cc74aca..4ce9550 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,30 +1,53 @@ import { schemaToString } from './stringify'; -import type { SomeSchema } from './types'; +import type { Primitives, SomeSchema } from './types'; + +export type Result = Record | Primitives | Date | ValidationError; export class ValidationError extends Error { public readonly details: ErrorDetails; + private readonly schema: SomeSchema | undefined; + private readonly value: unknown; + private readonly result?: Result; - constructor(schema: SomeSchema, value: any) { - const expected = schemaToString(schema); - const got = typeof value === 'function' ? String(value) : JSON.stringify(value); + constructor(schema?: SomeSchema | undefined, value?: unknown, result?: Result) { + const expectedStr = schemaToString(schema); + const gotStr = typeof value === 'function' ? String(value) : JSON.stringify(value); const details: ErrorDetails = { - kind: 'TYPE_MISMATCH', - got, - expected, + fields: result, }; - super(`Invalid type! Expected ${details.expected} but got ${details.got}!`); + super(`Invalid type! Expected ${expectedStr} but got ${gotStr}!`); this.details = details; this.name = 'ValidationError'; + this.schema = schema; + this.value = value; + this.result = result; Error.captureStackTrace(this); Object.setPrototypeOf(this, ValidationError.prototype); } + + getDetails(): Record { + if (this.result instanceof ValidationError) { + return this.result.getDetails(); + } else if (typeof this.result === 'object' && this.result && !Array.isArray(this.result)) { + return Object.fromEntries( + Object.entries(this.result).map(([key, error]) => { + /* istanbul ignore else */ + if (error instanceof ValidationError) { + return [key, error.getDetails()]; + } else { + return [key, error]; + } + }), + ); + } else { + return { expected: schemaToString(this.schema), got: this.value }; + } + } } type ErrorDetails = { - readonly kind: 'TYPE_MISMATCH'; - readonly expected: string; - readonly got: string; + readonly fields: any; }; diff --git a/src/modifiers/minArrayLength.ts b/src/modifiers/minArrayLength.ts index 4bc28fe..678ef44 100644 --- a/src/modifiers/minArrayLength.ts +++ b/src/modifiers/minArrayLength.ts @@ -1,4 +1,4 @@ -import { refine } from '../refine'; +import { modifierToString, refine } from '../refine'; import type { TupleOf } from '../types'; export const minArrayLength = (minLength: L) => @@ -11,4 +11,4 @@ export const minArrayLength = (minLength: L) => ], ) : t.left(value); - }); + }, modifierToString(`minArrayLength(${minLength})`)); diff --git a/src/modifiers/minStringLength.ts b/src/modifiers/minStringLength.ts index 589e2c7..8ebbc7c 100644 --- a/src/modifiers/minStringLength.ts +++ b/src/modifiers/minStringLength.ts @@ -1,4 +1,7 @@ -import { refine } from '../refine'; +import { modifierToString, refine } from '../refine'; export const minStringLength = (minLength: L) => - refine((value: string, t) => (value.length >= minLength ? t.nextValid(value) : t.left(value))); + refine( + (value: string, t) => (value.length >= minLength ? t.nextValid(value) : t.left(value)), + modifierToString(`minStringLength(${minLength})`), + ); diff --git a/src/modifiers/nil.ts b/src/modifiers/nil.ts index 2785a62..c186e67 100644 --- a/src/modifiers/nil.ts +++ b/src/modifiers/nil.ts @@ -2,5 +2,5 @@ import { refine } from '../refine'; export const nil = refine( (value, t) => (value === null || value === undefined ? t.right(value) : t.nextNotValid(value)), - () => `undefined | null`, + () => [`undefined`, `null`], ); diff --git a/src/modifiers/nullable.ts b/src/modifiers/nullable.ts index 1d0ac09..a351266 100644 --- a/src/modifiers/nullable.ts +++ b/src/modifiers/nullable.ts @@ -1,7 +1,6 @@ import { refine } from '../refine'; -import { typeToPrint } from '../stringify'; export const nullable = refine( (value, t) => (value === null ? t.right(null) : t.nextNotValid(value)), - () => typeToPrint('null'), + `null`, ); diff --git a/src/modifiers/optional.ts b/src/modifiers/optional.ts index d816a04..9e6bc69 100644 --- a/src/modifiers/optional.ts +++ b/src/modifiers/optional.ts @@ -1,7 +1,6 @@ import { refine } from '../refine'; -import { typeToPrint } from '../stringify'; export const optional = refine( (value, t) => (value === undefined ? t.right(undefined) : t.nextNotValid(value)), - () => typeToPrint('undefined'), + `undefined`, ); diff --git a/src/refine.ts b/src/refine.ts index 50bceb9..54e3b8c 100644 --- a/src/refine.ts +++ b/src/refine.ts @@ -1,4 +1,4 @@ -import { unionToPrint } from './stringify'; +import { getErrorArray, unionToPrint } from './stringify'; import type { Either, If, Next, Pretty, SomeSchema } from './types'; import { left, right, nextValid, nextNotValid } from './utils/either'; @@ -18,8 +18,10 @@ type RefinementToolkit = typeof refinementToolkit; export const refine = ( refinement: Refinement, - toString?: () => string, -) => >(schema?: S) => { + nameOrToString: + | string + | ((outerToString?: () => string | readonly string[]) => string | readonly string[]), +) => >(innerSchema?: S) => { type HasExitEarlyResult = unknown extends ExitEarlyResult ? false : ExitEarlyResult extends never @@ -56,9 +58,16 @@ export const refine = ( | If; return { - ...schema, - toString() { - return unionToPrint([schema?.toString()!, toString?.()!].filter(Boolean)); + ...innerSchema, + name: refinement.name || typeof nameOrToString === 'string' ? nameOrToString : nameOrToString(), + toString(outerToString) { + const currentToString = + typeof nameOrToString === 'string' ? simpleTypeToString(nameOrToString) : nameOrToString; + + if (!innerSchema) { + return currentToString(outerToString); + } + return unionToPrint([innerSchema?.toString(currentToString)].flat()); }, __validate(val) { // eslint-disable-next-line functional/no-this-expression @@ -67,10 +76,22 @@ export const refine = ( if (innerResult?._t === 'left' || innerResult?._t === 'right') { return innerResult; } - if (!schema) { + if (!innerSchema) { return innerResult; } - return schema.__validate(innerResult.value); + return innerSchema.__validate(innerResult.value); }, } as SomeSchema>; }; + +export const modifierToString = (str: string): Exclude[1], string> => ( + outerToString, +) => { + return getErrorArray(outerToString?.(), str).reduce((acc, el) => `${el}(${acc})`); +}; + +export const simpleTypeToString = (str: string): Exclude[1], string> => ( + outerToString, +) => { + return getErrorArray(str, outerToString?.()); +}; diff --git a/src/stringify.ts b/src/stringify.ts index 31d9665..4bd9ece 100644 --- a/src/stringify.ts +++ b/src/stringify.ts @@ -1,17 +1,22 @@ import type { SomeSchema } from './types'; -export const schemaToString = (schema: SomeSchema): string => { - return schema.toString(); +export const schemaToString = (schema?: SomeSchema): string => { + return getErrorArray(schema?.toString())[0] ?? ''; }; export const typeToPrint = (str: string) => str; export const objectToPrint = (str: string) => '{' + str + '}'; export const quote = (str: string) => (/\s/.test(str) ? `"${str}"` : str); -export const unionToPrint = (arr: readonly string[]): string => { - const str = arr.join(' | '); +export const getErrorArray = ( + ...val: ReadonlyArray> +): readonly string[] => [...val].flat().filter((x): x is string => typeof x === 'string'); - if (arr.length > 1) { +export const unionToPrint = (arr: readonly (string | undefined)[]): string => { + const filteredArray = getErrorArray(arr); + const str = filteredArray.join(' | '); + + if (filteredArray.length > 1) { return `(${str})`; } return str; diff --git a/src/types.ts b/src/types.ts index 68fb618..8d25c25 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,7 +9,9 @@ export interface Schema { * @internal */ readonly __validate: (val: unknown) => Either | Next; - toString(): string; + toString(innerToString?: () => string | readonly string[]): string | readonly string[]; + + readonly name?: string; } type Left = { readonly _t: 'left'; readonly value: L }; diff --git a/src/validators/__validate.ts b/src/validators/__validate.ts index 3e8ecb4..f44d84d 100644 --- a/src/validators/__validate.ts +++ b/src/validators/__validate.ts @@ -1,3 +1,4 @@ +import type { Result } from '../errors'; import { ValidationError } from '../errors'; import type { SomeSchema, TypeOf } from '../types'; @@ -8,6 +9,6 @@ export const validate = >(schema: S) => (value: un return result.value as TypeOf; } else { // throw result.value; - throw new ValidationError(schema, value); + throw new ValidationError(schema, value, result.value as Result); } }; diff --git a/src/validators/array.ts b/src/validators/array.ts index 3ef9d97..6847580 100644 --- a/src/validators/array.ts +++ b/src/validators/array.ts @@ -1,7 +1,7 @@ /* eslint-disable functional/no-loop-statement */ import { ValidationError } from '../errors'; import { refine } from '../refine'; -import { schemaToString, typeToPrint } from '../stringify'; +import { schemaToString } from '../stringify'; import type { SomeSchema, TypeOf } from '../types'; export const array = []>(...validators: readonly [...U]) => { @@ -35,9 +35,9 @@ export const array = []>(...validators: r } return t.nextValid(result as TypeOfResult); }, - () => { + (() => { const str = validators.map((s) => schemaToString(s)).join(' | '); - return validators.length > 1 ? typeToPrint(`(${str})[]`) : typeToPrint(`${str}[]`); - }, + return validators.length > 1 ? `(${str})[]` : `${str}[]`; + })(), ); }; diff --git a/src/validators/boolean.ts b/src/validators/boolean.ts index b9931cb..a7e3b96 100644 --- a/src/validators/boolean.ts +++ b/src/validators/boolean.ts @@ -1,12 +1,8 @@ import { refine } from '../refine'; -import { typeToPrint } from '../stringify'; -export const boolean = refine( - (value, t) => { - if (typeof value !== 'boolean') { - return t.left(value); - } - return t.nextValid(value); - }, - () => typeToPrint('boolean'), -); +export const boolean = refine((value, t) => { + if (typeof value !== 'boolean') { + return t.left(value); + } + return t.nextValid(value); +}, 'boolean'); diff --git a/src/validators/date.ts b/src/validators/date.ts index e2a5208..a4eee98 100644 --- a/src/validators/date.ts +++ b/src/validators/date.ts @@ -1,17 +1,13 @@ import { refine } from '../refine'; -import { typeToPrint } from '../stringify'; import { isDate, isISODateString } from '../utils/dateUtils'; -export const date = refine( - (value, t) => { - const parsedValue = parseDate(value); - if (!isDate(parsedValue) || Number.isNaN(Number(parsedValue))) { - return t.left(parsedValue); - } - return t.nextValid(parsedValue); - }, - () => typeToPrint('Date'), -); +export const date = refine((value, t) => { + const parsedValue = parseDate(value); + if (!isDate(parsedValue) || Number.isNaN(Number(parsedValue))) { + return t.left(value); + } + return t.nextValid(parsedValue); +}, 'Date'); function parseDate(value: unknown) { if (typeof value === 'string' && isISODateString(value)) { diff --git a/src/validators/number.ts b/src/validators/number.ts index 4b2b924..b79383d 100644 --- a/src/validators/number.ts +++ b/src/validators/number.ts @@ -1,16 +1,12 @@ import { refine } from '../refine'; -import { typeToPrint } from '../stringify'; -export const number = refine( - (value, t) => { - const parsedValue = parseNumber(value); - if (typeof parsedValue !== 'number' || Number.isNaN(parsedValue)) { - return t.left(parsedValue); - } - return t.nextValid(parsedValue); - }, - () => typeToPrint('number'), -); +export const number = refine((value, t) => { + const parsedValue = parseNumber(value); + if (typeof parsedValue !== 'number' || Number.isNaN(parsedValue)) { + return t.left(value); + } + return t.nextValid(parsedValue); +}, 'number'); function parseNumber(value: unknown) { if (typeof value === 'string') { diff --git a/src/validators/object.ts b/src/validators/object.ts index d0f41da..f317aba 100644 --- a/src/validators/object.ts +++ b/src/validators/object.ts @@ -21,7 +21,7 @@ export const object = >>( return refine( function (obj, t) { if (typeof obj !== 'object' || obj === null) { - return t.left(obj); + return t.left(new ValidationError(this, obj)); } const object = obj as Record; @@ -30,6 +30,7 @@ export const object = >>( let isError = false; const result = {} as Record; + const errors = {} as Record; for (const key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) { continue; @@ -39,8 +40,12 @@ export const object = >>( const validator = validators[key]; if (validator) { const r = validator.__validate(value); - result[key] = r.value; - isError ||= r._t === 'left'; + if (r._t === 'left') { + errors[key] = new ValidationError(validator, value, r.value); + isError = true; + } else { + result[key] = r.value; + } continue; } else { if (allowUnknownKeys) { @@ -48,7 +53,7 @@ export const object = >>( continue; } else { isError = true; - result[key] = new ValidationError(this, object); + errors[key] = new ValidationError(undefined, value); continue; } } @@ -58,21 +63,24 @@ export const object = >>( if (!Object.prototype.hasOwnProperty.call(validators, key)) { continue; } - if (key in result) { + if (key in result || key in errors) { continue; } const validator = validators[key]!; const value = object[key]; const r = validator.__validate(value); - isError ||= r._t === 'left'; + if (r._t === 'left') { + errors[key] = new ValidationError(validator, value, r.value); + isError = true; + } } if (isError) { - return t.left(result as TypeOfResult); + return t.left(errors); } return t.nextValid(result as TypeOfResult); }, - () => { + (() => { const entries = Object.entries(schemasObject).map( ([key, val]) => [key, schemaToString(val)] as const, ); @@ -80,8 +88,8 @@ export const object = >>( return objectToPrint(''); } return objectToPrint( - ' ' + entries.map(([key, val]) => quote(key) + ': ' + val).join(', ') + ' ', + ' ' + entries.map(([key, val]) => quote(key) + ': ' + String(val)).join(', ') + ' ', ); - }, + })(), ); }; diff --git a/src/validators/oneOf.ts b/src/validators/oneOf.ts index 0dda4c3..5282baa 100644 --- a/src/validators/oneOf.ts +++ b/src/validators/oneOf.ts @@ -33,12 +33,12 @@ export const oneOf = )[]>( } return t.left(value as TypeOfResult); }, - () => { + (() => { const str = validatorsOrLiterals .map((s) => (isSchema(s) ? schemaToString(s) : JSON.stringify(s))) .join(' | '); return validatorsOrLiterals.length > 1 ? `(${str})` : str; - }, + })(), ); }; diff --git a/src/validators/string.ts b/src/validators/string.ts index 7dbf093..0beb737 100644 --- a/src/validators/string.ts +++ b/src/validators/string.ts @@ -1,17 +1,13 @@ import { refine } from '../refine'; -import { typeToPrint } from '../stringify'; import { isDate } from '../utils/dateUtils'; -export const string = refine( - (value, t) => { - const parsedValue = parseString(value); - if (typeof parsedValue !== 'string') { - return t.left(parsedValue); - } - return t.nextValid(parsedValue); - }, - () => typeToPrint('string'), -); +export const string = refine((value, t) => { + const parsedValue = parseString(value); + if (typeof parsedValue !== 'string') { + return t.left(value); + } + return t.nextValid(parsedValue); +}, 'string'); function parseString(value: unknown) { if (isDate(value)) { diff --git a/src/validators/tuple.ts b/src/validators/tuple.ts index e971614..b2700e4 100644 --- a/src/validators/tuple.ts +++ b/src/validators/tuple.ts @@ -12,45 +12,41 @@ export const tuple = )[]>( readonly [Index in keyof U]: U[Index] extends SomeSchema ? TypeOf : U[Index]; }; - return refine( - function (values, t) { - if (!Array.isArray(values) || values.length !== validatorsOrLiterals.length) { - return t.left(values); - } + return refine(function (values, t) { + if (!Array.isArray(values) || values.length !== validatorsOrLiterals.length) { + return t.left(values); + } - let isError = false; - const result = new Array(values.length); - for (let i = 0; i < values.length; ++i) { - const valueOrSchema = validatorsOrLiterals[i]; - const value = values[i] as unknown; + let isError = false; + const result = new Array(values.length); + for (let i = 0; i < values.length; ++i) { + const valueOrSchema = validatorsOrLiterals[i]; + const value = values[i] as unknown; - if (isSchema(valueOrSchema)) { - const r = valueOrSchema.__validate(value); - result[i] = r.value as unknown; - isError ||= r._t === 'left'; + if (isSchema(valueOrSchema)) { + const r = valueOrSchema.__validate(value); + result[i] = r.value as unknown; + isError ||= r._t === 'left'; + continue; + } else { + if (valueOrSchema === value) { + result[i] = value; continue; } else { - if (valueOrSchema === value) { - result[i] = value; - continue; - } else { - result[i] = new ValidationError(this, validatorsOrLiterals); - isError = true; - continue; - } + result[i] = new ValidationError(this, validatorsOrLiterals); + isError = true; + continue; } } + } - if (isError) { - return t.left((result as unknown) as TypeOfResult); - } - return t.nextValid((result as unknown) as TypeOfResult); - }, - () => - '[' + - validatorsOrLiterals - .map((s) => (isSchema(s) ? schemaToString(s) : JSON.stringify(s))) - .join(', ') + - ']', - ); + if (isError) { + return t.left((result as unknown) as TypeOfResult); + } + return t.nextValid((result as unknown) as TypeOfResult); + }, '[' + + validatorsOrLiterals + .map((s) => (isSchema(s) ? schemaToString(s) : JSON.stringify(s))) + .join(', ') + + ']'); }; diff --git a/src/validators/unknown.ts b/src/validators/unknown.ts index adf5530..326e61a 100644 --- a/src/validators/unknown.ts +++ b/src/validators/unknown.ts @@ -1,9 +1,5 @@ import { refine } from '../refine'; -import { typeToPrint } from '../stringify'; -export const unknown = refine( - (value, t) => { - return t.nextValid(value); - }, - () => typeToPrint('unknown'), -); +export const unknown = refine((value, t) => { + return t.nextValid(value); +}, 'unknown');