Files
zopu-code/repos/effect-smol/packages/effect/src/SchemaAST.ts
T

4002 lines
111 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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.
*
* @since 4.0.0
*/
import * as Arr from "./Array.ts"
import * as Cause from "./Cause.ts"
import type * as Combiner from "./Combiner.ts"
import * as Effect from "./Effect.ts"
import * as Exit from "./Exit.ts"
import { format, formatPropertyKey } from "./Formatter.ts"
import { memoize } from "./Function.ts"
import { effectIsExit, iterateEager } 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 Option from "./Option.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 FastCheck from "./testing/FastCheck.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 {@link Base} fields: `annotations`, `checks`,
* `encoding`, `context`.
* - Discriminate on the `_tag` field (e.g. `"String"`, `"Objects"`, `"Union"`).
*
* @see {@link Base}
* @see {@link isAST}
* @category models
* @since 3.10.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<T extends AST["_tag"]>(tag: T) {
return (ast: AST): ast is Extract<AST, { _tag: T }> => 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}
* @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
*
* @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
*
* @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`
*
* @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`
*
* @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
*
* @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`
*
* @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`
*
* @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
*
* @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.
*
* @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
*
* @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
*
* @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
*
* @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
*
* @category guards
* @since 3.10.0
*/
export const isLiteral = makeGuard("Literal")
/**
* Narrows an {@link AST} to {@link UniqueSymbol}.
*
* @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
*
* @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
*
* @category guards
* @since 4.0.0
*/
export const isEnum = makeGuard("Enum")
/**
* Narrows an {@link AST} to {@link TemplateLiteral}.
*
* @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
*
* @category guards
* @since 4.0.0
*/
export const isArrays = makeGuard("Arrays")
/**
* Narrows an {@link AST} to {@link Objects}.
*
* @category guards
* @since 4.0.0
*/
export const isObjects = makeGuard("Objects")
/**
* Narrows an {@link AST} to {@link Union}.
*
* @category guards
* @since 3.10.0
*/
export const isUnion = makeGuard("Union")
/**
* Narrows an {@link AST} to {@link Suspend}.
*
* @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}
* @category models
* @since 4.0.0
*/
export class Link {
readonly to: AST
readonly transformation:
| SchemaTransformation.Transformation<any, any, any, any>
| SchemaTransformation.Middleware<any, any, any, any, any, any>
constructor(
to: AST,
transformation:
| SchemaTransformation.Transformation<any, any, any, any>
| SchemaTransformation.Middleware<any, any, any, any, any, any>
) {
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 {@link Base.encoding}. When `undefined`, the node has no
* encoding transformation (type and encoded forms are identical).
*
* @see {@link Link}
* @see {@link toEncoded}
* @category models
* @since 4.0.0
*/
export type Encoding = readonly [Link, ...Array<Link>]
/**
* Options that control schema parsing, validation, transformation, and output behavior.
*
* **Details**
*
* Pass to `Schema.decodeUnknown`, `Schema.encode`, and related APIs to customize
* error reporting, excess property handling, output key ordering, check
* execution, and asynchronous parser concurrency.
*
* - `errors` — `"first"` (default) stops at the first error; `"all"` collects
* every error.
* - `onExcessProperty` — `"ignore"` (default) strips unknown object keys;
* `"error"` fails; `"preserve"` keeps them.
* - `propertyOrder` — `"none"` (default) lets the system choose key order;
* `"original"` preserves input key order.
* - `disableChecks` — skips validation checks while still applying defaults and
* transformations.
* - `concurrency` — maximum number of async parse effects to run concurrently;
* defaults to `1`, or use `"unbounded"`.
*
* @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, or `"preserve"` to
* keep excess properties in the output.
*
* @default "ignore"
*/
readonly onExcessProperty?: "ignore" | "error" | "preserve" | undefined
/**
* The `propertyOrder` option provides control over the order of object fields
* in the output. This feature is useful when the sequence of keys is
* important for the consuming processes or when maintaining the input order
* enhances readability and usability.
*
* **Details**
*
* By default, the `propertyOrder` option is set to `"none"`. This means that
* the internal system decides the order of keys to optimize parsing speed.
*
* Setting `propertyOrder` to `"original"` ensures that the keys are ordered
* as they appear in the input during the decoding/encoding process.
*
* **Gotchas**
*
* The key order for `"none"` should not be considered stable and may change
* in future updates without notice.
*
* @default "none"
*/
readonly propertyOrder?: "none" | "original" | undefined
/**
* Whether to disable checks while still applying defaults and
* transformations.
*/
readonly disableChecks?: boolean | undefined
/**
* The maximum number of async effects to run concurrently.
*
* @default 1
*/
readonly concurrency?: number | "unbounded" | undefined
}
/** @internal */
export const defaultParseOptions: ParseOptions = {}
/**
* Represents per-property metadata attached to AST nodes via {@link Base.context}.
*
* **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`.
* - `defaultValue` — an {@link Encoding} applied during construction to
* supply missing values.
* - `annotations` — key-level annotations (e.g. description of the key
* itself).
*
* @see {@link optionalKey}
* @see {@link isOptional}
* @category models
* @since 4.0.0
*/
export class Context {
readonly isOptional: boolean
readonly isMutable: boolean
/** Used for constructor default values (e.g. `withConstructorDefault` API) */
readonly defaultValue: Encoding | undefined
readonly annotations: Schema.Annotations.Key<unknown> | undefined
constructor(
isOptional: boolean,
isMutable: boolean,
/** Used for constructor default values (e.g. `withConstructorDefault` API) */
defaultValue: Encoding | undefined = undefined,
annotations: Schema.Annotations.Key<unknown> | undefined = undefined
) {
this.isOptional = isOptional
this.isMutable = isMutable
this.defaultValue = defaultValue
this.annotations = annotations
}
}
/**
* Non-empty array of validation {@link Check} values attached to an AST node
* via {@link Base.checks}.
*
* **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}
* @category models
* @since 4.0.0
*/
export type Checks = readonly [Check<any>, ...Array<Check<any>>]
const TypeId = "~effect/Schema"
/**
* Represents the abstract base class for all {@link AST} node variants.
*
* **Details**
*
* Every AST node extends `Base` and inherits these fields:
*
* - `annotations` — user-supplied metadata (identifier, title, description,
* arbitrary keys).
* - `checks` — optional {@link Checks} for post-type-match validation.
* - `encoding` — optional {@link Encoding} chain for type ↔ wire
* transformations.
* - `context` — optional {@link Context} for per-property metadata.
*
* Subclasses add a `_tag` discriminant and variant-specific data.
*
* @see {@link AST}
* @category models
* @since 4.0.0
*/
export abstract class Base {
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}>`
}
}
/**
* 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.
*
* @see {@link isDeclaration}
* @category models
* @since 3.10.0
*/
export class Declaration extends Base {
readonly _tag = "Declaration"
readonly typeParameters: ReadonlyArray<AST>
readonly run: (
typeParameters: ReadonlyArray<AST>
) => (input: unknown, self: Declaration, options: ParseOptions) => Effect.Effect<any, SchemaIssue.Issue, any>
readonly encodingChecks: Checks | undefined
constructor(
typeParameters: ReadonlyArray<AST>,
run: (
typeParameters: ReadonlyArray<AST>
) => (input: unknown, self: Declaration, options: ParseOptions) => Effect.Effect<any, SchemaIssue.Issue, any>,
annotations?: Schema.Annotations.Annotations,
checks?: Checks,
encoding?: Encoding,
context?: Context,
encodingChecks?: Checks
) {
super(annotations, checks, encoding, context)
this.typeParameters = typeParameters
this.run = run
this.encodingChecks = encodingChecks
}
/** @internal */
getParser(): SchemaParser.Parser {
const run = this.run(this.typeParameters)
return (oinput, options) => {
if (Option.isNone(oinput)) return Effect.succeedNone
return Effect.mapEager(run(oinput.value, this, options), Option.some)
}
}
private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) {
const tps = mapOrSame(this.typeParameters, recur)
return tps === this.typeParameters && checks === this.checks && encodingChecks === this.encodingChecks ?
this :
new Declaration(tps, this.run, 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 {
const expected = this.annotations?.expected
if (typeof expected === "string") return expected
return "<Declaration>"
}
}
/**
* AST node matching the `null` literal value.
*
* **Details**
*
* Parsing succeeds only when the input is exactly `null`.
*
* @see {@link null_ null}
* @see {@link isNull}
* @category models
* @since 4.0.0
*/
export class Null extends Base {
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.
*
* @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}
* @category models
* @since 4.0.0
*/
export class Undefined extends Base {
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.
*
* @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}
* @category models
* @since 4.0.0
*/
export class Void extends Base {
readonly _tag = "Void"
/** @internal */
getParser() {
return fromAnyToConst(undefined)
}
/** @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
*
* @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}
* @category models
* @since 4.0.0
*/
export class Never extends Base {
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
*
* @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}
*
* @category models
* @since 4.0.0
*/
export class Any extends Base {
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
*
* @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}
* @category models
* @since 4.0.0
*/
export class Unknown extends Base {
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`
*
* @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}
*
* @category models
* @since 3.10.0
*/
export class ObjectKeyword extends Base {
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
*
* @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}
* @category models
* @since 4.0.0
*/
export class Enum extends Base {
readonly _tag = "Enum"
readonly enums: ReadonlyArray<readonly [string, string | number]>
constructor(
enums: ReadonlyArray<readonly [string, string | number]>,
annotations?: Schema.Annotations.Annotations,
checks?: Checks,
encoding?: Encoding,
context?: Context
) {
super(annotations, checks, encoding, context)
this.enums = enums
}
/** @internal */
getParser() {
const values = new Set<unknown>(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)), "anyOf"),
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<TemplateLiteralPart>
function isTemplateLiteralPart(ast: AST): ast is TemplateLiteralPart {
switch (ast._tag) {
case "String":
case "Number":
case "BigInt":
return true
case "Literal":
case "TemplateLiteral":
return ast.checks === undefined
case "Union":
return ast.checks === undefined && ast.types.every(isTemplateLiteralPart)
default:
return false
}
}
/**
* 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}
* @category models
* @since 3.10.0
*/
export class TemplateLiteral extends Base {
readonly _tag = "TemplateLiteral"
readonly parts: ReadonlyArray<AST>
/** @internal */
readonly encodedParts: ReadonlyArray<TemplateLiteralPart>
constructor(
parts: ReadonlyArray<AST>,
annotations?: Schema.Annotations.Annotations,
checks?: Checks,
encoding?: Encoding,
context?: Context
) {
super(annotations, checks, encoding, context)
const encodedParts: Array<TemplateLiteralPart> = []
for (const part of parts) {
const encoded = toEncoded(part)
if (isTemplateLiteralPart(encoded)) {
encodedParts.push(encoded)
} else {
throw new Error(`Invalid TemplateLiteral part ${encoded._tag}`)
}
}
this.parts = parts
this.encodedParts = encodedParts
}
/** @internal */
getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser {
const parser = recur(this.asTemplateLiteralParser())
return (oinput: Option.Option<unknown>, options: ParseOptions) =>
Effect.mapBothEager(parser(oinput, options), {
onSuccess: () => oinput,
onFailure: (issue) => new SchemaIssue.Composite(this, oinput, [issue])
})
}
/** @internal */
getExpected(): string {
return "string"
}
/** @internal */
matchPart(s: string, options: ParseOptions): string | undefined {
return segmentTemplateLiteralParts(this.encodedParts, s, options) === undefined ? undefined : s
}
/** @internal */
asTemplateLiteralParser(): Arrays {
const tuple = new Arrays(false, this.parts.map(partFromString), [])
return decodeTo(
string,
tuple,
new SchemaTransformation.Transformation(
SchemaGetter.transformOrFail((s: string, options) => {
const segments = segmentTemplateLiteralParts(this.encodedParts, s, options)
if (segments !== undefined) return Effect.succeed(segments)
return Effect.fail(
new SchemaIssue.InvalidValue(Option.some(s), {
message: `Expected a string matching template literal parts, got ${format(s)}`
})
)
}),
SchemaGetter.transform((parts) => 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}
* @category models
* @since 3.10.0
*/
export class UniqueSymbol extends Base {
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 {
return replaceEncoding(this, [symbolToString])
}
/** @internal */
getExpected(): string {
return globalThis.String(this.symbol)
}
}
/**
* The set of primitive types that can appear as a {@link Literal} value.
*
* @see {@link Literal}
*
* @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`. Numeric literals must be finite — `Infinity`, `-Infinity`,
* and `NaN` are rejected at construction time.
*
* **Example** (Creating a literal AST)
*
* ```ts
* import { SchemaAST } from "effect"
*
* const ast = new SchemaAST.Literal("active")
* console.log(ast.literal) // "active"
* ```
*
* @see {@link LiteralValue}
* @see {@link isLiteral}
* @category models
* @since 3.10.0
*/
export class Literal extends Base {
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}
*
* @category models
* @since 4.0.0
*/
export class String extends Base {
readonly _tag = "String"
/** @internal */
getParser() {
return fromRefinement(this, Predicate.isString)
}
/** @internal */
matchPart(s: string, options: ParseOptions): string | undefined {
return applyTemplateLiteralPartChecks(this, s, options)
}
/** @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
*
* @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}
* @category models
* @since 4.0.0
*/
export class Number extends Base {
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 {
return regexp.test(s)
? applyTemplateLiteralPartChecks(this, globalThis.Number(s), options)
: undefined
}
/** @internal */
toCodecJson(): AST {
if (this.checks && (hasCheck(this.checks, "isFinite") || hasCheck(this.checks, "isInt"))) {
return this
}
return replaceEncoding(this, [numberToJson])
}
/** @internal */
toCodecStringTree(): AST {
if (this.checks && (hasCheck(this.checks, "isFinite") || hasCheck(this.checks, "isInt"))) {
return replaceEncoding(this, [finiteToString])
}
return replaceEncoding(this, [numberToString])
}
/** @internal */
getExpected(): string {
return "number"
}
}
// oxlint-disable-next-line only-used-in-recursion - @gcanti what's this? :-)
function hasCheck(checks: ReadonlyArray<Check<unknown>>, tag: string): boolean {
return checks.some((c) => {
switch (c._tag) {
case "Filter":
return c.annotations?.meta?._tag === tag
case "FilterGroup":
return hasCheck(c.checks, tag)
}
})
}
/**
* 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
*
* @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}
*
* @category models
* @since 4.0.0
*/
export class Boolean extends Base {
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
*
* @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}
* @category models
* @since 4.0.0
*/
export class Symbol extends Base {
readonly _tag = "Symbol"
/** @internal */
getParser() {
return fromRefinement(this, Predicate.isSymbol)
}
/** @internal */
matchKey(s: symbol, options: ParseOptions): symbol | undefined {
return applyTemplateLiteralPartChecks(this, s, options)
}
/** @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
*
* @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}
* @category models
* @since 4.0.0
*/
export class BigInt extends Base {
readonly _tag = "BigInt"
/** @internal */
getParser() {
return fromRefinement(this, Predicate.isBigInt)
}
/** @internal */
matchPart(s: string, options: ParseOptions): bigint | undefined {
return isStringBigIntRegExp.test(s)
? applyTemplateLiteralPartChecks(this, globalThis.BigInt(s), options)
: undefined
}
/** @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
*
* @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 {@link Context.isOptional} is `true`.
* - `rest` — the rest/variadic element types. When non-empty, the first
* entry is the "spread" type (e.g. `...Array<string>`), 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 { Schema, SchemaAST } from "effect"
*
* const schema = Schema.Tuple([Schema.String, Schema.Number])
* const ast = schema.ast
*
* if (SchemaAST.isArrays(ast)) {
* console.log(ast.elements.length) // 2
* console.log(ast.rest.length) // 0
* }
* ```
*
* @see {@link isArrays}
* @see {@link Objects}
* @category models
* @since 4.0.0
*/
export class Arrays extends Base {
readonly _tag = "Arrays"
readonly isMutable: boolean
readonly elements: ReadonlyArray<AST>
readonly rest: ReadonlyArray<AST>
readonly encodingChecks: Checks | undefined
constructor(
isMutable: boolean,
elements: ReadonlyArray<AST>,
rest: ReadonlyArray<AST>,
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
// A required element cannot follow an optional element. ts(1257)
const i = elements.findIndex(isOptional)
if (i !== -1 && (elements.slice(i + 1).some((e) => !isOptional(e)) || 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)
if (rest.length > 1 && rest.slice(1).some(isOptional)) {
throw new Error("An optional element cannot follow a rest element. ts(1266)")
}
}
/** @internal */
getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser {
// oxlint-disable-next-line @typescript-eslint/no-this-alias
const ast = this
const elements = ast.elements.map((ast) => ({ ast, parser: recur(ast) }))
const rest = ast.rest.map((ast) => ({ ast, parser: recur(ast) }))
const elementLen = elements.length
const [head, ...tail] = rest
const tailLen = tail.length
function getParser(
tailThreshold: number,
index: number
): { readonly ast: AST; readonly parser: SchemaParser.Parser } {
if (index < elementLen) {
return elements[index]
} else if (index >= tailThreshold) {
return tail[index - tailThreshold]
}
return head
}
return Effect.fnUntracedEager(function*(oinput, options) {
if (oinput._tag === "None") {
return oinput
}
const input = oinput.value
// If the input is not an array, return early with an error
if (!Array.isArray(input)) {
return yield* Effect.fail(new SchemaIssue.InvalidType(ast, oinput))
}
const len = input.length
const state = {
ast,
getParser,
oinput,
len,
tailThreshold: resolveTailThreshold(len, elementLen, tailLen),
output: new globalThis.Array(len),
issues: undefined as Arr.NonEmptyArray<SchemaIssue.Issue> | undefined,
options
}
const concurrency = resolveConcurrency(options?.concurrency)
const eff = parseArray(state, input, {
concurrency: concurrency?.concurrency,
end: ast.rest.length === 0 ? elementLen : Math.max(len, elementLen + tailLen)
})
if (eff) yield* eff
// ---------------------------------------------
// handle excess indexes
// ---------------------------------------------
if (ast.rest.length === 0 && len > elementLen) {
for (let i = elementLen; i <= len - 1; i++) {
const issue = new SchemaIssue.Pointer([i], new SchemaIssue.UnexpectedKey(ast, input[i]))
if (options.errors === "all") {
if (state.issues) state.issues.push(issue)
else state.issues = [issue]
} else {
return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, [issue]))
}
}
}
if (state.issues) {
return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, state.issues))
}
return Option.some(state.output)
})
}
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"
}
}
const parseArray = iterateEager<{
readonly ast: AST
readonly oinput: Option.Option<unknown>
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<unknown>
issues: Array<SchemaIssue.Issue> | undefined
}, unknown>()({
onItem(s, item, i) {
const value = i < s.len ? Option.some(item) : Option.none()
return s.getParser(s.tailThreshold, i).parser(value, s.options)
},
step(s, _, exit, i) {
if (exit._tag === "Failure") {
return wrapPropertyKeyIssue(s, s.ast, i, exit)
} else if (exit.value._tag === "Some") {
s.output[i] = exit.value.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, s.oinput, [issue]))
}
}
}
})
function resolveTailThreshold(
inputLen: number,
elementLen: number,
tailLen: number
) {
return Math.max(elementLen, inputLen - tailLen)
}
const resolveConcurrency = (value: number | "unbounded" | undefined) => {
value = value === "unbounded" ? Infinity : value ?? 1
return value > 1 ? { concurrency: value } : undefined
}
const wrapPropertyKeyIssue = (
s: {
readonly oinput: Option.Option<unknown>
readonly options: ParseOptions
issues: Array<SchemaIssue.Issue> | undefined
},
ast: AST,
key: PropertyKey,
exit: Exit.Failure<any, SchemaIssue.Issue>
) => {
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, s.oinput, [new SchemaIssue.Pointer([key], issue)])
)
)
}
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, s.oinput, [pointer]))
}
}
/**
* floating point or integer, with optional exponent
* @internal
*/
export const FINITE_PATTERN = "[+-]?\\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<PropertyKey> {
let stringKeys: ReadonlyArray<string> | undefined
let symbolKeys: ReadonlyArray<symbol> | undefined
function go(parameter: AST): ReadonlyArray<PropertyKey> {
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) =>
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}
* @category models
* @since 3.10.0
*/
export class PropertySignature {
readonly name: PropertyKey
readonly type: AST
constructor(
name: PropertyKey,
type: AST
) {
this.name = name
this.type = type
}
}
/**
* Represents a bidirectional merge strategy for index signature key-value pairs.
*
* **Details**
*
* Used by {@link IndexSignature} when the same key appears multiple times
* (e.g. from `Schema.extend` or overlapping records). Provides separate
* `decode` and `encode` combiners that determine how duplicate entries are
* merged.
*
* @see {@link IndexSignature}
* @category models
* @since 4.0.0
*/
export class KeyValueCombiner {
readonly decode: Combiner.Combiner<readonly [key: PropertyKey, value: any]> | undefined
readonly encode: Combiner.Combiner<readonly [key: PropertyKey, value: any]> | undefined
constructor(
decode: Combiner.Combiner<readonly [key: PropertyKey, value: any]> | undefined,
encode: Combiner.Combiner<readonly [key: PropertyKey, value: any]> | undefined
) {
this.decode = decode
this.encode = encode
}
/** @internal */
flip(): KeyValueCombiner {
return new KeyValueCombiner(this.encode, this.decode)
}
}
type IndexSignatureParameter =
| String
| Number
| Symbol
| TemplateLiteral
| Union<IndexSignatureParameter>
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 isIndexSignatureParameter(ast: AST): ast is IndexSignatureParameter {
return isIndexSignatureParameterSide(ast) && isIndexSignatureParameterSide(toEncoded(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.
* - `merge` — optional {@link KeyValueCombiner} for handling duplicate keys.
*
* **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}
* @category models
* @since 3.10.0
*/
export class IndexSignature {
readonly parameter: IndexSignatureParameter
readonly type: AST
readonly merge: KeyValueCombiner | undefined
constructor(
parameter: AST,
type: AST,
merge: KeyValueCombiner | undefined
) {
if (!isIndexSignatureParameter(parameter)) {
throw new Error(`Invalid index signature parameter ${parameter._tag}`)
}
this.parameter = parameter
this.type = type
this.merge = merge
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 { Schema, SchemaAST } from "effect"
*
* const schema = Schema.Struct({ name: Schema.String })
* const ast = schema.ast
*
* if (SchemaAST.isObjects(ast)) {
* for (const ps of ast.propertySignatures) {
* console.log(ps.name, ps.type._tag)
* }
* // "name" "String"
* }
* ```
*
* @see {@link isObjects}
* @see {@link PropertySignature}
* @see {@link IndexSignature}
* @see {@link Arrays}
* @category models
* @since 4.0.0
*/
export class Objects extends Base {
readonly _tag = "Objects"
readonly propertySignatures: ReadonlyArray<PropertySignature>
readonly indexSignatures: ReadonlyArray<IndexSignature>
readonly encodingChecks: Checks | undefined
constructor(
propertySignatures: ReadonlyArray<PropertySignature>,
indexSignatures: ReadonlyArray<IndexSignature>,
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
// Duplicate property signatures
const duplicates = propertySignatures.map((ps) => ps.name).filter((name, i, arr) => arr.indexOf(name) !== i)
if (duplicates.length > 0) {
throw new Error(`Duplicate identifiers: ${JSON.stringify(duplicates)}. ts(2300)`)
}
}
/** @internal */
getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser {
// oxlint-disable-next-line @typescript-eslint/no-this-alias
const ast = this
const expectedKeys: Array<PropertyKey> = []
const expectedKeysSet = new Set<PropertyKey>()
const properties: Array<{
readonly ps: PropertySignature | IndexSignature
readonly parser: SchemaParser.Parser
readonly name: PropertyKey
readonly type: AST
}> = []
for (const ps of ast.propertySignatures) {
expectedKeys.push(ps.name)
expectedKeysSet.add(ps.name)
properties.push({
ps,
parser: recur(ps.type),
name: ps.name,
type: ps.type
})
}
const indexCount = ast.indexSignatures.length
// ---------------------------------------------
// handle empty struct
// ---------------------------------------------
if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) {
return fromRefinement(ast, Predicate.isNotNullish)
}
const parseIndexes = indexCount > 0 ?
iterateEager<{
readonly oinput: Option.Option<unknown>
readonly input: Record<PropertyKey, unknown>
readonly options: ParseOptions
readonly out: Record<PropertyKey, unknown>
issues: Array<SchemaIssue.Issue> | undefined
}, [key: PropertyKey, is: IndexSignature]>()({
onItem: Effect.fnUntracedEager(function*(
s,
[key, is]
) {
const parserKey = recur(parameterFromPropertyKey(is.parameter))
const effKey = parserKey(Option.some(key), s.options)
const exitKey = (effectIsExit(effKey) ? effKey : yield* Effect.exit(effKey)) as Exit.Exit<
Option.Option<PropertyKey>,
SchemaIssue.Issue
>
if (exitKey._tag === "Failure") {
const eff = wrapPropertyKeyIssue(s, ast, key, exitKey)
if (eff) yield* eff
return
}
const value: Option.Option<unknown> = Option.some(s.input[key])
const parserValue = recur(is.type)
const effValue = parserValue(value, s.options)
const exitValue = effectIsExit(effValue) ? effValue : yield* Effect.exit(effValue)
if (exitValue._tag === "Failure") {
const eff = wrapPropertyKeyIssue(s, ast, key, exitValue)
if (eff) yield* eff
return
} else if (exitKey.value._tag === "Some" && exitValue.value._tag === "Some") {
const k2 = exitKey.value.value
if (expectedKeysSet.has(key) || expectedKeysSet.has(k2)) {
return
}
const v2 = exitValue.value.value
if (is.merge && is.merge.decode && Object.hasOwn(s.out, k2)) {
const [k, v] = is.merge.decode.combine([k2, s.out[k2]], [k2, v2])
internalRecord.set(s.out, k, v)
} else {
internalRecord.set(s.out, k2, v2)
}
}
}),
step: (_s, _, exit: Exit.Exit<void, SchemaIssue.Issue>) => exit._tag === "Failure" ? exit : undefined
}) :
undefined
return Effect.fnUntracedEager(function*(oinput, options) {
if (oinput._tag === "None") {
return oinput
}
const input = oinput.value as Record<PropertyKey, unknown>
// 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, oinput))
}
const out: Record<PropertyKey, unknown> = {}
const state = {
ast,
oinput,
input,
out,
issues: undefined as Arr.NonEmptyArray<SchemaIssue.Issue> | undefined,
options
}
const errorsAllOption = options.errors === "all"
const onExcessPropertyError = options.onExcessProperty === "error"
const onExcessPropertyPreserve = options.onExcessProperty === "preserve"
// ---------------------------------------------
// handle excess properties
// ---------------------------------------------
let inputKeys: Array<PropertyKey> | undefined
if (ast.indexSignatures.length === 0 && (onExcessPropertyError || onExcessPropertyPreserve)) {
inputKeys = Reflect.ownKeys(input)
for (let i = 0; i < inputKeys.length; i++) {
const key = inputKeys[i]
if (!expectedKeysSet.has(key)) {
// key is unexpected
if (onExcessPropertyError) {
const issue = new SchemaIssue.Pointer([key], new SchemaIssue.UnexpectedKey(ast, input[key]))
if (errorsAllOption) {
if (state.issues) {
state.issues.push(issue)
} else {
state.issues = [issue]
}
continue
} else {
return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, [issue]))
}
} else {
// preserve key
internalRecord.set(out, key, input[key])
}
}
}
}
const concurrency = resolveConcurrency(options?.concurrency)
// ---------------------------------------------
// handle property signatures
// ---------------------------------------------
const eff = parseProperties(state, properties, concurrency)
if (eff) yield* eff
// ---------------------------------------------
// handle index signatures
// ---------------------------------------------
if (parseIndexes) {
const keyPairs = Arr.empty<[PropertyKey, IndexSignature]>()
for (let i = 0; i < indexCount; i++) {
const is = ast.indexSignatures[i]
const keys = getIndexSignatureKeys(input, is.parameter, options)
for (let j = 0; j < keys.length; j++) {
const key = keys[j]
keyPairs.push([key, is])
}
}
const eff = parseIndexes(state, keyPairs, concurrency)
if (eff) yield* eff
}
if (state.issues) {
return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, state.issues))
}
if (options.propertyOrder === "original") {
// preserve input keys order
const keys = (inputKeys ?? Reflect.ownKeys(input)).concat(expectedKeys)
const preserved: Record<PropertyKey, unknown> = {}
for (const key of keys) {
if (Object.hasOwn(out, key)) {
internalRecord.set(preserved, key, out[key])
}
}
return Option.some(preserved)
}
return Option.some(out)
})
}
private _rebuild(
recur: (ast: AST) => AST,
recurParameter: (ast: AST) => AST,
flipMerge: boolean,
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)
const merge = flipMerge ? is.merge?.flip() : is.merge
return p === is.parameter && t === is.type && merge === is.merge
? is
: new IndexSignature(p, t, merge)
})
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, true, this.encodingChecks, this.checks)
}
/** @internal */
recur(recur: (ast: AST) => AST, recurParameter: (ast: AST) => AST = recur): AST {
return this._rebuild(recur, recurParameter, false, this.checks, this.encodingChecks)
}
/** @internal */
getExpected(): string {
if (this.propertySignatures.length === 0 && this.indexSignatures.length === 0) return "object | array"
return "object"
}
}
type ParsedProperty = {
readonly ps: PropertySignature | IndexSignature
readonly parser: SchemaParser.Parser
readonly name: PropertyKey
readonly type: AST
}
const parseProperties = iterateEager<{
readonly ast: AST
readonly oinput: Option.Option<unknown>
readonly input: Record<PropertyKey, unknown>
readonly options: ParseOptions
readonly out: Record<PropertyKey, unknown>
issues: Array<SchemaIssue.Issue> | undefined
}, ParsedProperty>()({
onItem(
s: {
readonly oinput: Option.Option<unknown>
readonly input: Record<PropertyKey, unknown>
readonly options: ParseOptions
readonly out: Record<PropertyKey, unknown>
issues: Array<SchemaIssue.Issue> | undefined
},
p
) {
const value: Option.Option<unknown> = Object.hasOwn(s.input, p.name)
? Option.some(s.input[p.name])
: Option.none()
return p.parser(value, s.options)
},
step(s, p, exit) {
if (exit._tag === "Failure") {
return wrapPropertyKeyIssue(s, s.ast, p.name, exit)
} else if (exit.value._tag === "Some") {
internalRecord.set(s.out, p.name, exit.value.value)
} else 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, s.oinput, [issue])
)
}
}
}
})
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 extends Schema.Struct.Fields>(
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<S extends { readonly ast: AST }>(self: S): S["ast"] {
return self.ast
}
/** @internal */
export function tuple<Elements extends Schema.Tuple.Elements>(
elements: Elements,
checks: Checks | undefined = undefined
): Arrays {
return new Arrays(false, elements.map((e) => e.ast), [], undefined, checks)
}
/** @internal */
export function union<Members extends ReadonlyArray<{ readonly ast: AST }>>(
members: Members,
mode: "anyOf" | "oneOf",
checks: Checks | undefined
): Union<Members[number]["ast"]> {
return new Union(members.map(getAST), mode, undefined, checks)
}
/** @internal */
export function structWithRest(ast: Objects, records: ReadonlyArray<Objects>): 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) {
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<AST>): Arrays {
if (ast.encoding) {
throw new Error("TupleWithRest does not support encodings")
}
return new Arrays(ast.isMutable, ast.elements, rest, undefined, ast.checks)
}
type Type =
| "null"
| "array"
| "object"
| "string"
| "number"
| "boolean"
| "symbol"
| "undefined"
| "bigint"
| "function"
/** @internal */
export type Sentinel = {
readonly key: PropertyKey
readonly literal: LiteralValue | symbol
}
function getCandidateTypes(ast: AST): ReadonlyArray<Type> {
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"]
: ["object", "array"]
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): Array<Sentinel> {
switch (ast._tag) {
default:
return []
case "Declaration": {
const s = ast.annotations?.["~sentinels"]
return Array.isArray(s) ? s : []
}
case "Objects":
return ast.propertySignatures.flatMap((ps): Array<Sentinel> => {
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) => {
return isLiteral(e) && !isOptional(e)
? [{ key: i, literal: e.literal }]
: []
})
case "Suspend":
return collectSentinels(ast.thunk())
}
}
type CandidateIndex = {
byType?: { [K in Type]?: Array<number> }
bySentinel?: Map<PropertyKey, Map<LiteralValue | symbol, Array<number>>>
otherwise?: { [K in Type]?: Array<number> }
}
const candidateIndexCache = new WeakMap<ReadonlyArray<AST>, CandidateIndex>()
function getIndex(types: ReadonlyArray<AST>): CandidateIndex {
let idx = candidateIndexCache.get(types)
if (idx) return idx
idx = {}
for (let i = 0; i < types.length; i++) {
const a = types[i]
const encoded = toEncoded(a)
if (isNever(encoded)) continue
const candidateTypes = getCandidateTypes(encoded)
const sentinels = collectSentinels(encoded)
// by-type (always filled – cheap primary filter)
idx.byType ??= {}
for (const t of candidateTypes) (idx.byType[t] ??= []).push(i)
if (sentinels.length > 0) { // discriminated variants
idx.bySentinel ??= new Map()
for (const { key, literal } of sentinels) {
let m = idx.bySentinel.get(key)
if (!m) idx.bySentinel.set(key, m = new Map())
let arr = m.get(literal)
if (!arr) m.set(literal, arr = [])
arr.push(i)
}
} else { // non-discriminated
idx.otherwise ??= {}
for (const t of candidateTypes) (idx.otherwise[t] ??= []).push(i)
}
}
candidateIndexCache.set(types, idx)
return idx
}
function filterLiterals(input: any) {
return (ast: AST) => {
const encoded = toEncoded(ast)
return encoded._tag === "Literal" ?
encoded.literal === input
: encoded._tag === "UniqueSymbol" ?
encoded.symbol === input
: true
}
}
/**
* The goal is to reduce the number of a union members that will be checked.
* This is useful to reduce the number of issues that will be returned.
*
* @internal
*/
export function getCandidates(input: any, types: ReadonlyArray<AST>): ReadonlyArray<AST> {
const idx = getIndex(types)
const runtimeType: Type = input === null ? "null" : Array.isArray(input) ? "array" : typeof input
// 1. Try sentinel-based dispatch (most selective)
if (idx.bySentinel) {
const base = idx.otherwise?.[runtimeType] ?? []
if (runtimeType === "object" || runtimeType === "array") {
const selected = new Set(base)
for (const [k, m] of idx.bySentinel) {
if (Object.hasOwn(input, k)) {
const match = m.get((input as any)[k])
if (match) {
for (const candidate of match) selected.add(candidate)
}
}
}
return Array.from(selected).sort((a, b) => a - b).map((i) => types[i]).filter(filterLiterals(input))
}
return base.map((i) => types[i])
}
// 2. Fallback: runtime-type dispatch only
return (idx.byType?.[runtimeType] ?? []).map((i) => types[i]).filter(filterLiterals(input))
}
/**
* AST node representing a union of schemas.
*
* **Details**
*
* - `types` — the member AST nodes.
* - `mode` — `"anyOf"` succeeds on the first match (like TypeScript unions);
* `"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 { Schema, SchemaAST } from "effect"
*
* const schema = Schema.Union([Schema.String, Schema.Number])
* const ast = schema.ast
*
* if (SchemaAST.isUnion(ast)) {
* console.log(ast.types.length) // 2
* console.log(ast.mode) // "anyOf"
* }
* ```
*
* @see {@link isUnion}
* @category models
* @since 3.10.0
*/
export class Union<A extends AST = AST> extends Base {
readonly _tag = "Union"
readonly types: ReadonlyArray<A>
readonly mode: "anyOf" | "oneOf"
readonly encodingChecks: Checks | undefined
constructor(
types: ReadonlyArray<A>,
mode: "anyOf" | "oneOf",
annotations?: Schema.Annotations.Annotations,
checks?: Checks,
encoding?: Encoding,
context?: Context,
encodingChecks?: Checks
) {
super(annotations, checks, encoding, context)
this.types = types
this.mode = mode
this.encodingChecks = encodingChecks
}
/** @internal */
getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser {
// oxlint-disable-next-line @typescript-eslint/no-this-alias
const ast = this
return (oinput, options) => {
if (oinput._tag === "None") {
return Effect.succeed(oinput)
}
const input = oinput.value
const candidates = getCandidates(input, ast.types)
const state = {
ast,
recur,
oinput,
input,
out: undefined,
successes: [],
issues: undefined as Arr.NonEmptyArray<SchemaIssue.Issue> | undefined,
options
}
const concurrency = resolveConcurrency(options?.concurrency)
const eff = parseUnion(state, candidates, concurrency ? { ...concurrency, orderedStep: true } : undefined)
if (!eff) {
return state.out
? Effect.succeed(state.out)
: Effect.fail(new SchemaIssue.AnyOf(ast, input, state.issues ?? []))
}
return Effect.flatMap(eff, (_) => {
return state.out
? Effect.succeed(state.out)
: Effect.fail(new SchemaIssue.AnyOf(ast, input, state.issues ?? []))
})
}
}
private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) {
const types = mapOrSame(this.types, recur)
return types === this.types && checks === this.checks && encodingChecks === this.encodingChecks ?
this :
new Union(types, this.mode, 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 */
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(" | ")
}
}
const parseUnion = iterateEager<{
readonly recur: (ast: AST) => SchemaParser.Parser
readonly ast: Union
readonly oinput: Option.Option<unknown>
readonly input: unknown
readonly options: ParseOptions
out: Option.Option<unknown> | undefined
successes: Array<AST>
issues: Array<SchemaIssue.Issue> | undefined
}, AST>()({
onItem(s, ast) {
const parser = s.recur(ast)
return parser(s.oinput, s.options)
},
step(s, candidate, 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.ast.mode === "oneOf") {
s.successes.push(candidate)
return Exit.fail(new SchemaIssue.OneOf(s.ast, s.input, s.successes))
}
s.out = exit.value
s.successes.push(candidate)
if (s.ast.mode === "anyOf") {
return Exit.void
}
}
}
})
const nonFiniteLiterals = new Union([
new Literal("Infinity"),
new Literal("-Infinity"),
new Literal("NaN")
], "anyOf")
const numberToJson = new Link(
new Union([number, nonFiniteLiterals], "anyOf"),
new SchemaTransformation.Transformation(
SchemaGetter.Number(),
SchemaGetter.transform((n) => globalThis.Number.isFinite(n) ? n : globalThis.String(n))
)
)
function formatIsMutable(isMutable: boolean | undefined): string {
return isMutable ? "" : "readonly "
}
function formatIsOptional(isOptional: boolean | undefined): string {
return isOptional ? "?" : ""
}
/** @internal */
export function memoizeThunk<A>(f: () => A): () => A {
let done = false
let a: A
return () => {
if (done) {
return a
}
a = f()
done = true
return a
}
}
/**
* 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 { Schema, SchemaAST } from "effect"
*
* interface Category {
* readonly name: string
* readonly children: ReadonlyArray<Category>
* }
*
* const Category = Schema.Struct({
* name: Schema.String,
* children: Schema.Array(Schema.suspend((): Schema.Codec<Category> => Category))
* })
*
* // The recursive branch is a Suspend node
* ```
*
* @see {@link isSuspend}
* @category models
* @since 3.10.0
*/
export class Suspend extends Base {
readonly _tag = "Suspend"
readonly thunk: () => AST
constructor(
thunk: () => AST,
annotations?: Schema.Annotations.Annotations,
checks?: Checks,
encoding?: Encoding,
context?: Context
) {
if (checks !== undefined) {
throw new Error("Cannot add checks to Suspend")
}
super(annotations, undefined, encoding, context)
this.thunk = memoizeThunk(thunk)
}
/** @internal */
getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser {
return recur(this.thunk())
}
/** @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 metadata (expected message, meta
* tags, 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}
* @category models
* @since 4.0.0
*/
export class Filter<in E> 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<E> {
return new Filter(this.run, { ...this.annotations, ...annotations }, this.aborted)
}
abort(): Filter<E> {
return new Filter(this.run, this.annotations, true)
}
and(other: Check<E>, annotations?: Schema.Annotations.Filter): FilterGroup<E>
and(other: Check<E>, annotations?: Schema.Annotations.Filter): FilterGroup<E> {
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}
* @category models
* @since 4.0.0
*/
export class FilterGroup<in E> extends Pipeable.Class {
readonly _tag = "FilterGroup"
readonly checks: readonly [Check<E>, ...Array<Check<E>>]
readonly annotations: Schema.Annotations.Filter | undefined
constructor(
checks: readonly [Check<E>, ...Array<Check<E>>],
annotations: Schema.Annotations.Filter | undefined = undefined
) {
super()
this.checks = checks
this.annotations = annotations
}
annotate(annotations: Schema.Annotations.Filter): FilterGroup<E> {
return new FilterGroup(this.checks, { ...this.annotations, ...annotations })
}
and(other: Check<E>, annotations?: Schema.Annotations.Filter): FilterGroup<E>
and(other: Check<E>, annotations?: Schema.Annotations.Filter): FilterGroup<E> {
return new FilterGroup([this, other], annotations)
}
}
/**
* A validation check — either a single {@link Filter} or a composite
* {@link FilterGroup}.
*
* **Details**
*
* Stored in the {@link Checks} array on {@link Base.checks}.
*
* @see {@link Filter}
* @see {@link FilterGroup}
* @category models
* @since 4.0.0
*/
export type Check<T> = Filter<T> | FilterGroup<T>
/** @internal */
export function makeFilter<T>(
filter: (input: T, ast: AST, options: ParseOptions) => Schema.FilterOutput,
annotations?: Schema.Annotations.Filter | undefined,
aborted: boolean = false
): Filter<T> {
return new Filter(
(input, ast, options) => SchemaIssue.make(input, ast, filter(input, ast, options)),
annotations,
aborted
)
}
/** @internal */
export function makeFilterByGuard<T extends E, E>(
is: (value: E) => value is T,
annotations?: Schema.Annotations.Filter
): Filter<any> {
return new Filter(
(input: E) => is(input) ? undefined : new SchemaIssue.InvalidValue(Option.some(input)),
annotations,
true // after a guard, we always want to abort
)
}
/**
* 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.filter` or attached directly to a
* `String` AST node through checks. The regular expression source is stored in
* annotations for serialization and arbitrary generation.
*
* **Gotchas**
*
* Use a non-global, non-sticky regular expression, or reset `lastIndex`
* yourself, because `RegExp.test` is stateful for expressions with the `g` or
* `y` flag.
*
* **Example** (Validating an email pattern)
*
* ```ts
* import { SchemaAST } from "effect"
*
* const emailFilter = SchemaAST.isPattern(/^[^@]+@[^@]+$/)
* ```
*
* @see {@link Filter}
* @category constructors
* @since 4.0.0
*/
export function isPattern(regExp: globalThis.RegExp, annotations?: Schema.Annotations.Filter) {
const source = regExp.source
return makeFilter(
(s: string) => regExp.test(s),
{
expected: `a string matching the RegExp ${source}`,
meta: {
_tag: "isPattern",
regExp
},
arbitrary: {
constraint: {
patterns: [regExp.source]
}
},
...annotations
}
)
}
function modifyOwnPropertyDescriptors<A extends AST>(
ast: A,
f: (
d: { [P in keyof A]: TypedPropertyDescriptor<A[P]> }
) => void
): A {
const d = Object.getOwnPropertyDescriptors(ast)
f(d)
return Object.create(Object.getPrototypeOf(ast), d)
}
/** @internal */
export function replaceEncoding<A extends AST>(ast: A, encoding: Encoding | undefined): A {
if (ast.encoding === encoding) {
return ast
}
return modifyOwnPropertyDescriptors(ast, (d) => {
d.encoding.value = encoding
})
}
/** @internal */
export function replaceContext<A extends AST>(ast: A, context: Context | undefined): A {
if (ast.context === context) {
return ast
}
return modifyOwnPropertyDescriptors(ast, (d) => {
d.context.value = context
})
}
/** @internal */
export function getLastEncoding(ast: AST): AST {
return ast.encoding ? getLastEncoding(ast.encoding[ast.encoding.length - 1].to) : ast
}
/** @internal */
export function annotate<A extends AST>(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 modifyOwnPropertyDescriptors(ast, (d) => {
d.annotations.value = { ...d.annotations.value, ...annotations }
})
}
/** @internal */
export function replaceChecks<A extends AST>(ast: A, checks: Checks | undefined): A {
if (ast._tag === "Suspend" && checks !== undefined) {
throw new Error("Cannot add checks to Suspend")
}
if (ast.checks === checks) {
return ast
}
return modifyOwnPropertyDescriptors(ast, (d) => {
d.checks.value = checks
})
}
/** @internal */
export function appendChecks<A extends AST>(ast: A, checks: Checks | undefined): A {
return replaceChecks(ast, combineChecks(ast.checks, checks))
}
function updateLastLink(encoding: Encoding, f: (ast: AST) => AST): Encoding {
const links = encoding
const last = links[links.length - 1]
const to = f(last.to)
if (to !== last.to) {
return Arr.append(encoding.slice(0, encoding.length - 1), new Link(to, last.transformation))
}
return encoding
}
/** @internal */
export function applyToLastLink(f: (ast: AST) => AST) {
return <A extends AST>(ast: A): A => ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, f)) : 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 middlewareDecoding(
ast: AST,
middleware: SchemaTransformation.Middleware<any, any, any, any, any, any>
): AST {
return appendTransformation(ast, middleware, toType(ast))
}
/** @internal */
export function middlewareEncoding(
ast: AST,
middleware: SchemaTransformation.Middleware<any, any, any, any, any, any>
): AST {
return appendTransformation(toEncoded(ast), middleware, ast)
}
function appendTransformation<A extends AST>(
from: AST,
transformation:
| SchemaTransformation.Transformation<any, any, any, any>
| SchemaTransformation.Middleware<any, any, any, any, any, any>,
to: A
): A {
const link = new Link(from, transformation)
return replaceEncoding(to, to.encoding ? [...to.encoding, link] : [link])
}
/** @internal */
export function brand(ast: AST, brand: string): AST {
const existing = InternalAnnotations.resolveBrands(ast)
const brands = existing ? [...existing, brand] : [brand]
return annotate(ast, { brands })
}
/**
* Maps over the array but will return the original array if no changes occur.
* @internal
*/
export function mapOrSame<A>(as: Arr.NonEmptyReadonlyArray<A>, f: (a: A) => A): Arr.NonEmptyReadonlyArray<A>
export function mapOrSame<A>(as: ReadonlyArray<A>, f: (a: A) => A): ReadonlyArray<A>
export function mapOrSame<A>(as: ReadonlyArray<A>, f: (a: A) => A): ReadonlyArray<A> {
let changed = false
const out: Array<A> = new Array(as.length)
for (let i = 0; i < as.length; i++) {
const a = as[i]
const fa = f(a)
if (fa !== a) {
changed = true
}
out[i] = fa
}
return changed ? out : as
}
/** @internal */
export function annotateKey<A extends AST>(ast: A, annotations: Schema.Annotations.Key<unknown>): A {
const context = ast.context ?
new Context(
ast.context.isOptional,
ast.context.isMutable,
ast.context.defaultValue,
{ ...ast.context.annotations, ...annotations }
) :
new Context(false, false, undefined, annotations)
return replaceContext(ast, context)
}
/** @internal */
export const optionalKeyLastLink = applyToLastLink(optionalKey)
/**
* Marks an AST node's property key as optional by setting
* {@link Context.isOptional} to `true`.
*
* **Details**
*
* Also propagates the optional flag through the last link of the encoding
* chain if present.
*
* @see {@link isOptional}
* @see {@link Context}
* @category transforming
* @since 4.0.0
*/
export function optionalKey<A extends AST>(ast: A): A {
const context = ast.context ?
ast.context.isOptional === false ?
new Context(true, ast.context.isMutable, ast.context.defaultValue, ast.context.annotations) :
ast.context :
new Context(true, false)
return optionalKeyLastLink(replaceContext(ast, context))
}
const mutableKeyLastLink = applyToLastLink(mutableKey)
/** @internal */
export function mutableKey<A extends AST>(ast: A): A {
const context = ast.context ?
ast.context.isMutable === false ?
new Context(ast.context.isOptional, true, ast.context.defaultValue, ast.context.annotations) :
ast.context :
new Context(false, true)
return mutableKeyLastLink(replaceContext(ast, context))
}
/** @internal */
export function withConstructorDefault<A extends AST>(
ast: A,
defaultValue: Effect.Effect<unknown, SchemaIssue.Issue>
): A {
const transformation = new SchemaTransformation.Transformation(
SchemaGetter.withDefault(defaultValue),
SchemaGetter.passthrough()
)
const encoding: Encoding = [new Link(unknown, transformation)]
const context = ast.context ?
new Context(ast.context.isOptional, ast.context.isMutable, encoding, ast.context.annotations) :
new Context(false, false, encoding)
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.transform` and
* `Schema.transformOrFail`. 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}
* @category transforming
* @since 4.0.0
*/
export function decodeTo<A extends AST>(
from: AST,
to: A,
transformation: SchemaTransformation.Transformation<any, any, any, any>
): A {
return appendTransformation(from, transformation, to)
}
function parseParameter(ast: AST): {
literals: ReadonlyArray<PropertyKey>
parameters: ReadonlyArray<AST>
} {
const literals: Array<PropertyKey> = []
const parameters: Array<AST> = []
function go(ast: AST) {
switch (ast._tag) {
case "Literal":
if (Predicate.isPropertyKey(ast.literal)) {
literals.push(ast.literal)
}
return
case "UniqueSymbol":
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, keyValueCombiner: KeyValueCombiner | undefined): Objects {
const { literals, parameters: indexSignatures } = parseParameter(key)
return new Objects(
literals.map((literal) => new PropertySignature(literal, value)),
indexSignatures.map((parameter) => new IndexSignature(parameter, value, keyValueCombiner))
)
}
// -------------------------------------------------------------------------------------
// 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 {@link optionalKey}
* @see {@link Context}
* @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
}
/**
* 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 { Schema, SchemaAST } from "effect"
*
* const schema = Schema.NumberFromString
* const typeAst = SchemaAST.toType(schema.ast)
* console.log(typeAst._tag) // "Number"
* ```
*
* @see {@link toEncoded}
* @see {@link flip}
* @category transforming
* @since 4.0.0
*/
export const toType = memoize(<A extends AST>(ast: A): A => {
if (ast.encoding) {
return toType(replaceEncoding(ast, undefined))
}
const out: any = ast
const type = out.recur?.(toType) ?? out
const encodingChecks = type.encodingChecks
if (encodingChecks) {
return modifyOwnPropertyDescriptors(type, (d) => {
d.encodingChecks.value = undefined
if (type === ast) {
d.checks.value = combineChecks(type.checks, encodingChecks)
}
})
}
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 { Schema, SchemaAST } from "effect"
*
* const schema = Schema.NumberFromString
* const encodedAst = SchemaAST.toEncoded(schema.ast)
* console.log(encodedAst._tag) // "String"
* ```
*
* @see {@link toType}
* @see {@link flip}
* @category transforming
* @since 4.0.0
*/
export const toEncoded = memoize((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<Link> = [
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.encode` — 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}
* @category transforming
* @since 4.0.0
*/
export const flip = memoize((ast: AST): AST => {
if (ast.encoding) {
return flipEncoding(ast, ast.encoding)
}
const out: any = ast
return out.flip?.(flip) ?? out.recur?.(flip) ?? out
})
/** @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<const T>(
ast: AST,
value: T
): SchemaParser.Parser {
const succeed = Effect.succeedSome(value)
return (oinput) => {
if (oinput._tag === "None") {
return Effect.succeedNone
}
return oinput.value === value
? succeed
: Effect.fail(new SchemaIssue.InvalidType(ast, oinput))
}
}
function fromAnyToConst<const T>(value: T): SchemaParser.Parser {
const succeed = Effect.succeedSome(value)
return (oinput) => oinput._tag === "None" ? Effect.succeedNone : succeed
}
function fromRefinement<T>(
ast: AST,
refinement: (input: unknown) => input is T
): SchemaParser.Parser {
return (oinput) => {
if (oinput._tag === "None") {
return Effect.succeedNone
}
return refinement(oinput.value)
? Effect.succeed(oinput)
: Effect.fail(new SchemaIssue.InvalidType(ast, oinput))
}
}
function applyTemplateLiteralPartChecks<A>(ast: AST, value: A, options: ParseOptions): A | undefined {
if (options?.disableChecks || ast.checks === undefined) return value
const issues: Array<SchemaIssue.Issue> = []
collectIssues(ast.checks, value, issues, ast, options)
return issues.length === 0 ? value : undefined
}
function segmentTemplateLiteralParts(
parts: ReadonlyArray<TemplateLiteralPart>,
input: string,
options: ParseOptions
): Array<string> | undefined {
const out = new Array<string>(parts.length)
const failures = new Set<string>()
function go(i: number, pos: number): boolean {
if (i === parts.length) return pos === input.length
const key = `${i}/${pos}`
if (failures.has(key)) 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 = globalThis.String(part.literal)
if (input.startsWith(s, pos) && go(i + 1, pos + s.length)) {
out[i] = s
return true
}
} else {
for (let end = input.length; end >= pos; end--) {
const s = input.slice(pos, end)
if (part.matchPart(s, options) !== undefined && go(i + 1, end)) {
out[i] = s
return true
}
}
}
failures.add(key)
return false
}
return go(0, 0) ? out : undefined
}
/** @internal */
export const enumsToLiterals = memoize((ast: Enum): Union<Literal> => {
return new Union(
ast.enums.map((e) => new Literal(e[1], { title: e[0] })),
"anyOf"
)
})
const parameterFromPropertyKey = applyToSelfOrLastLinkEncoding((ast) => {
switch (ast._tag) {
default:
return ast
case "Number":
return ast.toCodecStringTree()
case "Union":
return ast.recur(parameterFromPropertyKey)
}
})
/** @internal */
export const parameterFromString = applyToSelfOrLastLinkEncoding((ast) => {
switch (ast._tag) {
default:
return ast
case "Symbol":
case "UniqueSymbol":
return ast.toCodecStringTree()
case "Union":
return ast.recur(parameterFromString)
}
})
const partFromString = applyToSelfOrLastLinkEncoding((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",
meta: {
_tag: "isStringFinite",
regExp: isStringFiniteRegExp
},
...annotations
}
)
}
const finiteString = appendChecks(string, [isStringFinite()])
const finiteToString = new Link(
finiteString,
SchemaTransformation.numberFromString
)
const numberToString = new Link(
new Union([finiteString, nonFiniteLiterals], "anyOf"),
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",
meta: {
_tag: "isStringBigInt",
regExp: isStringBigIntRegExp
},
...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\\((.*)\\)"
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.transformOrFail((sym: symbol) => {
const key = globalThis.Symbol.keyFor(sym)
if (key !== undefined) {
return Effect.succeed(globalThis.String(sym))
}
return Effect.fail(
new SchemaIssue.Forbidden(Option.some(sym), { message: "cannot serialize to string, Symbol is not registered" })
)
})
)
)
/** @internal */
export function isStringSymbol(annotations?: Schema.Annotations.Filter) {
return isPattern(
isStringSymbolRegExp,
{
expected: "a string representing a symbol",
meta: {
_tag: "isStringSymbol",
regExp: isStringSymbolRegExp
},
...annotations
}
)
}
/** @internal */
export function collectIssues<T>(
checks: ReadonlyArray<Check<T>>,
value: T,
issues: Array<SchemaIssue.Issue>,
ast: AST,
options: ParseOptions
) {
for (let i = 0; i < checks.length; i++) {
const check = checks[i]
if (check._tag === "FilterGroup") {
collectIssues(check.checks, value, issues, ast, options)
} else {
const issue = check.run(value, ast, options)
if (issue) {
issues.push(new SchemaIssue.Filter(value, check, issue))
if (check.aborted || options?.errors !== "all") {
return
}
}
}
}
}
/** @internal */
export function runChecks<T>(
checks: readonly [Check<T>, ...Array<Check<T>>],
s: T
): Result.Result<T, SchemaIssue.Issue> {
const issues: Array<SchemaIssue.Issue> = []
collectIssues(checks, s, issues, unknown, { errors: "all" })
if (Arr.isArrayNonEmpty(issues)) {
const issue = new SchemaIssue.Composite(unknown, Option.some(s), issues)
return Result.fail(issue)
}
return Result.succeed(s)
}
/** @internal */
export const ClassTypeId = "~effect/Schema/Class"
/** @internal */
export const STRUCTURAL_ANNOTATION_KEY = "~structural"
/**
* 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.annotations(...))`).
* Otherwise returns `Base.annotations` directly.
*
* **Example** (Reading annotations)
*
* ```ts
* import { Schema, SchemaAST } from "effect"
*
* const schema = Schema.String.annotate({ title: "Name" })
* const annotations = SchemaAST.resolve(schema.ast)
* console.log(annotations?.title) // "Name"
* ```
*
* @see {@link resolveAt}
* @see {@link resolveIdentifier}
* @see {@link resolveTitle}
* @see {@link resolveDescription}
* @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}
* @category annotations
* @since 4.0.0
*/
export const resolveAt: <A>(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.annotations({ identifier: "..." })`
* and is used for error messages and schema identification.
*
* @see {@link resolve}
* @see {@link resolveTitle}
* @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}
*
* @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}
*
* @category annotations
* @since 4.0.0
*/
export const resolveDescription: (ast: AST) => string | undefined = InternalAnnotations.resolveDescription
/**
* 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 {
// `onPath` is the current recursion stack: nodes between the root and the
// one being visited. A hit here means we looped back to an ancestor — a
// real cycle, not a DAG — so the value is not JSON.
const onPath = new Set<unknown>()
// `validated` memoizes subtrees we've already fully checked. Without it, a
// diamond-shaped DAG (same node reached through multiple parents) would be
// re-traversed once per parent, which is exponential in the nesting depth.
const validated = new Set<unknown>()
return recur(u)
function recur(u: unknown): boolean {
if (u === null || typeof u === "string" || typeof u === "boolean") {
return true
}
if (typeof u === "number") {
return globalThis.Number.isFinite(u)
}
if (typeof u !== "object" || u === undefined) {
return false
}
if (onPath.has(u)) {
return false
}
if (validated.has(u)) {
return true
}
const isArray = Array.isArray(u)
if (!isArray) {
const prototype = Object.getPrototypeOf(u)
if (prototype !== null && Object.getPrototypeOf(prototype) !== null) {
return false
}
}
onPath.add(u)
const ok = isArray
? u.every(recur)
: Object.keys(u).every((key) => recur((u as Record<string, unknown>)[key]))
// Pop on exit so siblings reaching the same node via a different path
// don't see it as an ancestor (that would reject valid DAGs).
onPath.delete(u)
if (ok) {
validated.add(u)
}
return ok
}
}
/** @internal */
export const Json = new Declaration(
[],
() => (input, ast) =>
isJson(input) ?
Effect.succeed(input) :
Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))),
{
typeConstructor: {
_tag: "effect/Json"
},
generation: {
runtime: `Schema.Json`,
Type: `Schema.Json`
},
expected: "JSON value",
toCodecJson: () => new Link(unknown, SchemaTransformation.passthrough()),
toArbitrary: () => (fc: typeof FastCheck) => fc.jsonValue()
}
)
/** @internal */
export const MutableJson = annotate(Json, {
typeConstructor: {
_tag: "effect/MutableJson"
},
generation: {
runtime: `Schema.MutableJson`,
Type: `Schema.MutableJson`
}
})
/** @internal */
export const unknownToNull = new Link(
null_,
new SchemaTransformation.Transformation(
SchemaGetter.passthrough(),
SchemaGetter.transform(() => null)
)
)
/** @internal */
export const unknownToJson = new Link(
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 {
const seen = new Set<unknown>()
return recur(u)
function recur(u: unknown): boolean {
if (u === undefined || typeof u === "string") {
return true
}
if (typeof u !== "object" || u === null) {
return false
}
if (seen.has(u)) {
return false
}
seen.add(u)
if (Array.isArray(u)) {
return u.every(recur)
}
return Object.keys(u).every((key) => recur((u as Record<string, unknown>)[key]))
}
}
const StringTree = new Declaration(
[],
() => (input, ast) =>
isStringTree(input) ?
Effect.succeed(input) :
Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))),
{ expected: "StringTree" }
)
/** @internal */
export const unknownToStringTree = new Link(
StringTree,
SchemaTransformation.passthrough()
)