/** * Runs schemas against real values. * * Schema parsers construct values from schema input, check whether a value * matches a schema, decode encoded input, and encode decoded values back to * their external form. This module exposes those operations through several * result styles, including `Effect`, `Promise`, `Exit`, `Option`, `Result`, and * synchronous functions that throw. It also contains the lower-level runner that * walks a schema AST and reports schema failures as `SchemaIssue.Issue` values. * * @since 4.0.0 */ import * as Arr from "./Array.ts" import * as Cause from "./Cause.ts" import * as Effect from "./Effect.ts" import * as Exit from "./Exit.ts" import { memoize } from "./Function.ts" import * as InternalSchemaCause from "./internal/schema/cause.ts" import * as Option from "./Option.ts" import * as Predicate from "./Predicate.ts" import * as Result from "./Result.ts" import type * as Schema from "./Schema.ts" import * as SchemaAST from "./SchemaAST.ts" import * as SchemaIssue from "./SchemaIssue.ts" const recurDefaults = memoize((ast: SchemaAST.AST): SchemaAST.AST => { switch (ast._tag) { case "Declaration": { const getLink = ast.annotations?.[SchemaAST.ClassTypeId] if (Predicate.isFunction(getLink)) { const link = getLink(ast.typeParameters) const to = recurDefaults(link.to) return SchemaAST.replaceEncoding(ast, to === link.to ? [link] : [new SchemaAST.Link(to, link.transformation)]) } return ast } case "Objects": case "Arrays": return ast.recur((ast) => { const defaultValue = ast.context?.defaultValue if (defaultValue) { return SchemaAST.replaceEncoding(recurDefaults(ast), defaultValue) } return recurDefaults(ast) }) case "Suspend": return ast.recur(recurDefaults) default: return ast } }) /** * Creates an effectful maker for the schema's decoded type side. * * **When to use** * * Use to construct decoded schema values in `Effect` while preserving * construction failures as `SchemaIssue.Issue` values in the error channel. * * **Details** * * The returned function accepts constructor input, applies constructor defaults, * runs type-side validation unless checks are disabled, and fails with a * `SchemaIssue.Issue` when construction fails. * * @category constructors * @since 4.0.0 */ export function makeEffect(schema: S) { const ast = recurDefaults(SchemaAST.toType(schema.ast)) const parser = run(ast) return (input: S["~type.make.in"], options?: Schema.MakeOptions): Effect.Effect => { return parser( input, options?.disableChecks ? options?.parseOptions ? { ...options.parseOptions, disableChecks: true } : { disableChecks: true } : options?.parseOptions ) } } /** * Creates a synchronous maker that returns `Option.some` with the constructed * value on success, or `Option.none` when construction fails with schema issues. * * **When to use** * * Use when you need to validate schema constructor input and only care whether * construction succeeds, without exposing `SchemaIssue.Issue` details. * * **Gotchas** * * Only causes made entirely of schema issues are converted to `Option.none`. * Causes that contain defects, interruptions, or asynchronous work at this * synchronous boundary throw an `Error` whose cause is the underlying `Cause`. * * @category constructors * @since 4.0.0 */ export function makeOption(schema: S) { const parser = makeEffect(schema) return (input: S["~type.make.in"], options?: Schema.MakeOptions): Option.Option => { const exit = Effect.runSyncExit(parser(input, options)) if (Exit.isSuccess(exit)) { return Option.some(exit.value) } InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Option adapter can only return none for schema issues") return Option.none() } } /** * Creates a synchronous maker for the schema's decoded type side. * * **When to use** * * Use to construct decoded schema values synchronously when invalid input * should throw an `Error` whose cause is `SchemaIssue.Issue`. * * **Details** * * The returned function constructs a value from constructor input and throws an * `Error` with the `SchemaIssue.Issue` in its `cause` when construction fails. * * **Gotchas** * * Causes that contain defects, interruptions, or asynchronous work at this * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, * instead of being converted to a schema validation error. * * @category constructors * @since 4.0.0 */ export function make(schema: S) { const parser = makeEffect(schema) return (input: S["~type.make.in"], options?: Schema.MakeOptions): S["Type"] => { const exit = Effect.runSyncExit(parser(input, options)) if (Exit.isSuccess(exit)) { return exit.value } const issue = InternalSchemaCause.getSchemaIssueOrThrow( exit.cause, "Constructor adapter can only throw schema issues" ) throw new Error(issue.toString(), { cause: issue }) } } /** * Creates a type guard that checks whether an input satisfies the schema's decoded * type side. * * **When to use** * * Use to build a type guard for checking the decoded side of a schema without * exposing issue details. * * **Details** * * The guard returns `true` on successful validation and `false` when validation * fails only with schema issues, without exposing issue details. * * **Gotchas** * * Only causes made entirely of schema issues are converted to `false`. Causes * that contain defects, interruptions, or asynchronous work at this synchronous * boundary throw an `Error` whose cause is the underlying `Cause`. * * @category Asserting * @since 3.10.0 */ export function is(schema: S): (input: I) => input is I & S["Type"] { return _is(schema.ast) } /** @internal */ export function _is(ast: SchemaAST.AST) { const parser = asExit(run(SchemaAST.toType(ast))) return (input: I): input is I & T => { const exit = parser(input, SchemaAST.defaultParseOptions) if (Exit.isSuccess(exit)) { return true } InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Type guard adapter can only return false for schema issues") return false } } /** @internal */ export function _issue(ast: SchemaAST.AST) { const parser = run(ast) return (input: unknown, options: SchemaAST.ParseOptions): SchemaIssue.Issue | undefined => { const exit = Effect.runSyncExit(parser(input, options)) if (Exit.isSuccess(exit)) { return undefined } return InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Issue adapter can only return schema issues") } } /** * Asserts that an input satisfies the schema's decoded type side. * * **When to use** * * Use to assert that an input satisfies the decoded side of a schema when schema * validation failures should throw an `Error` whose cause is `SchemaIssue.Issue`. * * **Details** * * The assertion returns normally when validation succeeds. When the input does * not satisfy the schema with a schema-only failure, it throws an `Error` with * the `SchemaIssue.Issue` in its `cause`. * * **Gotchas** * * Causes that contain defects, interruptions, or asynchronous work at this * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, * instead of being converted to a schema validation error. * * @category Asserting * @since 4.0.0 */ export function asserts(schema: S, input: I): asserts input is I & S["Type"] { const parser = asExit(run(SchemaAST.toType(schema.ast))) const exit = parser(input, SchemaAST.defaultParseOptions) if (Exit.isFailure(exit)) { const issue = InternalSchemaCause.getSchemaIssueOrThrow( exit.cause, "Assertion adapter can only throw schema issues" ) throw new Error(issue.toString(), { cause: issue }) } } /** * Creates an effectful decoder for `unknown` input. * * **When to use** * * Use when you need to decode untyped boundary input in an `Effect` whose * failure channel is `SchemaIssue.Issue`, while preserving transformations * and service requirements. * * **Details** * * The returned function succeeds with the schema's decoded `Type` or fails with a * `SchemaIssue.Issue`. Decoding service requirements are preserved in the returned * `Effect`. Parse options may be provided when creating the decoder and overridden * when applying it. * * @see {@link decodeEffect} for input already typed as the schema's `Encoded` type * * @category decoding * @since 4.0.0 */ export function decodeUnknownEffect( schema: S, options?: SchemaAST.ParseOptions ): ( input: unknown, options?: SchemaAST.ParseOptions ) => Effect.Effect { const parser = run(schema.ast) return options === undefined ? parser : (input, overrideOptions) => parser(input, mergeParseOptions(options, overrideOptions)) } /** * Creates an effectful decoder for input already typed as the schema's `Encoded` * type. * * **When to use** * * Use when you already have input typed as the schema's `Encoded` type and * need an `Effect` whose failure channel is `SchemaIssue.Issue`, while * preserving decoding service requirements. * * **Details** * * The returned function succeeds with the decoded `Type` or fails with a * `SchemaIssue.Issue`, preserving any decoding service requirements in the * returned `Effect`. * * @see {@link decodeUnknownEffect} for untyped boundary input * @see {@link encodeEffect} for the opposite direction * * @category decoding * @since 4.0.0 */ export const decodeEffect: ( schema: S, options?: SchemaAST.ParseOptions ) => ( input: S["Encoded"], options?: SchemaAST.ParseOptions ) => Effect.Effect = decodeUnknownEffect /** * Creates a Promise-based decoder for `unknown` input. * * **When to use** * * Use when you need to decode untyped input with a service-free schema and * return a JavaScript `Promise`. * * **Details** * * The returned function resolves with the decoded `Type` on success and rejects * with an `Error` whose cause is a `SchemaIssue.Issue` on decoding failure. * * **Gotchas** * * Causes that contain defects, interruptions, or other non-schema reasons reject * with an `Error` whose cause is the underlying `Cause`. * * @see {@link decodePromise} for input already typed as the schema's `Encoded` type * @see {@link decodeUnknownEffect} for schemas that require decoding services or when failures should remain in `Effect` * * @category decoding * @since 3.10.0 */ export function decodeUnknownPromise>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Promise { return asPromise(decodeUnknownEffect(schema, options)) } /** * Creates a Promise-based decoder for input already typed as the schema's * `Encoded` type. * * **When to use** * * Use when you already have input typed as the schema's `Encoded` type and need * decoding to return a JavaScript `Promise`. * * **Details** * * The returned function resolves with the decoded `Type` on success and rejects * with an `Error` whose cause is a `SchemaIssue.Issue` on decoding failure. * * **Gotchas** * * Causes that contain defects, interruptions, or other non-schema reasons reject * with an `Error` whose cause is the underlying `Cause`. * * @see {@link decodeUnknownPromise} for untyped input returning a JavaScript `Promise` * @see {@link decodeEffect} for preserving decoding services and failures in `Effect` * * @category decoding * @since 3.10.0 */ export function decodePromise>( schema: S, options?: SchemaAST.ParseOptions ): (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Promise { return asPromise(decodeEffect(schema, options)) } /** * Creates a synchronous decoder for `unknown` input that reports failure safely * as an `Exit`. * * **When to use** * * Use when you need to decode unknown input synchronously into an `Exit` whose * failure contains `SchemaIssue.Issue`. * * **Details** * * The returned function produces `Exit.Success` with the decoded `Type`. * Schema issues are represented by an `Exit.Failure` cause containing a * `SchemaIssue.Issue`. * * **Gotchas** * * Because this adapter runs synchronously, async decoding work can produce an * `Exit.Failure` with a defect cause. When the cause contains both schema * issues and non-schema reasons, all reasons remain in the returned `Cause`. * * @see {@link decodeExit} for input already typed as the schema's `Encoded` type * @see {@link decodeUnknownEffect} for preserving decoding services and failures in `Effect` * @see {@link decodeUnknownResult} for returning schema issues as data * @see {@link decodeUnknownSync} for throwing on decoding failure * * @category decoding * @since 4.0.0 */ export function decodeUnknownExit>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Exit.Exit { return asExit(decodeUnknownEffect(schema, options)) } /** * Creates a synchronous decoder for input already typed as the schema's `Encoded` * type, reporting failure safely as an `Exit`. * * **When to use** * * Use when you need synchronous decoding of already typed `Encoded` input into * an `Exit` whose failure contains `SchemaIssue.Issue`. * * **Details** * * The returned function produces `Exit.Success` with the decoded `Type` or * `Exit.Failure` with a `SchemaIssue.Issue`. * * **Gotchas** * * Because this adapter runs synchronously, async decoding work can produce an * `Exit.Failure` with a defect cause. When the cause contains both schema * issues and non-schema reasons, all reasons remain in the returned `Cause`. * * @see {@link decodeUnknownExit} for untyped input with the same `Exit` result shape * @see {@link decodeEffect} for preserving decoding services and failures in `Effect` * * @category decoding * @since 4.0.0 */ export const decodeExit: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Exit.Exit = decodeUnknownExit /** @internal */ export function decodeUnknownOption>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Option.Option { return asOption(decodeUnknownEffect(schema, options)) } /** @internal */ export const decodeOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Option.Option = decodeUnknownOption /** * Creates a decoder for `unknown` input that reports failure safely as a * `Result`. * * **When to use** * * Use when decoding untyped boundary input and you want `SchemaIssue.Issue` * failures returned as data in a `Result`. * * **Details** * * The returned function produces `Result.succeed` with the decoded `Type` on * success or `Result.fail` with a `SchemaIssue.Issue` on decoding failure. * * **Gotchas** * * This adapter runs synchronously. Causes made entirely of schema issues become * `Result.fail`, but causes that contain defects, interruptions, or asynchronous * work at this synchronous boundary throw instead. * * @see {@link decodeResult} for input already typed as the schema's `Encoded` type * @see {@link decodeUnknownEffect} for effectful or service-requiring decoding * * @category decoding * @since 4.0.0 */ export function decodeUnknownResult>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Result.Result { return asResult(decodeUnknownEffect(schema, options)) } /** * Creates a decoder for input already typed as the schema's `Encoded` type, * reporting failure safely as a `Result`. * * **When to use** * * Use when you already have input typed as the schema's `Encoded` type and want * schema decoding failures represented as `Result.fail` with `SchemaIssue.Issue`. * * **Details** * * The returned function produces `Result.succeed` with the decoded `Type` on * success or `Result.fail` with a `SchemaIssue.Issue` on decoding failure. * * **Gotchas** * * This synchronous adapter returns `Result.fail` for causes made entirely of * schema issues, but causes that contain defects, interruptions, or other * non-schema reasons throw instead. * * @see {@link decodeUnknownResult} for untyped input with the same `Result` shape * @see {@link decodeEffect} for effectful or service-requiring decoding * * @category decoding * @since 4.0.0 */ export const decodeResult: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Result.Result = decodeUnknownResult /** * Creates a synchronous decoder for `unknown` input. * * **When to use** * * Use to decode untrusted or dynamically typed input at a synchronous boundary * where invalid data should throw an `Error` whose cause is `SchemaIssue.Issue`. * * **Details** * * The returned function returns the decoded `Type` on success and throws an * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure. * * **Gotchas** * * Causes that contain defects, interruptions, or asynchronous work at this * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, * instead of being converted to a schema validation error. * * @see {@link decodeSync} for input already typed as the schema's `Encoded` type * @see {@link decodeUnknownEffect} for preserving decoding failures in `Effect` * @see {@link decodeUnknownResult} for returning schema issues as data * * @category decoding * @since 3.10.0 */ export function decodeUnknownSync>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => S["Type"] { return asSync(decodeUnknownEffect(schema, options)) } /** * Creates a synchronous decoder for input already typed as the schema's `Encoded` * type. * * **When to use** * * Use to decode values already typed as the schema's `Encoded` input when * decoding failure should throw an `Error` whose cause is `SchemaIssue.Issue`. * * **Details** * * The returned function returns the decoded `Type` on success and throws an * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure. * * **Gotchas** * * Causes that contain defects, interruptions, or asynchronous work at this * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, * instead of being converted to a schema validation error. * * @see {@link decodeUnknownSync} for untrusted or dynamically typed input * @see {@link decodeResult} for returning schema issues as data * @see {@link decodeEffect} for preserving decoding failures in `Effect` * * @category decoding * @since 3.10.0 */ export const decodeSync: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => S["Type"] = decodeUnknownSync /** * Creates an effectful encoder for `unknown` input. * * **When to use** * * Use when you need to encode untyped boundary input in an `Effect` whose * failure channel is `SchemaIssue.Issue`, while preserving service * requirements. * * **Details** * * The returned function succeeds with the schema's `Encoded` value or fails with a * `SchemaIssue.Issue`. Encoding service requirements are preserved in the returned * `Effect`. Parse options may be provided when creating the encoder and overridden * when applying it. * * @see {@link encodeEffect} for the typed-input variant when the value is already typed as the schema's decoded `Type` * * @category encoding * @since 4.0.0 */ export function encodeUnknownEffect( schema: S, options?: SchemaAST.ParseOptions ): ( input: unknown, options?: SchemaAST.ParseOptions ) => Effect.Effect { const parser = run(SchemaAST.flip(schema.ast)) return options === undefined ? parser : (input, overrideOptions) => parser(input, mergeParseOptions(options, overrideOptions)) } /** * Creates an effectful encoder for input already typed as the schema's decoded * `Type`. * * **When to use** * * Use when you need to encode values already typed as the schema's decoded * `Type` in an `Effect` whose failure channel is `SchemaIssue.Issue`, while * preserving service requirements. * * **Details** * * The returned function succeeds with the schema's `Encoded` value or fails with a * `SchemaIssue.Issue`, preserving any encoding service requirements in the * returned `Effect`. * * @see {@link encodeUnknownEffect} for encoding unknown input before the value is statically typed as the schema's `Type` * * @category encoding * @since 4.0.0 */ export const encodeEffect: ( schema: S, options?: SchemaAST.ParseOptions ) => ( input: S["Type"], options?: SchemaAST.ParseOptions ) => Effect.Effect = encodeUnknownEffect /** * Creates a Promise-based encoder for `unknown` input. * * **When to use** * * Use when you need to encode untrusted or dynamically typed values with a * service-free schema and return a JavaScript `Promise`. * * **Details** * * The returned function resolves with the schema's `Encoded` value on success and * rejects with an `Error` whose cause is a `SchemaIssue.Issue` on encoding failure. * * **Gotchas** * * Causes that contain defects, interruptions, or other non-schema reasons reject * with an `Error` whose cause is the underlying `Cause`. * * @see {@link encodePromise} for input already typed as the schema's decoded `Type` * @see {@link encodeUnknownEffect} for schemas that require encoding services or when failures should remain in `Effect` * * @category encoding * @since 3.10.0 */ export const encodeUnknownPromise = >( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Promise => asPromise(encodeUnknownEffect(schema, options)) /** * Creates a Promise-based encoder for input already typed as the schema's decoded * `Type`. * * **When to use** * * Use when you already have values typed as the schema's decoded `Type` and * need encoding to return a JavaScript `Promise`. * * **Details** * * The returned function resolves with the schema's `Encoded` value on success and * rejects with an `Error` whose cause is a `SchemaIssue.Issue` on encoding failure. * * **Gotchas** * * Causes that contain defects, interruptions, or other non-schema reasons reject * with an `Error` whose cause is the underlying `Cause`. * * @see {@link encodeUnknownPromise} for encoding untyped input * @see {@link encodeEffect} for effectful encoding or schemas with encoding service requirements * * @category encoding * @since 3.10.0 */ export const encodePromise: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Promise = encodeUnknownPromise /** * Creates a synchronous encoder for `unknown` input that reports failure safely * as an `Exit`. * * **When to use** * * Use when you need synchronous encoding of unknown input into an `Exit` whose * failure contains `SchemaIssue.Issue`. * * **Details** * * The returned function produces `Exit.Success` with the schema's `Encoded` value * or `Exit.Failure` with a `SchemaIssue.Issue`. * * **Gotchas** * * Because this adapter runs synchronously, async encoding work can produce an * `Exit.Failure` with a defect cause. When the cause contains both schema * issues and non-schema reasons, all reasons remain in the returned `Cause`. * * @see {@link encodeExit} for input already typed as the schema's decoded `Type` * @see {@link encodeUnknownEffect} for effectful encoding that preserves service requirements * * @category encoding * @since 4.0.0 */ export function encodeUnknownExit>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Exit.Exit { return asExit(encodeUnknownEffect(schema, options)) } /** * Creates a synchronous encoder for input already typed as the schema's decoded * `Type`, reporting failure safely as an `Exit`. * * **When to use** * * Use when you need synchronous encoding of already typed schema values into * an `Exit` whose failure contains `SchemaIssue.Issue`. * * **Details** * * The returned function produces `Exit.Success` with the schema's `Encoded` value * or `Exit.Failure` with a `SchemaIssue.Issue`. * * **Gotchas** * * Because this adapter runs synchronously, async encoding work can produce an * `Exit.Failure` with a defect cause. When the cause contains both schema * issues and non-schema reasons, all reasons remain in the returned `Cause`. * * @see {@link encodeUnknownExit} for unknown input with the same `Exit` result shape * @see {@link encodeEffect} for effectful encoding that preserves service requirements * * @category encoding * @since 4.0.0 */ export const encodeExit: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Exit.Exit = encodeUnknownExit /** @internal */ export function encodeUnknownOption>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Option.Option { return asOption(encodeUnknownEffect(schema, options)) } /** @internal */ export const encodeOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Option.Option = encodeUnknownOption /** * Creates an encoder for `unknown` input that reports failure safely as a * `Result`. * * **When to use** * * Use when encoding values from an unknown or dynamically typed boundary * synchronously, and you want `SchemaIssue.Issue` failures returned as `Result` * data. * * **Details** * * The returned function produces `Result.succeed` with the schema's `Encoded` * value on success or `Result.fail` with a `SchemaIssue.Issue` on encoding * failure. * * **Gotchas** * * This adapter runs synchronously. Causes made entirely of schema issues become * `Result.fail`, but causes that contain defects, interruptions, or asynchronous * work at this synchronous boundary throw instead. * * @see {@link encodeResult} for input already typed as the schema's decoded `Type` * @see {@link encodeUnknownEffect} for effectful encoding, including schemas with encoding service requirements * * @category encoding * @since 4.0.0 */ export function encodeUnknownResult>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Result.Result { return asResult(encodeUnknownEffect(schema, options)) } /** * Creates an encoder for input already typed as the schema's decoded `Type`, * reporting failure safely as a `Result`. * * **When to use** * * Use when you already have input typed as the schema's decoded `Type` and want * encoding failures returned as `Result.fail` with `SchemaIssue.Issue`. * * **Details** * * The returned function produces `Result.succeed` with the schema's `Encoded` * value on success or `Result.fail` with a `SchemaIssue.Issue` on encoding * failure. * * **Gotchas** * * This synchronous adapter returns `Result.fail` for causes made entirely of * schema issues, but causes that contain defects, interruptions, or other * non-schema reasons throw instead. * * @see {@link encodeUnknownResult} for the same `Result` shape when the input is not already typed * * @category encoding * @since 4.0.0 */ export const encodeResult: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Result.Result = encodeUnknownResult /** * Creates a synchronous encoder for `unknown` input. * * **When to use** * * Use when you need to encode values from untyped input in synchronous code and * want encoding failures to throw an `Error` whose cause is `SchemaIssue.Issue`. * * **Details** * * The returned function returns the schema's `Encoded` value on success and throws * an `Error` with the `SchemaIssue.Issue` in its `cause` on encoding failure. * * **Gotchas** * * Causes that contain defects, interruptions, or asynchronous work at this * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, * instead of being converted to a schema validation error. * * @see {@link encodeSync} for input already typed as the schema's decoded `Type` * @see {@link encodeUnknownEffect} for effectful encoding that preserves service requirements * * @category encoding * @since 3.10.0 */ export function encodeUnknownSync>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => S["Encoded"] { return asSync(encodeUnknownEffect(schema, options)) } /** * Creates a synchronous encoder for input already typed as the schema's decoded * `Type`. * * **When to use** * * Use to encode already typed schema values synchronously when encoding failure * should throw an `Error` whose cause is `SchemaIssue.Issue`. * * **Details** * * The returned function returns the schema's `Encoded` value on success and throws * an `Error` with the `SchemaIssue.Issue` in its `cause` on encoding failure. * * **Gotchas** * * Causes that contain defects, interruptions, or asynchronous work at this * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, * instead of being converted to a schema validation error. * * @see {@link encodeUnknownSync} for unknown input with the same throwing boundary * @see {@link encodeResult} for returning schema issues as data * @see {@link encodeEffect} for effectful encoding that preserves service requirements * * @category encoding * @since 3.10.0 */ export const encodeSync: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => S["Encoded"] = encodeUnknownSync const mergeParseOptions = ( options: SchemaAST.ParseOptions, overrideOptions: SchemaAST.ParseOptions | undefined ): SchemaAST.ParseOptions => overrideOptions === undefined ? options : { ...options, ...overrideOptions } /** @internal */ export function run(ast: SchemaAST.AST) { const parser = recur(ast) return (input: unknown, options?: SchemaAST.ParseOptions): Effect.Effect => Effect.flatMapEager(parser(Option.some(input), options ?? SchemaAST.defaultParseOptions), (oa) => { if (oa._tag === "None") { return Effect.fail(new SchemaIssue.InvalidValue(oa)) } return Effect.succeed(oa.value as T) }) } function asPromise( parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect ): (input: E, options?: SchemaAST.ParseOptions) => Promise { return (input: E, options?: SchemaAST.ParseOptions) => Effect.runPromiseExit(parser(input, options)).then((exit) => { if (Exit.isSuccess(exit)) { return exit.value } const issue = InternalSchemaCause.getSchemaIssueOrThrow( exit.cause, "Promise adapter can only reject schema issues" ) throw new Error(issue.toString(), { cause: issue }) }) } function asExit( parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect ): (input: E, options?: SchemaAST.ParseOptions) => Exit.Exit { return (input: E, options?: SchemaAST.ParseOptions) => Effect.runSyncExit(parser(input, options) as any) } /** @internal */ export function asOption( parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect ): (input: E, options?: SchemaAST.ParseOptions) => Option.Option { const parserExit = asExit(parser) return (input: E, options?: SchemaAST.ParseOptions) => { const exit = parserExit(input, options) if (Exit.isSuccess(exit)) { return Option.some(exit.value) } InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Option adapter can only return none for schema issues") return Option.none() } } function asResult( parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect ): (input: E, options?: SchemaAST.ParseOptions) => Result.Result { const parserExit = asExit(parser) return (input: E, options?: SchemaAST.ParseOptions) => { const exit = parserExit(input, options) if (Exit.isSuccess(exit)) { return Result.succeed(exit.value) } return Result.fail( InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Result adapter can only return schema issues") ) } } function asSync( parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect ): (input: E, options?: SchemaAST.ParseOptions) => T { const parserExit = asExit(parser) return (input: E, options?: SchemaAST.ParseOptions) => { const exit = parserExit(input, options) if (Exit.isSuccess(exit)) { return exit.value } const issue = InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Sync adapter can only throw schema issues") throw new Error(issue.toString(), { cause: issue }) } } function mapSchemaIssueEffect( self: Effect.Effect, f: (issue: SchemaIssue.Issue) => SchemaIssue.Issue ): Effect.Effect { return Effect.catchCause(self, (cause) => Effect.failCauseSync(() => Cause.map(cause, f))) } /** @internal */ export interface Parser { ( input: Option.Option, options: SchemaAST.ParseOptions ): Effect.Effect, SchemaIssue.Issue, any> } const recur = memoize( (ast: SchemaAST.AST): Parser => { let parser: Parser const checks = ast.checks const encoding = ast.encoding const links = encoding const len = links?.length ?? 0 const encodingChecks = (ast as any).encodingChecks const astOptions = (checks ? checks[checks.length - 1].annotations : ast.annotations) ?.["parseOptions"] if (!ast.context && !encoding && !checks && !encodingChecks) { return (ou, options) => { parser ??= ast.getParser(recur) if (astOptions) { options = { ...options, ...astOptions } } return parser(ou, options) } } const isStructural = SchemaAST.isArrays(ast) || SchemaAST.isObjects(ast) || (SchemaAST.isDeclaration(ast) && ast.typeParameters.length > 0) const structuralChecks = checks && isStructural ? checks.filter((check) => check.annotations?.[SchemaAST.STRUCTURAL_ANNOTATION_KEY]) : undefined return (ou, options) => { if (astOptions) { options = { ...options, ...astOptions } } let srou: Effect.Effect, SchemaIssue.Issue, unknown> | undefined if (links) { for (let i = len - 1; i >= 0; i--) { const link = links[i] const to = link.to const parser = recur(to) srou = srou ? Effect.flatMapEager(srou, (ou) => parser(ou, options)) : parser(ou, options) if (link.transformation._tag === "Transformation") { const getter = link.transformation.decode srou = Effect.flatMapEager(srou, (ou) => getter.run(ou, options)) } else { srou = link.transformation.decode(srou, options) } } srou = mapSchemaIssueEffect(srou!, (issue) => new SchemaIssue.Encoding(ast, ou, issue)) } parser ??= ast.getParser(recur) const parseLocal = (localOu: Option.Option) => { let sroa = parser(localOu, options) if (encodingChecks && !options?.disableChecks) { sroa = Effect.flatMapEager(sroa, (oa) => { if (Option.isSome(localOu) && Option.isSome(oa)) { const issues: Array = [] SchemaAST.collectIssues(encodingChecks, localOu.value, issues, ast, options) if (Arr.isArrayNonEmpty(issues)) { return Effect.fail(new SchemaIssue.Composite(ast, localOu, issues)) } } return Effect.succeed(oa) }) } if (checks && !options?.disableChecks) { if (options?.errors === "all" && structuralChecks && structuralChecks.length > 0 && Option.isSome(localOu)) { sroa = mapSchemaIssueEffect(sroa, (issue) => { const issues: Array = [] SchemaAST.collectIssues( structuralChecks, localOu.value, issues, ast, options ) const out: SchemaIssue.Issue = Arr.isArrayNonEmpty(issues) ? issue._tag === "Composite" && issue.ast === ast ? new SchemaIssue.Composite(ast, issue.actual, [...issue.issues, ...issues]) : new SchemaIssue.Composite(ast, localOu, [issue, ...issues]) : issue return out }) } sroa = Effect.flatMapEager(sroa, (oa) => { if (Option.isSome(oa)) { const value = oa.value const issues: Array = [] SchemaAST.collectIssues(checks, value, issues, ast, options) if (Arr.isArrayNonEmpty(issues)) { return Effect.fail(new SchemaIssue.Composite(ast, oa, issues)) } } return Effect.succeed(oa) }) } return sroa } const sroa = srou ? Effect.flatMapEager(srou, parseLocal) : parseLocal(ou) return sroa } } )