4002 lines
111 KiB
TypeScript
4002 lines
111 KiB
TypeScript
/**
|
||
* 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()
|
||
)
|