UNPKG

@apoyo/decoders

Version:
1,534 lines (1,523 loc) 53 kB
import * as _apoyo_std from '@apoyo/std'; import { Dict, Tree, Result, Option, Refinement, Predicate, InverseRefinement, Enum } from '@apoyo/std'; declare const _uuid: unique symbol; declare const _email: unique symbol; declare const _date: unique symbol; declare const _datetime: unique symbol; declare const _int: unique symbol; interface UUIDBrand { readonly [_uuid]: unknown; } interface EmailBrand { readonly [_email]: unknown; } interface DateBrand { readonly [_date]: unknown; } interface DatetimeBrand { readonly [_datetime]: unknown; } interface IntBrand { readonly [_int]: unknown; } /** * An UUID */ declare type UUID = string & UUIDBrand; /** * An email */ declare type Email = string & EmailBrand; /** * A number without a decimal point */ declare type Int = number & IntBrand; declare namespace ISO { /** * A date string with the following format: * * YYYY-MM-DD */ type Date = string & DateBrand; /** * An ISO datetime string with the following format: * * YYYY-MM-DD HH:mm:ssZ */ type Datetime = string & DatetimeBrand; } declare const enum ErrorCode { REQUIRED = "required", STRING = "string", STRING_LENGTH = "string.length", STRING_MIN = "string.min", STRING_MAX = "string.max", STRING_PATTERN = "string.pattern", STRING_EMAIL = "string.email", STRING_UUID = "string.uuid", STRING_EQUALS = "string.equals", STRING_ONE_OF = "string.oneOf", STRING_DATE = "string.date", STRING_DATETIME = "string.datetime", DATE = "date", DATE_STRICT = "date.strict", DATE_MIN = "date.min", DATE_MAX = "date.max", NUMBER = "number", NUMBER_STRICT = "number.strict", NUMBER_FROM_STRING = "number.fromString", NUMBER_MIN = "number.min", NUMBER_MAX = "number.max", INT = "int", INT_STRICT = "int.strict", INT_FROM_STRING = "int.fromString", INT_MIN = "int.min", INT_MAX = "int.max", BOOL = "bool", BOOL_STRICT = "bool.strict", BOOL_FROM_STRING = "bool.fromString", BOOL_FROM_NUMBER = "bool.fromNumber", BOOL_EQUALS = "bool.equals", ARRAY = "array", ARRAY_NON_EMPTY = "array.nonEmpty", ARRAY_LENGTH = "array.length", ARRAY_MIN = "array.min", ARRAY_MAX = "array.max", DICT = "dict", ENUM = "enum.native", ENUM_LITERAL = "enum.literal", ENUM_IS_IN = "enum.isIn" } declare const enum DecodeErrorTag { VALUE = "DE.Value", ARRAY = "DE.ArrayLike", INDEX = "DE.Index", OBJECT = "DE.ObjectLike", KEY = "DE.Key", UNION = "DE.UnionLike", MEMBER = "DE.Member" } declare namespace DecodeError { interface Value { tag: DecodeErrorTag.VALUE; value: unknown; message: string; meta: Dict; } interface Key { tag: DecodeErrorTag.KEY; key: string; error: DecodeError; } interface Index { tag: DecodeErrorTag.INDEX; index: number; error: DecodeError; } interface Member { tag: DecodeErrorTag.MEMBER; index: number; error: DecodeError; } interface ArrayLike { tag: DecodeErrorTag.ARRAY; kind: string; errors: DecodeError.Index[]; } interface ObjectLike { tag: DecodeErrorTag.OBJECT; kind: string; name?: string; errors: DecodeError.Key[]; } interface UnionLike { tag: DecodeErrorTag.UNION; kind: string; name?: string; errors: DecodeError.Member[]; } type Path = { tag: DecodeErrorTag.ARRAY; kind: string; } | { tag: DecodeErrorTag.INDEX; index: number; } | { tag: DecodeErrorTag.OBJECT; kind: string; name?: string; } | { tag: DecodeErrorTag.KEY; key: string; } | { tag: DecodeErrorTag.UNION; kind: string; name?: string; } | { tag: DecodeErrorTag.MEMBER; index: number; }; interface Flat { value: unknown; message: string; meta: Dict; path: Path[]; } interface Formatted { value: unknown; message: string; description: string; meta: Dict; path: string; } } declare type DecodeError = DecodeError.Value | DecodeError.ObjectLike | DecodeError.ArrayLike | DecodeError.UnionLike; declare const DecodeError: { value: (value: unknown, message: string, meta?: Dict) => DecodeError.Value; key: (key: string, error: DecodeError) => DecodeError.Key; index: (index: number, error: DecodeError) => DecodeError.Index; member: (index: number, error: DecodeError) => DecodeError.Member; array: (errors: DecodeError.Index[]) => DecodeError.ArrayLike; object: (errors: DecodeError.Key[], name?: string) => DecodeError.ObjectLike; union: (errors: DecodeError.Member[], name?: string) => DecodeError.UnionLike; fold: <T>(cases: { value(err: DecodeError.Value): T; array(err: DecodeError.ArrayLike): T; object(err: DecodeError.ObjectLike): T; union(err: DecodeError.UnionLike): T; }) => (err: DecodeError) => T; toTree: (e: DecodeError) => Tree<string>; /** * @description * Format your `DecodeError` into a easily displayable string. */ draw: (e: DecodeError) => string; /** * @description * Flatten your `DecodeError` into an `DecodeError.Flat`. * This allows you to more easily format custom errors. * * It is however recommended to simply use `DecodeError.format` or `DecodeError.formatBy` instead. */ flatten: (e: DecodeError) => DecodeError.Flat[]; /** * @description * Get full error description / stack from a `DecodeError.Flat`. * Use `DecodeError.flatten` to transform your `DecodeError` into an `DecodeError.Flat` */ getDescription: (err: DecodeError.Flat, separator?: string) => string; /** * @description * Get full property path from a `DecodeError.Flat`. * Use `DecodeError.flatten` to transform your `DecodeError` into an `DecodeError.Flat` */ getPath: (err: DecodeError.Flat) => string; /** * @description * Get formatted error from a `DecodeError.Flat`. * Use `DecodeError.flatten` to transform your `DecodeError` into an `DecodeError.Flat` */ getFormatted: (e: DecodeError.Flat) => DecodeError.Formatted; /** * @description * Create a custom error formatter, to fully customize your error output */ formatBy: <T_1>(fn: (flat: DecodeError.Flat) => T_1) => (e: DecodeError) => T_1[]; /** * @description * Default `DecodeError.formatBy` implementation, that returns `DecodeError.Formatted` errors */ format: (e: DecodeError) => DecodeError.Formatted[]; /** * @deprecated Use `DecodeError.getDescription` instead * * @see `DecodeError.getDescription` */ formatError: (e: DecodeError.Flat) => string; }; declare type DecoderResult<A> = Result<A, DecodeError>; declare function filter<A, B extends A>(fn: Refinement<A, B>, message: string, meta?: Dict<unknown>): <I>(value: Decoder<I, A>) => Decoder<I, B>; declare function filter<A>(fn: Predicate<A>, message: string, meta?: Dict<unknown>): <I>(value: Decoder<I, A>) => Decoder<I, A>; declare function reject<A, B extends A>(fn: Refinement<A, B>, message: string, meta?: Dict<unknown>): <I>(value: Decoder<I, A>) => Decoder<I, InverseRefinement<A, B>>; declare function reject<A>(fn: Predicate<A>, message: string, meta?: Dict<unknown>): <I>(value: Decoder<I, A>) => Decoder<I, A>; declare function validate<O>(decoder: Decoder<unknown, O>): (input: unknown) => DecoderResult<O>; declare function validate<I, O>(decoder: Decoder<I, O>): (input: I) => DecoderResult<O>; declare function defaultValue(value: never[]): <I, O extends any[]>(decoder: Decoder<I, O | undefined>) => Decoder<I, O>; declare function defaultValue<T>(value: T): <I, O>(decoder: Decoder<I, O | undefined>) => Decoder<I, O | T>; declare function union<I, O1, O2>(a: Decoder<I, O1>, b: Decoder<I, O2>): Decoder<I, O1 | O2>; declare function union<I, O1, O2, O3>(a: Decoder<I, O1>, b: Decoder<I, O2>, c: Decoder<I, O3>): Decoder<I, O1 | O2 | O3>; declare function union<I, O1, O2, O3, O4>(a: Decoder<I, O1>, b: Decoder<I, O2>, c: Decoder<I, O3>, d: Decoder<I, O4>): Decoder<I, O1 | O2 | O3 | O4>; declare type Decoder<I, O> = { decode(input: I): DecoderResult<O>; }; declare namespace Decoder { type TypeOf<A> = A extends Decoder<unknown, infer B> ? Option.Struct<B> : never; type InputOf<A> = A extends Decoder<infer B, unknown> ? Option.Struct<B> : never; } /** * @namespace Decoder * * @description * * A `Decoder` is a function, that from an input I to create an output O, or produce an DecodeError. * As such, a common use-case for `Decoder`s are type and value validations. * * This namespace contains the core utilities to: * - Create `Decoder`s * - Use or combine them * - Extract / infer the resulting type informations * * @example * ```ts * export const TodoDto = ObjectDecoder.struct({ * id: IntegerDecoder.int, * title: TextDecoder.varchar(1, 100), * description: pipe( * TextDecoder.varchar(0, 2000), * TextDecoder.nullable * ), * done: BooleanDecoder.boolean * }) * * export interface TodoDto extends Decoder.TypeOf<typeof TodoDto> {} * ``` */ declare const Decoder: { /** * @description * Create a new decoder */ create: <I, O>(fn: (input: I) => DecoderResult<O>) => Decoder<I, O>; /** * @description * Creates a new decoder from a type guard * * @example * ```ts * const stringDecoder = Decoder.fromGuard( * (input: unknown): input is string => typeof input === 'string', * 'value is not a string' * ) * * expect(pipe('Hello', Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe(42, Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ fromGuard: <I_1, O_1 extends I_1>(fn: Refinement<I_1, O_1>, message: string, meta?: Dict<unknown>) => Decoder<I_1, O_1>; /** * @description * Map over the resulting value of an `Decoder` * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * Decoder.map(str => str.trim()) * ) * * expect(pipe(' Hello ', Decoder.validate(decoder), Result.get)).toBe('Hello') * ``` */ map: <A, B>(fn: (input: A) => B) => <I_2>(decoder: Decoder<I_2, A>) => Decoder<I_2, B>; /** * @description * Map over the resulting error of an `Decoder` * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * Decoder.mapError(err => DecodeError.object([ * DecodeError.key('firstName', err) * ])) * ) * ``` */ mapError: <I_3>(fn: (err: DecodeError, input: I_3) => DecodeError) => <A_1>(decoder: Decoder<I_3, A_1>) => Decoder<I_3, A_1>; /** * @description * Catch the validation error and create a new error with the given message. * * @example * ```ts * const decoder = pipe( * Decoder.union( * NumberDecoder.number, * NumberDecoder.fromString * ), * Decoder.withMessage('The given value is not a number', { * code: 'invalid_number' * }) * ) * * const expectedError = DecodeError.value(' Hello ', 'This value is not a number', { * code: 'invalid_number' * }) * expect(pipe(' Hello ', Decoder.validate(decoder))).toEqual(Result.ko(expectedError)) * ``` */ withMessage: (msg: string, meta?: Dict<unknown>) => <I_4, A_2>(decoder: Decoder<I_4, A_2>) => Decoder<I_4, A_2>; /** * @description * Add a custom validation function to an `Decoder`. * * Compared to `Decoder.guard`, this function gives more control and allows the user to modify the resulting value. * * @see `Decoder.guard` * @see `Decoder.filter` * @see `Decoder.reject` * * @example * ```ts * const validateAge = (date: Date): Result<string, DecodeError> => { * const now = new Date() * * if (date.getFullYear() < now.getFullYear() - 100) { * return Result.ko(DecodeError.value(dob, 'Date of birth is more than 100 years ago')) * } * if (date.getFullYear() > now.getFullYear() - 18) { * return Result.ko(DecodeError.value(dob, 'Date of birth is less than 18 years ago')) * } * return Result.ok(dob) * } * * const birthdayDecoder = pipe( * DateDecoder.date, * Decoder.parse(validateAge) * ) * * expect(pipe('1930-01-01', Decoder.validate(birthdayDecoder), Result.isOk)).toBe(true) * expect(pipe('1920-01-01', Decoder.validate(birthdayDecoder), Result.isKo)).toBe(true) * ``` */ parse: <B_1, C>(fn: (input: B_1) => DecoderResult<C>) => <A_3>(decoder: Decoder<A_3, B_1>) => Decoder<A_3, C>; /** * @description * Chain another decoder to execute with the current input. * This allows you to dynamically compute the decoder to use depending on a value. */ chain: <B_2, C_1>(fn: (input: B_2) => Decoder<B_2, C_1>) => <A_4>(decoder: Decoder<A_4, B_2>) => Decoder<A_4, C_1>; /** * @description * Add a custom validation function, returning either: * - A `DecodeError` if the input is incorrect * - `undefined` if there is no error to report. * * This function gives more control about the returned error than `Decoder.filter` or `Decoder.reject`, but does not allow the value to be modified. * * @see `Decoder.parse` * @see `Decoder.filter` * @see `Decoder.reject` * * @example * ```ts * const validateAge = (date: Date): Option<DecodeError> => { * const now = new Date() * * if (date.getFullYear() < now.getFullYear() - 100) { * return DecodeError.value(dob, 'Date of birth is more than 100 years ago') * } * if (date.getFullYear() > now.getFullYear() - 18) { * return DecodeError.value(dob, 'Date of birth is less than 18 years ago') * } * return undefined * } * * const birthdayDecoder = pipe( * DateDecoder.date, * Decoder.guard(validateAge) * ) * * expect(pipe('1930-01-01', Decoder.validate(birthdayDecoder), Result.isOk)).toBe(true) * expect(pipe('1920-01-01', Decoder.validate(birthdayDecoder), Result.isKo)).toBe(true) * ``` */ guard: <O_2>(fn: (input: O_2) => Option<DecodeError>) => <A_5>(decoder: Decoder<A_5, O_2>) => Decoder<A_5, O_2>; /** * @description * Add a `Predicate` filter function to a `Decoder`. * If the value matches the `Predicate`, the value is kept. * Else, an DecodeError with the given message is returned. * * @see `Decoder.parse` * @see `Decoder.guard` * @see `Decoder.reject` * * @see `DateDecoder.min` * @see `DateDecoder.max` * * @example * ```ts * const maxAge = (age: number) => (dob: Date) => { * const now = new Date() * return date.getFullYear() > now.getFullYear() - age * } * const minAge = (age: number) => (dob: Date) => { * const now = new Date() * return date.getFullYear() < now.getFullYear() - age * } * * const birthdayDecoder = pipe( * DateDecoder.date, * Decoder.filter(maxAge(100), `Date of birth is more than 100 years ago`), * Decoder.filter(minAge(18), `Date of birth is less than 18 years ago`) * ) * * expect(pipe('1930-01-01', Decoder.validate(birthdayDecoder), Result.isOk)).toBe(true) * expect(pipe('1920-01-01', Decoder.validate(birthdayDecoder), Result.isKo)).toBe(true) * ``` */ filter: typeof filter; /** * @description * Add a `Predicate` filter function to a `Decoder`. * If the value matches the `Predicate`, an DecodeError with the given message is returned. * Else, the value is kept. * * @see `Decoder.parse` * @see `Decoder.guard` * @see `Decoder.filter` * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * Decoder.reject(str => str.length === 0, `string should not be empty`) * ) * * expect(pipe('Hello', Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe('', Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ reject: typeof reject; /** * @description * Makes the value nullable. If the input is undefined, the decoder will return null instead. * * **Note**: If you want to transform an empty string to `null`, use `TextDecoder.nullable` instead. * * @see `Decoder.optional` * @see `TextDecoder.nullable` * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * Decoder.nullable * ) * * expect(pipe('Hello', Decoder.validate(decoder), Result.get)).toBe('Hello') * expect(pipe('', Decoder.validate(decoder), Result.get)).toBe('') * expect(pipe(null, Decoder.validate(decoder), Result.get)).toBe(null) * expect(pipe(undefined, Decoder.validate(decoder), Result.get)).toBe(null) * ``` */ nullable: <I_5, O_3>(decoder: Decoder<I_5, O_3>) => Decoder<I_5, O_3 | null>; /** * @description * Makes the value optional (allows `undefined`). * * **Note**: If you want to transform an empty string to `undefined`, use `TextDecoder.optional` instead. * * @see `Decoder.nullable` * @see `TextDecoder.optional` * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * Decoder.optional * ) * * expect(pipe('Hello', Decoder.validate(decoder), Result.get)).toBe('Hello') * expect(pipe('', Decoder.validate(decoder), Result.get)).toBe('') * expect(pipe(undefined, Decoder.validate(decoder), Result.get)).toBe(undefined) * ``` */ optional: <I_6, O_4>(decoder: Decoder<I_6, O_4>) => Decoder<I_6, O_4 | undefined>; /** * @description * Explicitely return an "input is required" error when input is "null" or "undefined" * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * Decoder.required * ) * ``` */ required: <I_7, O_5>(decoder: Decoder<I_7, O_5>) => Decoder<I_7, O_5>; /** * @description * This function allows the creation of recursive type decoders. * * @example * ```ts * interface Tree<T> { * value: T * forest: Tree<T>[] * } * * // Recursive types require manual typing * const TreeDecoder = <O>(decoder: Decoder<unknown, O>): Decoder<unknown, Tree<O>> => * Decoder.lazy(() => * ObjectDecoder.struct({ * value: decoder, * forest: ArrayDecoder.array(TreeDecoder(decoder)) * }) * ) * * const input: unknown = { * value: 'Hello', * forest: [ * { * value: 'World', * forest: [] * } * ] * } * * expect(pipe(input, TreeDecoder(TextDecoder.string), Result.isOk)).toBe(true) * ``` */ lazy: <I_8, O_6>(fn: () => Decoder<I_8, O_6>) => Decoder<I_8, O_6>; /** * @description * This operator makes the decoder optional and returns the given value when the input is undefined. * * @example * ```ts * const decoder = pipe( * BooleanDecoder.boolean, * Decoder.default(false) * ) * * expect(pipe(true, Decoder.validate(decoder), Result.get)).toEqual(true) * expect(pipe(false, Decoder.validate(decoder), Result.get)).toEqual(false) * expect(pipe(undefined, Decoder.validate(decoder), Result.get)).toEqual(false) * ``` */ default: typeof defaultValue; /** * @description * Creates a union `Decoder` that tries, in the given order, if the input is valid. * * @example * ```ts * const decoder = Decoder.union( * NumberDecoder.number, * NumberDecoder.fromString * ) * * expect(pipe(42, Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe("42", Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe("Hello", Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ union: typeof union; /** * @description * Utility to bind a given generated type to a `Decoder` */ ref: <A_6>(decoder: Decoder<unknown, A_6>) => Decoder<unknown, A_6>; /** * @description * Validate an input by the given `Decoder` * * @example * ``` * const result = pipe( * input, * Decoder.validate(TextDecoder.string) * ) * * if (Result.isKo(result)) { * console.log(result.ko) * return * } * * const value = result.ok * console.log(value) * ``` */ validate: typeof validate; /** * @description * `Decoder` for an unknown value */ unknown: Decoder<unknown, unknown>; /** * @description * `Decoder` for any value */ any: Decoder<unknown, any>; }; declare function oneOf<T extends string>(arr: T[]): Decoder<unknown, T>; declare function oneOf<T extends string>(arr: Set<T>): Decoder<unknown, T>; declare type TextDecoder<I> = Decoder<I, string>; /** * @namespace TextDecoder * * @description * This namespace contains string decoders and additional utilities for string validations. */ declare const TextDecoder: { /** * @description * Check if the input is a string */ string: TextDecoder<unknown>; /** * @description * Check the length of the string */ length: (len: number) => <I>(value: Decoder<I, string>) => Decoder<I, string>; /** * @description * Check the minimum length of the string * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * TextDecoder.min(1) * ) * * expect(pipe('1', Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe('', Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ min: (minLength: number) => <I>(value: Decoder<I, string>) => Decoder<I, string>; /** * @description * Check the maximum length of the string * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * TextDecoder.max(5) * ) * * expect(pipe('12345', Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe('123456', Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ max: (maxLength: number) => <I>(value: Decoder<I, string>) => Decoder<I, string>; /** * @description * Check both the minimum and maximum length of the string * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * TextDecoder.between(1, 100) * ) * * expect(pipe('', Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ between: (minLength: number, maxLength: number) => <I>(value: Decoder<I, string>) => Decoder<I, string>; /** * @description * Trim the string * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * TextDecoder.trim, * TextDecoder.between(1, 100) * ) * * expect(pipe(' ', Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ trim: <I_1>(decoder: Decoder<I_1, string>) => Decoder<I_1, string>; /** * @description * Check if the string matches a given pattern / regexp * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * TextDecoder.pattern(/^Hello [a-zA-Z]+$/) * ) * * expect(pipe('Hello world', Decoder.validate(decoder), Result.isOk)).toBe(true) * ``` */ pattern: <T extends string = string>(regexp: RegExp, message?: string, meta?: Dict) => <I_2>(value: Decoder<I_2, string>) => Decoder<I_2, T>; /** * @description * Check if the string is a date in the ISO format (YYYY-MM-DD) * * @example * ```ts * expect(pipe("2020-06-13", Decoder.validate(TextDecoder.date), Result.isOk)).toBe(true) * expect(pipe("Hello", Decoder.validate(TextDecoder.date), Result.isKo)).toBe(true) * ``` */ date: Decoder<unknown, ISO.Date>; /** * @description * Check if the string is a datetime in the ISO format (YYYY-MM-DD HH:mm:ss) * * @example * ```ts * expect(pipe("2020-06-13", Decoder.validate(TextDecoder.datetime), Result.isOk)).toBe(true) * expect(pipe("2020-06-13 13:53:23Z", Decoder.validate(TextDecoder.datetime), Result.isOk)).toBe(true) * ``` */ datetime: Decoder<unknown, ISO.Datetime>; /** * @description * Check if the input is an email * * @example * ```ts * expect(pipe("test@example.com", Decoder.validate(TextDecoder.email), Result.isOk)).toBe(true) * expect(pipe("test", Decoder.validate(TextDecoder.email), Result.isKo)).toBe(true) * ``` */ email: Decoder<unknown, Email>; /** * @description * Check if the input is an UUID */ uuid: Decoder<unknown, UUID>; /** * @description * Check if the input is a string between the given length. * * @example * ```ts * // The following: * const decoder = TextDecoder.varchar(1, 100) * // is the same as: * const decoder = pipe(TextDecoder.string, TextDecoder.between(1, 100)) * ``` */ varchar: (minLength: number, maxLength: number) => Decoder<unknown, string>; /** * @description * This makes the string nullable. If the string is empty, `null` is returned. * * @see `TextDecoder.optional` * @see `Decoder.nullable` * * @example * ```ts * const decoder = pipe(TextDecoder.string, TextDecoder.nullable) * * expect(pipe("Hello", Decoder.validate(decoder), Result.get)).toBe("Hello") * expect(pipe(null, Decoder.validate(decoder), Result.get)).toBe(null) * expect(pipe("", Decoder.validate(decoder), Result.get)).toBe(null) * ``` */ nullable: <I_3>(decoder: Decoder<I_3, string>) => Decoder<I_3, string | null>; /** * @description * This makes the string optional (allows `undefined`). If the string is empty, `undefined` is returned. * * @see `TextDecoder.nullable` * @see `Decoder.optional` * * @example * ```ts * const decoder = pipe(TextDecoder.string, TextDecoder.optional) * * expect(pipe("Hello", Decoder.validate(decoder), Result.get)).toBe("Hello") * expect(pipe(undefined, Decoder.validate(decoder), Result.get)).toBe(undefined) * expect(pipe("", Decoder.validate(decoder), Result.get)).toBe(undefined) * ``` */ optional: <I_4>(decoder: Decoder<I_4, string>) => Decoder<I_4, string | undefined>; /** * @deprecated Use `EnumDecoder.isIn` instead. * * @description * Check if the string is included in the given values * * @example * ```ts * const decoder = TextDecoder.oneOf(['todo', 'in-progress', 'done', 'archived'] as const) * * expect(pipe("todo", Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe("unknown", Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ oneOf: typeof oneOf; /** * @deprecated Use `EnumDecoder.literal` instead. * * @description * Check if the string is included in the given values * * @example * ```ts * const decoder = TextDecoder.equals('ongoing') * * expect(pipe("todo", Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe("unknown", Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ equals: <T_1 extends string>(value: T_1) => Decoder<unknown, T_1>; /** * @description * Escapes the HTML in the string. * * @example * ```ts * const decoder = pipe( * TextDecoder.string, * TextDecoder.trim, * TextDecoder.htmlEscape, * TextDecoder.between(1, 2000) * ) * * const escaped = pipe( * "<script>window.alert("Hello")</script>", * Decoder.validate(decoder), * Result.get * ) * * expect(escaped).toBe('&lt;script&gt;window.alert(&quot;Hello&quot;)&lt;/script&gt;') * ``` */ htmlEscape: <I_1>(decoder: Decoder<I_1, string>) => Decoder<I_1, string>; }; declare type NumberDecoder<I> = Decoder<I, number>; /** * @namespace NumberDecoder * * @description * This namespace contains number decoders and additional utilities for number validations. */ declare const NumberDecoder: { /** * @description * Check if the input is a number. * If the number is NaN, the number will fail validation. * * This function is strict and does not autocast the input into a number. */ strict: NumberDecoder<unknown>; /** * @description * Check if the input is a number. * If the number is NaN, the number will fail validation. * * This function will autocast the input into a number if possible. */ number: NumberDecoder<unknown>; /** * @description * Check if the number is equal or greater to the given minimum * * @example * ```ts * const decoder = pipe( * NumberDecoder.number, * NumberDecoder.min(1) * ) * * expect(pipe(1, Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe(0, Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ min: (minimum: number) => <I>(value: Decoder<I, number>) => Decoder<I, number>; /** * @description * Check if the number is equal or greater to the given minimum * * @example * ```ts * const decoder = pipe( * NumberDecoder.number, * NumberDecoder.max(100) * ) * * expect(pipe(100, Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe(101, Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ max: (maximum: number) => <I>(value: Decoder<I, number>) => Decoder<I, number>; /** * @description * Check if the number is between to the given minimum and maximum * * @example * ```ts * const decoder = pipe( * NumberDecoder.number, * NumberDecoder.between(1, 100) * ) * * expect(pipe(1, Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe(100, Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe(0, Decoder.validate(decoder), Result.isKo)).toBe(true) * expect(pipe(101, Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ between: (minimum: number, maximum: number) => <I>(value: Decoder<I, number>) => Decoder<I, number>; /** * @description * Check if the input is a number between the given minimum and maximum. * * @example * ```ts * // The following: * const decoder = NumberDecoder.range(1, 100) * // is the same as: * const decoder = pipe(NumberDecoder.number, NumberDecoder.between(1, 100)) * ``` */ range: (minimum: number, maximum: number) => Decoder<unknown, number>; /** * @description * Decodes an input from a string into a number. * * @example * ``` * expect(pipe('10.13', Decoder.validate(NumberDecoder.fromString), Result.get)).toBe(10.13) * expect(pipe('Hello', Decoder.validate(NumberDecoder.fromString), Result.isKo)).toBe(true) * ``` */ fromString: Decoder<unknown, number>; }; declare type IntegerDecoder<I> = Decoder<I, Int>; /** * @namespace IntegerDecoder * * @description * This namespace contains integer decoders and additional utilities for integer validations. */ declare const IntegerDecoder: { /** * @description * Check if the input is an integer. * If the integer is NaN or contains decimals, the number will fail validation. * * This function is strict and does not autocast the input into a number. */ strict: IntegerDecoder<unknown>; /** * @description * Check if the input is an integer. * If the integer is NaN or contains decimals, the number will fail validation. * * This function will autocast the input into a number if possible. */ int: IntegerDecoder<unknown>; /** * @description * Check if the number is equal or greater to the given minimum * * @example * ```ts * const decoder = pipe( * IntegerDecoder.number, * IntegerDecoder.min(1) * ) * * expect(pipe(1, Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe(0, Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ min: (minimum: number) => <I>(decoder: IntegerDecoder<I>) => IntegerDecoder<I>; /** * @description * Check if the number is equal or greater to the given minimum * * @example * ```ts * const decoder = pipe( * IntegerDecoder.number, * IntegerDecoder.max(100) * ) * * expect(pipe(100, Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe(101, Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ max: (maximum: number) => <I_1>(decoder: IntegerDecoder<I_1>) => IntegerDecoder<I_1>; /** * @description * Check if the number is between to the given minimum and maximum * * @example * ```ts * const decoder = pipe( * IntegerDecoder.number, * IntegerDecoder.between(1, 100) * ) * * expect(pipe(1, Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe(100, Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe(0, Decoder.validate(decoder), Result.isKo)).toBe(true) * expect(pipe(101, Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ between: (minimum: number, maximum: number) => <I_2>(decoder: IntegerDecoder<I_2>) => IntegerDecoder<I_2>; /** * @description * Check if the input is a number between the given minimum and maximum. * * @example * ```ts * // The following: * const decoder = IntegerDecoder.range(1, 100) * // is the same as: * const decoder = pipe(IntegerDecoder.number, IntegerDecoder.between(1, 100)) * ``` */ range: (minimum: number, maximum: number) => IntegerDecoder<unknown>; /** * @description * Check if the input is a positive integer. * If the integer is NaN or contains decimals, the number will fail validation. */ positive: IntegerDecoder<unknown>; /** * @description * Decodes an input from a string into an integer. * * @example * ``` * expect(pipe('10', Decoder.validate(IntegerDecoder.fromString), Result.get)).toBe(10) * expect(pipe('Hello', Decoder.validate(IntegerDecoder.fromString), Result.isKo)).toBe(true) * ``` */ fromString: Decoder<unknown, Int>; }; declare type BooleanDecoder<I> = Decoder<I, boolean>; /** * @namespace BooleanDecoder * * @description * This namespace contains boolean decoders and additional utilities for boolean validations. */ declare const BooleanDecoder: { /** * @description * Check if the input is a boolean. * * This function is strict and does not autocast the input into a boolean. */ strict: BooleanDecoder<unknown>; /** * @description * Check if the input is a boolean. * * This function will autocast the input into a boolean if possible. */ boolean: BooleanDecoder<unknown>; /** * @description * Check if the boolean is true or false * * @example * ``` * const decoder = BooleanDecoder.equals(true) * * expect(pipe(true, Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe(false, Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ equals: <T extends boolean>(bool: T) => Decoder<unknown, T>; /** * @description * Decodes an input from a string into a boolean: * - 'true', 'yes', 'y', '1' becomes true * - 'false', 'no', 'n', '0' becomes false * * Any other value will fail validation. * * @example * ``` * expect(pipe('yes', Decoder.validate(BooleanDecoder.fromString), Result.get)).toBe(true) * ``` */ fromString: Decoder<unknown, boolean>; /** * @description * Decodes an input from a number into a boolean: * - 1 becomes true * - 0 becomes false * * Any other value will fail validation. * * @example * ``` * expect(pipe(1, Decoder.validate(BooleanDecoder.fromNumber), Result.get)).toBe(true) * ``` */ fromNumber: Decoder<unknown, boolean>; }; declare type Struct<A extends Dict<unknown>> = { [P in keyof A]-?: Decoder<unknown, A[P]>; }; declare function omit<I, O extends Dict, B extends keyof O>(props: B[]): (decoder: ObjectDecoder<I, O>) => ObjectDecoder<I, Omit<O, B>>; declare function pick<I, O extends Dict, B extends keyof O>(props: B[]): (decoder: ObjectDecoder<I, O>) => ObjectDecoder<I, Pick<O, B>>; declare function partial<I, O extends Dict>(decoder: ObjectDecoder<I, O>): ObjectDecoder<I, Partial<O>>; declare function merge<I, O1 extends Dict>(a: ObjectDecoder<I, O1>): ObjectDecoder<I, O1>; declare function merge<I, O1 extends Dict, O2 extends Dict>(a: ObjectDecoder<I, O1>, b: ObjectDecoder<I, O2>): ObjectDecoder<I, O2 & Omit<O1, keyof O2>>; declare function merge<I, O1 extends Dict, O2 extends Dict, O3 extends Dict>(a: ObjectDecoder<I, O1>, b: ObjectDecoder<I, O2>, c: ObjectDecoder<I, O3>): ObjectDecoder<I, O3 & Omit<O2, keyof O3> & Omit<O1, keyof O3 | keyof O2>>; declare function merge<I, O1 extends Dict, O2 extends Dict, O3 extends Dict, O4 extends Dict>(a: ObjectDecoder<I, O1>, b: ObjectDecoder<I, O2>, c: ObjectDecoder<I, O3>, d: ObjectDecoder<I, O4>): ObjectDecoder<I, O4 & Omit<O3, keyof O4> & Omit<O2, keyof O4 | keyof O3> & Omit<O1, keyof O4 | keyof O3 | keyof O2>>; declare type SumTypes<K extends string, T extends Dict> = { [P in keyof T]: T[P] extends ObjectDecoder<any, infer A> ? { [KP in K]: P; } & A : never; }[keyof T]; declare function sum<K extends string, I, T extends Dict<ObjectDecoder<I, any>>>(prop: K, cases: T): Decoder<I, SumTypes<K, T>>; declare type ObjectDecoder<I, O extends Dict> = Decoder<I, O> & { props: Dict; }; /** * @namespace ObjectDecoder * * @description * This namespace contains object decoders and additional utilities for object validations. */ declare const ObjectDecoder: { /** * @description * Check if the input is an object / record. * This function does not check the type of the properties. */ unknownDict: Decoder<unknown, Dict<unknown>>; /** * @description * Check if the input is an record, where all properties are of the given type. */ dict: <A>(decoder: Decoder<unknown, A>) => Decoder<unknown, Dict<A>>; /** * @description * Check if the input is an object, where all object properties match the given decoders. * All extraenous properties will be skipped and ignored. * * @example * ```ts * const TodoDto = ObjectDecoder.struct({ * id: IntegerDecoder.positive, * title: TextDecoder.varchar(1, 100), * done: BooleanDecoder.boolean * }) * * const input: unknown = { * id: 0, * title: 'Wake up', * description: 'Some description', // This property is not recognized by the decoder and will be ignored. * done: false * } * * expect(pipe(input, Decoder.validate(TodoDto), Result.isOk)).toBe(true) * expect(pipe(input, Decoder.validate(TodoDto), Result.get)).toEqual({ * id: 0, * title: 'Wake up', * done: false * }) * ``` */ struct: <A_1 extends Dict<any>>(props: Struct<A_1>, name?: string) => ObjectDecoder<unknown, A_1>; /** * @description * Omit given properties from an `ObjectDecoder`. * The resulting `ObjectDecoder` will not contain the omitted properties. * * @example * ```ts * const TodoPostDto = pipe(TodoDto, ObjectDecoder.omit(['id'])) * * const input: unknown = { * id: 0, // This property has been omitted and will be ignored. * title: 'Wake up', * done: false * } * * expect(pipe(input, Decoder.validate(TodoPostDto), Result.isOk)).toBe(true) * expect(pipe(input, Decoder.validate(TodoPostDto), Result.get)).toEqual({ * title: 'Wake up', * done: false * }) * ``` */ omit: typeof omit; /** * @description * Pick given properties from an `ObjectDecoder`. * The resulting `ObjectDecoder` will only contain the picked properties. * * @example * ```ts * const TodoPostDto = pipe(TodoDto, ObjectDecoder.pick(['title', 'done'])) * * const input: unknown = { * id: 0, // This property has not been picked and will be ignored. * title: 'Wake up', * done: false * } * * expect(pipe(input, Decoder.validate(TodoPostDto), Result.isOk)).toBe(true) * expect(pipe(input, Decoder.validate(TodoPostDto), Result.get)).toEqual({ * title: 'Wake up', * done: false * }) * ``` */ pick: typeof pick; /** * @description * Make all properties of an `ObjectDecoder` optional. */ partial: typeof partial; /** * @description * Add a custom validation function to the `ObjectDecoder`. * * @example * ```ts * const SignupDto = pipe( * ObjectDecoder.struct({ * email: TextDecoder.email, * password: TextDecoder.varchar(5, 50), * passwordRepeat: TextDecoder.varchar(5, 50) * }), * ObjectDecoder.guard(user => { * return user.password === user.passwordRepeat * ? undefined * : DecodeError.object([ * DecodeError.key('passwordRepeat', DecodeError.value(user.passwordRepeat, `Password does not match password confirmation`)) * ]) * }) * ) * * const input: unknown = { * email: 'test@example.com', * password: '12345', * passwordRepeat: '12345' * } * * expect(pipe(input, Decoder.validate(SignupDto), Result.isKo)).toBe(true) * ``` */ guard: <I, O extends Dict<any>>(fn: (input: O) => Option<DecodeError.Value | DecodeError.ObjectLike>) => (decoder: ObjectDecoder<I, O>) => ObjectDecoder<I, O>; /** * @description * Merge multiple `ObjectDecoder`s. * If a property has already been declared, the decoder for this property will be overwritten. * * @example * ```ts * const A = ObjectDecoder.struct({ * a: TextDecoder.string, * ab: TextDecoder.string * }) * const B = ObjectDecoder.struct({ * ab: NumberDecoder.number, * b: NumberDecoder.number * }) * const C = ObjectDecoder.merge(A, B) * * interface C extends Decoder.TypeOf<typeof C> {} * * // Interface C is now equals to: * type C = { * a: string * ab: number * b: number * } * * // Note that "ab: TextDecoder.string" has been overwritten and will not be executed * ``` */ merge: typeof merge; /** * @description * By default, `ObjectDecoder.struct` will skip and ignore extra properties. * If you wish to keep extra properties (all properties not validated by the struct), you can use this util. * * @example * ```ts * const Response = pipe( * ObjectDecoder.struct({ * status: EnumDecoder.literal('OK', 'KO'), * message: TextDecoder.string, * }), * ObjectDecoder.additionalProperties * ) * ``` */ additionalProperties: <I_1, O_1 extends Dict<any>>(decoder: ObjectDecoder<I_1, O_1>) => Decoder<I_1, O_1 & Dict<any>>; /** * @description * Execute a specific decoder depending on the value of a given "type" property. * * @example * ```ts * const Geom = ObjectDecoder.sum('type', { * Circle: struct({ * radius: NumberDecoder.number * }), * Rectangle: struct({ * width: NumberDecoder.number, * height: NumberDecoder.number * }) * }) * * type Geom = Decoder.TypeOf<typeof Geom> * const geom: Geom = { * type: 'Rectangle', * height: 5 * } * ``` */ sum: typeof sum; }; declare type ArrayDecoder<I, O extends any[]> = Decoder<I, O>; /** * @namespace ArrayDecoder * * @description * This namespace contains array decoders and additional utilities for array validations. */ declare const ArrayDecoder: { /** * @description * Check if the input is an array. */ unknownArray: Decoder<unknown, unknown[]>; /** * @description * Check if the input is an array of a given type. */ array: <A>(decoder: Decoder<unknown, A>) => ArrayDecoder<unknown, A[]>; /** * @description * Check if the input is a non empty array of a given type. */ nonEmptyArray: <O>(decoder: Decoder<unknown, O>) => Decoder<unknown, _apoyo_std.NonEmptyArray<O>>; /** * @description * Check the length of the string */ length: (len: number) => <D extends ArrayDecoder<any, any>>(value: D) => D; /** * @description * Check the minimum length of the array * * @example * ```ts * const decoder = pipe( * ArrayDecoder.array(NumberDecoder.number), * ArrayDecoder.min(1) * ) * * expect(pipe([1], Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe([], Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ min: (minLength: number) => <D_1 extends ArrayDecoder<any, any>>(value: D_1) => D_1; /** * @description * Check the maximum length of the array * * @example * ```ts * const decoder = pipe( * ArrayDecoder.array(NumberDecoder.number), * ArrayDecoder.max(5) * ) * * expect(pipe([1,2,3,4,5], Decoder.validate(decoder), Result.isOk)).toBe(true) * expect(pipe([1,2,3,4,5,6], Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ max: (maxLength: number) => <D_2 extends ArrayDecoder<any, any>>(value: D_2) => D_2; /** * @description * Check both the minimum and maximum length of the array * * @example * ```ts * const decoder = pipe( * ArrayDecoder.array(NumberDecoder.number), * ArrayDecoder.between(1, 5) * ) * * expect(pipe([], Decoder.validate(decoder), Result.isKo)).toBe(true) * ``` */ between: (minLength: number, maxLength: number) => <D_1 extends ArrayDecoder<any, any>>(value: D_1) => D_1; }; declare type DateDecoder<I, O extends Date> = Decoder<I, O>; /** * @namespace DateDecoder * * @description * This namespace contains date decoders and additional utilities for date validations. */ declare const DateDecoder: { /** * @description * Check if the input is a date. * * @example * ```ts * expect(pipe('2021-01-01', Decoder.validate(DateDecoder.date), Result.isOk)).toBe(true) * expect(pipe('2021-01-01 12:00:00', Decoder.validate(DateDecoder.date), Result.isKo)).toBe(true) * ``` */ date: Decoder<unknown, Date>; /** * @description * Check if the input is a datetime. * @example * ```ts * expect(pipe('2021-01-01', Decoder.validate(DateDecoder.datetime), Result.isOk)).toBe(true) * expect(pipe('2021-01-01 12:00:00', Decoder.validate(DateDecoder.datetime), Result.isOk)).toBe(true) * expect(pipe('2021-01-01 12:00:00Z', Decoder.validate(DateDecoder.datetime), Result.isOk)).toBe(true) * ``` */ datetime: Decoder<unknown, Date>; /** * @description * Check if the input is a valid `Date` object. * * @example * ```ts * expect(pipe(new Date('2021-01-01'), Decoder.validate(DateDecoder.strict), Result.isOk)).toBe(true) * expect(pipe('2021-01-01', Decoder.validate(DateDecoder.strict), Result.isKo)).toBe(true) * ``` */ strict: Decoder<unknown, Date>; /** * @description * Check if the input can be cast to a valid `Date` object. * * expect(pipe(new Date('2021-01-01'), Decoder.validate(DateDecoder.native), Result.isOk)).toBe(true) * expect(pipe('2021-01-01', Decoder.validate(DateDecoder.native), Result.isOk)).toBe(true) * expect(pipe('2021-01-01 12:00:00Z', Decoder.validate(DateDecoder.native), Result.isOk)).toBe(true) */ native: Decoder<unknown, Date>; /** * @description * Check if the date is above a specific date * * @example * ```ts * // The date needs to be above the current date * const futureDate = pipe( * Decoder.date, * Decoder.min(() => { * const today = new Date().toISOString().split('T')[0] * return new Date(today) * }) * ) * ``` */ min: (minDate: Date | (() => Date)) => <A>(decoder: Decoder<A, Date>) => Decoder<A,