/** * Represents Effect schemas as runtime trees. * * Every `Schema` has an AST made from nodes for declarations, primitives, * literals, arrays, objects, unions, suspended schemas, checks, annotations, * encoding links, and parsing context. Most users work with the higher-level * `Schema` module. Use `SchemaAST` when you need to inspect schema nodes, build * ASTs programmatically, change encoded or decoded views, collect issues, or * run low-level schema checks. * * @stability stable * @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 { format, formatPropertyKey } from "./Formatter.ts" import { identity, memoize, memoizeIdempotent } from "./Function.ts" import { effectIsExit, iterateConcurrent, iterateEager, resolveConcurrency } from "./internal/effect.ts" import * as InternalRecord from "./internal/record.ts" import * as InternalAnnotations from "./internal/schema/annotations.ts" import * as InternalSchemaCause from "./internal/schema/cause.ts" import * as InternalParser from "./internal/schema/parser.ts" import * as Pipeable from "./Pipeable.ts" import * as Predicate from "./Predicate.ts" import * as Result from "./Result.ts" import type * as Schema from "./Schema.ts" import * as SchemaGetter from "./SchemaGetter.ts" import * as SchemaIssue from "./SchemaIssue.ts" import type * as SchemaParser from "./SchemaParser.ts" import * as SchemaTransformation from "./SchemaTransformation.ts" import type * as Types from "./Types.ts" /** * Discriminated union of all AST node types. * * **Details** * * Every `Schema` has an `.ast` property of this type. Use the guard functions * ({@link isString}, {@link isObjects}, etc.) to narrow to a specific variant, * then access variant-specific fields. * * - All variants share the `annotations`, `checks`, `encoding`, and `context` * fields. * - Discriminate on the `_tag` field (e.g. `"String"`, `"Objects"`, `"Union"`). * * @see {@link isAST} * @stability stable * @category models * @since 4.0.0 */ export type AST = | Declaration | Null | Undefined | Void | Never | Unknown | Any | String | Number | Boolean | BigInt | Symbol | Literal | UniqueSymbol | ObjectKeyword | Enum | TemplateLiteral | Arrays | Objects | Union | Suspend function makeGuard(tag: T) { return (ast: AST): ast is Extract => ast._tag === tag } /** * Returns `true` if the value is an {@link AST} node (any variant). * * **Details** * * Uses the internal `TypeId` brand to distinguish AST nodes from arbitrary * objects. * * @see {@link AST} * @stability stable * @category guards * @since 4.0.0 */ export function isAST(u: unknown): u is AST { return Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId } /** * Narrows an {@link AST} to {@link Declaration}. * * **When to use** * * Use to recognize declaration AST nodes before running declaration-specific * handling. * * @see {@link Declaration} for the AST node type narrowed by this guard * * @stability stable * @category guards * @since 3.10.0 */ export const isDeclaration = makeGuard("Declaration") /** * Narrows an {@link AST} to {@link Null}. * * **When to use** * * Use to recognize an AST node that represents exactly the `null` literal when * inspecting, traversing, or transforming schema ASTs. * * @see {@link Null} for the AST node type narrowed by this guard * @see {@link null_ null} for the singleton `Null` AST instance * @see {@link isLiteral} for exact primitive literal AST nodes * * @stability stable * @category guards * @since 4.0.0 */ export const isNull = makeGuard("Null") /** * Narrows an {@link AST} to {@link Undefined}. * * **When to use** * * Use to identify AST nodes that represent exactly the JavaScript `undefined` * value. * * @see {@link isVoid} for narrowing AST nodes that represent TypeScript `void` instead of exact `undefined` * * @stability stable * @category guards * @since 4.0.0 */ export const isUndefined = makeGuard("Undefined") /** * Narrows an {@link AST} to {@link Void}. * * **When to use** * * Use to identify AST nodes that represent the TypeScript `void` type before * handling `Void`-specific schema behavior. * * @see {@link isUndefined} for narrowing AST nodes that represent the literal `undefined` value instead of TypeScript `void` * * @stability stable * @category guards * @since 4.0.0 */ export const isVoid = makeGuard("Void") /** * Narrows an {@link AST} to {@link Never}. * * **When to use** * * Use to detect the AST node for a schema that can never match before handling * other schema variants. * * @see {@link Never} for the AST node type narrowed by this guard * @see {@link never} for the singleton `Never` AST instance * * @stability stable * @category guards * @since 4.0.0 */ export const isNever = makeGuard("Never") /** * Narrows an {@link AST} to {@link Unknown}. * * **When to use** * * Use when you need to inspect a schema AST and handle the `Unknown` node * variant specifically. * * @see {@link isAny} for the guard for the `Any` node, whose parsed result is typed as `any` rather than `unknown` * * @stability stable * @category guards * @since 4.0.0 */ export const isUnknown = makeGuard("Unknown") /** * Narrows an {@link AST} to {@link Any}. * * **When to use** * * Use when you need to inspect a schema AST and handle the `Any` node * variant specifically. * * @see {@link isUnknown} for the guard for the `Unknown` node, whose parsed result is typed as `unknown` rather than `any` * * @stability stable * @category guards * @since 4.0.0 */ export const isAny = makeGuard("Any") /** * Narrows an {@link AST} to {@link String}. * * **When to use** * * Use to detect schema AST nodes that match any string value while inspecting * or transforming a Schema AST. * * @see {@link String} for the AST node class narrowed by this guard * @see {@link string} for the singleton `String` AST instance * @see {@link isLiteral} for exact primitive literal AST nodes, including exact string literals * * @stability stable * @category guards * @since 4.0.0 */ export const isString = makeGuard("String") /** * Narrows an {@link AST} to {@link Number}. * * **When to use** * * Use to detect `Number` AST nodes while inspecting, traversing, or transforming * schema ASTs. * * @stability stable * @category guards * @since 4.0.0 */ export const isNumber = makeGuard("Number") /** * Narrows an {@link AST} to {@link Boolean}. * * **When to use** * * Use to identify the `Boolean` AST variant while inspecting, traversing, or * transforming schema definitions. * * @see {@link Boolean} for the AST node type matched by this guard * @see {@link boolean} for the singleton instance to use when constructing a boolean AST directly * * @stability stable * @category guards * @since 4.0.0 */ export const isBoolean = makeGuard("Boolean") /** * Narrows an {@link AST} to {@link BigInt}. * * **When to use** * * Use to identify bigint AST nodes while inspecting or transforming schema ASTs. * * @see {@link BigInt} for the AST node matched by this guard * @see {@link bigInt} for the singleton instance; use `isBigInt` when narrowing an existing `AST` value * * @stability stable * @category guards * @since 4.0.0 */ export const isBigInt = makeGuard("BigInt") /** * Narrows an {@link AST} to {@link Symbol}. * * **When to use** * * Use to narrow an `AST` node before handling the `Symbol` variant for schemas * that accept any JavaScript symbol value. * * @see {@link isUniqueSymbol} for the sibling guard that narrows the `UniqueSymbol` variant for one exact symbol value * * @stability stable * @category guards * @since 4.0.0 */ export const isSymbol = makeGuard("Symbol") /** * Narrows an {@link AST} to {@link Literal}. * * **When to use** * * Use to recognize exact string, number, boolean, or bigint literal AST nodes. * * @see {@link Literal} for the AST node type narrowed by this guard * @see {@link LiteralValue} for the values stored by literal nodes * * @stability stable * @category guards * @since 3.10.0 */ export const isLiteral = makeGuard("Literal") /** * Narrows an {@link AST} to {@link UniqueSymbol}. * * @stability stable * @category guards * @since 3.10.0 */ export const isUniqueSymbol = makeGuard("UniqueSymbol") /** * Narrows an {@link AST} to {@link ObjectKeyword}. * * **When to use** * * Use to identify the AST node for the TypeScript `object` keyword when * inspecting or transforming a Schema AST. * * @see {@link ObjectKeyword} for the AST node matched by this guard * @see {@link objectKeyword} for the singleton `ObjectKeyword` AST instance * @see {@link isObjects} for struct and record AST nodes * * @stability stable * @category guards * @since 3.10.0 */ export const isObjectKeyword = makeGuard("ObjectKeyword") /** * Narrows an {@link AST} to {@link Enum}. * * **When to use** * * Use to recognize enum AST nodes before reading enum cases or running * enum-specific handling. * * @see {@link Enum} for the AST node type narrowed by this guard * * @stability stable * @category guards * @since 4.0.0 */ export const isEnum = makeGuard("Enum") /** * Narrows an {@link AST} to {@link TemplateLiteral}. * * @stability stable * @category guards * @since 3.10.0 */ export const isTemplateLiteral = makeGuard("TemplateLiteral") /** * Narrows an {@link AST} to {@link Arrays}. * * **When to use** * * Use to recognize array-like AST nodes before reading their element, rest, or * mutability metadata. * * @see {@link Arrays} for the AST node type narrowed by this guard * * @stability stable * @category guards * @since 4.0.0 */ export const isArrays = makeGuard("Arrays") /** * Narrows an {@link AST} to {@link Objects}. * * @stability stable * @category guards * @since 4.0.0 */ export const isObjects = makeGuard("Objects") /** * Narrows an {@link AST} to {@link Union}. * * @stability stable * @category guards * @since 3.10.0 */ export const isUnion = makeGuard("Union") /** * Narrows an {@link AST} to {@link Suspend}. * * @stability stable * @category guards * @since 3.10.0 */ export const isSuspend = makeGuard("Suspend") /** * Represents a single step in an {@link Encoding} chain. * * **Details** * * A link pairs a target {@link AST} with a `Transformation` or `Middleware` * that converts values between the current node and the target. * * - `to` — the AST node on the other side of this transformation step. * - `transformation` — the bidirectional conversion logic (decode/encode). * * Links are composed into a non-empty array ({@link Encoding}) attached to * AST nodes that have a different encoded representation. * * @see {@link Encoding} * @see {@link decodeTo} * @stability stable * @category models * @since 4.0.0 */ export interface Link { readonly to: AST readonly transformation: | SchemaTransformation.Transformation | SchemaTransformation.Middleware } /** * Constructs a {@link Link}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Link: new( to: AST, transformation: | SchemaTransformation.Transformation | SchemaTransformation.Middleware ) => Link = class { readonly to: AST readonly transformation: | SchemaTransformation.Transformation | SchemaTransformation.Middleware constructor( to: AST, transformation: | SchemaTransformation.Transformation | SchemaTransformation.Middleware ) { this.to = to this.transformation = transformation } } /** * A non-empty chain of {@link Link} values representing the transformation * steps between a schema's decoded (type) form and its encoded (wire) form. * * **Details** * * Stored on an AST node's `encoding` field. When `undefined`, the node has no * encoding transformation (type and encoded forms are identical). * * @see {@link Link} * @see {@link toEncoded} * @stability stable * @category models * @since 4.0.0 */ export type Encoding = readonly [Link, ...Array] /** * Options that control schema parsing, validation, transformation, and output behavior. * * **Details** * * Pass to `Schema.decodeUnknownEffect`, `Schema.encodeEffect`, and related APIs to customize * error reporting, excess property handling, check execution, and product * concurrency. Options apply throughout the parse; schema annotations do not * override them. * * - `errors` — `"first"` (default) stops at the first error; `"all"` collects * every error. * - `onExcessProperty` — `"ignore"` (default) strips unknown object keys; * `"error"` fails. * - `disableChecks` — skips validation checks while still applying defaults and * transformations. * - `concurrency` — controls concurrent parsing of tuple elements, array * elements, struct fields, and record entries using the same semantics as * `Effect.forEach`; the default is sequential. * - `reportInput` — includes rejected input values in value-bearing schema * issues. * * Object property order is unspecified, including in values passed to checks. * Decoding and encoding do not guarantee preservation of input key order. * * @stability stable * @category options * @since 3.10.0 */ export interface ParseOptions { /** * Controls how many parsing errors are reported. * * **Details** * * The default, `"first"`, stops at the first error. Set the option to `"all"` * to collect every parsing error, which can help with debugging or with * presenting more complete error messages to a user. * * @default "first" */ readonly errors?: "first" | "all" | undefined /** * Controls how object parsing handles keys that are not declared by the schema. * * **Details** * * The default, `"ignore"`, strips unspecified properties from the output. Use * `"error"` to fail when an excess property is present. A key is covered by a * declared property or any index signature selecting that key. This applies * to structs, records, and structs with rest. Values must satisfy every * applicable index signature. Empty structs keep their non-nullish behavior. * * An excess property is an enumerable own property that is not covered by a * declared property or an index signature. Non-enumerable own properties, * such as `Error#stack`, are ignored unless explicitly declared by the * schema. Index signatures likewise select only enumerable own properties. * * @default "ignore" */ readonly onExcessProperty?: "ignore" | "error" | undefined /** * Whether to disable checks while still applying defaults and * transformations. */ readonly disableChecks?: boolean | undefined /** * Controls how many children of a product schema may parse concurrently. * * **Details** * * This option has the same concurrency semantics as {@link Effect.forEach}. * It applies to tuples, arrays, structs, records, and structs with rest. Each * nested product applies the limit independently. Union members remain * sequential, though products inside the current union member still receive * the option. * * Array and tuple outputs retain their element order. Other observable work, * including issue accumulation and colliding transformed record keys, follows * effect completion order when concurrency is greater than `1`. * * **Gotchas** * * With `errors: "first"`, the first observed child failure interrupts the * remaining children and may not belong to the earliest product position. * Concurrent children may perform effects before another child fails. * * @default undefined */ readonly concurrency?: Types.Concurrency | undefined /** * Whether schema issues should retain and report rejected input values. * * **Details** * * When enabled, value-bearing issues created by the parser expose an `input` * field. Built-in formatters may include reported input in default messages. * The input is retained by reference rather than copied. * * **Gotchas** * * Enabling this option can retain or disclose secrets, personally * identifiable information, and large object graphs. The `input` field is * enumerable and may be included by object enumeration, spread, or * serialization. Issues returned directly by user-defined declarations, * checks, transformations, and middleware are not modified; their authors * decide whether to retain an input. To respect this option, pass the * callback's input and parse options directly to a value-bearing issue * constructor. Custom messages and annotations remain the caller's * responsibility regardless of this option. * Formatting an issue with `SchemaIssue.makeFormatterDefault()`, reading * `SchemaError.message`, or formatting a Standard Schema failure can disclose * retained input. * * @default false */ readonly reportInput?: boolean | undefined } /** @internal */ export const defaultParseOptions: ParseOptions = {} /** * Represents per-property metadata attached to an AST node's `context` field. * * **Details** * * Tracks whether a property key is optional, mutable, has a constructor * default, or carries key-level annotations. Typically set by helpers like * {@link optionalKey} and `Schema.mutableKey`. * * - `isOptional` — the property key may be absent from the input. * - `isMutable` — the property is `readonly` when `false`. * - `constructorDefault` — an effect evaluated during construction to supply * missing values. * - `annotations` — key-level annotations (e.g. description of the key * itself). * * @see `Schema.optionalKey` * @see {@link isOptional} * @stability stable * @category models * @since 4.0.0 */ export interface Context { readonly isOptional: boolean readonly isMutable: boolean /** Used for constructor default values (e.g. `withConstructorDefault` API) */ readonly constructorDefault: Effect.Effect | undefined readonly annotations: Schema.Annotations.Key | undefined } /** * Constructs a {@link Context}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Context: new( isOptional: boolean, isMutable: boolean, /** Used for constructor default values (e.g. `withConstructorDefault` API) */ constructorDefault?: Effect.Effect | undefined, annotations?: Schema.Annotations.Key | undefined ) => Context = class { readonly isOptional: boolean readonly isMutable: boolean /** Used for constructor default values (e.g. `withConstructorDefault` API) */ readonly constructorDefault: Effect.Effect | undefined readonly annotations: Schema.Annotations.Key | undefined constructor( isOptional: boolean, isMutable: boolean, /** Used for constructor default values (e.g. `withConstructorDefault` API) */ constructorDefault: Effect.Effect | undefined = undefined, annotations: Schema.Annotations.Key | undefined = undefined ) { this.isOptional = isOptional this.isMutable = isMutable this.constructorDefault = constructorDefault this.annotations = annotations } } /** * Non-empty array of validation {@link Check} values attached to an AST node's * `checks` field. * * **Details** * * Checks are run after basic type matching succeeds. They represent * refinements like `minLength`, `pattern`, `int`, etc. * * @see {@link Check} * @see {@link Filter} * @see {@link FilterGroup} * @stability stable * @category models * @since 4.0.0 */ export type Checks = readonly [Check, ...Array>] const TypeId = "~effect/Schema" interface ASTNode { readonly [TypeId]: typeof TypeId readonly _tag: string readonly annotations: Schema.Annotations.Annotations | undefined readonly checks: Checks | undefined readonly encoding: Encoding | undefined readonly context: Context | undefined toString(): string } abstract class ASTNodeImpl implements ASTNode { readonly [TypeId] = TypeId abstract readonly _tag: string readonly annotations: Schema.Annotations.Annotations | undefined readonly checks: Checks | undefined readonly encoding: Encoding | undefined readonly context: Context | undefined constructor( annotations: Schema.Annotations.Annotations | undefined = undefined, checks: Checks | undefined = undefined, encoding: Encoding | undefined = undefined, context: Context | undefined = undefined ) { this.annotations = annotations this.checks = checks this.encoding = encoding this.context = context } toString() { return `<${this._tag}>` } } type DeclarationRun = ( typeParameters: ReadonlyArray ) => (input: unknown, self: Declaration, options: ParseOptions) => Effect.Effect /** * AST node for user-defined opaque types with custom parsing logic. * * **When to use** * * Use when you need a custom schema AST node because none of the built-in * nodes fit. * * **Details** * * - `typeParameters` — inner schemas this declaration is parameterized over * (e.g. the element type for a custom collection). * - `run` — factory that receives `typeParameters` and returns a parser that * validates or transforms raw input. The `Effect` returned by the parser must * complete synchronously. * * @see {@link isDeclaration} * @stability stable * @category models * @since 4.0.0 */ export interface Declaration extends ASTNode { readonly _tag: "Declaration" readonly typeParameters: ReadonlyArray readonly run: DeclarationRun readonly encodingChecks: Checks | undefined /** * Parser factory {@link flip} swaps in, so a declaration can behave * differently when encoding. `undefined` reuses `run`. */ readonly encodingRun: DeclarationRun | undefined /** @internal */ getParser(): SchemaParser.Parser /** @internal */ recur(recur: (ast: AST) => AST): Declaration /** @internal */ flip(recur: (ast: AST) => AST): Declaration /** @internal */ getExpected(): string } /** * Constructs a {@link Declaration}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Declaration: new( typeParameters: ReadonlyArray, run: DeclarationRun, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context, encodingChecks?: Checks, encodingRun?: DeclarationRun ) => Declaration = class extends ASTNodeImpl { readonly _tag = "Declaration" readonly typeParameters: ReadonlyArray readonly run: DeclarationRun readonly encodingChecks: Checks | undefined /** * Parser factory {@link flip} swaps in, so a declaration can behave * differently when encoding. `undefined` reuses `run`. */ readonly encodingRun: DeclarationRun | undefined constructor( typeParameters: ReadonlyArray, run: DeclarationRun, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context, encodingChecks?: Checks, encodingRun?: DeclarationRun ) { super(annotations, checks, encoding, context) this.typeParameters = typeParameters this.run = run this.encodingChecks = encodingChecks this.encodingRun = encodingRun } /** @internal */ getParser(): SchemaParser.Parser { let run: ReturnType return (input, options) => { if (input === InternalParser.missing) return InternalParser.missingExit // Keep this declaration's initialization errors outside its callback catch. run ??= this.run(this.typeParameters) try { return run(input, this, options) } catch (error) { return Effect.die(error) } } } private _rebuild( recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined, run: DeclarationRun, encodingRun: DeclarationRun | undefined ) { const tps = mapOrSame(this.typeParameters, recur) return tps === this.typeParameters && checks === this.checks && encodingChecks === this.encodingChecks && run === this.run && encodingRun === this.encodingRun ? this : new Declaration(tps, run, this.annotations, checks, undefined, this.context, encodingChecks, encodingRun) } /** @internal */ recur(recur: (ast: AST) => AST) { return this._rebuild(recur, this.checks, this.encodingChecks, this.run, this.encodingRun) } /** @internal */ flip(recur: (ast: AST) => AST) { return this._rebuild(recur, this.encodingChecks, this.checks, this.encodingRun ?? this.run, this.run) } /** @internal */ getExpected(): string { const expected = this.annotations?.expected if (typeof expected === "string") return expected return "" } } /** * AST node matching the `null` literal value. * * **Details** * * Parsing succeeds only when the input is exactly `null`. * * @see {@link null_ null} * @see {@link isNull} * @stability stable * @category models * @since 4.0.0 */ export interface Null extends ASTNode { readonly _tag: "Null" /** @internal */ getParser(): SchemaParser.Parser /** @internal */ getExpected(): string } /** * Constructs a {@link Null}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Null: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => Null = class extends ASTNodeImpl { readonly _tag = "Null" /** @internal */ getParser() { return fromConst(this, null) } /** @internal */ getExpected(): string { return "null" } } const null_ = new Null() export { /** * Provides the singleton {@link Null} AST instance. * * **When to use** * * Use when you need the shared AST node for exact null values while inspecting * or constructing schema ASTs. * * @stability stable * @category constants * @since 4.0.0 */ null_ as null } /** * AST node matching the `undefined` value. * * **Details** * * Parsing succeeds only when the input is exactly `undefined`. * * @see {@link undefined} * @see {@link isUndefined} * @stability stable * @category models * @since 4.0.0 */ export interface Undefined extends ASTNode { readonly _tag: "Undefined" /** @internal */ getParser(): SchemaParser.Parser /** @internal */ toCodecJson(): AST /** @internal */ getExpected(): string } /** * Constructs a {@link Undefined}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Undefined: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => Undefined = class extends ASTNodeImpl { readonly _tag = "Undefined" /** @internal */ getParser() { return fromConst(this, undefined) } /** @internal */ toCodecJson(): AST { return replaceEncoding(this, [undefinedToNull]) } /** @internal */ getExpected(): string { return "undefined" } } const undefinedToNull = new Link( null_, new SchemaTransformation.Transformation( SchemaGetter.transform(() => undefined), SchemaGetter.transform(() => null) ) ) const undefined_ = new Undefined() export { /** * Provides the singleton {@link Undefined} AST instance. * * **When to use** * * Use when you need the shared AST node for exact undefined values while * inspecting or constructing schema ASTs. * * @stability stable * @category constants * @since 4.0.0 */ undefined_ as undefined } /** * AST node matching TypeScript `void` return-value semantics. * * **When to use** * * Use when you need an AST node for a value whose result is intentionally * ignored. * * **Details** * * Parsers built from this node accept any present runtime input and map it to * `undefined`. Public schemas built from it may still expose `void` as their * typed decoded and encoded representation. * * @see {@link undefined} for the AST singleton that matches only exact `undefined` * @see {@link void_ void} * @see {@link isVoid} * @stability stable * @category models * @since 4.0.0 */ export interface Void extends ASTNode { readonly _tag: "Void" /** @internal */ getParser(): ( input: unknown ) => InternalParser.Success | InternalParser.Success /** @internal */ toCodecJson(): AST /** @internal */ getExpected(): string } /** * Constructs a {@link Void}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Void: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => Void = class extends ASTNodeImpl { readonly _tag = "Void" /** @internal */ getParser() { const succeed = InternalParser.succeed(undefined) return (input: unknown) => input === InternalParser.missing ? InternalParser.missingExit : succeed } /** @internal */ toCodecJson(): AST { return replaceEncoding(this, [undefinedToNull]) } /** @internal */ getExpected(): string { return "void" } } const void_ = new Void() export { /** * Provides the singleton {@link Void} AST instance. * * **When to use** * * Use when constructing or comparing AST nodes for TypeScript `void` return * values whose result is intentionally ignored. * * **Details** * * The node parses any present runtime value as `undefined`; schemas may still * expose `void` on their typed decoded and encoded sides. * * @see {@link Void} for the AST node class * @see {@link undefined} for the sibling AST singleton that matches exactly `undefined` * @see {@link isVoid} for narrowing an AST to a `Void` node * * @stability stable * @category constructors * @since 4.0.0 */ void_ as void } /** * AST node representing the `never` type — no value matches. * * **Details** * * Parsing always fails. Useful as a placeholder in unions or as the result * of narrowing that eliminates all options. * * @see {@link never} * @see {@link isNever} * @stability stable * @category models * @since 4.0.0 */ export interface Never extends ASTNode { readonly _tag: "Never" /** @internal */ getParser(): SchemaParser.Parser /** @internal */ getExpected(): string } /** * Constructs a {@link Never}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Never: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => Never = class extends ASTNodeImpl { readonly _tag = "Never" /** @internal */ getParser() { return fromRefinement(this, Predicate.isNever) } /** @internal */ getExpected(): string { return "never" } } /** * Provides the singleton {@link Never} AST instance. * * **When to use** * * Use to reuse the canonical bottom-type AST node when constructing, * comparing, or returning ASTs. * * @see {@link Never} for the AST node class * @see {@link isNever} for narrowing an AST to a `Never` node * * @stability stable * @category constructors * @since 4.0.0 */ export const never = new Never() /** * AST node representing the `any` type — every value matches. * * @see {@link any} * @see {@link isAny} * * @stability stable * @category models * @since 4.0.0 */ export interface Any extends ASTNode { readonly _tag: "Any" /** @internal */ getParser(): SchemaParser.Parser /** @internal */ getExpected(): string } /** * Constructs a {@link Any}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Any: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => Any = class extends ASTNodeImpl { readonly _tag = "Any" /** @internal */ getParser() { return fromRefinement(this, Predicate.isUnknown) } /** @internal */ getExpected(): string { return "any" } } /** * Provides the singleton {@link Any} AST instance. * * **When to use** * * Use when you need the singleton AST node for the TypeScript `any` type and * intentionally want parsing to accept every input value. * * @see {@link unknown} for the sibling AST singleton that also accepts every value while preserving the safer `unknown` type * * @stability stable * @category constructors * @since 4.0.0 */ export const any = new Any() /** * AST node representing the `unknown` type — every value matches. * * **Details** * * Unlike {@link Any}, this is type-safe: the parsed result is typed as * `unknown` rather than `any`. * * @see {@link unknown} * @see {@link isUnknown} * @stability stable * @category models * @since 4.0.0 */ export interface Unknown extends ASTNode { readonly _tag: "Unknown" /** @internal */ getParser(): SchemaParser.Parser /** @internal */ getExpected(): string } /** * Constructs a {@link Unknown}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Unknown: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => Unknown = class extends ASTNodeImpl { readonly _tag = "Unknown" /** @internal */ getParser() { return fromRefinement(this, Predicate.isUnknown) } /** @internal */ getExpected(): string { return "unknown" } } /** * Provides the singleton {@link Unknown} AST instance. * * **When to use** * * Use when you need the reusable AST singleton for a schema node that accepts * every value while keeping parsed values opaque. * * @see {@link any} for the singleton that accepts every value as `any` * * @stability stable * @category constructors * @since 4.0.0 */ export const unknown = new Unknown() /** * AST node matching the TypeScript `object` type — accepts objects, arrays, * and functions (anything non-primitive and non-null). * * @see {@link objectKeyword} * @see {@link isObjectKeyword} * * @stability stable * @category models * @since 4.0.0 */ export interface ObjectKeyword extends ASTNode { readonly _tag: "ObjectKeyword" /** @internal */ getParser(): SchemaParser.Parser /** @internal */ getExpected(): string } /** * Constructs a {@link ObjectKeyword}. * * @stability stable * @category constructors * @since 4.0.0 */ export const ObjectKeyword: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => ObjectKeyword = class extends ASTNodeImpl { readonly _tag = "ObjectKeyword" /** @internal */ getParser() { return fromRefinement(this, Predicate.isObjectKeyword) } /** @internal */ getExpected(): string { return "object | array | function" } } /** * Provides the singleton {@link ObjectKeyword} AST instance. * * **When to use** * * Use to reuse the canonical AST node for the TypeScript `object` keyword when * building or comparing `SchemaAST` values directly. * * @see {@link ObjectKeyword} for the AST node class * @see {@link isObjectKeyword} for narrowing an AST to an `ObjectKeyword` node * * @stability stable * @category constructors * @since 3.10.0 */ export const objectKeyword = new ObjectKeyword() /** * AST node representing a TypeScript `enum`. * * **Details** * * Holds `enums` as an array of `[name, value]` pairs where values are * `string | number`. Parsing succeeds when the input matches any enum value. * * @see {@link isEnum} * @stability stable * @category models * @since 4.0.0 */ export interface Enum extends ASTNode { readonly _tag: "Enum" readonly enums: ReadonlyArray< readonly [ string, string | number ] > /** @internal */ getParser(): SchemaParser.Parser /** @internal */ toCodecStringTree(): AST /** @internal */ getExpected(): string } /** * Constructs a {@link Enum}. Numeric values must be finite. * * @stability stable * @category constructors * @since 4.0.0 */ export const Enum: new( enums: ReadonlyArray< readonly [ string, string | number ] >, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context ) => Enum = class extends ASTNodeImpl { readonly _tag = "Enum" readonly enums: ReadonlyArray constructor( enums: ReadonlyArray, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context ) { super(annotations, checks, encoding, context) for (const [, value] of enums) { if (typeof value === "number" && !globalThis.Number.isFinite(value)) { throw new Error(`A numeric enum value must be finite, got ${format(value)}`) } } this.enums = enums } /** @internal */ getParser() { const values = new Set(this.enums.map(([, v]) => v)) return fromRefinement( this, (input): input is typeof this.enums[number][1] => values.has(input) ) } /** @internal */ toCodecStringTree(): AST { if (this.enums.some(([_, v]) => typeof v === "number")) { const coercions = Object.fromEntries(this.enums.map(([_, v]) => [globalThis.String(v), v])) return replaceEncoding(this, [ new Link( new Union(Object.keys(coercions).map((k) => new Literal(k))), new SchemaTransformation.Transformation( SchemaGetter.transform((s) => coercions[s]), SchemaGetter.String() ) ) ]) } return this } /** @internal */ getExpected(): string { return this.enums.map(([_, value]) => JSON.stringify(value)).join(" | ") } } type TemplateLiteralPart = | String | Number | BigInt | Literal | TemplateLiteral | Union function formatTemplateLiteralPath(path: string | number): string { return typeof path === "number" ? `parts[${path}]` : path } function isTemplateLiteralPart( ast: AST, path: string | number, validated: Set ): ast is TemplateLiteralPart { if (validated.has(ast)) return true if (ast.encoding) { throw new Error(`TemplateLiteral parts cannot have an encoding at ${formatTemplateLiteralPath(path)}`) } let valid: boolean switch (ast._tag) { case "String": case "Number": case "BigInt": valid = true break case "Literal": valid = !ast.checks break case "TemplateLiteral": { const currentPath = formatTemplateLiteralPath(path) valid = !ast.checks && ast.parts.every((part, index) => isTemplateLiteralPart(part, `${currentPath}.parts[${index}]`, validated)) break } case "Union": { const currentPath = formatTemplateLiteralPath(path) valid = !ast.checks && ast.types.every((part, index) => isTemplateLiteralPart(part, `${currentPath}.types[${index}]`, validated)) break } default: return false } if (valid) validated.add(ast) return valid } /** * AST node representing a TypeScript template literal type * (e.g. `` `user_${string}` ``). * * **Details** * * `parts` is an array of AST nodes; each part contributes to matching * strings at runtime. * * @see {@link isTemplateLiteral} * @stability stable * @category models * @since 4.0.0 */ export interface TemplateLiteral extends ASTNode { readonly _tag: "TemplateLiteral" readonly parts: ReadonlyArray /** @internal */ readonly encodedParts: ReadonlyArray /** @internal */ readonly literals: ReadonlyArray /** @internal */ readonly suffixLengths: ReadonlyArray /** @internal */ getParser(compile: SchemaParser.Compiler): SchemaParser.Parser /** @internal */ getExpected(): string /** @internal */ matchPart(s: string, options: ParseOptions): string | undefined } /** * Constructs a {@link TemplateLiteral}. * * **Gotchas** * * Throws if a part contains an encoding, including inside unions or nested * template literals. Parts must describe their values without transformations. * * @stability stable * @category constructors * @since 4.0.0 */ export const TemplateLiteral: new( parts: ReadonlyArray, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context ) => TemplateLiteral = class extends ASTNodeImpl { readonly _tag = "TemplateLiteral" readonly parts: ReadonlyArray /** @internal */ readonly encodedParts: ReadonlyArray /** @internal */ readonly literals: ReadonlyArray /** @internal */ readonly suffixLengths: ReadonlyArray constructor( parts: ReadonlyArray, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context ) { super(annotations, checks, encoding, context) const encodedParts: Array = [] const literals: Array = [] const validated = new Set() for (let index = 0; index < parts.length; index++) { const part = parts[index] if (!isTemplateLiteralPart(part, index, validated)) { throw new Error(`Invalid TemplateLiteral part ${part._tag}`) } encodedParts.push(part) literals.push(part._tag === "Literal" ? globalThis.String(part.literal) : undefined) } const suffixLengths = new Array(encodedParts.length + 1) suffixLengths[encodedParts.length] = 0 for (let i = encodedParts.length - 1; i >= 0; i--) { suffixLengths[i] = suffixLengths[i + 1] + (literals[i]?.length ?? 0) } this.parts = parts this.encodedParts = encodedParts this.literals = literals this.suffixLengths = suffixLengths } /** @internal */ getParser(compile: SchemaParser.Compiler): SchemaParser.Parser { const tuple = new Arrays(false, this.parts.map(partFromString), []) const parser = compile(decodeTo(string, tuple, templateLiteralTransformation(this))) return (input, options) => { if (input === InternalParser.missing) return InternalParser.missingExit const result = parser(input, options) if ((result as Exit.Exit)._tag === "Success") { return InternalParser.sameExit } return Effect.mapBothEager(result, { onSuccess: () => input, onFailure: (issue) => new SchemaIssue.Composite(this, [issue], input, options) }) } } /** @internal */ getExpected(): string { return "string" } /** @internal */ matchPart(s: string, options: ParseOptions): string | undefined { return segmentTemplateLiteralParts(this, s, options) === undefined ? undefined : s } } /** @internal */ export function templateLiteralParser(parts: ReadonlyArray): Arrays { // Encoded members can overlap even when decoding selects exactly one member. // Match their spellings here; the original tuple enforces the union's mode. const normalize = memoize((encoded: AST): AST => { if (encoded._tag !== "Union") return encoded const types = mapOrSame(encoded.types, normalize) return (encoded.options?.mode ?? "anyOf") === "anyOf" && types === encoded.types ? encoded : new Union( types, encoded.options === undefined ? undefined : { ...encoded.options, mode: "anyOf" }, encoded.annotations, encoded.checks, undefined, encoded.context ) }) const template = new TemplateLiteral(parts.map((part) => normalize(toEncoded(part)))) const tuple = new Arrays(false, parts.map(partFromString), []) return decodeTo(template, tuple, templateLiteralTransformation(template)) } function templateLiteralTransformation(template: TemplateLiteral) { return new SchemaTransformation.Transformation( SchemaGetter.transformEffect((s: string, options) => { const segments = segmentTemplateLiteralParts(template, s, options) if (segments) return Effect.succeed(segments) return Effect.fail( new SchemaIssue.InvalidValue( { expected: "a string matching template literal parts" }, s, options ) ) }), SchemaGetter.transform((parts: ReadonlyArray) => parts.join("")) ) } /** * AST node matching a specific `unique symbol` value. * * **Details** * * Parsing succeeds only when the input is reference-equal to the stored * `symbol`. * * @see {@link isUniqueSymbol} * @stability stable * @category models * @since 4.0.0 */ export interface UniqueSymbol extends ASTNode { readonly _tag: "UniqueSymbol" readonly symbol: symbol /** @internal */ getParser(): SchemaParser.Parser /** @internal */ toCodecStringTree(): AST /** @internal */ getExpected(): string } /** * Constructs a {@link UniqueSymbol}. * * @stability stable * @category constructors * @since 4.0.0 */ export const UniqueSymbol: new( symbol: symbol, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context ) => UniqueSymbol = class extends ASTNodeImpl { readonly _tag = "UniqueSymbol" readonly symbol: symbol constructor( symbol: symbol, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context ) { super(annotations, checks, encoding, context) this.symbol = symbol } /** @internal */ getParser() { return fromConst(this, this.symbol) } /** @internal */ toCodecStringTree(): AST { const key = globalThis.Symbol.keyFor(this.symbol) return replaceEncoding(this, [ new Link( key === undefined ? never : new Literal(globalThis.String(this.symbol)), symbolToString.transformation ) ]) } /** @internal */ getExpected(): string { return globalThis.String(this.symbol) } } /** * The set of primitive types that can appear as a {@link Literal} value. * * @see {@link Literal} * * @stability stable * @category models * @since 3.10.0 */ export type LiteralValue = string | number | boolean | bigint /** * AST node matching an exact primitive value (string, number, boolean, or * bigint). * * **Details** * * Parsing succeeds only when the input is strictly equal (`===`) to the * stored `literal`, preserving the input value. Both `0` and `-0` are accepted * by either zero literal, and parsing preserves the input's sign. Numeric * literals must be finite. `Infinity`, `-Infinity`, and `NaN` are rejected at * construction time. * * **Example** (Creating a literal AST) * * ```ts import.meta.vitest * import { SchemaAST } from "effect" * * const ast = new SchemaAST.Literal("active") * ast.literal // => "active" * ``` * * @see {@link LiteralValue} * @see {@link isLiteral} * @stability stable * @category models * @since 4.0.0 */ export interface Literal extends ASTNode { readonly _tag: "Literal" readonly literal: LiteralValue /** @internal */ getParser(): SchemaParser.Parser /** @internal */ matchPart(s: string, _options: ParseOptions): LiteralValue | undefined /** @internal */ toCodecJson(): AST /** @internal */ toCodecStringTree(): AST /** @internal */ getExpected(): string } /** * Constructs a {@link Literal}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Literal: new( literal: LiteralValue, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context ) => Literal = class extends ASTNodeImpl { readonly _tag = "Literal" readonly literal: LiteralValue constructor( literal: LiteralValue, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context ) { super(annotations, checks, encoding, context) if (typeof literal === "number" && !globalThis.Number.isFinite(literal)) { throw new Error(`A numeric literal must be finite, got ${format(literal)}`) } this.literal = literal } /** @internal */ getParser() { return fromConst(this, this.literal) } /** @internal */ matchPart(s: string, _options: ParseOptions): LiteralValue | undefined { return s === globalThis.String(this.literal) ? this.literal : undefined } /** @internal */ toCodecJson(): AST { return typeof this.literal === "bigint" ? literalToString(this) : this } /** @internal */ toCodecStringTree(): AST { return typeof this.literal === "string" ? this : literalToString(this) } /** @internal */ getExpected(): string { return typeof this.literal === "string" ? JSON.stringify(this.literal) : globalThis.String(this.literal) } } function literalToString(ast: Literal): Literal { const literalAsString = globalThis.String(ast.literal) return replaceEncoding(ast, [ new Link( new Literal(literalAsString), new SchemaTransformation.Transformation( SchemaGetter.transform(() => ast.literal), SchemaGetter.transform(() => literalAsString) ) ) ]) } /** * AST node matching any `string` value. * * @see {@link string} * @see {@link isString} * * @stability stable * @category models * @since 4.0.0 */ export interface String extends ASTNode { readonly _tag: "String" /** @internal */ getParser(): SchemaParser.Parser /** @internal */ matchPart(s: string, options: ParseOptions): string | undefined /** @internal */ getExpected(): string } /** * Constructs a {@link String}. * * @stability stable * @category constructors * @since 4.0.0 */ export const String: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => String = class extends ASTNodeImpl { readonly _tag = "String" /** @internal */ getParser() { return fromRefinement(this, Predicate.isString) } /** @internal */ matchPart(s: string, options: ParseOptions): string | undefined { const checks = this.checks return checks && !options.disableChecks && collectIssues(checks, s, undefined, this, options) ? undefined : s } /** @internal */ getExpected(): string { return "string" } } /** * Provides the singleton {@link String} AST instance. * * **When to use** * * Use as the shared `SchemaAST` node for unconstrained JavaScript strings. * * @see {@link String} for the AST node class * @see {@link isString} for narrowing an AST to a string node * * @stability stable * @category constructors * @since 4.0.0 */ export const string = new String() /** * AST node matching any `number` value (including `NaN`, `Infinity`, * `-Infinity`). * * **Details** * * Default JSON serialization: * * - Finite numbers are serialized as JSON numbers. * - `Infinity`, `-Infinity`, and `NaN` are serialized as JSON strings. * * If the node has an `isFinite` or `isInt` check, the string fallback is * skipped since non-finite values cannot occur. * * @see {@link number} * @see {@link isNumber} * @stability stable * @category models * @since 4.0.0 */ export interface Number extends ASTNode { readonly _tag: "Number" /** @internal */ getParser(): SchemaParser.Parser /** @internal */ matchKey(s: string, options: ParseOptions): number | undefined /** @internal */ matchPart(s: string, options: ParseOptions): number | undefined /** @internal */ toCodecJson(): AST /** @internal */ toCodecStringTree(): AST /** @internal */ getExpected(): string } /** * Constructs a {@link Number}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Number: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => Number = class extends ASTNodeImpl { readonly _tag = "Number" /** @internal */ getParser() { return fromRefinement(this, Predicate.isNumber) } /** @internal */ matchKey(s: string, options: ParseOptions): number | undefined { return this._match(isStringNumberRegExp, s, options) } /** @internal */ matchPart(s: string, options: ParseOptions): number | undefined { return this._match(isStringFiniteRegExp, s, options) } private _match(regexp: RegExp, s: string, options: ParseOptions): number | undefined { if (!regexp.test(s)) return undefined const value = globalThis.Number(s) if (options.disableChecks || !this.checks) return value return collectIssues(this.checks, value, undefined, this, options) ? undefined : value } /** @internal */ toCodecJson(): AST { if ( this.checks && (hasCheck(this.checks, "effect/schema/isFinite") || hasCheck(this.checks, "effect/schema/isInt")) ) { return this } return replaceEncoding(this, [numberToJson]) } /** @internal */ toCodecStringTree(): AST { if (this.toCodecJson() === this) { return replaceEncoding(this, [finiteToString]) } return replaceEncoding(this, [numberToString]) } /** @internal */ getExpected(): string { return "number" } } function hasCheck(checks: ReadonlyArray>, id: string): boolean { return checks.some((check) => check.annotations?.representation?.id === id || (check._tag === "FilterGroup" && hasCheck(check.checks, id)) ) } /** * Provides the singleton {@link Number} AST instance. * * **When to use** * * Use when you need the canonical `SchemaAST` node for schemas that accept any * JavaScript number value. * * @see {@link Number} for the AST node class and serialization behavior * @see {@link Literal} for exact finite numeric literal AST nodes * * @stability stable * @category constructors * @since 4.0.0 */ export const number = new Number() /** * AST node matching any `boolean` value (`true` or `false`). * * @see {@link boolean} * @see {@link isBoolean} * * @stability stable * @category models * @since 4.0.0 */ export interface Boolean extends ASTNode { readonly _tag: "Boolean" /** @internal */ getParser(): SchemaParser.Parser /** @internal */ getExpected(): string } /** * Constructs a {@link Boolean}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Boolean: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => Boolean = class extends ASTNodeImpl { readonly _tag = "Boolean" /** @internal */ getParser() { return fromRefinement(this, Predicate.isBoolean) } /** @internal */ getExpected(): string { return "boolean" } } /** * Provides the singleton {@link Boolean} AST instance. * * **When to use** * * Use to reuse the standard AST node that accepts either `true` or `false` when * constructing schema ASTs directly. * * @see {@link Boolean} for the AST node class * @see {@link Literal} for exact boolean literal AST nodes * * @stability stable * @category constructors * @since 4.0.0 */ export const boolean = new Boolean() /** * AST node matching any `symbol` value. * * **When to use** * * Use when you need the AST node class for schemas that match any JavaScript * symbol value. * * **Details** * * When serialized to a string-based codec, symbols are converted via * `Symbol.keyFor` and must be registered with `Symbol.for`. * * @see {@link symbol} * @see {@link isSymbol} * @stability stable * @category models * @since 4.0.0 */ export interface Symbol extends ASTNode { readonly _tag: "Symbol" /** @internal */ getParser(): SchemaParser.Parser /** @internal */ matchKey(s: symbol, options: ParseOptions): symbol | undefined /** @internal */ toCodecStringTree(): AST /** @internal */ getExpected(): string } /** * Constructs a {@link Symbol}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Symbol: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => Symbol = class extends ASTNodeImpl { readonly _tag = "Symbol" /** @internal */ getParser() { return fromRefinement(this, Predicate.isSymbol) } /** @internal */ matchKey(s: symbol, options: ParseOptions): symbol | undefined { if (options.disableChecks || !this.checks) return s return collectIssues(this.checks, s, undefined, this, options) ? undefined : s } /** @internal */ toCodecStringTree(): AST { return replaceEncoding(this, [symbolToString]) } /** @internal */ getExpected(): string { return "symbol" } } /** * Provides the singleton {@link Symbol} AST instance. * * **When to use** * * Use to reuse the singleton AST node for schemas that match any JavaScript * symbol value. * * **Gotchas** * * String-based codecs can encode only symbols registered with `Symbol.for`, * because the implementation uses `Symbol.keyFor`. * * @see {@link UniqueSymbol} for an AST node that matches one specific symbol * * @stability stable * @category constructors * @since 4.0.0 */ export const symbol = new Symbol() /** * AST node matching any `bigint` value. * * **Details** * * When serialized to a string-based codec, bigints are converted to/from * their decimal string representation. * * @see {@link bigInt} * @see {@link isBigInt} * @stability stable * @category models * @since 4.0.0 */ export interface BigInt extends ASTNode { readonly _tag: "BigInt" /** @internal */ getParser(): SchemaParser.Parser /** @internal */ matchPart(s: string, options: ParseOptions): bigint | undefined /** @internal */ toCodecStringTree(): AST /** @internal */ getExpected(): string } /** * Constructs a {@link BigInt}. * * @stability stable * @category constructors * @since 4.0.0 */ export const BigInt: new( annotations?: Schema.Annotations.Annotations | undefined, checks?: Checks | undefined, encoding?: Encoding | undefined, context?: Context | undefined ) => BigInt = class extends ASTNodeImpl { readonly _tag = "BigInt" /** @internal */ getParser() { return fromRefinement(this, Predicate.isBigInt) } /** @internal */ matchPart(s: string, options: ParseOptions): bigint | undefined { if (!isStringBigIntRegExp.test(s)) return undefined const value = globalThis.BigInt(s) if (options.disableChecks || !this.checks) return value return collectIssues(this.checks, value, undefined, this, options) ? undefined : value } /** @internal */ toCodecStringTree(): AST { return replaceEncoding(this, [bigIntToString]) } /** @internal */ getExpected(): string { return "bigint" } } /** * Provides the singleton {@link BigInt} AST instance. * * **When to use** * * Use to reuse the canonical `BigInt` AST node when constructing, inspecting, * or transforming schemas at the AST level. * * @see {@link BigInt} for the AST node class and string-codec behavior * @see {@link isBigInt} for narrowing an AST to a `BigInt` node * * @stability stable * @category constructors * @since 4.0.0 */ export const bigInt = new BigInt() /** * AST node for array-like types — both tuples and arrays. * * **When to use** * * Use when constructing or inspecting AST nodes for tuple or array-like schemas, * including rest elements. * * **Details** * * - `elements` — positional element types (tuple elements). An element is * optional if its context's `isOptional` field is `true`. * - `rest` — the rest/variadic element types. When non-empty, the first * entry is the "spread" type (e.g. `...Array`), and subsequent * entries are trailing positional elements after the spread. * - `isMutable` — whether the resulting array is `readonly` (`false`) or * mutable (`true`). * * **Gotchas** * * Construction enforces TypeScript ordering rules: a required element * cannot follow an optional one, and an optional element cannot follow a * rest element. * * **Example** (Inspecting a tuple AST) * * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.Tuple([Schema.String, Schema.Number]) * const ast = schema.ast * * if (SchemaAST.isArrays(ast)) { * [ast.elements.length, ast.rest.length] // => [2, 0] * } * ``` * * @see {@link isArrays} * @see {@link Objects} * @stability stable * @category models * @since 4.0.0 */ export interface Arrays extends ASTNode { readonly _tag: "Arrays" readonly isMutable: boolean readonly elements: ReadonlyArray readonly rest: ReadonlyArray readonly encodingChecks: Checks | undefined /** @internal */ getParser( compile: SchemaParser.Compiler, compileField?: SchemaParser.Compiler ): SchemaParser.Parser /** @internal */ recur(recur: (ast: AST) => AST): Arrays /** @internal */ flip(recur: (ast: AST) => AST): Arrays /** @internal */ getExpected(): string } /** * Constructs a {@link Arrays}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Arrays: new( isMutable: boolean, elements: ReadonlyArray, rest: ReadonlyArray, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context, encodingChecks?: Checks ) => Arrays = class extends ASTNodeImpl { readonly _tag = "Arrays" readonly isMutable: boolean readonly elements: ReadonlyArray readonly rest: ReadonlyArray readonly encodingChecks: Checks | undefined constructor( isMutable: boolean, elements: ReadonlyArray, rest: ReadonlyArray, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context, encodingChecks?: Checks ) { super(annotations, checks, encoding, context) this.isMutable = isMutable this.elements = elements this.rest = rest this.encodingChecks = encodingChecks let hasOptional = false for (let i = 0; i < elements.length; i++) { if (isOptional(elements[i])) { hasOptional = true } else if (hasOptional) { throw new Error("A required element cannot follow an optional element. ts(1257)") } } if (hasOptional && rest.length > 1) { throw new Error("A required element cannot follow an optional element. ts(1257)") } // An optional element cannot follow a rest element.ts(1266) for (let i = 1; i < rest.length; i++) { if (isOptional(rest[i])) { throw new Error("An optional element cannot follow a rest element. ts(1266)") } } } /** @internal */ getParser( compile: SchemaParser.Compiler, compileField: SchemaParser.Compiler = compile ): SchemaParser.Parser { // oxlint-disable-next-line @typescript-eslint/no-this-alias const ast = this type ElementParser = { readonly ast: AST; readonly parser: SchemaParser.Parser } let elements: Array | undefined let rest: Array | undefined const elementLen = ast.elements.length const tailLen = Math.max(0, ast.rest.length - 1) function getParser( tailThreshold: number, index: number ): { readonly ast: AST; readonly parser: SchemaParser.Parser } { if (index < elementLen) { return elements![index] } else if (index >= tailThreshold) { return rest![index - tailThreshold + 1] } return rest![0] } const finish = (state: ArrayParserState): Effect.Effect => { const { input, len, options } = state if (ast.rest.length === 0 && len > elementLen) { for (let i = elementLen; i < len; i++) { const unexpected = new SchemaIssue.UnexpectedKey(ast, input[i], options) const issue = new SchemaIssue.Pointer([i], unexpected) if (options.errors === "all") { if (state.issues) state.issues.push(issue) else state.issues = [issue] } else { return Effect.fail(new SchemaIssue.Composite(ast, [issue], input, options)) } } } if (state.issues) { return Effect.fail(new SchemaIssue.Composite(ast, state.issues, input, options)) } return InternalParser.succeed(state.output) } const parse = ( input: ReadonlyArray, options: ParseOptions ): Effect.Effect => { const len = input.length const state: ArrayParserState = { ast, getParser, input, len, tailThreshold: Math.max(elementLen, len - tailLen), output: [], issues: undefined, options } const end = ast.rest.length === 0 ? elementLen : Math.max(len, elementLen + tailLen) const concurrency = options.concurrency === undefined ? 1 : resolveConcurrency(options.concurrency) const eff = concurrency === 1 ? parseArray(state, input, 0, end) : parseArrayConcurrent(state, input, { concurrency, end }) if (!eff) return finish(state) if (effectIsExit(eff)) return Effect.flatMapEager(eff, () => finish(state)) // Reparse on later runs to avoid reusing mutable traversal state. let first = true return Effect.suspend(() => { if (!first) return parse(input, options) first = false return Effect.flatMap(eff, () => finish(state)) }) } return (input, options) => { if (input === InternalParser.missing) return InternalParser.missingExit try { if (!Array.isArray(input)) { return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } if (!elements) { elements = ast.elements.map((ast) => ({ ast, parser: compileField(ast) })) rest = ast.rest.map((ast) => ({ ast, parser: compileField(ast) })) } return parse(input, options) } catch (error) { return Effect.die(error) } } } private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) { const elements = mapOrSame(this.elements, recur) const rest = mapOrSame(this.rest, recur) return elements === this.elements && rest === this.rest && checks === this.checks && encodingChecks === this.encodingChecks ? this : new Arrays( this.isMutable, elements, rest, this.annotations, checks, undefined, this.context, encodingChecks ) } /** @internal */ recur(recur: (ast: AST) => AST) { return this._rebuild(recur, this.checks, this.encodingChecks) } /** @internal */ flip(recur: (ast: AST) => AST) { return this._rebuild(recur, this.encodingChecks, this.checks) } /** @internal */ getExpected(): string { return "array" } } type ArrayParserState = { readonly ast: AST readonly input: ReadonlyArray readonly len: number readonly getParser: ( tailThreshold: number, index: number ) => { readonly ast: AST; readonly parser: SchemaParser.Parser } readonly tailThreshold: number readonly options: ParseOptions readonly output: Array issues: Arr.NonEmptyArray | undefined } /** @internal */ export function stepArray( s: ArrayParserState, item: unknown, exit: Exit.Exit, i: number ) { if (exit._tag === "Failure") { return wrapPropertyKeyIssue(s, s.ast, i, exit) } const value = exit === InternalParser.sameExit ? item : (exit as InternalParser.Success)[InternalParser.args] if (value !== InternalParser.missing) { s.output[i] = value } else { const p = s.getParser(s.tailThreshold, i) if (isOptional(p.ast)) return const issue = new SchemaIssue.Pointer([i], new SchemaIssue.MissingKey(p.ast.context?.annotations)) if (s.options.errors === "all") { if (s.issues) s.issues.push(issue) else s.issues = [issue] } else { return Exit.fail( new SchemaIssue.Composite(s.ast, [issue], s.input, s.options) ) } } } const parseArrayOptions = { onItem(s: ArrayParserState, item: unknown, i: number) { const value = i < s.len ? item : InternalParser.missing return s.getParser(s.tailThreshold, i).parser(value, s.options) }, step: stepArray } /** @internal */ export const parseArray = iterateEager()(parseArrayOptions) const parseArrayConcurrent = iterateConcurrent()(parseArrayOptions) const wrapPropertyKeyIssue = ( s: { readonly input: unknown readonly options: ParseOptions issues: Array | undefined }, ast: AST, key: PropertyKey, exit: Exit.Failure ) => { if (exit.cause.reasons.length === 0) { return exit } const issue = InternalSchemaCause.getSchemaIssue(exit.cause) if (issue === undefined) { return Exit.failCause( Cause.map( exit.cause, (issue) => new SchemaIssue.Composite( ast, [new SchemaIssue.Pointer([key], issue)], s.input, s.options ) ) ) } const pointer = new SchemaIssue.Pointer([key], issue) if (s.options.errors === "all") { if (s.issues) s.issues.push(pointer) else s.issues = [pointer] } else { return Exit.fail( new SchemaIssue.Composite(ast, [pointer], s.input, s.options) ) } } /** * floating point or integer, with optional exponent * * The mantissa alternation keeps rejection linear: `\d*\.?\d+` retries * every split of a long digit run. * @internal */ export const FINITE_PATTERN = "[+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:[Ee][+-]?\\d+)?" /** * Returns the object keys that match the index signature parameter schema. * @internal */ export function getIndexSignatureKeys( input: { readonly [x: PropertyKey]: unknown }, parameter: IndexSignatureParameter, options: ParseOptions = defaultParseOptions ): ReadonlyArray { let stringKeys: ReadonlyArray | undefined let symbolKeys: ReadonlyArray | undefined function go(parameter: AST): ReadonlyArray { switch (parameter._tag) { case "String": case "TemplateLiteral": return (stringKeys ??= Object.keys(input)).filter((k) => parameter.matchPart(k, options) !== undefined) case "Number": return (stringKeys ??= Object.keys(input)).filter((k) => parameter.matchKey(k, options) !== undefined) case "Symbol": return (symbolKeys ??= Object.getOwnPropertySymbols(input)).filter((k) => Object.prototype.propertyIsEnumerable.call(input, k) && parameter.matchKey(k, options) !== undefined ) case "Union": return [...new Set(parameter.types.flatMap(go))] default: return [] } } return go(parameterFromPropertyKey(toEncoded(parameter))) } /** * Represents a named property within an {@link Objects} node. * * **Details** * * Pairs a `name` (any `PropertyKey`) with a `type` ({@link AST}). The * property's optionality and mutability are determined by the `type`'s * {@link Context}. * * @see {@link Objects} * @stability stable * @category models * @since 4.0.0 */ export interface PropertySignature { readonly name: PropertyKey readonly type: AST } /** * Constructs a {@link PropertySignature}. * * @stability stable * @category constructors * @since 4.0.0 */ export const PropertySignature: new(name: PropertyKey, type: AST) => PropertySignature = class { readonly name: PropertyKey readonly type: AST constructor( name: PropertyKey, type: AST ) { this.name = name this.type = type } } type IndexSignatureParameter = | String | Number | Symbol | TemplateLiteral | Union function isIndexSignatureParameterSide(ast: AST): ast is IndexSignatureParameter { switch (ast._tag) { case "String": case "Number": case "Symbol": case "TemplateLiteral": return true case "Union": return ast.types.every(isIndexSignatureParameterSide) default: return false } } function isIndexSignatureParameterEncodedSide(ast: AST): boolean { const encoded = getLastEncoding(ast) switch (encoded._tag) { case "String": case "Number": case "Symbol": case "TemplateLiteral": return true case "Union": return encoded.types.every(isIndexSignatureParameterEncodedSide) default: return false } } function isIndexSignatureParameter(ast: AST): ast is IndexSignatureParameter { return isIndexSignatureParameterSide(ast) && isIndexSignatureParameterEncodedSide(ast) } /** * Represents an index signature entry within an {@link Objects} node. * * **When to use** * * Use when constructing or inspecting object AST entries for record-like keys * and values. * * **Details** * * - `parameter` — the key type AST (e.g. {@link String} for `string` keys, * {@link TemplateLiteral} for patterned keys). * - `type` — the value type SchemaAST. * * **Gotchas** * * Using `Schema.optionalKey` on the value type is not allowed for index * signatures (throws at construction); use `Schema.optional` instead. * * @see {@link Objects} * @see {@link PropertySignature} * @stability stable * @category models * @since 4.0.0 */ export interface IndexSignature { readonly parameter: IndexSignatureParameter readonly type: AST } /** * Constructs a {@link IndexSignature}. * * @stability stable * @category constructors * @since 4.0.0 */ export const IndexSignature: new(parameter: AST, type: AST) => IndexSignature = class { readonly parameter: IndexSignatureParameter readonly type: AST constructor( parameter: AST, type: AST ) { if (!isIndexSignatureParameter(parameter)) { throw new Error(`Invalid index signature parameter ${parameter._tag}`) } this.parameter = parameter this.type = type if (isOptional(type) && !containsUndefined(type)) { throw new Error("Cannot use `Schema.optionalKey` with index signatures, use `Schema.optional` instead.") } } } /** * AST node for object-like schemas, including structs and records. * * **When to use** * * Use when constructing or inspecting AST nodes for structs or records rather * than array-like schemas. * * **Details** * * - `propertySignatures` — named properties with their types (struct fields). * - `indexSignatures` — index signature entries (record patterns), each with * a `parameter` AST for matching keys and a `type` AST for values. * * An `Objects` node with no properties and no index signatures performs only a * non-nullish check: it accepts any value except `null` and `undefined`, * including primitive values. * * **Gotchas** * * Duplicate property names throw at construction time. * * **Example** (Inspecting a struct AST) * * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.Struct({ name: Schema.String }) * const ast = schema.ast * * if (SchemaAST.isObjects(ast)) { * ast.propertySignatures.map((ps) => [ps.name, ps.type._tag]) // => [["name", "String"]] * } * ``` * * @see {@link isObjects} * @see {@link PropertySignature} * @see {@link IndexSignature} * @see {@link Arrays} * @stability stable * @category models * @since 4.0.0 */ export interface Objects extends ASTNode { readonly _tag: "Objects" readonly propertySignatures: ReadonlyArray readonly indexSignatures: ReadonlyArray readonly encodingChecks: Checks | undefined /** @internal */ getParser( compile: SchemaParser.Compiler, compileField?: SchemaParser.Compiler ): SchemaParser.Parser /** @internal */ flip(recur: (ast: AST) => AST): AST /** @internal */ recur(recur: (ast: AST) => AST, recurParameter?: (ast: AST) => AST): AST /** @internal */ getExpected(): string } /** * Constructs a {@link Objects}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Objects: new( propertySignatures: ReadonlyArray, indexSignatures: ReadonlyArray, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context, encodingChecks?: Checks ) => Objects = class extends ASTNodeImpl { readonly _tag = "Objects" readonly propertySignatures: ReadonlyArray readonly indexSignatures: ReadonlyArray readonly encodingChecks: Checks | undefined constructor( propertySignatures: ReadonlyArray, indexSignatures: ReadonlyArray, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context, encodingChecks?: Checks ) { super(annotations, checks, encoding, context) this.propertySignatures = propertySignatures this.indexSignatures = indexSignatures this.encodingChecks = encodingChecks } /** @internal */ getParser( compile: SchemaParser.Compiler, compileField: SchemaParser.Compiler = compile ): SchemaParser.Parser { // oxlint-disable-next-line @typescript-eslint/no-this-alias const ast = this const hasProperties = ast.propertySignatures.length const indexCount = ast.indexSignatures.length // --------------------------------------------- // handle empty struct // --------------------------------------------- if (!hasProperties && !indexCount) { return fromRefinement(ast, Predicate.isNotNullish) } let properties: Array | undefined let indexes: | Array<{ readonly is: IndexSignature readonly parserKey: SchemaParser.Parser readonly parserValue: SchemaParser.Parser }> | undefined type Index = NonNullable[number] const compileMembers = (): Array => { if (!properties) { properties = ast.propertySignatures.map((ps) => ({ parser: compileField(ps.type), name: ps.name, type: ps.type })) indexes = indexCount ? ast.indexSignatures.map((is) => ({ is, parserKey: compile(parameterFromPropertyKey(is.parameter)), parserValue: compileField(is.type) })) : undefined } return properties } const makeFallback = (): SchemaParser.Parser => { const expectedKeys = new Set( ast.propertySignatures.map((ps) => typeof ps.name === "number" ? globalThis.String(ps.name) : ps.name) ) const finishIndex = ( s: ObjectParserState, key: PropertyKey, k2: PropertyKey | typeof InternalParser.missing, inputValue: unknown, exitValue: Exit.Exit ): Effect.Effect => { if (exitValue._tag === "Failure") { return wrapPropertyKeyIssue(s, ast, key, exitValue) ?? Exit.void } const value = exitValue === InternalParser.sameExit ? inputValue : (exitValue as InternalParser.Success)[InternalParser.args] if (k2 !== InternalParser.missing && value !== InternalParser.missing) { if ( hasProperties && (expectedKeys.has(key) || expectedKeys.has(typeof k2 === "number" ? globalThis.String(k2) : k2)) ) return Exit.void InternalRecord.assignProperty(s.out, k2, value) } return Exit.void } const parseIndex = ( s: ObjectParserState, key: PropertyKey, index: Index, exitKey?: Exit.Exit ): Effect.Effect => { if (!exitKey) { const eff = index.parserKey(key, s.options) if (!effectIsExit(eff)) { return Effect.flatMap(Effect.exit(eff), (exit) => parseIndex(s, key, index, exit)) } exitKey = eff } if (exitKey._tag === "Failure") { return wrapPropertyKeyIssue(s, ast, key, exitKey) ?? Exit.void } const k2 = exitKey === InternalParser.sameExit ? key : (exitKey as InternalParser.Success)[InternalParser.args] const inputValue = s.input[key] const result = index.parserValue(inputValue, s.options) return effectIsExit(result) ? finishIndex(s, key, k2, inputValue, result) : Effect.flatMap(Effect.exit(result), (exit) => finishIndex(s, key, k2, inputValue, exit)) } const parseStringIndex = ( s: ObjectParserState, key: PropertyKey, index: Index ): Effect.Effect => { const inputValue = s.input[key] const result = index.parserValue(inputValue, s.options) return effectIsExit(result) ? finishIndex(s, key, key, inputValue, result) : Effect.flatMap(Effect.exit(result), (exit) => finishIndex(s, key, key, inputValue, exit)) } const parseIndexes = indexCount ? iterateConcurrent()({ onItem: (s, [key, index]) => index.is.parameter === string ? parseStringIndex(s, key, index) : parseIndex(s, key, index), step: (_s, _item, exit) => exit._tag === "Failure" ? exit : undefined }) : undefined return Effect.fnUntracedEager(function*(input, options) { if (input === InternalParser.missing) { return InternalParser.missing } // If the input is not a record, return early with an error if (!(typeof input === "object" && input !== null && !Array.isArray(input))) { return yield* Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } compileMembers() const record = input as Record const out: Record = {} const state = { ast, input: record, out, issues: undefined as Arr.NonEmptyArray | undefined, options } const errorsAllOption = options.errors === "all" const onExcessPropertyError = options.onExcessProperty === "error" const concurrency = options.concurrency === undefined ? 1 : resolveConcurrency(options.concurrency) // --------------------------------------------- // handle excess properties // --------------------------------------------- const indexKeys = indexCount && onExcessPropertyError ? ast.indexSignatures.map((index) => getIndexSignatureKeys(record, index.parameter, options)) : undefined if (onExcessPropertyError) { const coveredKeys = indexKeys ? new Set(expectedKeys) : expectedKeys if (indexKeys) { for (const keys of indexKeys) { for (const key of keys) coveredKeys.add(key) } } // Only enumerable own properties can be excess. Declared fields are // parsed separately, regardless of their enumerability. const inputKeys = Reflect.ownKeys(record) for (let i = 0; i < inputKeys.length; i++) { const key = inputKeys[i] if (!coveredKeys.has(key) && Object.prototype.propertyIsEnumerable.call(record, key)) { // key is unexpected const unexpected = new SchemaIssue.UnexpectedKey(ast, record[key], options) const issue = new SchemaIssue.Pointer([key], unexpected) if (errorsAllOption) { if (state.issues) { state.issues.push(issue) } else { state.issues = [issue] } continue } else { return yield* Effect.fail( new SchemaIssue.Composite(ast, [issue], input, options) ) } } } } // --------------------------------------------- // handle property signatures // --------------------------------------------- if (hasProperties) { const eff = concurrency === 1 ? parseProperties(state, properties!) : parsePropertiesConcurrent(state, properties!, { concurrency }) if (eff) yield* eff } // --------------------------------------------- // handle index signatures // --------------------------------------------- if (indexCount && concurrency === 1) { for (let i = 0; i < indexCount; i++) { const index = indexes![i] const parse = index.is.parameter === string ? parseStringIndex : parseIndex const keys = indexKeys?.[i] ?? (index.is.parameter === string ? Object.keys(record) : getIndexSignatureKeys(record, index.is.parameter, options)) for (let j = 0; j < keys.length; j++) { const eff = parse(state, keys[j], index) if (!effectIsExit(eff)) yield* eff else if (eff._tag === "Failure") return yield* eff as Exit.Exit } } } else if (parseIndexes) { const keyPairs = Arr.empty() for (let i = 0; i < indexCount; i++) { const index = indexes![i] const keys = indexKeys?.[i] ?? (index.is.parameter === string ? Object.keys(record) : getIndexSignatureKeys(record, index.is.parameter, options)) for (let j = 0; j < keys.length; j++) { keyPairs.push([keys[j], index]) } } const eff = parseIndexes(state, keyPairs, { concurrency }) if (eff) yield* eff } if (state.issues) { return yield* Effect.fail( new SchemaIssue.Composite(ast, state.issues, input, options) ) } return out }) } if (indexCount) return makeFallback() let fallback: SchemaParser.Parser | undefined // Resumes at the property whose parser suspended, without replaying the // properties already parsed. const resume = ( snapshot: ObjectParserState, index: number, pending: Effect.Effect ): Effect.Effect => { const property = properties![index] return Effect.flatMap(Effect.exit(pending), (exit) => { // Copy the parsed prefix per execution so reruns cannot overwrite earlier results. const state: ObjectParserState = { ...snapshot, out: { ...snapshot.out } } const terminal = stepProperty(state, property, exit) if (terminal) return terminal const done = () => InternalParser.succeed(state.out) const eff = parseProperties(state, properties!.slice(index + 1)) return eff ? Effect.flatMapEager(eff, done) : done() }) } // Fast path: a struct without index signatures, under the default parse // options, needs none of the generator the fallback runs per value. return (input, options) => { if (input === InternalParser.missing) return InternalParser.missingExit if ( options.errors === "all" || options.onExcessProperty !== undefined || (options.concurrency !== undefined && resolveConcurrency(options.concurrency) !== 1) ) { return (fallback ??= makeFallback())(input, options) } if (!(typeof input === "object" && input !== null && !Array.isArray(input))) { return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } const props = compileMembers() const record = input as Record const out: Record = {} const state: ObjectParserState = { ast, input: record, out, issues: undefined, options } try { for (let index = 0; index < props.length; index++) { const property = props[index] const name = property.name const hasKey = hasPropertySignature(record, name) const value = hasKey ? record[name] : InternalParser.missing const exit = property.parser(value, options) if (!effectIsExit(exit)) { return resume(state, index, exit) } if (exit === InternalParser.sameExit) { if (hasKey) InternalRecord.assignProperty(out, name, value) continue } const terminal = stepProperty(state, property, exit) if (terminal) return terminal } } catch (error) { return Effect.die(error) } return InternalParser.succeed(out) } } private _rebuild( recur: (ast: AST) => AST, recurParameter: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined ): Objects { const props = mapOrSame(this.propertySignatures, (ps) => { const t = recur(ps.type) return t === ps.type ? ps : new PropertySignature(ps.name, t) }) const indexes = mapOrSame(this.indexSignatures, (is) => { const p = recurParameter(is.parameter) const t = recur(is.type) return p === is.parameter && t === is.type ? is : new IndexSignature(p, t) }) return props === this.propertySignatures && indexes === this.indexSignatures && checks === this.checks && encodingChecks === this.encodingChecks ? this : new Objects( props, indexes, this.annotations, checks, undefined, this.context, encodingChecks ) } /** @internal */ flip(recur: (ast: AST) => AST): AST { return this._rebuild(recur, recur, this.encodingChecks, this.checks) } /** @internal */ recur(recur: (ast: AST) => AST, recurParameter: (ast: AST) => AST = recur): AST { return this._rebuild(recur, recurParameter, this.checks, this.encodingChecks) } /** @internal */ getExpected(): string { if (this.propertySignatures.length === 0 && this.indexSignatures.length === 0) return "object | array" return "object" } } type ObjectParserState = { readonly ast: Objects readonly input: Record readonly options: ParseOptions readonly out: Record issues: Arr.NonEmptyArray | undefined } type ParsedProperty = { readonly parser: SchemaParser.Parser readonly name: PropertyKey readonly type: AST } /** @internal */ export function stepProperty( s: ObjectParserState, p: ParsedProperty, exit: Exit.Exit ): Exit.Exit | void { if (exit._tag === "Failure") { return wrapPropertyKeyIssue(s, s.ast, p.name, exit) } if (exit === InternalParser.sameExit) return const value = (exit as InternalParser.Success)[InternalParser.args] if (value !== InternalParser.missing) { InternalRecord.assignProperty(s.out, p.name, value) return } delete s.out[p.name] if (!isOptional(p.type)) { const issue = new SchemaIssue.Pointer([p.name], new SchemaIssue.MissingKey(p.type.context?.annotations)) if (s.options.errors === "all") { if (s.issues) s.issues.push(issue) else s.issues = [issue] return } else { return Exit.fail( new SchemaIssue.Composite(s.ast, [issue], s.input, s.options) ) } } } const parsePropertiesOptions = { onItem(s: ObjectParserState, p: ParsedProperty) { if (!hasPropertySignature(s.input, p.name)) { return p.parser(InternalParser.missing, s.options) } const value = s.input[p.name] InternalRecord.assignProperty(s.out, p.name, value) return p.parser(value, s.options) }, step: stepProperty } /** @internal */ export const parseProperties = iterateEager()(parsePropertiesOptions) const parsePropertiesConcurrent = iterateConcurrent()(parsePropertiesOptions) function combineChecks(a: Checks | undefined, b: Checks | undefined): Checks | undefined { if (!a) return b if (!b) return a return [...a, ...b] } /** @internal */ export function struct( fields: Fields, checks: Checks | undefined, annotations?: Schema.Annotations.Annotations ): Objects { return new Objects( Reflect.ownKeys(fields).map((key) => { return new PropertySignature(key, fields[key].ast) }), [], annotations, checks ) } /** @internal */ export function getAST(self: S): S["ast"] { return self.ast } /** @internal */ export function tuple( elements: Elements, checks: Checks | undefined = undefined ): Arrays { return new Arrays(false, elements.map((e) => e.ast), [], undefined, checks) } /** @internal */ export function union>( members: Members, options: UnionOptions | undefined, checks: Checks | undefined ): Union { return new Union(members.map(getAST), options, undefined, checks) } /** @internal */ export function structWithRest(ast: Objects, records: ReadonlyArray): Objects { if (ast.encoding || records.some((r) => r.encoding)) { throw new Error("StructWithRest does not support encodings") } let propertySignatures = ast.propertySignatures let indexSignatures = ast.indexSignatures let checks = ast.checks for (const record of records) { for (const propertySignature of record.propertySignatures) { if (propertySignatures.some((ps) => ps.name === propertySignature.name)) { throw new Error(`Duplicate identifier: ${JSON.stringify(propertySignature.name)}. ts(2300)`) } } propertySignatures = propertySignatures.concat(record.propertySignatures) indexSignatures = indexSignatures.concat(record.indexSignatures) checks = combineChecks(checks, record.checks) } return new Objects(propertySignatures, indexSignatures, undefined, checks) } /** @internal */ export function tupleWithRest(ast: Arrays, rest: ReadonlyArray): Arrays { if (ast.encoding) { throw new Error("TupleWithRest does not support encodings") } return new Arrays(ast.isMutable, ast.elements, rest, undefined, ast.checks) } /** @internal */ export function mutable(ast: Arrays): Arrays { if (ast.encoding) { throw new Error("mutable does not support encodings") } return new Arrays( true, ast.elements, ast.rest, ast.annotations, ast.checks, undefined, ast.context, ast.encodingChecks ) } type Type = | "null" | "array" | "object" | "string" | "number" | "boolean" | "symbol" | "undefined" | "bigint" | "function" /** @internal */ export type Sentinel = { readonly key: PropertyKey readonly literal: LiteralValue | symbol } const toCandidate = memoizeIdempotent((ast: AST): AST => { while (true) { if (isSuspend(ast)) return unknown const encoding = ast.encoding if (!encoding) { // Index signature parameters do not participate in union selection. return (ast as any).recur?.(toCandidate, identity) ?? ast } if ( encoding.some((link) => link.transformation._tag === "Middleware" && link.transformation.decode !== identity) ) return unknown ast = encoding[encoding.length - 1].to } }) function getCandidateTypes(ast: AST): ReadonlyArray { switch (ast._tag) { case "Null": return ["null"] case "Undefined": return ["undefined"] case "String": case "TemplateLiteral": return ["string"] case "Number": return ["number"] case "Boolean": return ["boolean"] case "Symbol": case "UniqueSymbol": return ["symbol"] case "BigInt": return ["bigint"] case "Arrays": return ["array"] case "ObjectKeyword": return ["object", "array", "function"] case "Objects": return ast.propertySignatures.length || ast.indexSignatures.length ? ["object"] : ["string", "number", "boolean", "symbol", "bigint", "object", "array", "function"] case "Enum": return Array.from(new Set(ast.enums.map(([, v]) => typeof v))) case "Literal": return [typeof ast.literal] case "Union": return Array.from(new Set(ast.types.flatMap(getCandidateTypes))) default: return [ "null", "undefined", "string", "number", "boolean", "symbol", "bigint", "object", "array", "function" ] } } /** @internal */ export function collectSentinels(ast: AST): ReadonlyArray { switch (ast._tag) { default: return [] case "Declaration": { const s = ast.annotations?.[InternalAnnotations.SENTINELS_ANNOTATION_KEY] return Array.isArray(s) ? s : [] } case "Objects": return ast.propertySignatures.flatMap((ps): Array => { const type = ps.type if (!isOptional(type)) { if (isLiteral(type)) { return [{ key: ps.name, literal: type.literal }] } if (isUniqueSymbol(type)) { return [{ key: ps.name, literal: type.symbol }] } } return [] }) case "Arrays": return ast.elements.flatMap((e, i): Array => { if (!isOptional(e)) { if (isLiteral(e)) { return [{ key: i, literal: e.literal }] } if (isUniqueSymbol(e)) { return [{ key: i, literal: e.symbol }] } } return [] }) case "Union": { if (ast.types.length === 0) return [] const members = ast.types.map((type) => collectSentinels(toCandidate(type))) return members[0].filter((s) => members.every((sentinels) => sentinels.some((o) => o.key === s.key && o.literal === s.literal)) ) } case "Suspend": return collectSentinels(ast.thunk()) } } /** @internal */ export type CandidateIndex = (input: any, isConstructor: boolean) => ReadonlyArray type SentinelEntry = readonly [ byValue: Map>, all: Set ] type SentinelIndex = Map const candidateIndexCache = new WeakMap, CandidateIndex>() const emptyCandidates: ReadonlyArray = Object.freeze([]) const getRuntimeType = (input: unknown): Type => input === null ? "null" : Array.isArray(input) ? "array" : typeof input const hasPropertySignature = (input: object, key: PropertyKey): boolean => key === "__proto__" ? Object.hasOwn(input, key) : key in input /** @internal */ export function getCandidateIndex(types: ReadonlyArray): CandidateIndex { let index = candidateIndexCache.get(types) if (index) return index let bySentinel: SentinelIndex | undefined let sentinelCandidateCount = 0 let otherwise: { [K in Type]?: Array } | undefined let literalCandidates: Map> | undefined let onlyLiterals = true const literalOf: Array = [] for (let i = 0; i < types.length; i++) { const a = types[i] const encoded = toCandidate(a) if (isNever(encoded)) continue if (isLiteral(encoded) || isUniqueSymbol(encoded)) { literalCandidates ??= new Map() const literal = isLiteral(encoded) ? encoded.literal : encoded.symbol literalOf[i] = literal let arr = literalCandidates.get(literal) if (!arr) literalCandidates.set(literal, arr = []) arr.push(i) } else { onlyLiterals = false } const sentinels = collectSentinels(encoded) if (sentinels.length) { // discriminated variants bySentinel ??= new Map() sentinelCandidateCount++ for (const { key, literal } of sentinels) { let entry = bySentinel.get(key) if (!entry) bySentinel.set(key, entry = [new Map(), new Set()]) entry[1].add(i) let indexes = entry[0].get(literal) if (!indexes) entry[0].set(literal, indexes = new Set()) indexes.add(i) } } else { // non-discriminated otherwise ??= {} const candidateTypes = getCandidateTypes(encoded) for (const t of candidateTypes) (otherwise[t] ??= []).push(i) } } // Non-discriminated members are grouped by runtime type once and reused by every decode. const fallbacks: { [K in Type]?: ReadonlyArray } = {} const getFallback = (type: Type): ReadonlyArray => fallbacks[type] ??= Object.freeze(otherwise?.[type] ?? emptyCandidates) if (onlyLiterals && literalCandidates) { literalCandidates.forEach(Object.freeze) index = (input) => literalCandidates.get(input) ?? emptyCandidates } else if (bySentinel?.size === 1 && !otherwise) { const [key, [byValue]] = bySentinel.entries().next().value! const candidates = new Map>() for (const [literal, indexes] of byValue) { candidates.set(literal, Object.freeze(Array.from(indexes))) } const all = Object.freeze(types.map((_, i) => i)) index = (input, isConstructor) => { if (Predicate.isObjectKeyword(input)) { const value = hasPropertySignature(input, key) ? (input as any)[key] : undefined if (value !== undefined) return candidates.get(value) ?? emptyCandidates if (isConstructor) return all } return emptyCandidates } } else if (bySentinel) { // A key owned by every discriminated candidate is safe to use as the initial selector: no candidate can // be excluded merely because it uses a different sentinel key. Prefer the key with the most distinct values // to minimize the matching bucket. let commonSentinel: [PropertyKey, SentinelEntry] | undefined for (const entry of bySentinel) { if ( (!commonSentinel || entry[1][0].size > commonSentinel[1][0].size) && entry[1][1].size === sentinelCandidateCount ) { commonSentinel = entry } } index = (input, isConstructor) => { const runtimeType = getRuntimeType(input) if (!Predicate.isObjectKeyword(input)) return getFallback(runtimeType) // Non-discriminated candidates are runtime-type fallbacks and are never removed by sentinel checks. const selected = new Set(otherwise?.[runtimeType]) let directKey: PropertyKey | undefined // An observed common key can seed the selection directly; an unknown value rules out every // discriminated candidate. if (commonSentinel) { const [key, [byValue]] = commonSentinel const hasKey = hasPropertySignature(input, key) const value = hasKey ? (input as any)[key] : undefined if (hasKey && (!isConstructor || value !== undefined)) { const match = byValue.get(value) if (!match) return getFallback(runtimeType) for (const i of match) selected.add(i) directKey = key } } // Without an observed common key, collect positive matches from every sentinel. Constructor mode treats // absent and undefined keys as unconstrained and therefore selects every candidate that owns the key. if (directKey === undefined) { for (const [key, [byValue, all]] of bySentinel) { const hasKey = hasPropertySignature(input, key) const value = hasKey ? (input as any)[key] : undefined if (hasKey && (!isConstructor || value !== undefined)) { const match = byValue.get(value) if (match) { for (const i of match) selected.add(i) } } else if (isConstructor) { for (const i of all) selected.add(i) } } } // Missing keys are neutral. An observed key rejects only selected candidates that own it and do not match. for (const [key, [byValue, all]] of bySentinel) { if (key === directKey) continue const hasKey = hasPropertySignature(input, key) const value = hasKey ? (input as any)[key] : undefined if (hasKey && (!isConstructor || value !== undefined)) { const match = byValue.get(value) for (const i of selected) { if (all.has(i) && !match?.has(i)) selected.delete(i) } } } return Array.from(selected).sort((a, b) => a - b) } } else { index = (input) => { const fallback = getFallback(getRuntimeType(input)) return literalCandidates ? fallback.filter((i) => literalOf[i] === undefined || literalOf[i] === input) : fallback } } candidateIndexCache.set(types, index) return index } /** * AST node representing a union of schemas. * * **Details** * * - `types` — the member AST nodes. * - `options.mode` selects matching behavior. `"anyOf"` succeeds on the first match; * `"oneOf"` requires exactly one member to match (fails if multiple do). * * During parsing, members are tried in order. An internal candidate index * narrows which members to try based on the runtime type of the input and * discriminant ("sentinel") fields, making large unions efficient. * * **Example** (Inspecting a union AST) * * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.Union([Schema.String, Schema.Number]) * const ast = schema.ast * * if (SchemaAST.isUnion(ast)) { * [ast.types.length, ast.options?.mode ?? "anyOf"] // => [2, "anyOf"] * } * ``` * * @see {@link isUnion} * @stability stable * @category models * @since 4.0.0 */ export interface Union extends ASTNode { readonly _tag: "Union" readonly types: ReadonlyArray readonly options: UnionOptions | undefined readonly encodingChecks: Checks | undefined /** @internal */ getParser(compile: SchemaParser.Compiler, compileField?: SchemaParser.Compiler): SchemaParser.Parser /** @internal */ recur(recur: (ast: AST) => AST): Union /** @internal */ flip(recur: (ast: AST) => AST): Union /** @internal */ matchPart(s: string, options: ParseOptions): LiteralValue | undefined /** @internal */ getExpected(getExpected: (ast: AST) => string): string } /** * Local matching options stored on {@link Union} nodes. * * @stability stable * @category options * @since 4.0.0 */ export interface UnionOptions { /** Defaults to `"anyOf"`; `"oneOf"` requires exactly one matching member. */ readonly mode?: "anyOf" | "oneOf" | undefined } /** * Constructs a {@link Union}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Union: new( types: ReadonlyArray, options?: UnionOptions, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context, encodingChecks?: Checks ) => Union = class extends ASTNodeImpl { readonly _tag = "Union" readonly types: ReadonlyArray readonly options: UnionOptions | undefined readonly encodingChecks: Checks | undefined constructor( types: ReadonlyArray, options?: UnionOptions, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context, encodingChecks?: Checks ) { super(annotations, checks, encoding, context) this.types = types this.options = options this.encodingChecks = encodingChecks } /** @internal */ getParser( compile: SchemaParser.Compiler, compileField?: SchemaParser.Compiler ): SchemaParser.Parser { // oxlint-disable-next-line @typescript-eslint/no-this-alias const ast = this const isConstructor = compileField !== undefined const parsers: Array = [] const parser = (i: number): SchemaParser.Parser => parsers[i] ??= compile(ast.types[i]) let index: CandidateIndex | undefined return (input, options) => { if (input === InternalParser.missing) { return InternalParser.missingExit } const candidates = (index ??= getCandidateIndex(ast.types))(input, isConstructor) if (candidates.length === 0) { return Effect.fail(new SchemaIssue.AnyOf(ast, [], input, options)) } if (candidates.length === 1) { const result = parser(candidates[0])(input, options) if ((result as Exit.Exit)._tag === "Success") return result return effectIsExit(result) ? failSingleUnionCandidate(ast, (result as Exit.Failure).cause, input, options) : catchSingleUnionCandidate(ast, result, input, options) } return parseUnionCandidates(ast, parser, candidates, input, options) } } private _rebuild( recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined ): Union { const types = mapOrSame(this.types, recur) return types === this.types && checks === this.checks && encodingChecks === this.encodingChecks ? this : new Union(types, this.options, this.annotations, checks, undefined, this.context, encodingChecks) } /** @internal */ recur(recur: (ast: AST) => AST): Union { return this._rebuild(recur, this.checks, this.encodingChecks) } /** @internal */ flip(recur: (ast: AST) => AST): Union { return this._rebuild(recur, this.encodingChecks, this.checks) } /** @internal */ matchPart(s: string, options: ParseOptions): LiteralValue | undefined { for (const type of this.types) { const out = (type as TemplateLiteralPart).matchPart(s, options) if (out !== undefined) return out } return undefined } /** @internal */ getExpected(getExpected: (ast: AST) => string): string { const expected = this.annotations?.expected if (typeof expected === "string") return expected if (this.types.length === 0) return "never" const types = this.types.map((type) => { const encoded = toEncoded(type) switch (encoded._tag) { case "Arrays": { const literals = encoded.elements.filter(isLiteral) if (literals.length > 0) { return `${formatIsMutable(encoded.isMutable)}[ ${ literals.map((e) => getExpected(e) + formatIsOptional(e.context?.isOptional)).join(", ") }, ... ]` } break } case "Objects": { const literals = encoded.propertySignatures.filter((ps) => isLiteral(ps.type)) if (literals.length > 0) { return `{ ${ literals.map((ps) => `${formatIsMutable(ps.type.context?.isMutable)}${formatPropertyKey(ps.name)}${ formatIsOptional(ps.type.context?.isOptional) }: ${getExpected(ps.type)}` ).join(", ") }, ... }` } break } } return getExpected(encoded) }) return Array.from(new Set(types)).join(" | ") } } function failSingleUnionCandidate( ast: Union, cause: Cause.Cause, input: unknown, options: ParseOptions ) { const issue = InternalSchemaCause.getSchemaIssue(cause) if (!issue) return Exit.failCause(cause) return Exit.fail(new SchemaIssue.AnyOf(ast, [issue], input, options)) } function catchSingleUnionCandidate( ast: Union, result: Effect.Effect, input: unknown, options: ParseOptions ) { return Effect.catchCause(result, (cause) => failSingleUnionCandidate(ast, cause, input, options)) } type UnionParserState = { readonly parser: (i: number) => SchemaParser.Parser readonly ast: Union readonly input: unknown readonly options: ParseOptions out: Exit.Success | undefined readonly successes: Array | undefined issues: Array | undefined } function parseUnionCandidates( ast: Union, parser: (i: number) => SchemaParser.Parser, candidates: ReadonlyArray, input: unknown, options: ParseOptions ): Effect.Effect { const state: UnionParserState = { ast, parser, input, out: undefined, successes: ast.options?.mode === "oneOf" ? [] : undefined, issues: undefined, options } const eff = parseUnion(state, candidates) if (!eff) { if (state.out) return state.out return Effect.fail(new SchemaIssue.AnyOf(ast, state.issues ?? [], input, options)) } if (effectIsExit(eff)) return resumeUnion(eff, state) // Reparse on later runs to avoid reusing mutable traversal state. let first = true return Effect.suspend(() => { if (!first) return parseUnionCandidates(ast, parser, candidates, input, options) first = false return resumeUnion(eff, state) }) } function resumeUnion( eff: Effect.Effect, state: UnionParserState ): Effect.Effect { return Effect.flatMapEager(eff, (_) => { if (state.out === InternalParser.sameExit) return Effect.succeed(state.input) if (state.out) return state.out return Effect.fail(new SchemaIssue.AnyOf(state.ast, state.issues ?? [], state.input, state.options)) }) } const parseUnion = iterateEager()({ onItem(s, i) { return s.parser(i)(s.input, s.options) }, step(s, i, exit) { if (exit._tag === "Failure") { const issue = InternalSchemaCause.getSchemaIssue(exit.cause) if (issue === undefined) { return exit } if (s.issues) s.issues.push(issue) else s.issues = [issue] } else { if (s.out && s.successes) { s.successes.push(s.ast.types[i]) return Exit.fail(new SchemaIssue.OneOf(s.ast, s.successes, s.input, s.options)) } s.out = exit if (s.successes) { s.successes.push(s.ast.types[i]) } else { return Exit.void } } } }) const nonFiniteLiterals = new Union([ new Literal("Infinity"), new Literal("-Infinity"), new Literal("NaN") ]) function formatIsMutable(isMutable: boolean | undefined): string { return isMutable ? "" : "readonly " } function formatIsOptional(isOptional: boolean | undefined): string { return isOptional ? "?" : "" } /** * AST node for lazy/recursive schemas. * * **Details** * * Wraps a thunk (`() => AST`) that is memoized on first call. Use this to * define recursive or mutually recursive schemas without infinite loops at * construction time. * * **Example** (Defining recursive schema ASTs) * * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * interface Category { * readonly name: string * readonly children: ReadonlyArray * } * * const Category = Schema.Struct({ * name: Schema.String, * children: Schema.Array(Schema.suspend((): Schema.Codec => Category)) * }) * * SchemaAST.isObjects(Category.ast) // => true * ``` * * @see {@link isSuspend} * @stability stable * @category models * @since 4.0.0 */ export interface Suspend extends ASTNode { readonly _tag: "Suspend" readonly thunk: () => AST /** @internal */ getParser(compile: SchemaParser.Compiler): SchemaParser.Parser /** @internal */ recur(recur: (ast: AST) => AST): Suspend /** @internal */ getExpected(getExpected: (ast: AST) => string): string } /** * Constructs a {@link Suspend}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Suspend: new( thunk: () => AST, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context ) => Suspend = class extends ASTNodeImpl { readonly _tag = "Suspend" readonly thunk: () => AST constructor( thunk: () => AST, annotations?: Schema.Annotations.Annotations, checks?: Checks, encoding?: Encoding, context?: Context ) { if (checks) { throw new Error("Cannot add checks to Suspend") } super(annotations, undefined, encoding, context) let ast: AST this.thunk = () => ast ??= thunk() } /** @internal */ getParser(compile: SchemaParser.Compiler): SchemaParser.Parser { let parser: SchemaParser.Parser return (input, options) => (parser ??= compile(this.thunk()))(input, options) } /** @internal */ recur(recur: (ast: AST) => AST) { return new Suspend( () => recur(this.thunk()), this.annotations, undefined, undefined, this.context ) } /** @internal */ getExpected(getExpected: (ast: AST) => string): string { return getExpected(this.thunk()) } } // ----------------------------------------------------------------------------- // Checks // ----------------------------------------------------------------------------- /** * Represents a single validation check attached to an AST node. * * **Details** * * - `run` — the validation function. Returns `undefined` on success, or an * `Issue` on failure. * - `annotations` — optional filter-level annotations (expected message, * representation, arbitrary constraint hints). * - `aborted` — when `true`, parsing stops immediately after this filter * fails (no further checks run). * * Use `.annotate()` to add metadata and `.abort()` to mark as aborting. * Combine with another check via `.and()` to form a {@link FilterGroup}. * * @see {@link FilterGroup} * @see {@link Check} * @see {@link isPattern} * @stability stable * @category models * @since 4.0.0 */ export interface Filter extends Pipeable.Pipeable { readonly _tag: "Filter" readonly run: (input: E, self: AST, options: ParseOptions) => SchemaIssue.Issue | undefined readonly annotations: Schema.Annotations.Filter | undefined /** * Whether the parsing process should be aborted after this check has failed. */ readonly aborted: boolean annotate(annotations: Schema.Annotations.Filter): Filter abort(): Filter and(other: Check, annotations?: Schema.Annotations.Filter): FilterGroup } /** * Constructs a {@link Filter}. * * @stability stable * @category constructors * @since 4.0.0 */ export const Filter: new( run: (input: E, self: AST, options: ParseOptions) => SchemaIssue.Issue | undefined, annotations?: Schema.Annotations.Filter | undefined, /** * Whether the parsing process should be aborted after this check has failed. */ aborted?: boolean ) => Filter = class extends Pipeable.Class { readonly _tag = "Filter" readonly run: (input: E, self: AST, options: ParseOptions) => SchemaIssue.Issue | undefined readonly annotations: Schema.Annotations.Filter | undefined /** * Whether the parsing process should be aborted after this check has failed. */ readonly aborted: boolean constructor( run: (input: E, self: AST, options: ParseOptions) => SchemaIssue.Issue | undefined, annotations: Schema.Annotations.Filter | undefined = undefined, /** * Whether the parsing process should be aborted after this check has failed. */ aborted: boolean = false ) { super() this.run = run this.annotations = annotations this.aborted = aborted } annotate(annotations: Schema.Annotations.Filter): Filter { return new Filter(this.run, { ...this.annotations, ...annotations }, this.aborted) } abort(): Filter { return new Filter(this.run, this.annotations, true) } and(other: Check, annotations?: Schema.Annotations.Filter): FilterGroup and(other: Check, annotations?: Schema.Annotations.Filter): FilterGroup { return new FilterGroup([this, other], annotations) } } /** * Represents a composite validation check grouping multiple {@link Check} values. * * **Details** * * Created by calling `.and()` on a {@link Filter} or another `FilterGroup`. * All inner checks are run; failures from aborted filters still stop * evaluation. * * @see {@link Filter} * @see {@link Check} * @stability stable * @category models * @since 4.0.0 */ export interface FilterGroup extends Pipeable.Pipeable { readonly _tag: "FilterGroup" readonly checks: readonly [ Check, ...Array> ] readonly annotations: Schema.Annotations.Filter | undefined annotate(annotations: Schema.Annotations.Filter): FilterGroup and(other: Check, annotations?: Schema.Annotations.Filter): FilterGroup } /** * Constructs a {@link FilterGroup}. * * @stability stable * @category constructors * @since 4.0.0 */ export const FilterGroup: new( checks: readonly [ Check, ...Array> ], annotations?: Schema.Annotations.Filter | undefined ) => FilterGroup = class extends Pipeable.Class { readonly _tag = "FilterGroup" readonly checks: readonly [Check, ...Array>] readonly annotations: Schema.Annotations.Filter | undefined constructor( checks: readonly [Check, ...Array>], annotations: Schema.Annotations.Filter | undefined = undefined ) { super() this.checks = checks this.annotations = annotations } annotate(annotations: Schema.Annotations.Filter): FilterGroup { return new FilterGroup(this.checks, { ...this.annotations, ...annotations }) } and(other: Check, annotations?: Schema.Annotations.Filter): FilterGroup and(other: Check, annotations?: Schema.Annotations.Filter): FilterGroup { return new FilterGroup([this, other], annotations) } } /** * A validation check — either a single {@link Filter} or a composite * {@link FilterGroup}. * * **Details** * * Stored in an AST node's {@link Checks} array. * * @see {@link Filter} * @see {@link FilterGroup} * @stability stable * @category models * @since 4.0.0 */ export type Check = Filter | FilterGroup /** @internal */ export function makeFilter( filter: (input: T, ast: AST, options: ParseOptions) => Schema.FilterOutput, annotations?: Schema.Annotations.Filter | undefined, aborted: boolean = false ): Filter { return new Filter( (input, ast, options) => SchemaIssue.normalizeFilterOutput(ast, filter(input, ast, options), input, options), annotations, aborted ) } /** @internal */ export function makeFilterByGuard( is: (value: E) => value is T, annotations?: Schema.Annotations.Filter ): Filter { return new Filter( (input: E, _ast, options) => is(input) ? undefined : new SchemaIssue.InvalidValue(undefined, input, options), annotations, true // after a guard, we always want to abort ) } /** @internal */ export function isFinite(annotations?: Schema.Annotations.Filter) { return makeFilter( (n: number) => globalThis.Number.isFinite(n), { expected: "a finite number", representation: { id: "effect/schema/isFinite", payload: null }, toJsonSchema: () => ({ type: "number" }), toCode: () => ({ runtime: "Schema.isFinite()" }), arbitraryConstraint: { number: "finite" }, ...annotations } ) } /** @internal */ export const finite = appendChecks(number, [isFinite()]) const numberToJson = new Link( new Union([finite, nonFiniteLiterals]), new SchemaTransformation.Transformation( SchemaGetter.Number(), SchemaGetter.transform((n) => globalThis.Number.isFinite(n) ? n : globalThis.String(n)) ) ) /** * Creates a {@link Filter} that validates strings by running `RegExp.test`. * * **When to use** * * Use when string validation should be represented as a schema `Filter` backed * by a regular expression. * * **Details** * * The filter can be used with `Schema.check` or attached directly to a * `String` AST node through checks. The regular expression is cloned and its * `lastIndex` is reset before each test, so global and sticky expressions are * deterministic and the provided regular expression is not mutated. The * regular expression source is stored in annotations for serialization and * arbitrary generation. * * **Gotchas** * * Arbitrary metadata preserves both `regExp.source` and `regExp.flags`. * Implementations that cannot consume all flags may still use the source as a * generation hint because the Schema filter validates every generated value. * JSON Schema has no way to carry JavaScript regular-expression flags. Unless * annotations provide `toJsonSchema`, the RegExp constraint is omitted from * JSON Schema export. * * **Example** (Validating an email pattern) * * ```ts import.meta.vitest * import { SchemaAST } from "effect" * * const emailFilter = SchemaAST.isPattern(/^[^@]+@[^@]+$/) * emailFilter.run("alice@example.com", SchemaAST.string, {}) // => undefined * emailFilter.run("invalid", SchemaAST.string, {})?._tag // => "InvalidValue" * ``` * * @see {@link Filter} * @stability stable * @category constructors * @since 4.0.0 */ export function isPattern(regExp: globalThis.RegExp, annotations?: Schema.Annotations.Filter) { const copy = new globalThis.RegExp(regExp) const payload = { source: copy.source, flags: copy.flags } return makeFilter( (s: string) => { copy.lastIndex = 0 return copy.test(s) }, { expected: `a string matching the RegExp ${payload.source}`, representation: { id: "effect/schema/isPattern", payload }, // Keep the filter exportable so its JSON Schema annotations are preserved // when the RegExp constraint itself is omitted. toJsonSchema: () => [{}, true], arbitraryConstraint: { patterns: [payload] }, ...annotations } ) } // Copies that update only context or encoding share a body. // All other fields describe the body itself, including child contexts. const bodyOwners = new WeakMap() function copy(ast: A, changes: Partial): A { // AST copies preserve the prototype and enumerable values, not property descriptors. const out = Object.assign(Object.create(Object.getPrototypeOf(ast)), ast, changes) as A if (Reflect.ownKeys(changes).every((key) => key === "context" || key === "encoding")) { bodyOwners.set(out, getContextOwner(ast)) } return out } /** @internal */ export function getContextOwner(ast: A): A { const existing = bodyOwners.get(ast) if (existing !== undefined) return existing as A if (ast.encoding === undefined) return ast // A body representative must not expose or retain the node's own codec. const owner = Object.assign(Object.create(Object.getPrototypeOf(ast)), ast, { encoding: undefined }) as A bodyOwners.set(ast, owner) return owner } /** @internal */ export function replaceEncoding(ast: A, encoding: Encoding | undefined): A { return ast.encoding === encoding ? ast : copy(ast, { encoding }) } /** @internal */ export function replaceContext(ast: A, context: Context | undefined): A { if (ast.context === context) { return ast } const owner = getContextOwner(ast) if (owner.context === context && owner.encoding === ast.encoding) { return owner } return copy(ast, { context }) } /** @internal */ export function getLastEncoding(ast: AST): AST { return ast.encoding ? getLastEncoding(ast.encoding[ast.encoding.length - 1].to) : ast } /** @internal */ export function annotate(ast: A, annotations: Schema.Annotations.Annotations): A { if (ast.checks) { const last = ast.checks[ast.checks.length - 1] return replaceChecks(ast, Arr.append(ast.checks.slice(0, -1), last.annotate(annotations))) } return copy(ast, { annotations: { ...ast.annotations, ...annotations } }) } /** @internal */ export function replaceChecks(ast: A, checks: Checks | undefined): A { if (ast._tag === "Suspend" && checks) { throw new Error("Cannot add checks to Suspend") } if (ast.checks === checks) { return ast } return copy(ast, { checks }) } /** @internal */ export function appendChecks(ast: A, checks: Checks | undefined): A { return replaceChecks(ast, combineChecks(ast.checks, checks)) } /** @internal */ export function mapLink(link: Link, f: (ast: AST) => AST): Link { const to = f(link.to) return to === link.to ? link : new Link(to, link.transformation) } function updateLastLink(encoding: Encoding, f: (ast: AST) => AST): Encoding { const links = encoding const last = links[links.length - 1] const out = mapLink(last, f) return out === last ? encoding : Arr.append(encoding.slice(0, encoding.length - 1), out) } /** @internal */ export function applyToLastLink(f: (ast: AST) => AST) { return (ast: A): A => ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, f)) : ast } /** @internal */ export function replaceContextLastLink(ast: A, context: Context): A { return applyToLastLink((ast) => replaceContext(ast, context))(ast) } /** @internal */ export function applyToSelfOrLastLinkEncoding(f: (ast: AST) => AST) { function out(ast: AST): AST { return ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, out)) : f(ast) } return memoize(out) } /** @internal */ export function applyToSelfOrLastLinkEncodingIdempotent( f: (ast: AST) => AST, options?: { readonly stopAt?: (link: Link) => boolean } ) { function out(ast: AST): AST { if (ast.encoding) { const last = ast.encoding[ast.encoding.length - 1] return options?.stopAt?.(last) ? ast : replaceEncoding(ast, updateLastLink(ast.encoding, out)) } return f(ast) } return memoizeIdempotent(out) } /** @internal */ export function middlewareDecoding( ast: AST, middleware: SchemaTransformation.Middleware ): AST { return appendTransformation(ast, middleware, toType(ast)) } /** @internal */ export function middlewareEncoding( ast: AST, middleware: SchemaTransformation.Middleware ): AST { return appendTransformation(toEncoded(ast), middleware, ast) } function appendTransformation( from: AST, transformation: | SchemaTransformation.Transformation | SchemaTransformation.Middleware, to: A ): A { const link = new Link(from, transformation) return replaceEncoding(to, to.encoding ? [...to.encoding, link] : [link]) } /** * Maps over the array but will return the original array if no changes occur. * @internal */ export function mapOrSame(as: Arr.NonEmptyReadonlyArray, f: (a: A) => A): Arr.NonEmptyReadonlyArray export function mapOrSame(as: ReadonlyArray, f: (a: A) => A): ReadonlyArray export function mapOrSame(as: ReadonlyArray, f: (a: A) => A): ReadonlyArray { let out: Array | undefined for (let i = 0; i < as.length; i++) { const a = as[i] const fa = f(a) if (out) { out[i] = fa } else if (fa !== a) { out = new Array(as.length) for (let j = 0; j < i; j++) { out[j] = as[j] } out[i] = fa } } return out ?? as } /** @internal */ export function annotateKey(ast: A, annotations: Schema.Annotations.Key): A { const context = ast.context ? new Context( ast.context.isOptional, ast.context.isMutable, ast.context.constructorDefault, { ...ast.context.annotations, ...annotations } ) : new Context(false, false, undefined, annotations) return replaceContext(ast, context) } /** @internal */ export const optionalKey: (ast: A) => A = memoizeIdempotent((ast: A): A => { const context = ast.context ? ast.context.isOptional === false ? new Context(true, ast.context.isMutable, ast.context.constructorDefault, ast.context.annotations) : ast.context : new Context(true, false) return optionalKeyLastLink(replaceContext(ast, context)) }) const optionalKeyLastLink = applyToLastLink(optionalKey) /** @internal */ export const optional = memoize((ast: A): Union => optionalKey(new Union([ast, undefined_])) ) /** @internal */ export const mutableKey = memoizeIdempotent((ast: A): A => { const context = ast.context ? ast.context.isMutable === false ? new Context(ast.context.isOptional, true, ast.context.constructorDefault, ast.context.annotations) : ast.context : new Context(false, true) return mutableKeyLastLink(replaceContext(ast, context)) }) const mutableKeyLastLink = applyToLastLink(mutableKey) /** @internal */ export function withConstructorDefault( ast: A, defaultValue: Effect.Effect ): A { const context = ast.context ? new Context(ast.context.isOptional, ast.context.isMutable, defaultValue, ast.context.annotations) : new Context(false, false, defaultValue) return replaceContext(ast, context) } /** * Attaches a `Transformation` to the `to` AST, making it decode from the * `from` AST and encode back to it. * * **Details** * * This is the low-level primitive behind `Schema.decodeTo`. It appends a * {@link Link} to the `to` node's encoding chain. * * - Returns a new AST with the same type as `to`. * * @see {@link Link} * @see {@link Encoding} * @see {@link flip} * @stability stable * @category transforming * @since 4.0.0 */ export function decodeTo( from: AST, to: A, transformation: SchemaTransformation.Transformation ): A { return appendTransformation(from, transformation, to) } function parseParameter(ast: AST): { literals: ReadonlyArray parameters: ReadonlyArray } { const literals: Array = [] const parameters: Array = [] function go(ast: AST) { switch (ast._tag) { case "Literal": if (Predicate.isPropertyKey(ast.literal) && !literals.includes(ast.literal)) { literals.push(ast.literal) } return case "UniqueSymbol": if (!literals.includes(ast.symbol)) { literals.push(ast.symbol) } return case "Never": return case "Union": for (let i = 0; i < ast.types.length; i++) { go(ast.types[i]) } return default: parameters.push(ast) } } go(ast) return { literals, parameters } } /** @internal */ export function record(key: AST, value: AST): Objects { const { literals, parameters: indexSignatures } = parseParameter(key) return new Objects( literals.map((literal) => new PropertySignature(literal, value)), indexSignatures.map((parameter) => new IndexSignature(parameter, value)) ) } // ------------------------------------------------------------------------------------- // Public APIs // ------------------------------------------------------------------------------------- /** * Returns `true` if the AST node represents an optional property. * * **Details** * * Checks `ast.context?.isOptional`. Defaults to `false` when no * {@link Context} is set. * * @see `Schema.optionalKey` * @see {@link Context} * @stability stable * @category predicates * @since 4.0.0 */ export function isOptional(ast: AST): boolean { return ast.context?.isOptional ?? false } /** @internal */ export function isMutable(ast: AST): boolean { return ast.context?.isMutable ?? false } function isStructuralCheck(check: Check): boolean { return check.annotations?.[InternalAnnotations.STRUCTURAL_ANNOTATION_KEY] === true || check._tag === "FilterGroup" && check.checks.every(isStructuralCheck) } function extractStructuralChecks(checks: Checks): Checks | undefined { function extract(check: Check): Array> { if (isStructuralCheck(check)) return [check] return check._tag === "FilterGroup" ? check.checks.flatMap(extract) : [] } const out = checks.flatMap(extract) return Arr.isArrayNonEmpty(out) ? out : undefined } function canPreserveEncodingChecks(ast: AST): boolean { let preserve = true function visit(child: AST): AST { preserve = preserve && !child.encoding && !isSuspend(child) if (preserve && "recur" in child) child.recur(visit) return child } if ("recur" in ast) ast.recur(visit) return preserve } /** * Strips all encoding transformations from an AST, returning the decoded * (type-level) representation. * * **Details** * * - Memoized: same input reference → same output reference. * - Recursively walks into composite nodes ({@link Arrays}, {@link Objects}, * {@link Union}, {@link Suspend}). * * **Example** (Getting the type AST) * * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.NumberFromString * const typeAst = SchemaAST.toType(schema.ast) * typeAst._tag // => "Number" * ``` * * @see {@link toEncoded} * @see {@link flip} * @stability stable * @category transforming * @since 4.0.0 */ export const toType = memoizeIdempotent((ast: A): A => { const owner = getContextOwner(ast) if (owner !== ast) { const type = toType(owner) return type === owner && ast.encoding === undefined ? ast : replaceContext(type, ast.context) } const type = ("recur" in ast ? ast.recur(toType) : ast) as A if ("encodingChecks" in type && type.encodingChecks) { const checks = canPreserveEncodingChecks(ast) ? type.encodingChecks : isArrays(type) || isObjects(type) || isDeclaration(type) && type.typeParameters.length > 0 ? extractStructuralChecks(type.encodingChecks) : undefined return copy(type, { encodingChecks: undefined, checks: combineChecks(type.checks, checks) }) } return type }) /** * Returns the encoded (wire-format) AST by flipping and then stripping * encodings. * * **Details** * * Equivalent to `toType(flip(ast))`. This gives you the AST that describes * the shape of the serialized/encoded data. * * - Memoized: same input reference → same output reference. * * **Example** (Getting the encoded AST) * * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.NumberFromString * const encodedAst = SchemaAST.toEncoded(schema.ast) * encodedAst._tag // => "String" * ``` * * @see {@link toType} * @see {@link flip} * @stability stable * @category transforming * @since 4.0.0 */ export const toEncoded = memoizeIdempotent((ast: AST): AST => { return toType(flip(ast)) }) function flipEncoding(ast: AST, encoding: Encoding): AST { const links = encoding const len = links.length const last = links[len - 1] const ls: Arr.NonEmptyArray = [ new Link(flip(replaceEncoding(ast, undefined)), links[0].transformation.flip()) ] for (let i = 1; i < len; i++) { ls.unshift(new Link(flip(links[i - 1].to), links[i].transformation.flip())) } const to = flip(last.to) if (to.encoding) { return replaceEncoding(to, [...to.encoding, ...ls]) } else { return replaceEncoding(to, ls) } } /** * Swaps the decode and encode directions of an AST's {@link Encoding} chain. * * **Details** * * After flipping, what was decoding becomes encoding and vice versa. This is * the core operation behind `Schema.encodeEffect` — encoding a value is decoding * with a flipped SchemaAST. * * - Memoized: same input reference → same output reference. * - Recursively walks composite nodes. * * @see {@link toType} * @see {@link toEncoded} * @stability stable * @category transforming * @since 4.0.0 */ export const flip = memoize((ast: AST): AST => { if (ast.encoding) { return flipEncoding(ast, ast.encoding) } const owner = getContextOwner(ast) if (owner !== ast) { const flipped = flip(owner) return flipped === owner ? ast : replaceContext(flipped, ast.context) } return "flip" in ast ? ast.flip(flip) : "recur" in ast ? ast.recur(flip) : ast }) /** @internal */ export function containsUndefined(ast: AST): boolean { switch (ast._tag) { case "Undefined": return true case "Union": return ast.types.some(containsUndefined) default: return false } } function fromConst( ast: AST, value: T ): SchemaParser.Parser { const succeed = value === 0 ? InternalParser.sameExit : InternalParser.succeed(value) return (input, options) => { if (input === InternalParser.missing) return InternalParser.missingExit if (input === value) return succeed return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } } function fromRefinement( ast: AST, refinement: (input: unknown) => input is T ): SchemaParser.Parser { return (input, options) => { if (input === InternalParser.missing) return InternalParser.missingExit if (refinement(input)) return InternalParser.sameExit return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } } function segmentTemplateLiteralParts( ast: TemplateLiteral, input: string, options: ParseOptions ): Array | undefined { const parts = ast.encodedParts const literals = ast.literals const inputLength = input.length for (let i = 0; i < literals.length; i++) { const literal = literals[i] if (literal && !input.includes(literal)) return undefined } if (ast.suffixLengths[0] > inputLength) return undefined const out = new Array(parts.length) let failures: Set | undefined function go(i: number, pos: number): boolean { if (i === parts.length) return pos === inputLength if (failures?.has(i * (inputLength + 1) + pos)) return false const part = parts[i] if (i === parts.length - 1) { const s = input.slice(pos) if (part.matchPart(s, options) !== undefined) { out[i] = s return true } } else if (part._tag === "Literal") { const s = literals[i]! if (input.startsWith(s, pos) && go(i + 1, pos + s.length)) { out[i] = s return true } } else { const maximumEnd = inputLength - ast.suffixLengths[i + 1] // Splits preceding a literal only need to consider occurrences of that literal. const anchor = literals[i + 1] let end = anchor === undefined ? maximumEnd : input.lastIndexOf(anchor, maximumEnd) while (end >= pos) { const s = input.slice(pos, end) if (part.matchPart(s, options) !== undefined && go(i + 1, end)) { out[i] = s return true } if (end === 0) break end = anchor === undefined ? end - 1 : input.lastIndexOf(anchor, end - 1) } } failures ??= new Set() failures.add(i * (inputLength + 1) + pos) return false } return go(0, 0) ? out : undefined } /** @internal */ export const parameterFromPropertyKey = applyToSelfOrLastLinkEncodingIdempotent((ast) => { switch (ast._tag) { default: return ast case "Number": return ast.toCodecStringTree() case "Union": return ast.recur(parameterFromPropertyKey) } }) /** @internal */ export const parameterFromString = applyToSelfOrLastLinkEncodingIdempotent((ast) => { switch (ast._tag) { default: return ast case "Symbol": case "UniqueSymbol": return ast.toCodecStringTree() case "Union": return ast.recur(parameterFromString) } }) const partFromString = applyToSelfOrLastLinkEncodingIdempotent((ast) => { switch (ast._tag) { default: return ast case "Number": case "Literal": case "BigInt": return ast.toCodecStringTree() case "Union": return ast.recur(partFromString) } }) /** * any string, including newlines * @internal */ export const STRING_PATTERN = "[\\s\\S]*?" const isStringFiniteRegExp = new globalThis.RegExp(`^${FINITE_PATTERN}$`) const isStringNumberRegExp = new globalThis.RegExp(`^(?:${FINITE_PATTERN}|Infinity|-Infinity|NaN)$`) /** @internal */ export function isStringFinite(annotations?: Schema.Annotations.Filter) { return isPattern( isStringFiniteRegExp, { expected: "a string representing a finite number", representation: { id: "effect/schema/isStringFinite", payload: null }, toJsonSchema: () => ({ pattern: isStringFiniteRegExp.source }), ...annotations } ) } const finiteString = appendChecks(string, [isStringFinite()]) const finiteToString = new Link( finiteString, SchemaTransformation.numberFromString ) const numberToString = new Link( new Union([finiteString, nonFiniteLiterals]), SchemaTransformation.numberFromString ) /** * signed integer only (no leading "+" because TypeScript doesn't support it) */ const BIGINT_PATTERN = "-?\\d+" const isStringBigIntRegExp = new globalThis.RegExp(`^${BIGINT_PATTERN}$`) /** @internal */ export function isStringBigInt(annotations?: Schema.Annotations.Filter) { return isPattern( isStringBigIntRegExp, { expected: "a string representing a bigint", representation: { id: "effect/schema/isStringBigInt", payload: null }, toJsonSchema: () => ({ pattern: isStringBigIntRegExp.source }), ...annotations } ) } /** @internal */ export const bigIntString = appendChecks(string, [isStringBigInt({ expected: "a string representing a bigint" })]) const bigIntToString = new Link( bigIntString, SchemaTransformation.bigintFromString ) const REGEXP_PATTERN = "Symbol\\(([\\s\\S]*)\\)" const isStringSymbolRegExp = new globalThis.RegExp(`^${REGEXP_PATTERN}$`) /** @internal */ export const symbolString = appendChecks(string, [isStringSymbol()]) /** * to distinguish between Symbol and String, we need to add a check to the string keyword */ const symbolToString = new Link( symbolString, new SchemaTransformation.Transformation( SchemaGetter.transform((description) => globalThis.Symbol.for(isStringSymbolRegExp.exec(description)![1])), SchemaGetter.transformEffect((sym: symbol, options) => { const key = globalThis.Symbol.keyFor(sym) if (key !== undefined) { return Effect.succeed(globalThis.String(sym)) } return Effect.fail( new SchemaIssue.Forbidden( { message: "cannot serialize to string, Symbol is not registered" }, sym, options ) ) }) ) ) /** @internal */ export function isStringSymbol(annotations?: Schema.Annotations.Filter) { return isPattern( isStringSymbolRegExp, { expected: "a string representing a symbol", representation: { id: "effect/schema/isStringSymbol", payload: null }, toJsonSchema: () => ({ pattern: isStringSymbolRegExp.source }), ...annotations } ) } /** @internal */ export function collectIssues( checks: ReadonlyArray>, value: T, issues: Arr.NonEmptyArray | undefined, ast: AST, options: ParseOptions ): Arr.NonEmptyArray | undefined { for (let i = 0; i < checks.length; i++) { const check = checks[i] if (check._tag === "FilterGroup") { issues = collectIssues(check.checks, value, issues, ast, options) if ( issues && (options.errors !== "all" || (issues[issues.length - 1] as SchemaIssue.Filter).filter.aborted) ) { return issues } } else { const issue = check.run(value, ast, options) if (issue) { const filter = new SchemaIssue.Filter(check, issue, value, options) if (issues) issues.push(filter) else issues = [filter] if (options.errors !== "all" || check.aborted) { return issues } } } } return issues } /** @internal */ export function runChecks( checks: readonly [Check, ...Array>], s: T ): Result.Result { const issues = collectIssues(checks, s, undefined, unknown, { errors: "all" }) if (issues) { const issue = new SchemaIssue.Composite(unknown, issues) return Result.fail(issue) } return Result.succeed(s) } /** @internal */ export interface ConstructorDescriptor { readonly isConstructed: Predicate.Predicate readonly link: Link } /** @internal */ export function getConstructorDescriptor(ast: AST): ConstructorDescriptor | undefined { if (!isDeclaration(ast)) return undefined const getDescriptor = ast.annotations?.[InternalAnnotations.CONSTRUCTOR_ANNOTATION_KEY] return Predicate.isFunction(getDescriptor) ? getDescriptor(ast.typeParameters) : undefined } /** * Returns all annotations from the AST node. * * **Details** * * If the node has {@link Checks}, returns annotations from the last check * (which is where user-supplied annotations end up after `.pipe(Schema.annotate(...))`). * Otherwise returns the node's `annotations` directly. * * **Example** (Reading annotations) * * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.String.annotate({ title: "Name" }) * const annotations = SchemaAST.resolve(schema.ast) * annotations?.title // => "Name" * ``` * * @see {@link resolveAt} * @see {@link resolveIdentifier} * @see {@link resolveTitle} * @see {@link resolveDescription} * @stability stable * @category annotations * @since 4.0.0 */ export const resolve: (ast: AST) => Schema.Annotations.Annotations | undefined = InternalAnnotations.resolve /** * Returns a single annotation value by key from the AST node. * * **Details** * * Like {@link resolve}, reads from the last check's annotations when checks * are present. Returns `undefined` if the key is not found. * * @see {@link resolve} * @stability stable * @category annotations * @since 4.0.0 */ export const resolveAt: (key: string) => (ast: AST) => A | undefined = InternalAnnotations.resolveAt /** * Returns the `identifier` annotation from the AST node, if set. * * **Details** * * The identifier is typically set by `Schema.annotate({ identifier: "..." })` * and is used for error messages and schema identification. * * @see {@link resolve} * @see {@link resolveTitle} * @stability stable * @category annotations * @since 4.0.0 */ export const resolveIdentifier: (ast: AST) => string | undefined = InternalAnnotations.resolveIdentifier /** * Returns the `title` annotation from the AST node, if set. * * @see {@link resolve} * @see {@link resolveIdentifier} * @see {@link resolveDescription} * * @stability stable * @category annotations * @since 4.0.0 */ export const resolveTitle: (ast: AST) => string | undefined = InternalAnnotations.resolveTitle /** * Returns the `description` annotation from the AST node, if set. * * @see {@link resolve} * @see {@link resolveTitle} * @see {@link resolveIdentifier} * * @stability stable * @category annotations * @since 4.0.0 */ export const resolveDescription: (ast: AST) => string | undefined = InternalAnnotations.resolveDescription type TreeFrame = { readonly value: object // Object keys or an array length snapshot. readonly keys: ReadonlyArray | number index: number } function isJsonLeaf(u: unknown): boolean { return u === null || typeof u === "string" || typeof u === "boolean" || typeof u === "number" && globalThis.Number.isFinite(u) } function isStringTreeLeaf(u: unknown): boolean { return u === undefined || typeof u === "string" } function isTree(u: unknown, isLeaf: (u: unknown) => boolean): boolean { const cache = new WeakMap() const stack: Array = [] outer: while (true) { if (typeof u !== "object" || u === null) { if (!isLeaf(u)) { return false } } else { const value = u const cached = cache.get(value) // `false` marks a node on the current path, while `true` marks a fully // validated node that can be safely reused by a DAG. if (cached === false) { return false } if (cached === undefined) { const isArray = Array.isArray(value) if (!isArray) { const prototype = Object.getPrototypeOf(value) // A plain object from another realm has a different Object.prototype, // but that prototype still has a null prototype. if ( prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null ) { return false } } cache.set(value, false) stack.push({ value, keys: isArray ? value.length : Object.keys(value), index: 0 }) } } while (stack.length > 0) { const frame = stack[stack.length - 1] const keys = frame.keys if (typeof keys === "number") { if (frame.index < keys) { // A sparse slot is read as `undefined`; the leaf predicate determines // whether that is valid for the current tree. u = (frame.value as ReadonlyArray)[frame.index++] continue outer } } else if (frame.index < keys.length) { u = (frame.value as Record)[keys[frame.index++]] continue outer } cache.set(frame.value, true) stack.pop() } return true } } /** * Returns true if the value is a JSON value. * * When a cyclic reference is detected, returns false. * * @internal */ export function isJson(u: unknown): u is Schema.Json { return isTree(u, isJsonLeaf) } /** @internal */ export const Json = new Declaration( [], () => (input, ast, options) => isJson(input) ? InternalParser.sameExit : Effect.fail(new SchemaIssue.InvalidType(ast, input, options)), { representation: { id: "effect/schema/Json", payload: null }, expected: "JSON value", toCodecJson: () => undefined, toCodecStringTree: () => unknownToStringTree } ) /** @internal */ export const MutableJson = annotate(Json, { representation: { id: "effect/schema/MutableJson", payload: null } }) /** @internal */ export const unknownToJson = new Link( Json, SchemaTransformation.passthrough() ) /** @internal */ export const objectKeywordToJson = new Link( new Union([ new Arrays(false, [], [Json]), new Objects([], [new IndexSignature(string, Json)]) ]), SchemaTransformation.passthrough() ) /** * Returns true if the value is a StringTree value. * * When a cyclic reference is detected, returns false. * * @internal */ export function isStringTree(u: unknown): u is Schema.StringTree { return isTree(u, isStringTreeLeaf) } const StringTree = new Declaration( [], () => (input, ast, options) => isStringTree(input) ? InternalParser.sameExit : Effect.fail(new SchemaIssue.InvalidType(ast, input, options)), { expected: "StringTree", toCodecStringTree: () => undefined } ) /** @internal */ export const unknownToStringTree = new Link( StringTree, SchemaTransformation.passthrough() )