| /** | |
| * 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 Option from "./Option.ts" | |
| import * as Pipeable from "./Pipeable.ts" | |
| import * as Predicate from "./Predicate.ts" | |
| import * as RegEx from "./RegExp.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 ? | |
| 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 the `void` type (accepts `undefined` at runtime). | |
| * | |
| * **Details** | |
| * | |
| * Behaves like {@link Undefined} for parsing but represents the TypeScript | |
| * `void` type semantically. | |
| * | |
| * @see {@link void_ void} | |
| * @see {@link isVoid} | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export class Void extends Base { | |
| readonly _tag = "Void" | |
| /** @internal */ | |
| getParser() { | |
| return fromConst(this, 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 that represent the TypeScript | |
| * `void` type and accept `undefined` at runtime. | |
| * | |
| * @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": | |
| case "Literal": | |
| case "TemplateLiteral": | |
| return true | |
| case "Union": | |
| return 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 the | |
| * template literal pattern. A regex is derived from the parts to validate | |
| * 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 */ | |
| asTemplateLiteralParser(): Arrays { | |
| const tuple = new Arrays(false, this.parts.map(templateLiteralPartFromString), []) | |
| const regExp = getTemplateLiteralRegExp(this) | |
| return decodeTo( | |
| string, | |
| tuple, | |
| new SchemaTransformation.Transformation( | |
| SchemaGetter.transformOrFail((s: string) => { | |
| const match = regExp.exec(s) | |
| if (match) return Effect.succeed(match.slice(1, this.parts.length + 1)) | |
| return Effect.fail( | |
| new SchemaIssue.InvalidValue(Option.some(s), { | |
| message: `Expected a value matching ${regExp.source}, 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 ? | |
| 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> | |
| ) => { | |
| const issueResult = Cause.findError(exit.cause) | |
| if (Result.isFailure(issueResult)) { | |
| return exit | |
| } | |
| const issue = new SchemaIssue.Pointer([key], issueResult.success) | |
| if (s.options.errors === "all") { | |
| if (s.issues) s.issues.push(issue) | |
| else s.issues = [issue] | |
| } else { | |
| return Exit.fail(new SchemaIssue.Composite(ast, s.oinput, [issue])) | |
| } | |
| } | |
| /** | |
| * floating point or integer, with optional exponent | |
| * @internal | |
| */ | |
| export const FINITE_PATTERN = "[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?" | |
| const isNumberStringRegExp = new globalThis.RegExp(`(?:${FINITE_PATTERN}|Infinity|-Infinity|NaN)`) | |
| /** | |
| * Returns the object keys that match the index signature parameter schema. | |
| * @internal | |
| */ | |
| export function getIndexSignatureKeys( | |
| input: { readonly [x: PropertyKey]: unknown }, | |
| parameter: AST | |
| ): ReadonlyArray<PropertyKey> { | |
| const encoded = toEncoded(parameter) | |
| switch (encoded._tag) { | |
| case "String": | |
| return Object.keys(input) | |
| case "TemplateLiteral": { | |
| const regExp = getTemplateLiteralRegExp(encoded) | |
| return Object.keys(input).filter((k) => regExp.test(k)) | |
| } | |
| case "Symbol": | |
| return Object.getOwnPropertySymbols(input) | |
| case "Number": | |
| return Object.keys(input).filter((k) => isNumberStringRegExp.test(k)) | |
| case "Union": | |
| return [...new Set(encoded.types.flatMap((t) => getIndexSignatureKeys(input, t)))] | |
| default: | |
| return [] | |
| } | |
| } | |
| /** | |
| * 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) | |
| } | |
| } | |
| /** | |
| * 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: AST | |
| readonly type: AST | |
| readonly merge: KeyValueCombiner | undefined | |
| constructor( | |
| parameter: AST, | |
| type: AST, | |
| merge: KeyValueCombiner | undefined | |
| ) { | |
| 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(indexSignatureParameterFromString(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) | |
| 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, | |
| 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 = recur(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 | |
| ? this | |
| : new Objects( | |
| props, | |
| indexes, | |
| this.annotations, | |
| checks, | |
| undefined, | |
| this.context, | |
| encodingChecks | |
| ) | |
| } | |
| /** @internal */ | |
| flip(recur: (ast: AST) => AST): AST { | |
| return this.rebuild(recur, true, this.encodingChecks, this.checks) | |
| } | |
| /** @internal */ | |
| recur(recur: (ast: AST) => AST): AST { | |
| return this.rebuild(recur, 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 mergeChecks(checks: Checks | undefined, b: AST): Checks | undefined { | |
| if (!checks) { | |
| return b.checks | |
| } | |
| if (!b.checks) { | |
| return checks | |
| } | |
| return [...checks, ...b.checks] | |
| } | |
| /** @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 Schema.Top>(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<Schema.Top>>( | |
| 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 r of records) { | |
| propertySignatures = propertySignatures.concat(r.propertySignatures) | |
| indexSignatures = indexSignatures.concat(r.indexSignatures) | |
| checks = mergeChecks(checks, r) | |
| } | |
| 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": | |
| case "Void": | |
| 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<AST> } | |
| bySentinel?: Map<PropertyKey, Map<LiteralValue | symbol, Array<AST>>> | |
| otherwise?: { [K in Type]?: Array<AST> } | |
| } | |
| const candidateIndexCache = new WeakMap<ReadonlyArray<AST>, CandidateIndex>() | |
| function getIndex(types: ReadonlyArray<AST>): CandidateIndex { | |
| let idx = candidateIndexCache.get(types) | |
| if (idx) return idx | |
| idx = {} | |
| for (const a of types) { | |
| const encoded = toEncoded(a) | |
| if (isNever(encoded)) continue | |
| const types = getCandidateTypes(encoded) | |
| const sentinels = collectSentinels(encoded) | |
| // by-type (always filled – cheap primary filter) | |
| idx.byType ??= {} | |
| for (const t of types) (idx.byType[t] ??= []).push(a) | |
| 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(a) | |
| } | |
| } else { // non-discriminated | |
| idx.otherwise ??= {} | |
| for (const t of types) (idx.otherwise[t] ??= []).push(a) | |
| } | |
| } | |
| 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") { | |
| for (const [k, m] of idx.bySentinel) { | |
| if (Object.hasOwn(input, k)) { | |
| const match = m.get((input as any)[k]) | |
| if (match) return [...match, ...base].filter(filterLiterals(input)) | |
| } | |
| } | |
| } | |
| return base | |
| } | |
| // 2. Fallback: runtime-type dispatch only | |
| return (idx.byType?.[runtimeType] ?? []).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) | |
| 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 ? | |
| 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 */ | |
| 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 issueResult = Cause.findError(exit.cause) | |
| if (Result.isFailure(issueResult)) { | |
| return exit | |
| } | |
| if (s.issues) s.issues.push(issueResult.success) | |
| else s.issues = [issueResult.success] | |
| } 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** (Recursive schema AST) | |
| * | |
| * ```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()) | |
| } | |
| } | |
| /** @internal */ | |
| export function getEncodingChecks(ast: AST): Checks | undefined { | |
| switch (ast._tag) { | |
| case "Declaration": | |
| case "Arrays": | |
| case "Objects": | |
| case "Union": | |
| return ast.encodingChecks | |
| default: | |
| return undefined | |
| } | |
| } | |
| // ----------------------------------------------------------------------------- | |
| // 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): A { | |
| return replaceChecks(ast, ast.checks ? [...ast.checks, ...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 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> | |
| } { | |
| switch (ast._tag) { | |
| case "Literal": | |
| return { | |
| literals: Predicate.isPropertyKey(ast.literal) ? [ast.literal] : [], | |
| parameters: [] | |
| } | |
| case "UniqueSymbol": | |
| return { | |
| literals: [ast.symbol], | |
| parameters: [] | |
| } | |
| case "String": | |
| case "Number": | |
| case "Symbol": | |
| case "TemplateLiteral": | |
| return { | |
| literals: [], | |
| parameters: [ast] | |
| } | |
| case "Union": { | |
| const out: { | |
| literals: ReadonlyArray<PropertyKey> | |
| parameters: ReadonlyArray<AST> | |
| } = { literals: [], parameters: [] } | |
| for (let i = 0; i < ast.types.length; i++) { | |
| const parsed = parseParameter(ast.types[i]) | |
| out.literals = out.literals.concat(parsed.literals) | |
| out.parameters = out.parameters.concat(parsed.parameters) | |
| } | |
| return out | |
| } | |
| } | |
| 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 | |
| if (getEncodingChecks(type)) { | |
| return modifyOwnPropertyDescriptors(type, (d) => { | |
| d.encodingChecks.value = undefined | |
| }) | |
| } | |
| 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 getTemplateLiteralSource(ast: TemplateLiteral, top: boolean): string { | |
| return ast.encodedParts.map((part) => | |
| handleTemplateLiteralASTPartParens(part, getTemplateLiteralASTPartPattern(part), top) | |
| ).join("") | |
| } | |
| /** @internal */ | |
| export const getTemplateLiteralRegExp = memoize((ast: TemplateLiteral): RegExp => { | |
| return new globalThis.RegExp(`^${getTemplateLiteralSource(ast, true)}$`) | |
| }) | |
| function getTemplateLiteralASTPartPattern(part: TemplateLiteralPart): string { | |
| switch (part._tag) { | |
| case "Literal": | |
| return RegEx.escape(globalThis.String(part.literal)) | |
| case "String": | |
| return STRING_PATTERN | |
| case "Number": | |
| return FINITE_PATTERN | |
| case "BigInt": | |
| return BIGINT_PATTERN | |
| case "TemplateLiteral": | |
| return getTemplateLiteralSource(part, false) | |
| case "Union": | |
| return part.types.map(getTemplateLiteralASTPartPattern).join("|") | |
| } | |
| } | |
| function handleTemplateLiteralASTPartParens(part: TemplateLiteralPart, s: string, top: boolean): string { | |
| if (isUnion(part)) { | |
| if (!top) { | |
| return `(?:${s})` | |
| } | |
| } else if (!top) { | |
| return s | |
| } | |
| return `(${s})` | |
| } | |
| 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 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)) | |
| } | |
| } | |
| /** @internal */ | |
| export const enumsToLiterals = memoize((ast: Enum): Union<Literal> => { | |
| return new Union( | |
| ast.enums.map((e) => new Literal(e[1], { title: e[0] })), | |
| "anyOf" | |
| ) | |
| }) | |
| /** @internal */ | |
| export function toCodec(f: (ast: AST) => AST) { | |
| function out(ast: AST): AST { | |
| return ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, out)) : f(ast) | |
| } | |
| return memoize(out) | |
| } | |
| const indexSignatureParameterFromString = toCodec((ast) => { | |
| switch (ast._tag) { | |
| default: | |
| return ast | |
| case "Number": | |
| return ast.toCodecStringTree() | |
| case "Union": | |
| return ast.recur(indexSignatureParameterFromString) | |
| } | |
| }) | |
| const templateLiteralPartFromString = toCodec((ast) => { | |
| switch (ast._tag) { | |
| default: | |
| return ast | |
| case "String": | |
| case "TemplateLiteral": | |
| return ast | |
| case "BigInt": | |
| case "Number": | |
| case "Literal": | |
| return ast.toCodecStringTree() | |
| case "Union": | |
| return ast.recur(templateLiteralPartFromString) | |
| } | |
| }) | |
| /** | |
| * any string, including newlines | |
| * @internal | |
| */ | |
| export const STRING_PATTERN = "[\\s\\S]*?" | |
| const isStringFiniteRegExp = new globalThis.RegExp(`^${FINITE_PATTERN}$`) | |
| /** @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 | |
| } | |
| onPath.add(u) | |
| const ok = Array.isArray(u) | |
| ? 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", | |
| toCodecStringTree: () => new Link(unknown, SchemaTransformation.passthrough()) | |
| } | |
| ) | |
| /** @internal */ | |
| export const unknownToStringTree = new Link( | |
| StringTree, | |
| SchemaTransformation.passthrough() | |
| ) | |
Xet Storage Details
- Size:
- 109 kB
- Xet hash:
- e5ebb9abcc9e354f63f0b171eb92a739aafb69029929fe2c524080e84f0ebc79
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.