diff --git a/src/error.ts b/src/error.ts index e5b0f2fc..15ca5c6f 100644 --- a/src/error.ts +++ b/src/error.ts @@ -2,7 +2,7 @@ * A `StructFailure` represents a single specific failure in validation. */ -export type Failure = { +export type Failure = { value: any key: any type: string @@ -10,6 +10,41 @@ export type Failure = { message: string branch: Array path: Array + detail: E + failures?: Failure[] +} + +export type Error = ErrorDetail | never +export interface ErrorDetail { + class: string + message?: string +} +export interface GenericErrorDetail extends ErrorDetail { + class: 'generic' + message: string +} + +export interface TypeErrorDetail extends ErrorDetail { + class: 'type' + except: string + actually: unknown +} + +export interface ValuesErrorDetail extends ErrorDetail { + class: 'values' + except: T[] + actually: T +} + +export interface ValueErrorDetail extends ErrorDetail { + class: 'value' + except: T + actually: unknown +} + +export interface ThrowErrorDetail extends ErrorDetail { + class: 'throw' + error: any } /** @@ -21,24 +56,26 @@ export type Failure = { * continue validation and receive all the failures in the data. */ -export class StructError extends TypeError { +export class StructError extends TypeError { value: any key!: any type!: string refinement!: string | undefined path!: Array branch!: Array - failures: () => Array; + failures: () => Array> + detail: E; [x: string]: any - constructor(failure: Failure, failures: () => Generator) { - let cached: Array | undefined + constructor(failure: Failure, failures: () => Generator>) { + let cached: Array> | undefined const { message, ...rest } = failure - const { path } = failure + const { path, detail } = failure const msg = path.length === 0 ? message : `At path: ${path.join('.')} -- ${message}` super(msg) Object.assign(this, rest) + this.detail = detail this.name = this.constructor.name this.failures = () => { return (cached ??= [failure, ...failures()]) diff --git a/src/struct.ts b/src/struct.ts index ce4ee6d4..4e2b8c60 100644 --- a/src/struct.ts +++ b/src/struct.ts @@ -1,5 +1,11 @@ import { toFailures, shiftIterator, StructSchema, run } from './utils' -import { StructError, Failure } from './error' +import { + StructError, + Failure, + Error, + ErrorDetail, + ThrowErrorDetail, +} from './error' import { masked } from './structs/coercions' /** @@ -7,26 +13,31 @@ import { masked } from './structs/coercions' * values. Once constructed, you use the `assert`, `is` or `validate` helpers to * validate unknown input data against the struct. */ - -export class Struct { +export class Struct { readonly TYPE!: T type: string schema: S coercer: (value: unknown, context: Context) => unknown - validator: (value: unknown, context: Context) => Iterable - refiner: (value: T, context: Context) => Iterable + validator: (value: unknown, context: Context) => Iterable> + refiner: (value: T, context: Context) => Iterable> entries: ( value: unknown, context: Context - ) => Iterable<[string | number, unknown, Struct | Struct]> + ) => Iterable< + [ + string | number, + unknown, + Struct | Struct + ] + > constructor(props: { type: string schema: S coercer?: Coercer - validator?: Validator - refiner?: Refiner - entries?: Struct['entries'] + validator?: Validator + refiner?: Refiner + entries?: Struct['entries'] }) { const { type, @@ -45,6 +56,7 @@ export class Struct { if (validator) { this.validator = (value, context) => { const result = validator(value, context) + if (result === true || result === undefined) return [] return toFailures(result, context, this, value) } } else { @@ -54,6 +66,7 @@ export class Struct { if (refiner) { this.refiner = (value, context) => { const result = refiner(value, context) + if (result === true || result === undefined) return [] return toFailures(result, context, this, value) } } else { @@ -108,7 +121,7 @@ export class Struct { options: { coerce?: boolean } = {} - ): [StructError, undefined] | [undefined, T] { + ): [StructError, undefined] | [undefined, T] { return validate(value, this, options) } } @@ -117,9 +130,9 @@ export class Struct { * Assert that a value passes a struct, throwing if it doesn't. */ -export function assert( +export function assert( value: unknown, - struct: Struct + struct: Struct ): asserts value is T { const result = validate(value, struct) @@ -132,7 +145,10 @@ export function assert( * Create a value with the coercion logic of struct and validate it. */ -export function create(value: unknown, struct: Struct): T { +export function create( + value: unknown, + struct: Struct +): T { const result = validate(value, struct, { coerce: true }) if (result[0]) { @@ -146,7 +162,10 @@ export function create(value: unknown, struct: Struct): T { * Mask a value, returning only the subset of properties defined by a struct. */ -export function mask(value: unknown, struct: Struct): T { +export function mask( + value: unknown, + struct: Struct +): T { const M = masked(struct) const ret = create(value, M) return ret @@ -156,7 +175,10 @@ export function mask(value: unknown, struct: Struct): T { * Check if a value passes a struct. */ -export function is(value: unknown, struct: Struct): value is T { +export function is( + value: unknown, + struct: Struct +): value is T { const result = validate(value, struct) return !result[0] } @@ -166,13 +188,13 @@ export function is(value: unknown, struct: Struct): value is T { * value (with potential coercion) if valid. */ -export function validate( +export function validate( value: unknown, - struct: Struct, + struct: Struct, options: { coerce?: boolean } = {} -): [StructError, undefined] | [undefined, T] { +): [StructError, undefined] | [undefined, T] { const tuples = run(value, struct, options) const tuple = shiftIterator(tuples)! @@ -206,23 +228,31 @@ export type Context = { * A type utility to extract the type from a `Struct` class. */ -export type Infer> = T['TYPE'] +export type Infer> = T['TYPE'] /** * A type utility to describe that a struct represents a TypeScript type. */ -export type Describe = Struct> +export type Describe = Struct, any> // todo + +export type InferError = T extends Struct ? E : Error /** * A `Result` is returned from validation functions. */ -export type Result = - | boolean - | string - | Partial - | Iterable> +export type Result = + // | boolean + // | string + // | Partial> + // | Iterable>> + // undefined | Iterable> | Failure + /* BasicResult |*/ | DescribedResult + | Iterable> + +// export type BasicResult = boolean | string; +export type DescribedResult = E | Failure /** * A `Coercer` takes an unknown value and optionally coerces it. @@ -234,11 +264,27 @@ export type Coercer = (value: T, context: Context) => unknown * A `Validator` takes an unknown value and validates it. */ -export type Validator = (value: unknown, context: Context) => Result +export type Validator = ( + value: unknown, + context: Context +) => Iterable> | true + +export type SimpleValidator = ( + value: unknown, + context: Context +) => string | boolean | undefined /** * A `Refiner` takes a value of a known type and validates it against a further * constraint. */ -export type Refiner = (value: T, context: Context) => Result +export type Refiner = ( + value: T, + context: Context +) => Iterable> | true + +export type SimpleRefiner = ( + value: T, + context: Context +) => string | boolean | undefined diff --git a/src/structs/coercions.ts b/src/structs/coercions.ts index af065080..714a454d 100644 --- a/src/structs/coercions.ts +++ b/src/structs/coercions.ts @@ -1,3 +1,4 @@ +import { Error, TypeErrorDetail } from '../error' import { Struct, is, Coercer } from '../struct' import { isPlainObject } from '../utils' import { string, unknown } from './types' @@ -13,12 +14,12 @@ import { string, unknown } from './types' * take effect! Using simply `assert()` or `is()` will not use coercion. */ -export function coerce( - struct: Struct, - condition: Struct, +export function coerce( + struct: Struct, + condition: Struct, coercer: Coercer -): Struct { - return new Struct({ +): Struct { + return new Struct({ ...struct, coercer: (value, ctx) => { return is(value, condition) @@ -35,13 +36,13 @@ export function coerce( * take effect! Using simply `assert()` or `is()` will not use coercion. */ -export function defaulted( - struct: Struct, +export function defaulted( + struct: Struct, fallback: any, options: { strict?: boolean } = {} -): Struct { +): Struct { return coerce(struct, unknown(), (x) => { const f = typeof fallback === 'function' ? fallback() : fallback @@ -76,7 +77,9 @@ export function defaulted( * take effect! Using simply `assert()` or `is()` will not use coercion. */ -export function masked(struct: Struct): Struct { +export function masked( + struct: Struct +): Struct { return coerce(struct, unknown(), (x) => { if ( typeof struct.schema !== 'object' || @@ -106,6 +109,8 @@ export function masked(struct: Struct): Struct { * take effect! Using simply `assert()` or `is()` will not use coercion. */ -export function trimmed(struct: Struct): Struct { +export function trimmed( + struct: Struct +): Struct { return coerce(struct, string(), (x) => x.trim()) } diff --git a/src/structs/refinements.ts b/src/structs/refinements.ts index 3eab2447..63086f8d 100644 --- a/src/structs/refinements.ts +++ b/src/structs/refinements.ts @@ -1,27 +1,60 @@ -import { Struct, Refiner } from '../struct' +/* eslint-disable no-shadow */ +import { ErrorDetail, GenericErrorDetail, ValueErrorDetail } from '../error' +import { Struct, Refiner, SimpleRefiner } from '../struct' import { toFailures } from '../utils' +interface SizeErrorDetail extends ErrorDetail { + class: 'size' + actually: T + min: T | undefined + max: T | undefined + minExlusive: boolean + maxExlusive: boolean +} + /** * Ensure that a string, array, map, or set is empty. */ export function empty< T extends string | any[] | Map | Set, - S extends any ->(struct: Struct): Struct { + S extends any, + E extends ErrorDetail +>(struct: Struct): Struct> { const expected = `Expected an empty ${struct.type}` - return refine(struct, 'empty', (value) => { + return refine>(struct, 'empty', (value) => { if (value instanceof Map || value instanceof Set) { const { size } = value return ( - size === 0 || `${expected} but received one with a size of \`${size}\`` + size === 0 || + ([ + { + class: 'size', + actually: size, + min: 0, + max: 0, + minExlusive: false, + maxExlusive: false, + message: `${expected} but received one with a size of \`${size}\``, + }, + ] as SizeErrorDetail[]) ) } else { const { length } = value as string | any[] return ( length === 0 || - `${expected} but received one with a length of \`${length}\`` + ([ + { + class: 'size', + actually: length, + min: 0, + max: 0, + minExlusive: false, + maxExlusive: false, + message: `${expected} but received one with a length of \`${length}\``, + }, + ] as SizeErrorDetail[]) ) } }) @@ -31,21 +64,35 @@ export function empty< * Ensure that a number or date is below a threshold. */ -export function max( - struct: Struct, +export function max< + T extends number | Date, + S extends any, + E extends ErrorDetail +>( + struct: Struct, threshold: T, options: { exclusive?: boolean } = {} -): Struct { +): Struct> { const { exclusive } = options return refine(struct, 'max', (value) => { - return exclusive - ? value < threshold - : value <= threshold || - `Expected a ${struct.type} greater than ${ + return ( + (exclusive ? value < threshold : value <= threshold) || + ([ + { + class: 'size', + actually: value, + min: undefined, + max: threshold, + minExlusive: false, + maxExlusive: exclusive, + message: `Expected a ${struct.type} greater than ${ exclusive ? '' : 'or equal to ' - }${threshold} but received \`${value}\`` + }${threshold} but received \`${value}\``, + }, + ] as SizeErrorDetail[]) + ) }) } @@ -53,35 +100,56 @@ export function max( * Ensure that a number or date is above a threshold. */ -export function min( - struct: Struct, +export function min< + T extends number | Date, + S extends any, + E extends ErrorDetail +>( + struct: Struct, threshold: T, options: { exclusive?: boolean } = {} -): Struct { +): Struct> { const { exclusive } = options return refine(struct, 'min', (value) => { - return exclusive - ? value > threshold - : value >= threshold || - `Expected a ${struct.type} greater than ${ + return ( + (exclusive ? value > threshold : value >= threshold) || + ([ + { + class: 'size', + actually: value, + min: threshold, + max: undefined, + minExlusive: exclusive, + maxExlusive: false, + message: `Expected a ${struct.type} greater than ${ exclusive ? '' : 'or equal to ' - }${threshold} but received \`${value}\`` + }${threshold} but received \`${value}\``, + }, + ] as SizeErrorDetail[]) + ) }) } /** * Ensure that a string matches a regular expression. */ -export function pattern( - struct: Struct, +export function pattern( + struct: Struct, regexp: RegExp -): Struct { +): Struct> { return refine(struct, 'pattern', (value) => { return ( regexp.test(value) || - `Expected a ${struct.type} matching \`/${regexp.source}/\` but received "${value}"` + ([ + { + class: 'value', + except: regexp.source, + actually: value, + message: `Expected a ${struct.type} matching \`/${regexp.source}/\` but received "${value}"`, + }, + ] as ValueErrorDetail[]) ) }) } @@ -92,8 +160,14 @@ export function pattern( export function size< T extends string | number | Date | any[] | Map | Set, - S extends any ->(struct: Struct, min: number, max: number = min): Struct { + S extends any, + E extends ErrorDetail +>( + struct: Struct, + min: number, + max: number = min +): Struct> { + // todo fix return type const expected = `Expected a ${struct.type}` const of = min === max ? `of \`${min}\`` : `between \`${min}\` and \`${max}\`` @@ -101,19 +175,49 @@ export function size< if (typeof value === 'number' || value instanceof Date) { return ( (min <= value && value <= max) || - `${expected} ${of} but received \`${value}\`` + ([ + { + class: 'size', + actually: value, + min, + max, + minExlusive: false, + maxExlusive: false, + message: `${expected} ${of} but received \`${value}\``, + }, + ] as SizeErrorDetail[]) ) } else if (value instanceof Map || value instanceof Set) { const { size } = value return ( (min <= size && size <= max) || - `${expected} with a size ${of} but received one with a size of \`${size}\`` + ([ + { + class: 'size', + actually: value, + min, + max, + minExlusive: false, + maxExlusive: false, + message: `${expected} with a size ${of} but received one with a size of \`${size}\``, + }, + ] as SizeErrorDetail[]) ) } else { const { length } = value as string | any[] return ( (min <= length && length <= max) || - `${expected} with a length ${of} but received one with a length of \`${length}\`` + ([ + { + class: 'size', + actually: value, + min, + max, + minExlusive: false, + maxExlusive: false, + message: `${expected} with a length ${of} but received one with a length of \`${length}\``, + }, + ] as SizeErrorDetail[]) ) } }) @@ -127,17 +231,37 @@ export function size< * allows you to layer additional validation on top of existing structs. */ +export function refine( + struct: Struct, + name: string, + refiner: Refiner +): Struct +export function refine( + struct: Struct, + name: string, + refiner: SimpleRefiner +): Struct export function refine( - struct: Struct, + struct: Struct, name: string, - refiner: Refiner -): Struct { - return new Struct({ + refiner: Refiner | SimpleRefiner +): Struct { + return new Struct({ ...struct, *refiner(value, ctx) { yield* struct.refiner(value, ctx) - const result = refiner(value, ctx) - const failures = toFailures(result, ctx, struct, value) + let result = refiner(value, ctx) + if (result === false || typeof result === 'string') { + result = [ + { + class: 'generic', + message: result || 'error', + }, + ] as GenericErrorDetail[] + } else if (result === true || result === undefined) { + return + } + const failures = toFailures(result, ctx, struct, value) for (const failure of failures) { yield { ...failure, refinement: name } diff --git a/src/structs/types.ts b/src/structs/types.ts index 10cbe47c..21742514 100644 --- a/src/structs/types.ts +++ b/src/structs/types.ts @@ -1,4 +1,6 @@ -import { Infer, Struct } from '../struct' +/* eslint-disable no-redeclare */ + +import { Infer, InferError, Struct } from '../struct' import { define } from './utilities' import { TupleSchema, @@ -7,14 +9,22 @@ import { print, run, isObject, + ObjectError, } from '../utils' +import { + ValuesErrorDetail, + Error, + ErrorDetail, + TypeErrorDetail, + ValueErrorDetail, +} from '../error' /** * Ensure that any value passes validation. */ -export function any(): Struct { - return define('any', () => true) +export function any(): Struct { + return define('any', () => []) } /** @@ -25,10 +35,12 @@ export function any(): Struct { * and it is preferred to using `array(any())`. */ -export function array>(Element: T): Struct[], T> -export function array(): Struct -export function array>(Element?: T): any { - return new Struct({ +export function array>( + Element: T +): Struct[], T, InferError | TypeErrorDetail> +export function array(): Struct +export function array(Element?: Struct): any { + return new Struct({ type: 'array', schema: Element, *entries(value) { @@ -44,7 +56,14 @@ export function array>(Element?: T): any { validator(value) { return ( Array.isArray(value) || - `Expected an array value, but received: ${print(value)}` + ([ + { + class: 'type', + except: 'array', + actually: value, + message: `Expected an array value, but received: ${print(value)}`, + }, + ] as TypeErrorDetail[]) ) }, }) @@ -54,9 +73,18 @@ export function array>(Element?: T): any { * Ensure that a value is a boolean. */ -export function boolean(): Struct { +export function boolean(): Struct { return define('boolean', (value) => { - return typeof value === 'boolean' + return ( + typeof value === 'boolean' || + ([ + { + class: 'type', + except: 'boolean', + actually: value, + }, + ] as TypeErrorDetail[]) + ) }) } @@ -67,11 +95,17 @@ export function boolean(): Struct { * which can occur when parsing a date fails but still returns a `Date`. */ -export function date(): Struct { +export function date(): Struct { return define('date', (value) => { return ( (value instanceof Date && !isNaN(value.getTime())) || - `Expected a valid \`Date\` object, but received: ${print(value)}` + ([ + { + class: 'type', + except: 'date', + actually: value, + }, + ] as TypeErrorDetail[]) ) }) } @@ -85,10 +119,10 @@ export function date(): Struct { export function enums( values: readonly T[] -): Struct +): Struct> export function enums( values: readonly T[] -): Struct +): Struct> export function enums(values: readonly T[]): any { const schema: any = {} const description = values.map((v) => print(v)).join() @@ -103,7 +137,16 @@ export function enums(values: readonly T[]): any { validator(value) { return ( values.includes(value as any) || - `Expected one of \`${description}\`, but received: ${print(value)}` + ([ + { + class: 'values', + except: values, + actually: value, + message: `Expected one of \`${description}\`, but received: ${print( + value + )}`, + }, + ] as ValuesErrorDetail[]) ) }, }) @@ -113,11 +156,18 @@ export function enums(values: readonly T[]): any { * Ensure that a value is a function. */ -export function func(): Struct { +export function func(): Struct { return define('func', (value) => { return ( typeof value === 'function' || - `Expected a function, but received: ${print(value)}` + ([ + { + class: 'type', + except: 'funciton', + actually: value, + message: `Expected a function, but received: ${print(value)}`, + }, + ] as TypeErrorDetail[]) ) }) } @@ -128,11 +178,20 @@ export function func(): Struct { export function instance( Class: T -): Struct, null> { +): Struct, null, TypeErrorDetail> { return define('instance', (value) => { return ( - value instanceof Class || - `Expected a \`${Class.name}\` instance, but received: ${print(value)}` + value instanceof Class || [ + { + class: 'type', + varidator: 'instance', + except: Class.name, + actually: value, + message: `Expected a \`${ + Class.name + }\` instance, but received: ${print(value)}`, + }, + ] ) }) } @@ -141,11 +200,19 @@ export function instance( * Ensure that a value is an integer. */ -export function integer(): Struct { +export function integer(): Struct { return define('integer', (value) => { return ( - (typeof value === 'number' && !isNaN(value) && Number.isInteger(value)) || - `Expected an integer, but received: ${print(value)}` + (typeof value === 'number' && + !isNaN(value) && + Number.isInteger(value)) || [ + { + class: 'type', + except: 'number', + actually: value, + message: `Expected an integer, but received: ${print(value)}`, + }, + ] ) }) } @@ -154,59 +221,70 @@ export function integer(): Struct { * Ensure that a value matches all of a set of types. */ -export function intersection(Structs: TupleSchema<[A]>): Struct +export function intersection( + Structs: TupleSchema<[A]> +): Struct // todo export function intersection( Structs: TupleSchema<[A, B]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D, E]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D, E, F]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D, E, F, G]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D, E, F, G, H]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D, E, F, G, H, I]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N]> -): Struct +): Struct export function intersection( Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O]> -): Struct +): Struct< + A & B & C & D & E & F & G & H & I & J & K & L & M & N & O, + null, + Error +> export function intersection( Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P]> -): Struct +): Struct< + A & B & C & D & E & F & G & H & I & J & K & L & M & N & O & P, + null, + Error +> export function intersection( Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q]> ): Struct< A & B & C & D & E & F & G & H & I & J & K & L & M & N & O & P & Q, - null + null, + Error > -export function intersection(Structs: Array>): any { +export function intersection(Structs: Array>): any { return new Struct({ type: 'intersection', schema: null, @@ -232,16 +310,31 @@ export function intersection(Structs: Array>): any { * Ensure that a value is an exact value, using `===` for comparison. */ -export function literal(constant: T): Struct -export function literal(constant: T): Struct -export function literal(constant: T): Struct -export function literal(constant: T): Struct +export function literal( + constant: T +): Struct> +export function literal( + constant: T +): Struct> +export function literal( + constant: T +): Struct> +export function literal(constant: T): Struct> export function literal(constant: T): any { const description = print(constant) return define('literal', (value) => { return ( value === constant || - `Expected the literal \`${description}\`, but received: ${print(value)}` + ([ + { + class: 'value', + except: constant, + actually: value, + message: `Expected the literal \`${description}\`, but received: ${print( + value + )}`, + }, + ] as ValueErrorDetail[]) ) }) } @@ -251,12 +344,15 @@ export function literal(constant: T): any { * specific types. */ -export function map(): Struct, null> +export function map(): Struct, null, TypeErrorDetail> +export function map( + Key: Struct, + Value: Struct +): Struct, null, TypeErrorDetail | KE | VE> export function map( - Key: Struct, - Value: Struct -): Struct, null> -export function map(Key?: Struct, Value?: Struct): any { + Key?: Struct, + Value?: Struct +): any { return new Struct({ type: 'map', schema: null, @@ -274,7 +370,14 @@ export function map(Key?: Struct, Value?: Struct): any { validator(value) { return ( value instanceof Map || - `Expected a \`Map\` object, but received: ${print(value)}` + ([ + { + class: 'type', + except: 'Map', + actually: value, + message: `Expected a \`Map\` object, but received: ${print(value)}`, + }, + ] as TypeErrorDetail[]) ) }, }) @@ -284,19 +387,29 @@ export function map(Key?: Struct, Value?: Struct): any { * Ensure that no value ever passes validation. */ -export function never(): Struct { - return define('never', () => false) +export function never(): Struct { + return define('never', (value) => + [ + { + class: 'type', + except: 'never', + actually: value, + }, + ] as TypeErrorDetail[]) } /** * Augment an existing struct to allow `null` values. */ -export function nullable(struct: Struct): Struct { +export function nullable( + struct: Struct +): Struct { return new Struct({ ...struct, - validator: (value, ctx) => value === null || struct.validator(value, ctx), - refiner: (value, ctx) => value === null || struct.refiner(value, ctx), + validator: (value, ctx) => + value === null ? [] : struct.validator(value, ctx), + refiner: (value, ctx) => (value === null ? [] : struct.refiner(value, ctx)), }) } @@ -304,11 +417,18 @@ export function nullable(struct: Struct): Struct { * Ensure that a value is a number. */ -export function number(): Struct { +export function number(): Struct { return define('number', (value) => { return ( (typeof value === 'number' && !isNaN(value)) || - `Expected a number, but received: ${print(value)}` + ([ + { + class: 'type', + except: 'number', + actually: value, + message: `Expected a number, but received: ${print(value)}`, + }, + ] as TypeErrorDetail[]) ) }) } @@ -320,14 +440,14 @@ export function number(): Struct { * Note: Unrecognized properties will fail validation. */ -export function object(): Struct, null> +export function object(): Struct, null, Error> export function object( schema: S -): Struct, S> +): Struct, S, ObjectError | TypeErrorDetail> export function object(schema?: S): any { const knowns = schema ? Object.keys(schema) : [] const Never = never() - return new Struct({ + return new Struct, S | null, ObjectError | TypeErrorDetail>({ type: 'object', schema: schema ? schema : null, *entries(value) { @@ -346,7 +466,15 @@ export function object(schema?: S): any { }, validator(value) { return ( - isObject(value) || `Expected an object, but received: ${print(value)}` + isObject(value) || + ([ + { + class: 'type', + except: 'object', + actually: value, + message: `Expected an object, but received: ${print(value)}`, + }, + ] as TypeErrorDetail[]) ) }, coercer(value) { @@ -359,12 +487,15 @@ export function object(schema?: S): any { * Augment a struct to allow `undefined` values. */ -export function optional(struct: Struct): Struct { +export function optional( + struct: Struct +): Struct { return new Struct({ ...struct, validator: (value, ctx) => - value === undefined || struct.validator(value, ctx), - refiner: (value, ctx) => value === undefined || struct.refiner(value, ctx), + value === undefined ? [] : struct.validator(value, ctx), + refiner: (value, ctx) => + value === undefined ? [] : struct.refiner(value, ctx), }) } @@ -375,11 +506,11 @@ export function optional(struct: Struct): Struct { * Like TypeScript's `Record` utility. */ -export function record( - Key: Struct, - Value: Struct -): Struct, null> { - return new Struct({ +export function record( + Key: Struct, + Value: Struct +): Struct, null, KE | VE | TypeErrorDetail> { + return new Struct, null, KE | VE | TypeErrorDetail>({ type: 'record', schema: null, *entries(value) { @@ -393,7 +524,15 @@ export function record( }, validator(value) { return ( - isObject(value) || `Expected an object, but received: ${print(value)}` + isObject(value) || + ([ + { + class: 'type', + except: 'object', + actually: value, + message: `Expected an object, but received: ${print(value)}`, + }, + ] as TypeErrorDetail[]) ) }, }) @@ -406,9 +545,18 @@ export function record( * you need to use the `pattern()` refinement. */ -export function regexp(): Struct { +export function regexp(): Struct { return define('regexp', (value) => { - return value instanceof RegExp + return ( + value instanceof RegExp || + ([ + { + class: 'type', + except: 'regexp', + actually: value, + }, + ] as TypeErrorDetail[]) + ) }) } @@ -417,10 +565,14 @@ export function regexp(): Struct { * specific type. */ -export function set(): Struct, null> -export function set(Element: Struct): Struct, null> -export function set(Element?: Struct): any { - return new Struct({ +export function set(): Struct, null, Error> +export function set( + Element: Struct +): Struct, null, E | TypeErrorDetail> +export function set( + Element?: Struct +): any { + return new Struct({ type: 'set', schema: null, *entries(value) { @@ -436,7 +588,14 @@ export function set(Element?: Struct): any { validator(value) { return ( value instanceof Set || - `Expected a \`Set\` object, but received: ${print(value)}` + ([ + { + class: 'type', + except: 'Set', + actually: value, + message: `Expected a \`Set\` object, but received: ${print(value)}`, + }, + ] as TypeErrorDetail[]) ) }, }) @@ -446,11 +605,18 @@ export function set(Element?: Struct): any { * Ensure that a value is a string. */ -export function string(): Struct { +export function string(): Struct { return define('string', (value) => { return ( typeof value === 'string' || - `Expected a string, but received: ${print(value)}` + ([ + { + class: 'type', + except: 'string', + actually: value, + message: `Expected a string, but received: ${print(value)}`, + }, + ] as TypeErrorDetail[]) ) }) } @@ -458,56 +624,557 @@ export function string(): Struct { /** * Ensure that a value is a tuple of a specific length, and that each of its * elements is of a specific type. - */ - -export function tuple(Structs: TupleSchema<[A]>): Struct<[A], null> -export function tuple(Structs: TupleSchema<[A, B]>): Struct<[A, B], null> -export function tuple( - Structs: TupleSchema<[A, B, C]> -): Struct<[A, B, C], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D]> -): Struct<[A, B, C, D], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E]> -): Struct<[A, B, C, D, E], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F]> -): Struct<[A, B, C, D, E, F], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F, G]> -): Struct<[A, B, C, D, E, F, G], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F, G, H]> -): Struct<[A, B, C, D, E, F, G, H], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I]> -): Struct<[A, B, C, D, E, F, G, H, I], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J]> -): Struct<[A, B, C, D, E, F, G, H, I, J], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K]> -): Struct<[A, B, C, D, E, F, G, H, I, J, K], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L]> -): Struct<[A, B, C, D, E, F, G, H, I, J, K, L], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M]> -): Struct<[A, B, C, D, E, F, G, H, I, J, K, L, M], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N]> -): Struct<[A, B, C, D, E, F, G, H, I, J, K, L, M, N], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O]> -): Struct<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P]> -): Struct<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P], null> -export function tuple( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q]> -): Struct<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q], null> -export function tuple(Elements: Struct[]): any { + */ export function tuple>( + Structs: [A] +): Struct<[Infer], null, InferError> +export function tuple< + A extends Struct, + B extends Struct +>( + Structs: [A, B] +): Struct<[Infer, Infer], null, InferError | InferError> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct +>( + Structs: [A, B, C] +): Struct< + [Infer, Infer, Infer], + null, + InferError | InferError | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct +>( + Structs: [A, B, C, D] +): Struct< + [Infer, Infer, Infer, Infer], + null, + InferError | InferError | InferError | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct +>( + Structs: [A, B, C, D, E] +): Struct< + [Infer, Infer, Infer, Infer, Infer], + null, + InferError | InferError | InferError | InferError | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct +>( + Structs: [A, B, C, D, E, F] +): Struct< + [Infer, Infer, Infer, Infer, Infer, Infer], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct +>( + Structs: [A, B, C, D, E, F, G] +): Struct< + [Infer, Infer, Infer, Infer, Infer, Infer, Infer], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct +>( + Structs: [A, B, C, D, E, F, G, H] +): Struct< + [ + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer + ], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I] +): Struct< + [ + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer + ], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J] +): Struct< + [ + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer + ], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K] +): Struct< + [ + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer + ], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L] +): Struct< + [ + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer + ], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct, + M extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L, M] +): Struct< + [ + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer + ], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct, + M extends Struct, + N extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L, M, N] +): Struct< + [ + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer + ], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct, + M extends Struct, + N extends Struct, + O extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O] +): Struct< + [ + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer + ], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct, + M extends Struct, + N extends Struct, + O extends Struct, + P extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P] +): Struct< + [ + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer

+> +export function tuple< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct, + M extends Struct, + N extends Struct, + O extends Struct, + P extends Struct, + Q extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q] +): Struct< + [ + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer, + Infer

, + Infer + ], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError

