UNPKG

effect

Version:

The missing standard library for TypeScript, for writing production-grade software.

1,559 lines (1,471 loc) 67.3 kB
/** * @since 3.10.0 */ import * as Arr from "./Array.js" import * as Cause from "./Cause.js" import { TaggedError } from "./Data.js" import * as Effect from "./Effect.js" import * as Either from "./Either.js" import * as Exit from "./Exit.js" import type { LazyArg } from "./Function.js" import { dual } from "./Function.js" import { globalValue } from "./GlobalValue.js" import * as Inspectable from "./Inspectable.js" import * as util_ from "./internal/schema/util.js" import * as Option from "./Option.js" import * as Predicate from "./Predicate.js" import * as Scheduler from "./Scheduler.js" import type * as Schema from "./Schema.js" import * as AST from "./SchemaAST.js" import type { Concurrency } from "./Types.js" /** * `ParseIssue` is a type that represents the different types of errors that can occur when decoding/encoding a value. * * @category model * @since 3.10.0 */ export type ParseIssue = // leaf | Type | Missing | Unexpected | Forbidden // composite | Pointer | Refinement | Transformation | Composite /** * @category model * @since 3.10.0 */ export type SingleOrNonEmpty<A> = A | Arr.NonEmptyReadonlyArray<A> /** * @category model * @since 3.10.0 */ export type Path = SingleOrNonEmpty<PropertyKey> /** * @category model * @since 3.10.0 */ export class Pointer { /** * @since 3.10.0 */ readonly _tag = "Pointer" constructor( readonly path: Path, readonly actual: unknown, readonly issue: ParseIssue ) {} } /** * Error that occurs when an unexpected key or index is present. * * @category model * @since 3.10.0 */ export class Unexpected { /** * @since 3.10.0 */ readonly _tag = "Unexpected" constructor( readonly actual: unknown, /** * @since 3.10.0 */ readonly message?: string ) {} } /** * Error that occurs when a required key or index is missing. * * @category model * @since 3.10.0 */ export class Missing { /** * @since 3.10.0 */ readonly _tag = "Missing" /** * @since 3.10.0 */ readonly actual = undefined constructor( /** * @since 3.10.0 */ readonly ast: AST.Type, /** * @since 3.10.0 */ readonly message?: string ) {} } /** * Error that contains multiple issues. * * @category model * @since 3.10.0 */ export class Composite { /** * @since 3.10.0 */ readonly _tag = "Composite" constructor( readonly ast: AST.AST, readonly actual: unknown, readonly issues: SingleOrNonEmpty<ParseIssue>, readonly output?: unknown ) {} } /** * Error that occurs when a refinement has an error. * * @category model * @since 3.10.0 */ export class Refinement { /** * @since 3.10.0 */ readonly _tag = "Refinement" constructor( readonly ast: AST.Refinement, readonly actual: unknown, readonly kind: "From" | "Predicate", readonly issue: ParseIssue ) {} } /** * Error that occurs when a transformation has an error. * * @category model * @since 3.10.0 */ export class Transformation { /** * @since 3.10.0 */ readonly _tag = "Transformation" constructor( readonly ast: AST.Transformation, readonly actual: unknown, readonly kind: "Encoded" | "Transformation" | "Type", readonly issue: ParseIssue ) {} } /** * The `Type` variant of the `ParseIssue` type represents an error that occurs when the `actual` value is not of the expected type. * The `ast` field specifies the expected type, and the `actual` field contains the value that caused the error. * * @category model * @since 3.10.0 */ export class Type { /** * @since 3.10.0 */ readonly _tag = "Type" constructor( readonly ast: AST.AST, readonly actual: unknown, readonly message?: string ) {} } /** * The `Forbidden` variant of the `ParseIssue` type represents a forbidden operation, such as when encountering an Effect that is not allowed to execute (e.g., using `runSync`). * * @category model * @since 3.10.0 */ export class Forbidden { /** * @since 3.10.0 */ readonly _tag = "Forbidden" constructor( readonly ast: AST.AST, readonly actual: unknown, readonly message?: string ) {} } /** * @category type id * @since 3.10.0 */ export const ParseErrorTypeId: unique symbol = Symbol.for("effect/Schema/ParseErrorTypeId") /** * @category type id * @since 3.10.0 */ export type ParseErrorTypeId = typeof ParseErrorTypeId /** * @since 3.10.0 */ export const isParseError = (u: unknown): u is ParseError => Predicate.hasProperty(u, ParseErrorTypeId) /** * @since 3.10.0 */ export class ParseError extends TaggedError("ParseError")<{ readonly issue: ParseIssue }> { /** * @since 3.10.0 */ readonly [ParseErrorTypeId] = ParseErrorTypeId get message() { return this.toString() } /** * @since 3.10.0 */ toString() { return TreeFormatter.formatIssueSync(this.issue) } /** * @since 3.10.0 */ toJSON() { return { _id: "ParseError", message: this.toString() } } /** * @since 3.10.0 */ [Inspectable.NodeInspectSymbol]() { return this.toJSON() } } /** * @category constructors * @since 3.10.0 */ export const parseError = (issue: ParseIssue): ParseError => new ParseError({ issue }) /** * @category constructors * @since 3.10.0 */ export const succeed: <A>(a: A) => Either.Either<A, ParseIssue> = Either.right /** * @category constructors * @since 3.10.0 */ export const fail: (issue: ParseIssue) => Either.Either<never, ParseIssue> = Either.left const _try: <A>(options: { try: LazyArg<A> catch: (e: unknown) => ParseIssue }) => Either.Either<A, ParseIssue> = Either.try export { /** * @category constructors * @since 3.10.0 */ _try as try } /** * @category constructors * @since 3.10.0 */ export const fromOption: { /** * @category constructors * @since 3.10.0 */ (onNone: () => ParseIssue): <A>(self: Option.Option<A>) => Either.Either<A, ParseIssue> /** * @category constructors * @since 3.10.0 */ <A>(self: Option.Option<A>, onNone: () => ParseIssue): Either.Either<A, ParseIssue> } = Either.fromOption const isEither: <A, E, R>(self: Effect.Effect<A, E, R>) => self is Either.Either<A, E> = Either.isEither as any /** * @category optimisation * @since 3.10.0 */ export const flatMap: { /** * @category optimisation * @since 3.10.0 */ <A, B, E1, R1>(f: (a: A) => Effect.Effect<B, E1, R1>): <E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<B, E1 | E, R1 | R> /** * @category optimisation * @since 3.10.0 */ <A, E, R, B, E1, R1>(self: Effect.Effect<A, E, R>, f: (a: A) => Effect.Effect<B, E1, R1>): Effect.Effect<B, E | E1, R | R1> } = dual(2, <A, E, R, B, E1, R1>( self: Effect.Effect<A, E, R>, f: (a: A) => Effect.Effect<B, E1, R1> ): Effect.Effect<B, E | E1, R | R1> => { return isEither(self) ? Either.match(self, { onLeft: Either.left, onRight: f }) : Effect.flatMap(self, f) }) /** * @category optimisation * @since 3.10.0 */ export const map: { /** * @category optimisation * @since 3.10.0 */ <A, B>(f: (a: A) => B): <E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<B, E, R> /** * @category optimisation * @since 3.10.0 */ <A, E, R, B>(self: Effect.Effect<A, E, R>, f: (a: A) => B): Effect.Effect<B, E, R> } = dual(2, <A, E, R, B>(self: Effect.Effect<A, E, R>, f: (a: A) => B): Effect.Effect<B, E, R> => { return isEither(self) ? Either.map(self, f) : Effect.map(self, f) }) /** * @category optimisation * @since 3.10.0 */ export const mapError: { /** * @category optimisation * @since 3.10.0 */ <E, E2>(f: (e: E) => E2): <A, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E2, R> /** * @category optimisation * @since 3.10.0 */ <A, E, R, E2>(self: Effect.Effect<A, E, R>, f: (e: E) => E2): Effect.Effect<A, E2, R> } = dual(2, <A, E, R, E2>(self: Effect.Effect<A, E, R>, f: (e: E) => E2): Effect.Effect<A, E2, R> => { return isEither(self) ? Either.mapLeft(self, f) : Effect.mapError(self, f) }) // TODO(4.0): remove /** * @category optimisation * @since 3.10.0 */ export const eitherOrUndefined = <A, E, R>( self: Effect.Effect<A, E, R> ): Either.Either<A, E> | undefined => { if (isEither(self)) { return self } } /** * @category optimisation * @since 3.10.0 */ export const mapBoth: { /** * @category optimisation * @since 3.10.0 */ <E, E2, A, A2>( options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } ): <R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A2, E2, R> /** * @category optimisation * @since 3.10.0 */ <A, E, R, E2, A2>( self: Effect.Effect<A, E, R>, options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } ): Effect.Effect<A2, E2, R> } = dual(2, <A, E, R, E2, A2>( self: Effect.Effect<A, E, R>, options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } ): Effect.Effect<A2, E2, R> => { return isEither(self) ? Either.mapBoth(self, { onLeft: options.onFailure, onRight: options.onSuccess }) : Effect.mapBoth(self, options) }) /** * @category optimisation * @since 3.10.0 */ export const orElse: { /** * @category optimisation * @since 3.10.0 */ <E, A2, E2, R2>(f: (e: E) => Effect.Effect<A2, E2, R2>): <A, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A2 | A, E2, R2 | R> /** * @category optimisation * @since 3.10.0 */ <A, E, R, A2, E2, R2>(self: Effect.Effect<A, E, R>, f: (e: E) => Effect.Effect<A2, E2, R2>): Effect.Effect<A2 | A, E2, R2 | R> } = dual(2, <A, E, R, A2, E2, R2>( self: Effect.Effect<A, E, R>, f: (e: E) => Effect.Effect<A2, E2, R2> ): Effect.Effect<A2 | A, E2, R2 | R> => { return isEither(self) ? Either.match(self, { onLeft: f, onRight: Either.right }) : Effect.catchAll(self, f) }) /** * @since 3.10.0 */ export type DecodeUnknown<Out, R> = (u: unknown, options?: AST.ParseOptions) => Effect.Effect<Out, ParseIssue, R> /** * @since 3.10.0 */ export type DeclarationDecodeUnknown<Out, R> = ( u: unknown, options: AST.ParseOptions, ast: AST.Declaration ) => Effect.Effect<Out, ParseIssue, R> /** @internal */ export const mergeInternalOptions = ( options: InternalOptions | undefined, overrideOptions: InternalOptions | number | undefined ): InternalOptions | undefined => { if (overrideOptions === undefined || Predicate.isNumber(overrideOptions)) { return options } if (options === undefined) { return overrideOptions } return { ...options, ...overrideOptions } } const getEither = (ast: AST.AST, isDecoding: boolean, options?: AST.ParseOptions) => { const parser = goMemo(ast, isDecoding) return (u: unknown, overrideOptions?: AST.ParseOptions): Either.Either<any, ParseIssue> => parser(u, mergeInternalOptions(options, overrideOptions)) as any } const getSync = (ast: AST.AST, isDecoding: boolean, options?: AST.ParseOptions) => { const parser = getEither(ast, isDecoding, options) return (input: unknown, overrideOptions?: AST.ParseOptions) => Either.getOrThrowWith(parser(input, overrideOptions), parseError) } /** @internal */ export const getOption = (ast: AST.AST, isDecoding: boolean, options?: AST.ParseOptions) => { const parser = getEither(ast, isDecoding, options) return (input: unknown, overrideOptions?: AST.ParseOptions): Option.Option<any> => Option.getRight(parser(input, overrideOptions)) } const getEffect = <R>(ast: AST.AST, isDecoding: boolean, options?: AST.ParseOptions) => { const parser = goMemo(ast, isDecoding) return (input: unknown, overrideOptions?: AST.ParseOptions): Effect.Effect<any, ParseIssue, R> => parser(input, { ...mergeInternalOptions(options, overrideOptions), isEffectAllowed: true }) } /** * @throws `ParseError` * @category decoding * @since 3.10.0 */ export const decodeUnknownSync = <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ): (u: unknown, overrideOptions?: AST.ParseOptions) => A => getSync(schema.ast, true, options) /** * @category decoding * @since 3.10.0 */ export const decodeUnknownOption = <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ): (u: unknown, overrideOptions?: AST.ParseOptions) => Option.Option<A> => getOption(schema.ast, true, options) /** * @category decoding * @since 3.10.0 */ export const decodeUnknownEither = <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ): (u: unknown, overrideOptions?: AST.ParseOptions) => Either.Either<A, ParseIssue> => getEither(schema.ast, true, options) /** * @category decoding * @since 3.10.0 */ export const decodeUnknownPromise = <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ) => { const parser = decodeUnknown(schema, options) return (u: unknown, overrideOptions?: AST.ParseOptions): Promise<A> => Effect.runPromise(parser(u, overrideOptions)) } /** * @category decoding * @since 3.10.0 */ export const decodeUnknown = <A, I, R>( schema: Schema.Schema<A, I, R>, options?: AST.ParseOptions ): (u: unknown, overrideOptions?: AST.ParseOptions) => Effect.Effect<A, ParseIssue, R> => getEffect(schema.ast, true, options) /** * @throws `ParseError` * @category encoding * @since 3.10.0 */ export const encodeUnknownSync = <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ): (u: unknown, overrideOptions?: AST.ParseOptions) => I => getSync(schema.ast, false, options) /** * @category encoding * @since 3.10.0 */ export const encodeUnknownOption = <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ): (u: unknown, overrideOptions?: AST.ParseOptions) => Option.Option<I> => getOption(schema.ast, false, options) /** * @category encoding * @since 3.10.0 */ export const encodeUnknownEither = <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ): (u: unknown, overrideOptions?: AST.ParseOptions) => Either.Either<I, ParseIssue> => getEither(schema.ast, false, options) /** * @category encoding * @since 3.10.0 */ export const encodeUnknownPromise = <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ) => { const parser = encodeUnknown(schema, options) return (u: unknown, overrideOptions?: AST.ParseOptions): Promise<I> => Effect.runPromise(parser(u, overrideOptions)) } /** * @category encoding * @since 3.10.0 */ export const encodeUnknown = <A, I, R>( schema: Schema.Schema<A, I, R>, options?: AST.ParseOptions ): (u: unknown, overrideOptions?: AST.ParseOptions) => Effect.Effect<I, ParseIssue, R> => getEffect(schema.ast, false, options) /** * @category decoding * @since 3.10.0 */ export const decodeSync: <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ) => (i: I, overrideOptions?: AST.ParseOptions) => A = decodeUnknownSync /** * @category decoding * @since 3.10.0 */ export const decodeOption: <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ) => (i: I, overrideOptions?: AST.ParseOptions) => Option.Option<A> = decodeUnknownOption /** * @category decoding * @since 3.10.0 */ export const decodeEither: <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ) => (i: I, overrideOptions?: AST.ParseOptions) => Either.Either<A, ParseIssue> = decodeUnknownEither /** * @category decoding * @since 3.10.0 */ export const decodePromise: <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ) => (i: I, overrideOptions?: AST.ParseOptions) => Promise<A> = decodeUnknownPromise /** * @category decoding * @since 3.10.0 */ export const decode: <A, I, R>( schema: Schema.Schema<A, I, R>, options?: AST.ParseOptions ) => (i: I, overrideOptions?: AST.ParseOptions) => Effect.Effect<A, ParseIssue, R> = decodeUnknown /** * @throws `ParseError` * @category validation * @since 3.10.0 */ export const validateSync = <A, I, R>( schema: Schema.Schema<A, I, R>, options?: AST.ParseOptions ): (u: unknown, overrideOptions?: AST.ParseOptions) => A => getSync(AST.typeAST(schema.ast), true, options) /** * @category validation * @since 3.10.0 */ export const validateOption = <A, I, R>( schema: Schema.Schema<A, I, R>, options?: AST.ParseOptions ): (u: unknown, overrideOptions?: AST.ParseOptions) => Option.Option<A> => getOption(AST.typeAST(schema.ast), true, options) /** * @category validation * @since 3.10.0 */ export const validateEither = <A, I, R>( schema: Schema.Schema<A, I, R>, options?: AST.ParseOptions ): (u: unknown, overrideOptions?: AST.ParseOptions) => Either.Either<A, ParseIssue> => getEither(AST.typeAST(schema.ast), true, options) /** * @category validation * @since 3.10.0 */ export const validatePromise = <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ) => { const parser = validate(schema, options) return (u: unknown, overrideOptions?: AST.ParseOptions): Promise<A> => Effect.runPromise(parser(u, overrideOptions)) } /** * @category validation * @since 3.10.0 */ export const validate = <A, I, R>( schema: Schema.Schema<A, I, R>, options?: AST.ParseOptions ): (a: unknown, overrideOptions?: AST.ParseOptions) => Effect.Effect<A, ParseIssue, R> => getEffect(AST.typeAST(schema.ast), true, options) /** * By default the option `exact` is set to `true`. * * @category validation * @since 3.10.0 */ export const is = <A, I, R>(schema: Schema.Schema<A, I, R>, options?: AST.ParseOptions) => { const parser = goMemo(AST.typeAST(schema.ast), true) return (u: unknown, overrideOptions?: AST.ParseOptions | number): u is A => Either.isRight(parser(u, { exact: true, ...mergeInternalOptions(options, overrideOptions) }) as any) } /** * By default the option `exact` is set to `true`. * * @throws `ParseError` * @category validation * @since 3.10.0 */ export const asserts = <A, I, R>(schema: Schema.Schema<A, I, R>, options?: AST.ParseOptions) => { const parser = goMemo(AST.typeAST(schema.ast), true) return (u: unknown, overrideOptions?: AST.ParseOptions): asserts u is A => { const result: Either.Either<any, ParseIssue> = parser(u, { exact: true, ...mergeInternalOptions(options, overrideOptions) }) as any if (Either.isLeft(result)) { throw parseError(result.left) } } } /** * @category encoding * @since 3.10.0 */ export const encodeSync: <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ) => (a: A, overrideOptions?: AST.ParseOptions) => I = encodeUnknownSync /** * @category encoding * @since 3.10.0 */ export const encodeOption: <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ) => (input: A, overrideOptions?: AST.ParseOptions) => Option.Option<I> = encodeUnknownOption /** * @category encoding * @since 3.10.0 */ export const encodeEither: <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ) => (a: A, overrideOptions?: AST.ParseOptions) => Either.Either<I, ParseIssue> = encodeUnknownEither /** * @category encoding * @since 3.10.0 */ export const encodePromise: <A, I>( schema: Schema.Schema<A, I, never>, options?: AST.ParseOptions ) => (a: A, overrideOptions?: AST.ParseOptions) => Promise<I> = encodeUnknownPromise /** * @category encoding * @since 3.10.0 */ export const encode: <A, I, R>( schema: Schema.Schema<A, I, R>, options?: AST.ParseOptions ) => (a: A, overrideOptions?: AST.ParseOptions) => Effect.Effect<I, ParseIssue, R> = encodeUnknown interface InternalOptions extends AST.ParseOptions { readonly isEffectAllowed?: boolean } interface Parser { (i: any, options?: InternalOptions): Effect.Effect<any, ParseIssue, any> } const decodeMemoMap = globalValue( Symbol.for("effect/ParseResult/decodeMemoMap"), () => new WeakMap<AST.AST, Parser>() ) const encodeMemoMap = globalValue( Symbol.for("effect/ParseResult/encodeMemoMap"), () => new WeakMap<AST.AST, Parser>() ) const goMemo = (ast: AST.AST, isDecoding: boolean): Parser => { const memoMap = isDecoding ? decodeMemoMap : encodeMemoMap const memo = memoMap.get(ast) if (memo) { return memo } const raw = go(ast, isDecoding) const parseOptionsAnnotation = AST.getParseOptionsAnnotation(ast) const parserWithOptions: Parser = Option.isSome(parseOptionsAnnotation) ? (i, options) => raw(i, mergeInternalOptions(options, parseOptionsAnnotation.value)) : raw const decodingFallbackAnnotation = AST.getDecodingFallbackAnnotation(ast) const parser: Parser = isDecoding && Option.isSome(decodingFallbackAnnotation) ? (i, options) => handleForbidden(orElse(parserWithOptions(i, options), decodingFallbackAnnotation.value), ast, i, options) : parserWithOptions memoMap.set(ast, parser) return parser } const getConcurrency = (ast: AST.AST): Concurrency | undefined => Option.getOrUndefined(AST.getConcurrencyAnnotation(ast)) const getBatching = (ast: AST.AST): boolean | "inherit" | undefined => Option.getOrUndefined(AST.getBatchingAnnotation(ast)) const go = (ast: AST.AST, isDecoding: boolean): Parser => { switch (ast._tag) { case "Refinement": { if (isDecoding) { const from = goMemo(ast.from, true) return (i, options) => { options = options ?? AST.defaultParseOption const allErrors = options?.errors === "all" const result = flatMap( orElse(from(i, options), (ef) => { const issue = new Refinement(ast, i, "From", ef) if (allErrors && AST.hasStableFilter(ast) && isComposite(ef)) { return Option.match( ast.filter(i, options, ast), { onNone: () => Either.left<ParseIssue>(issue), onSome: (ep) => Either.left(new Composite(ast, i, [issue, new Refinement(ast, i, "Predicate", ep)])) } ) } return Either.left(issue) }), (a) => Option.match( ast.filter(a, options, ast), { onNone: () => Either.right(a), onSome: (ep) => Either.left(new Refinement(ast, i, "Predicate", ep)) } ) ) return handleForbidden(result, ast, i, options) } } else { const from = goMemo(AST.typeAST(ast), true) const to = goMemo(dropRightRefinement(ast.from), false) return (i, options) => handleForbidden(flatMap(from(i, options), (a) => to(a, options)), ast, i, options) } } case "Transformation": { const transform = getFinalTransformation(ast.transformation, isDecoding) const from = isDecoding ? goMemo(ast.from, true) : goMemo(ast.to, false) const to = isDecoding ? goMemo(ast.to, true) : goMemo(ast.from, false) return (i, options) => handleForbidden( flatMap( mapError( from(i, options), (e) => new Transformation(ast, i, isDecoding ? "Encoded" : "Type", e) ), (a) => flatMap( mapError( transform(a, options ?? AST.defaultParseOption, ast, i), (e) => new Transformation(ast, i, "Transformation", e) ), (i2) => mapError( to(i2, options), (e) => new Transformation(ast, i, isDecoding ? "Type" : "Encoded", e) ) ) ), ast, i, options ) } case "Declaration": { const parse = isDecoding ? ast.decodeUnknown(...ast.typeParameters) : ast.encodeUnknown(...ast.typeParameters) return (i, options) => handleForbidden(parse(i, options ?? AST.defaultParseOption, ast), ast, i, options) } case "Literal": return fromRefinement(ast, (u): u is typeof ast.literal => u === ast.literal) case "UniqueSymbol": return fromRefinement(ast, (u): u is typeof ast.symbol => u === ast.symbol) case "UndefinedKeyword": return fromRefinement(ast, Predicate.isUndefined) case "NeverKeyword": return fromRefinement(ast, Predicate.isNever) case "UnknownKeyword": case "AnyKeyword": case "VoidKeyword": return Either.right case "StringKeyword": return fromRefinement(ast, Predicate.isString) case "NumberKeyword": return fromRefinement(ast, Predicate.isNumber) case "BooleanKeyword": return fromRefinement(ast, Predicate.isBoolean) case "BigIntKeyword": return fromRefinement(ast, Predicate.isBigInt) case "SymbolKeyword": return fromRefinement(ast, Predicate.isSymbol) case "ObjectKeyword": return fromRefinement(ast, Predicate.isObject) case "Enums": return fromRefinement(ast, (u): u is any => ast.enums.some(([_, value]) => value === u)) case "TemplateLiteral": { const regex = AST.getTemplateLiteralRegExp(ast) return fromRefinement(ast, (u): u is any => Predicate.isString(u) && regex.test(u)) } case "TupleType": { const elements = ast.elements.map((e) => goMemo(e.type, isDecoding)) const rest = ast.rest.map((annotatedAST) => goMemo(annotatedAST.type, isDecoding)) let requiredTypes: Array<AST.Type> = ast.elements.filter((e) => !e.isOptional) if (ast.rest.length > 0) { requiredTypes = requiredTypes.concat(ast.rest.slice(1)) } const requiredLen = requiredTypes.length const expectedIndexes = ast.elements.length > 0 ? ast.elements.map((_, i) => i).join(" | ") : "never" const concurrency = getConcurrency(ast) const batching = getBatching(ast) return (input: unknown, options) => { if (!Arr.isArray(input)) { return Either.left(new Type(ast, input)) } const allErrors = options?.errors === "all" const es: Array<[number, ParseIssue]> = [] let stepKey = 0 const output: Array<[number, any]> = [] // --------------------------------------------- // handle missing indexes // --------------------------------------------- const len = input.length for (let i = len; i <= requiredLen - 1; i++) { const e = new Pointer(i, input, new Missing(requiredTypes[i - len])) if (allErrors) { es.push([stepKey++, e]) continue } else { return Either.left(new Composite(ast, input, e, output)) } } // --------------------------------------------- // handle excess indexes // --------------------------------------------- if (ast.rest.length === 0) { for (let i = ast.elements.length; i <= len - 1; i++) { const e = new Pointer(i, input, new Unexpected(input[i], `is unexpected, expected: ${expectedIndexes}`)) if (allErrors) { es.push([stepKey++, e]) continue } else { return Either.left(new Composite(ast, input, e, output)) } } } let i = 0 type State = { es: typeof es output: typeof output } let queue: | Array<(_: State) => Effect.Effect<void, ParseIssue, any>> | undefined = undefined // --------------------------------------------- // handle elements // --------------------------------------------- for (; i < elements.length; i++) { if (len < i + 1) { if (ast.elements[i].isOptional) { // the input element is missing continue } } else { const parser = elements[i] const te = parser(input[i], options) if (isEither(te)) { if (Either.isLeft(te)) { // the input element is present but is not valid const e = new Pointer(i, input, te.left) if (allErrors) { es.push([stepKey++, e]) continue } else { return Either.left(new Composite(ast, input, e, sortByIndex(output))) } } output.push([stepKey++, te.right]) } else { const nk = stepKey++ const index = i if (!queue) { queue = [] } queue.push(({ es, output }: State) => Effect.flatMap(Effect.either(te), (t) => { if (Either.isLeft(t)) { // the input element is present but is not valid const e = new Pointer(index, input, t.left) if (allErrors) { es.push([nk, e]) return Effect.void } else { return Either.left(new Composite(ast, input, e, sortByIndex(output))) } } output.push([nk, t.right]) return Effect.void }) ) } } } // --------------------------------------------- // handle rest element // --------------------------------------------- if (Arr.isNonEmptyReadonlyArray(rest)) { const [head, ...tail] = rest for (; i < len - tail.length; i++) { const te = head(input[i], options) if (isEither(te)) { if (Either.isLeft(te)) { const e = new Pointer(i, input, te.left) if (allErrors) { es.push([stepKey++, e]) continue } else { return Either.left(new Composite(ast, input, e, sortByIndex(output))) } } else { output.push([stepKey++, te.right]) } } else { const nk = stepKey++ const index = i if (!queue) { queue = [] } queue.push( ({ es, output }: State) => Effect.flatMap(Effect.either(te), (t) => { if (Either.isLeft(t)) { const e = new Pointer(index, input, t.left) if (allErrors) { es.push([nk, e]) return Effect.void } else { return Either.left(new Composite(ast, input, e, sortByIndex(output))) } } else { output.push([nk, t.right]) return Effect.void } }) ) } } // --------------------------------------------- // handle post rest elements // --------------------------------------------- for (let j = 0; j < tail.length; j++) { const index = i + j if (len < index + 1) { continue } else { const te = tail[j](input[index], options) if (isEither(te)) { if (Either.isLeft(te)) { // the input element is present but is not valid const e = new Pointer(index, input, te.left) if (allErrors) { es.push([stepKey++, e]) continue } else { return Either.left(new Composite(ast, input, e, sortByIndex(output))) } } output.push([stepKey++, te.right]) } else { const nk = stepKey++ if (!queue) { queue = [] } queue.push( ({ es, output }: State) => Effect.flatMap(Effect.either(te), (t) => { if (Either.isLeft(t)) { // the input element is present but is not valid const e = new Pointer(index, input, t.left) if (allErrors) { es.push([nk, e]) return Effect.void } else { return Either.left(new Composite(ast, input, e, sortByIndex(output))) } } output.push([nk, t.right]) return Effect.void }) ) } } } } // --------------------------------------------- // compute result // --------------------------------------------- const computeResult = ({ es, output }: State) => Arr.isNonEmptyArray(es) ? Either.left(new Composite(ast, input, sortByIndex(es), sortByIndex(output))) : Either.right(sortByIndex(output)) if (queue && queue.length > 0) { const cqueue = queue return Effect.suspend(() => { const state: State = { es: Arr.copy(es), output: Arr.copy(output) } return Effect.flatMap( Effect.forEach(cqueue, (f) => f(state), { concurrency, batching, discard: true }), () => computeResult(state) ) }) } return computeResult({ output, es }) } } case "TypeLiteral": { if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { return fromRefinement(ast, Predicate.isNotNullable) } const propertySignatures: Array<readonly [Parser, AST.PropertySignature]> = [] const expectedKeysMap: Record<PropertyKey, null> = {} const expectedKeys: Array<PropertyKey> = [] for (const ps of ast.propertySignatures) { propertySignatures.push([goMemo(ps.type, isDecoding), ps]) expectedKeysMap[ps.name] = null expectedKeys.push(ps.name) } const indexSignatures = ast.indexSignatures.map((is) => [ goMemo(is.parameter, isDecoding), goMemo(is.type, isDecoding), is.parameter ] as const ) const expectedAST = AST.Union.make( ast.indexSignatures.map((is): AST.AST => is.parameter).concat( expectedKeys.map((key) => Predicate.isSymbol(key) ? new AST.UniqueSymbol(key) : new AST.Literal(key)) ) ) const expected = goMemo(expectedAST, isDecoding) const concurrency = getConcurrency(ast) const batching = getBatching(ast) return (input: unknown, options) => { if (!Predicate.isRecord(input)) { return Either.left(new Type(ast, input)) } const allErrors = options?.errors === "all" const es: Array<[number, ParseIssue]> = [] let stepKey = 0 // --------------------------------------------- // handle excess properties // --------------------------------------------- const onExcessPropertyError = options?.onExcessProperty === "error" const onExcessPropertyPreserve = options?.onExcessProperty === "preserve" const output: Record<PropertyKey, unknown> = {} let inputKeys: Array<PropertyKey> | undefined if (onExcessPropertyError || onExcessPropertyPreserve) { inputKeys = Reflect.ownKeys(input) for (const key of inputKeys) { const te = expected(key, options) if (isEither(te) && Either.isLeft(te)) { // key is unexpected if (onExcessPropertyError) { const e = new Pointer( key, input, new Unexpected(input[key], `is unexpected, expected: ${String(expectedAST)}`) ) if (allErrors) { es.push([stepKey++, e]) continue } else { return Either.left(new Composite(ast, input, e, output)) } } else { // preserve key output[key] = input[key] } } } } // --------------------------------------------- // handle property signatures // --------------------------------------------- type State = { es: typeof es output: typeof output } let queue: | Array<(state: State) => Effect.Effect<void, ParseIssue, any>> | undefined = undefined const isExact = options?.exact === true for (let i = 0; i < propertySignatures.length; i++) { const ps = propertySignatures[i][1] const name = ps.name const hasKey = Object.prototype.hasOwnProperty.call(input, name) if (!hasKey) { if (ps.isOptional) { continue } else if (isExact) { const e = new Pointer(name, input, new Missing(ps)) if (allErrors) { es.push([stepKey++, e]) continue } else { return Either.left(new Composite(ast, input, e, output)) } } } const parser = propertySignatures[i][0] const te = parser(input[name], options) if (isEither(te)) { if (Either.isLeft(te)) { const e = new Pointer(name, input, hasKey ? te.left : new Missing(ps)) if (allErrors) { es.push([stepKey++, e]) continue } else { return Either.left(new Composite(ast, input, e, output)) } } output[name] = te.right } else { const nk = stepKey++ const index = name if (!queue) { queue = [] } queue.push( ({ es, output }: State) => Effect.flatMap(Effect.either(te), (t) => { if (Either.isLeft(t)) { const e = new Pointer(index, input, hasKey ? t.left : new Missing(ps)) if (allErrors) { es.push([nk, e]) return Effect.void } else { return Either.left(new Composite(ast, input, e, output)) } } output[index] = t.right return Effect.void }) ) } } // --------------------------------------------- // handle index signatures // --------------------------------------------- for (let i = 0; i < indexSignatures.length; i++) { const indexSignature = indexSignatures[i] const parameter = indexSignature[0] const type = indexSignature[1] const keys = util_.getKeysForIndexSignature(input, indexSignature[2]) for (const key of keys) { // --------------------------------------------- // handle keys // --------------------------------------------- const keu = parameter(key, options) if (isEither(keu) && Either.isRight(keu)) { // --------------------------------------------- // handle values // --------------------------------------------- const vpr = type(input[key], options) if (isEither(vpr)) { if (Either.isLeft(vpr)) { const e = new Pointer(key, input, vpr.left) if (allErrors) { es.push([stepKey++, e]) continue } else { return Either.left(new Composite(ast, input, e, output)) } } else { if (!Object.prototype.hasOwnProperty.call(expectedKeysMap, key)) { output[key] = vpr.right } } } else { const nk = stepKey++ const index = key if (!queue) { queue = [] } queue.push( ({ es, output }: State) => Effect.flatMap( Effect.either(vpr), (tv) => { if (Either.isLeft(tv)) { const e = new Pointer(index, input, tv.left) if (allErrors) { es.push([nk, e]) return Effect.void } else { return Either.left(new Composite(ast, input, e, output)) } } else { if (!Object.prototype.hasOwnProperty.call(expectedKeysMap, key)) { output[key] = tv.right } return Effect.void } } ) ) } } } } // --------------------------------------------- // compute result // --------------------------------------------- const computeResult = ({ es, output }: State) => { if (Arr.isNonEmptyArray(es)) { return Either.left(new Composite(ast, input, sortByIndex(es), output)) } if (options?.propertyOrder === "original") { // preserve input keys order const keys = inputKeys || Reflect.ownKeys(input) for (const name of expectedKeys) { if (keys.indexOf(name) === -1) { keys.push(name) } } const out: any = {} for (const key of keys) { if (Object.prototype.hasOwnProperty.call(output, key)) { out[key] = output[key] } } return Either.right(out) } return Either.right(output) } if (queue && queue.length > 0) { const cqueue = queue return Effect.suspend(() => { const state: State = { es: Arr.copy(es), output: Object.assign({}, output) } return Effect.flatMap( Effect.forEach(cqueue, (f) => f(state), { concurrency, batching, discard: true }), () => computeResult(state) ) }) } return computeResult({ es, output }) } } case "Union": { const searchTree = getSearchTree(ast.types, isDecoding) const ownKeys = Reflect.ownKeys(searchTree.keys) const ownKeysLen = ownKeys.length const astTypesLen = ast.types.length const map = new Map<any, Parser>() for (let i = 0; i < astTypesLen; i++) { map.set(ast.types[i], goMemo(ast.types[i], isDecoding)) } const concurrency = getConcurrency(ast) ?? 1 const batching = getBatching(ast) return (input, options) => { const es: Array<[number, ParseIssue]> = [] let stepKey = 0 let candidates: Array<AST.AST> = [] if (ownKeysLen > 0) { if (Predicate.isRecordOrArray(input)) { for (let i = 0; i < ownKeysLen; i++) { const name = ownKeys[i] const buckets = searchTree.keys[name].buckets // for each property that should contain a literal, check if the input contains that property if (Object.prototype.hasOwnProperty.call(input, name)) { const literal = String(input[name]) // check that the value obtained from the input for the property corresponds to an existing bucket if (Object.prototype.hasOwnProperty.call(buckets, literal)) { // retrive the minimal set of candidates for decoding candidates = candidates.concat(buckets[literal]) } else { const { candidates, literals } = searchTree.keys[name] const literalsUnion = AST.Union.make(literals) const errorAst = candidates.length === astTypesLen ? new AST.TypeLiteral([new AST.PropertySignature(name, literalsUnion, false, true)], []) : AST.Union.make(candidates) es.push([ stepKey++, new Composite(errorAst, input, new Pointer(name, input, new Type(literalsUnion, input[name]))) ]) } } else { const { candidates, literals } = searchTree.keys[name] const fakePropertySignature = new AST.PropertySignature(name, AST.Union.make(literals), false, true) const errorAst = candidates.length === astTypesLen ? new AST.TypeLiteral([fakePropertySignature], []) : AST.Union.make(candidates) es.push([ stepKey++, new Composite(errorAst, input, new Pointer(name, input, new Missing(fakePropertySignature))) ]) } } } else { const errorAst = searchTree.candidates.length === astTypesLen ? ast : AST.Union.make(searchTree.candidates) es.push([stepKey++, new Type(errorAst, input)]) } } if (searchTree.otherwise.length > 0) { candidates = candidates.concat(searchTree.otherwise) } let queue: | Array<(state: State) => Effect.Effect<unknown, ParseIssue, any>> | undefined = undefined type State = { finalResult?: any es: typeof es } for (let i = 0; i < candidates.length; i++) { const candidate = candidates[i] const pr = map.get(candidate)!(input, options) // the members of a union are ordered based on which one should be decoded first, // therefore if one member has added a task, all subsequent members must // also add a task to the queue even if they are synchronous if (isEither(pr) && (!queue || queue.length === 0)) { if (Either.isRight(pr)) { return pr } else { es.push([stepKey++, pr.left]) } } else { const nk = stepKey++ if (!queue) { queue = [] } queue.push( (state) => Effect.suspend(() => { if ("finalResult" in state) { return Effect.void } else { return Effect.flatMap(Effect.either(pr), (t) => { if (Either.isRight(t)) { state.finalResult = t } else { state.es.push([nk, t.left]) } return Effect.void }) } }) ) } } // --------------------------------------------- // compute result // --------------------------------------------- const computeResult = (es: State["es"]) => Arr.isNonEmptyArray(es) ? es.length === 1 && es[0][1]._tag === "Type" ? Either.left(es[0][1]) : Either.left(new Composite(ast, input, sortByIndex(es))) : // this should never happen Either.left(new Type(ast, input)) if (queue && queue.length > 0) { const cqueue = queue return Effect.suspend(() => { const state: State = { es: Arr.copy(es) } return Effect.flatMap( Effect.forEach(cqueue, (f) => f(state), { concurrency, batching, discard: true }), () => { if ("finalResult" in state) { return state.finalResult } return computeResult(state.es) } ) }) } return computeResult(es) } } case "Suspend": { const get = util_.memoizeThunk(() => goMemo(ast.f(), isDecoding)) return (a, options) => get()(a, options) } } } const fromRefinement = <A>(ast: AST.AST, refinement: (u: unknown) => u is A): Parser => (u) => refinement(u) ? Either.right(u) : Either.left(new Type(ast, u)) /** @internal */ export const getLiterals = ( ast: AST.AST, isDecoding: boolean ): ReadonlyArray<[PropertyKey, AST.Literal]> => { switch (ast._tag) { case "Declaration": { const annotation = AST.getSurrogateAnnotation(ast) if (Option.isSome(annotation)) { return getLiterals(annotation.value, isDecoding) } break } case "TypeLiteral": { const out: Array<[PropertyKey, AST.Literal]> = [] for (let i = 0; i < ast.propertySignatures.length; i++) { const propertySignature = ast.propertySignatures[i] const type = isDecoding ? AST.encodedAST(propertySignature.type) : AST.typeAST(propertySignature.type) if (AST.isLiteral(type) && !propertySignature.isOptional) { out.push([propertySignature.name, type]) } } return out } case "TupleType": { const out: Array<[PropertyKey, AST.Literal]> = [] for (let i = 0; i < ast.elements.length; i++) { const element = ast.elements[i] const type = isDecoding ? AST.encodedAST(element.type) : AST.typeAST(element.type) if (AST.isLiteral(type) && !element.isOptional) { out.