+ | InferError +> +/* +// generate script +var keys = 'A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q'.split(',').map(s=>s.trim()); +keys.map((v,i) => { + const types = keys.slice(0, i+1); + return `export function tuple<${types.map(t => `${t} extends Struct`).join(', ')}>( + Structs: [${types.join(', ')}] +): Struct<[${types.map(t => `Infer<${t}>`).join(', ')}], null, ${types.map(t => `InferError<${t}>`).join(' | ')}>`; +}).join('\n') +*/ +export function tuple(Elements: Struct[]): any { const Never = never() return new Struct({ @@ -525,7 +1192,14 @@ export function tuple(Elements: Struct[]): any { validator(value) { return ( Array.isArray(value) || - `Expected an array, but received: ${print(value)}` + ([ + { + class: 'type', + except: 'array', + actually: value, + message: `Expected an array, but received: ${print(value)}`, + }, + ] as TypeErrorDetail[]) ) }, }) @@ -540,7 +1214,7 @@ export function tuple(Elements: Struct[]): any { export function type( schema: S -): Struct, S> { +): Struct, S, ObjectError | TypeErrorDetail> { const keys = Object.keys(schema) return new Struct({ type: 'type', @@ -554,7 +1228,15 @@ export function type( }, validator(value) { return ( - isObject(value) || `Expected an object, but received: ${print(value)}` + isObject(value) || + ([ + { + class: 'type', + except: 'object', + actually: value, + message: `Expected an object, but received: ${print(value)}`, + }, + ] as TypeErrorDetail[]) ) }, }) @@ -563,58 +1245,537 @@ export function type( /** * Ensure that a value matches one of a set of types. */ - -export function union(Structs: TupleSchema<[A]>): Struct -export function union(Structs: TupleSchema<[A, B]>): Struct -export function union( - Structs: TupleSchema<[A, B, C]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F, G]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F, G, H]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P]> -): Struct -export function union( - Structs: TupleSchema<[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q]> +export function union>( + Structs: [A] +): Struct, null, InferError> +export function union< + A extends Struct, + B extends Struct +>( + Structs: [A, B] +): Struct | Infer, null, InferError | InferError> +export function union< + A extends Struct, + B extends Struct, + C extends Struct +>( + Structs: [A, B, C] +): Struct< + Infer | Infer | Infer, + null, + InferError | InferError | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct +>( + Structs: [A, B, C, D] +): Struct< + Infer | Infer | Infer | Infer, + null, + InferError | InferError | InferError | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct +>( + Structs: [A, B, C, D, E] +): Struct< + Infer | Infer | Infer | Infer | Infer, + null, + InferError | InferError | InferError | InferError | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct +>( + Structs: [A, B, C, D, E, F] +): Struct< + Infer | Infer | Infer | Infer | Infer | Infer, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct +>( + Structs: [A, B, C, D, E, F, G] +): Struct< + Infer | Infer | Infer | Infer | Infer | Infer | Infer, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct +>( + Structs: [A, B, C, D, E, F, G, H] +): Struct< + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I] +): Struct< + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J] +): Struct< + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K] +): Struct< + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L] +): Struct< + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct, + M extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L, M] +): Struct< + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct, + M extends Struct, + N extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L, M, N] +): Struct< + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct, + M extends Struct, + N extends Struct, + O extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O] +): Struct< + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError +> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct, + M extends Struct, + N extends Struct, + O extends Struct, + P extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P] +): Struct< + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer

, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError

+> +export function union< + A extends Struct, + B extends Struct, + C extends Struct, + D extends Struct, + E extends Struct, + F extends Struct, + G extends Struct, + H extends Struct, + I extends Struct, + J extends Struct, + K extends Struct, + L extends Struct, + M extends Struct, + N extends Struct, + O extends Struct, + P extends Struct, + Q extends Struct +>( + Structs: [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q] ): Struct< - A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q, - null + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer + | Infer

+ | Infer, + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError

+ | InferError > -export function union(Structs: Struct[]): any { +/* +// generate script +var keys = 'A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q'.split(',').map(s=>s.trim()); +keys.map((v,i) => { + const types = keys.slice(0, i+1); + return `export function union<${types.map(t => `${t} extends Struct`).join(', ')}>( + Structs: [${types.join(', ')}] +): Struct<${types.map(t => `Infer<${t}>`).join(' | ')}, null, ${types.map(t => `InferError<${t}>`).join(' | ')}>`; +}).join('\n') +*/ +export function union(Structs: Struct[]): any { const description = Structs.map((s) => s.type).join(' | ') return new Struct({ type: 'union', @@ -637,10 +1798,25 @@ export function union(Structs: Struct[]): any { } } + const message = `Expected the value to satisfy a union of \`${description}\`, but received: ${print( + value + )}` return [ - `Expected the value to satisfy a union of \`${description}\`, but received: ${print( - value - )}`, + { + value, + key: ctx.path[ctx.path.length - 1], + type: 'union', + refinement: undefined, + message, + branch: ctx.branch, + path: ctx.path, + detail: { + class: 'type', + except: description, + actually: value, + message, + } as TypeErrorDetail, + }, ...failures, ] }, @@ -651,6 +1827,6 @@ export function union(Structs: Struct[]): any { * Ensure that any value passes validation, without widening its type to `any`. */ -export function unknown(): Struct { - return define('unknown', () => true) +export function unknown(): Struct { + return define('unknown', () => []) } diff --git a/src/structs/utilities.ts b/src/structs/utilities.ts index 443ebe98..565e5cfe 100644 --- a/src/structs/utilities.ts +++ b/src/structs/utilities.ts @@ -1,6 +1,13 @@ -import { Struct, Context, Validator } from '../struct' +import { Struct, Context, Validator, SimpleValidator } from '../struct' import { object, optional } from './types' -import { ObjectSchema, Assign, ObjectType, PartialObjectSchema } from '../utils' +import { + ObjectSchema, + Assign, + ObjectType, + PartialObjectSchema, + ObjectError, +} from '../utils' +import { Error, GenericErrorDetail, TypeErrorDetail } from '../error' /** * Create a new struct that combines the properties properties from multiple @@ -9,50 +16,73 @@ import { ObjectSchema, Assign, ObjectType, PartialObjectSchema } from '../utils' * Like JavaScript's `Object.assign` utility. */ -export function assign( - A: Struct, A>, - B: Struct, B> -): Struct>, Assign> export function assign< A extends ObjectSchema, B extends ObjectSchema, - C extends ObjectSchema + E1 extends Error, + E2 extends Error >( - A: Struct, A>, - B: Struct, B>, - C: Struct, C> -): Struct, C>>, Assign, C>> + A: Struct, A, E1>, + B: Struct, B, E2> +): Struct>, Assign, E1 | E2> export function assign< A extends ObjectSchema, B extends ObjectSchema, C extends ObjectSchema, - D extends ObjectSchema + E1 extends Error, + E2 extends Error, + E3 extends Error >( - A: Struct, A>, - B: Struct, B>, - C: Struct, C>, - D: Struct, D> + A: Struct, A, E1>, + B: Struct, B, E2>, + C: Struct, C, E3> +): Struct< + ObjectType, C>>, + Assign, C>, + E1 | E2 | E3 +> +export function assign< + A extends ObjectSchema, + B extends ObjectSchema, + C extends ObjectSchema, + D extends ObjectSchema, + E1 extends Error, + E2 extends Error, + E3 extends Error, + E4 extends Error +>( + A: Struct, A, E1>, + B: Struct, B, E2>, + C: Struct, C, E3>, + D: Struct, D, E4> ): Struct< ObjectType, C>, D>>, - Assign, C>, D> + Assign, C>, D>, + E1 | E2 | E3 | E4 > export function assign< A extends ObjectSchema, B extends ObjectSchema, C extends ObjectSchema, D extends ObjectSchema, - E extends ObjectSchema + E extends ObjectSchema, + E1 extends Error, + E2 extends Error, + E3 extends Error, + E4 extends Error, + E5 extends Error >( - A: Struct, A>, - B: Struct, B>, - C: Struct, C>, - D: Struct, D>, - E: Struct, E> + A: Struct, A, E1>, + B: Struct, B, E2>, + C: Struct, C, E3>, + D: Struct, D, E4>, + E: Struct, E, E5> ): Struct< ObjectType, C>, D>, E>>, - Assign, C>, D>, E> + Assign, C>, D>, E>, + E1 | E2 | E3 | E4 | E5 > -export function assign(...Structs: Struct[]): any { +export function assign(...Structs: Struct[]): any { const schemas = Structs.map((s) => s.schema) const schema = Object.assign({}, ...schemas) return object(schema) @@ -62,8 +92,42 @@ export function assign(...Structs: Struct[]): any { * Define a new struct type with a custom validation function. */ -export function define(name: string, validator: Validator): Struct { - return new Struct({ type: name, schema: null, validator }) +// export function define( +// name: string, +// validator: Validator +// ): Struct { +// return new Struct({ type: name, schema: null, validator }) +// } +export function define( + name: string, + validator: Validator +): Struct +export function define( + name: string, + validator: SimpleValidator +): Struct +export function define( + name: string, + validator: Validator | SimpleValidator +): Struct { + return new Struct({ + type: name, + schema: null, + validator(value, context) { + const res = validator(value, context) + if (res === false || typeof res === 'string') { + return [ + { + class: 'generic', + message: res || 'error', + }, + ] as GenericErrorDetail[] + } else if (res === undefined || res === true) { + return [] + } + return res + }, + }) } /** @@ -74,9 +138,9 @@ export function define(name: string, validator: Validator): Struct { * validation logic that changes based on its input. */ -export function dynamic( - fn: (value: unknown, ctx: Context) => Struct -): Struct { +export function dynamic( + fn: (value: unknown, ctx: Context) => Struct +): Struct { return new Struct({ type: 'dynamic', schema: null, @@ -104,8 +168,10 @@ export function dynamic( * circular definition problem. */ -export function lazy(fn: () => Struct): Struct { - let struct: Struct | undefined +export function lazy( + fn: () => Struct +): Struct { + let struct: Struct | undefined return new Struct({ type: 'lazy', schema: null, @@ -132,9 +198,13 @@ export function lazy(fn: () => Struct): Struct { */ export function omit( - struct: Struct, S>, + struct: Struct, S, any>, keys: K[] -): Struct>, Omit> { +): Struct< + ObjectType>, + Omit, + ObjectError | TypeErrorDetail +> { const { schema } = struct const subschema: any = { ...schema } @@ -153,8 +223,12 @@ export function omit( */ export function partial( - struct: Struct, S> | S -): Struct>, PartialObjectSchema> { + struct: Struct, S, any> | S +): Struct< + ObjectType>, + PartialObjectSchema, + ObjectError +> { const schema: any = struct instanceof Struct ? { ...struct.schema } : { ...struct } @@ -173,9 +247,13 @@ export function partial( */ export function pick( - struct: Struct, S>, + struct: Struct, S, any>, keys: K[] -): Struct>, Pick> { +): Struct< + ObjectType>, + Pick, + ObjectError | TypeErrorDetail +> { const { schema } = struct const subschema: any = {} @@ -192,7 +270,10 @@ export function pick( * @deprecated This function has been renamed to `define`. */ -export function struct(name: string, validator: Validator): Struct { +export function struct( + name: string, + validator: Validator +): Struct { console.warn( 'superstruct@0.11 - The `struct` helper has been renamed to `define`.' ) diff --git a/src/utils.ts b/src/utils.ts index 2a98e40d..db251eba 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,11 +1,18 @@ -import { Struct, Infer, Result, Context, Describe } from './struct' -import { Failure } from './error' +import { + Struct, + Infer, + Result, + Context, + Describe, + DescribedResult, +} from './struct' +import { Error, ErrorDetail, Failure, ThrowErrorDetail } from './error' /** * Check if a value is an iterator. */ -function isIterable(x: unknown): x is Iterable { +export function isIterable(x: unknown): x is Iterable { return isObject(x) && typeof x[Symbol.iterator] === 'function' } @@ -52,38 +59,37 @@ export function shiftIterator(input: Iterator): T | undefined { * Convert a single validation result to a failure. */ -export function toFailure( - result: string | boolean | Partial, +export function toFailure( + result: DescribedResult, context: Context, - struct: Struct, + struct: Struct, value: any -): Failure | undefined { - if (result === true) { - return - } else if (result === false) { - result = {} - } else if (typeof result === 'string') { - result = { message: result } +): Failure { + if (!('class' in result)) { + return result } + let { message } = result + const { path, branch } = context const { type } = struct - const { - refinement, - message = `Expected a value of type \`${type}\`${ - refinement ? ` with refinement \`${refinement}\`` : '' - }, but received: \`${print(value)}\``, - } = result + + if (message === undefined) { + message = `Expected a value of type \`${type}\, but received: \`${print( + value + )}\`` + result.message = message + } return { value, type, - refinement, + refinement: undefined, key: path[path.length - 1], path, branch, - ...result, message, + detail: result, } } @@ -91,22 +97,18 @@ export function toFailure( * Convert a validation result to an iterable of failures. */ -export function* toFailures( - result: Result, +export function* toFailures( + result: Result, context: Context, - struct: Struct, + struct: Struct, value: any -): IterableIterator { +): IterableIterator> { if (!isIterable(result)) { result = [result] } for (const r of result) { - const failure = toFailure(r, context, struct, value) - - if (failure) { - yield failure - } + yield toFailure(r, context, struct, value) } } @@ -115,15 +117,17 @@ export function* toFailures( * returning an iterator of failures or success. */ -export function* run( +export function* run( value: unknown, - struct: Struct, + struct: Struct, options: { path?: any[] branch?: any[] coerce?: boolean } = {} -): IterableIterator<[Failure, undefined] | [undefined, T]> { +): IterableIterator< + [Failure, undefined] | [undefined, T] +> { const { path = [], branch = [value], coerce = false } = options const ctx: Context = { path, branch } @@ -133,13 +137,33 @@ export function* run( let valid = true - for (const failure of struct.validator(value, ctx)) { + let failures: Iterable> = [] + try { + failures = struct.validator(value, ctx) + } catch (e) { + yield [ + toFailure( + { + class: 'throw', + error: e, + message: `Throw error in run validation: \`${e?.toString()}\``, + } as ThrowErrorDetail, + ctx, + struct, + value + ), + undefined, + ] + return + } + + for (const failure of failures) { valid = false yield [failure, undefined] } for (let [k, v, s] of struct.entries(value, ctx)) { - const ts = run(v, s as Struct, { + const ts = run(v, s as Struct, { path: k === undefined ? path : [...path, k], branch: k === undefined ? branch : [...branch, v], coerce, @@ -256,7 +280,7 @@ export type Assign = Simplify> * A schema for object structs. */ -export type ObjectSchema = Record> +export type ObjectSchema = Record> /** * Infer a type from an object struct schema. @@ -266,6 +290,16 @@ export type ObjectType = Simplify< Optionalize<{ [K in keyof S]: Infer }> > +/** + * Extract error types from an object struct schema. + */ + +export type ObjectError = { + [K in keyof S]: S[K] +} extends Record> + ? E + : never + /** * Transform an object schema type to represent a partial. */ @@ -305,7 +339,7 @@ export type StructSchema = [T] extends [string] : T extends Array ? T extends IsTuple ? null - : Struct + : Struct : T extends Promise ? null : T extends object @@ -324,4 +358,4 @@ export type EnumSchema = { [K in T]: K } * A schema for tuple structs. */ -export type TupleSchema = { [K in keyof T]: Struct } +export type TupleSchema = { [K in keyof T]: Struct } diff --git a/test/api/throw.ts b/test/api/throw.ts new file mode 100644 index 00000000..6ea2b769 --- /dev/null +++ b/test/api/throw.ts @@ -0,0 +1,29 @@ +import { deepStrictEqual, strictEqual } from 'assert' +import { StructError, define } from '../..' + +describe('throw errors', () => { + it('throw error in define', () => { + const S = define('email', () => { + throw Error('validation error') + }) + const [err, value] = S.validate(null) + strictEqual(value, undefined) + strictEqual(err instanceof StructError, true) + deepStrictEqual(Array.from(err!.failures()), [ + { + value: null, + key: undefined, + type: 'email', + refinement: undefined, + message: 'Throw error in run validation: `Error: validation error`', + path: [], + branch: [null], + detail: { + class: 'throw', + error: new Error('validation error'), + message: 'Throw error in run validation: `Error: validation error`', + }, + }, + ]) + }) +}) diff --git a/test/api/validate.ts b/test/api/validate.ts index f1e6590e..61998dde 100644 --- a/test/api/validate.ts +++ b/test/api/validate.ts @@ -17,7 +17,7 @@ describe('validate', () => { const [err, value] = validate(42, S) strictEqual(value, undefined) strictEqual(err instanceof StructError, true) - deepStrictEqual(Array.from((err as StructError).failures()), [ + deepStrictEqual(Array.from(err!.failures()), [ { value: 42, key: undefined, @@ -26,6 +26,12 @@ describe('validate', () => { message: 'Expected a string, but received: 42', path: [], branch: [42], + detail: { + actually: 42, + class: 'type', + except: 'string', + message: 'Expected a string, but received: 42', + }, }, ]) }) @@ -35,7 +41,7 @@ describe('validate', () => { const [err, value] = S.validate(42) strictEqual(value, undefined) strictEqual(err instanceof StructError, true) - deepStrictEqual(Array.from((err as StructError).failures()), [ + deepStrictEqual(Array.from(err!.failures()), [ { value: 42, key: undefined, @@ -44,6 +50,12 @@ describe('validate', () => { message: 'Expected a string, but received: 42', path: [], branch: [42], + detail: { + actually: 42, + class: 'type', + except: 'string', + message: 'Expected a string, but received: 42', + }, }, ]) }) @@ -52,14 +64,14 @@ describe('validate', () => { const S = object({ author: object({ name: string() }) }) const [err] = S.validate({ author: { name: 42 } }) strictEqual( - (err as StructError).message, + err!.message, 'At path: author.name -- Expected a string, but received: 42' ) }) it('early exit', () => { let ranA = false - let ranB = false + const ranB = false const A = define('A', (x) => { ranA = true diff --git a/test/index.ts b/test/index.ts index 96fadef7..af793e9d 100644 --- a/test/index.ts +++ b/test/index.ts @@ -11,6 +11,7 @@ describe('superstruct', () => { require('./api/is') require('./api/mask') require('./api/validate') + require('./api/throw') }) describe('validation', () => { diff --git a/test/validation/throw/invalid.ts b/test/validation/throw/invalid.ts new file mode 100644 index 00000000..1709946a --- /dev/null +++ b/test/validation/throw/invalid.ts @@ -0,0 +1,17 @@ +import { define } from '../../..' + +export const Struct = define('email', () => { + throw Error('validation error') +}) + +export const data = 'invalid' + +export const failures = [ + { + value: 'invalid', + type: 'email', + refinement: undefined, + path: [], + branch: [data], + }, +] diff --git a/yarn.lock b/yarn.lock index 9ef5d009..5d2632e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1173,14 +1173,6 @@ "@typescript-eslint/typescript-estree" "4.13.0" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.12.0.tgz#beeb8beca895a07b10c593185a5612f1085ef279" - integrity sha512-QVf9oCSVLte/8jvOsxmgBdOaoe2J0wtEmBr13Yz0rkBNkl5D8bfnf6G4Vhox9qqMIoG7QQoVwd2eG9DM/ge4Qg== - dependencies: - "@typescript-eslint/types" "4.12.0" - "@typescript-eslint/visitor-keys" "4.12.0" - "@typescript-eslint/scope-manager@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.13.0.tgz#5b45912a9aa26b29603d8fa28f5e09088b947141" @@ -1189,30 +1181,11 @@ "@typescript-eslint/types" "4.13.0" "@typescript-eslint/visitor-keys" "4.13.0" -"@typescript-eslint/types@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.12.0.tgz#fb891fe7ccc9ea8b2bbd2780e36da45d0dc055e5" - integrity sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g== - "@typescript-eslint/types@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.13.0.tgz#6a7c6015a59a08fbd70daa8c83dfff86250502f8" integrity sha512-/+aPaq163oX+ObOG00M0t9tKkOgdv9lq0IQv/y4SqGkAXmhFmCfgsELV7kOCTb2vVU5VOmVwXBXJTDr353C1rQ== -"@typescript-eslint/typescript-estree@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz#3963418c850f564bdab3882ae23795d115d6d32e" - integrity sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w== - dependencies: - "@typescript-eslint/types" "4.12.0" - "@typescript-eslint/visitor-keys" "4.12.0" - debug "^4.1.1" - globby "^11.0.1" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" - "@typescript-eslint/typescript-estree@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.13.0.tgz#cf6e2207c7d760f5dfd8d18051428fadfc37b45e" @@ -1227,14 +1200,6 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz#a470a79be6958075fa91c725371a83baf428a67a" - integrity sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw== - dependencies: - "@typescript-eslint/types" "4.12.0" - eslint-visitor-keys "^2.0.0" - "@typescript-eslint/visitor-keys@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.13.0.tgz#9acb1772d3b3183182b6540d3734143dce9476fe"

+ ], + null, + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError + | InferError