1/**
2 * Represents Effect schemas as runtime trees.
3 *
4 * Every `Schema` has an AST made from nodes for declarations, primitives,
5 * literals, arrays, objects, unions, suspended schemas, checks, annotations,
6 * encoding links, and parsing context. Most users work with the higher-level
7 * `Schema` module. Use `SchemaAST` when you need to inspect schema nodes, build
8 * ASTs programmatically, change encoded or decoded views, collect issues, or
9 * run low-level schema checks.
10 *
11 * @since 4.0.0
12 */
13
14import * as Arr from "./Array.ts"
15import * as Cause from "./Cause.ts"
16import * as Effect from "./Effect.ts"
17import * as Exit from "./Exit.ts"
18import { format, formatPropertyKey } from "./Formatter.ts"
19import { identity, memoize, memoizeIdempotent } from "./Function.ts"
20import { effectIsExit, iterateConcurrent, iterateEager, resolveConcurrency } from "./internal/effect.ts"
21import * as InternalRecord from "./internal/record.ts"
22import * as InternalAnnotations from "./internal/schema/annotations.ts"
23import * as InternalSchemaCause from "./internal/schema/cause.ts"
24import * as InternalParser from "./internal/schema/parser.ts"
25import * as Pipeable from "./Pipeable.ts"
26import * as Predicate from "./Predicate.ts"
27import * as Result from "./Result.ts"
28import type * as Schema from "./Schema.ts"
29import * as SchemaGetter from "./SchemaGetter.ts"
30import * as SchemaIssue from "./SchemaIssue.ts"
31import type * as SchemaParser from "./SchemaParser.ts"
32import * as SchemaTransformation from "./SchemaTransformation.ts"
33import type * as Types from "./Types.ts"
34
35/**
36 * Discriminated union of all AST node types.
37 *
38 * **Details**
39 *
40 * Every `Schema` has an `.ast` property of this type. Use the guard functions
41 * ({@link isString}, {@link isObjects}, etc.) to narrow to a specific variant,
42 * then access variant-specific fields.
43 *
44 * - All variants share the `annotations`, `checks`, `encoding`, and `context`
45 * fields.
46 * - Discriminate on the `_tag` field (e.g. `"String"`, `"Objects"`, `"Union"`).
47 *
48 * @see {@link isAST}
49 * @category models
50 * @since 4.0.0
51 */
52export type AST =
53 | Declaration
54 | Null
55 | Undefined
56 | Void
57 | Never
58 | Unknown
59 | Any
60 | String
61 | Number
62 | Boolean
63 | BigInt
64 | Symbol
65 | Literal
66 | UniqueSymbol
67 | ObjectKeyword
68 | Enum
69 | TemplateLiteral
70 | Arrays
71 | Objects
72 | Union
73 | Suspend
74
75function makeGuard<T extends AST["_tag"]>(tag: T) {
76 return (ast: AST): ast is Extract<AST, { _tag: T }> => ast._tag === tag
77}
78
79/**
80 * Returns `true` if the value is an {@link AST} node (any variant).
81 *
82 * **Details**
83 *
84 * Uses the internal `TypeId` brand to distinguish AST nodes from arbitrary
85 * objects.
86 *
87 * @see {@link AST}
88 * @category guards
89 * @since 4.0.0
90 */
91export function isAST(u: unknown): u is AST {
92 return Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId
93}
94
95/**
96 * Narrows an {@link AST} to {@link Declaration}.
97 *
98 * **When to use**
99 *
100 * Use to recognize declaration AST nodes before running declaration-specific
101 * handling.
102 *
103 * @see {@link Declaration} for the AST node type narrowed by this guard
104 *
105 * @category guards
106 * @since 3.10.0
107 */
108export const isDeclaration = makeGuard("Declaration")
109
110/**
111 * Narrows an {@link AST} to {@link Null}.
112 *
113 * **When to use**
114 *
115 * Use to recognize an AST node that represents exactly the `null` literal when
116 * inspecting, traversing, or transforming schema ASTs.
117 *
118 * @see {@link Null} for the AST node type narrowed by this guard
119 * @see {@link null_ null} for the singleton `Null` AST instance
120 * @see {@link isLiteral} for exact primitive literal AST nodes
121 *
122 * @category guards
123 * @since 4.0.0
124 */
125export const isNull = makeGuard("Null")
126
127/**
128 * Narrows an {@link AST} to {@link Undefined}.
129 *
130 * **When to use**
131 *
132 * Use to identify AST nodes that represent exactly the JavaScript `undefined`
133 * value.
134 *
135 * @see {@link isVoid} for narrowing AST nodes that represent TypeScript `void` instead of exact `undefined`
136 *
137 * @category guards
138 * @since 4.0.0
139 */
140export const isUndefined = makeGuard("Undefined")
141
142/**
143 * Narrows an {@link AST} to {@link Void}.
144 *
145 * **When to use**
146 *
147 * Use to identify AST nodes that represent the TypeScript `void` type before
148 * handling `Void`-specific schema behavior.
149 *
150 * @see {@link isUndefined} for narrowing AST nodes that represent the literal `undefined` value instead of TypeScript `void`
151 *
152 * @category guards
153 * @since 4.0.0
154 */
155export const isVoid = makeGuard("Void")
156
157/**
158 * Narrows an {@link AST} to {@link Never}.
159 *
160 * **When to use**
161 *
162 * Use to detect the AST node for a schema that can never match before handling
163 * other schema variants.
164 *
165 * @see {@link Never} for the AST node type narrowed by this guard
166 * @see {@link never} for the singleton `Never` AST instance
167 *
168 * @category guards
169 * @since 4.0.0
170 */
171export const isNever = makeGuard("Never")
172
173/**
174 * Narrows an {@link AST} to {@link Unknown}.
175 *
176 * **When to use**
177 *
178 * Use when you need to inspect a schema AST and handle the `Unknown` node
179 * variant specifically.
180 *
181 * @see {@link isAny} for the guard for the `Any` node, whose parsed result is typed as `any` rather than `unknown`
182 *
183 * @category guards
184 * @since 4.0.0
185 */
186export const isUnknown = makeGuard("Unknown")
187
188/**
189 * Narrows an {@link AST} to {@link Any}.
190 *
191 * **When to use**
192 *
193 * Use when you need to inspect a schema AST and handle the `Any` node
194 * variant specifically.
195 *
196 * @see {@link isUnknown} for the guard for the `Unknown` node, whose parsed result is typed as `unknown` rather than `any`
197 *
198 * @category guards
199 * @since 4.0.0
200 */
201export const isAny = makeGuard("Any")
202
203/**
204 * Narrows an {@link AST} to {@link String}.
205 *
206 * **When to use**
207 *
208 * Use to detect schema AST nodes that match any string value while inspecting
209 * or transforming a Schema AST.
210 *
211 * @see {@link String} for the AST node class narrowed by this guard
212 * @see {@link string} for the singleton `String` AST instance
213 * @see {@link isLiteral} for exact primitive literal AST nodes, including exact string literals
214 *
215 * @category guards
216 * @since 4.0.0
217 */
218export const isString = makeGuard("String")
219
220/**
221 * Narrows an {@link AST} to {@link Number}.
222 *
223 * **When to use**
224 *
225 * Use to detect `Number` AST nodes while inspecting, traversing, or transforming
226 * schema ASTs.
227 *
228 * @category guards
229 * @since 4.0.0
230 */
231export const isNumber = makeGuard("Number")
232
233/**
234 * Narrows an {@link AST} to {@link Boolean}.
235 *
236 * **When to use**
237 *
238 * Use to identify the `Boolean` AST variant while inspecting, traversing, or
239 * transforming schema definitions.
240 *
241 * @see {@link Boolean} for the AST node type matched by this guard
242 * @see {@link boolean} for the singleton instance to use when constructing a boolean AST directly
243 *
244 * @category guards
245 * @since 4.0.0
246 */
247export const isBoolean = makeGuard("Boolean")
248
249/**
250 * Narrows an {@link AST} to {@link BigInt}.
251 *
252 * **When to use**
253 *
254 * Use to identify bigint AST nodes while inspecting or transforming schema ASTs.
255 *
256 * @see {@link BigInt} for the AST node matched by this guard
257 * @see {@link bigInt} for the singleton instance; use `isBigInt` when narrowing an existing `AST` value
258 *
259 * @category guards
260 * @since 4.0.0
261 */
262export const isBigInt = makeGuard("BigInt")
263
264/**
265 * Narrows an {@link AST} to {@link Symbol}.
266 *
267 * **When to use**
268 *
269 * Use to narrow an `AST` node before handling the `Symbol` variant for schemas
270 * that accept any JavaScript symbol value.
271 *
272 * @see {@link isUniqueSymbol} for the sibling guard that narrows the `UniqueSymbol` variant for one exact symbol value
273 *
274 * @category guards
275 * @since 4.0.0
276 */
277export const isSymbol = makeGuard("Symbol")
278
279/**
280 * Narrows an {@link AST} to {@link Literal}.
281 *
282 * **When to use**
283 *
284 * Use to recognize exact string, number, boolean, or bigint literal AST nodes.
285 *
286 * @see {@link Literal} for the AST node type narrowed by this guard
287 * @see {@link LiteralValue} for the values stored by literal nodes
288 *
289 * @category guards
290 * @since 3.10.0
291 */
292export const isLiteral = makeGuard("Literal")
293
294/**
295 * Narrows an {@link AST} to {@link UniqueSymbol}.
296 *
297 * @category guards
298 * @since 3.10.0
299 */
300export const isUniqueSymbol = makeGuard("UniqueSymbol")
301
302/**
303 * Narrows an {@link AST} to {@link ObjectKeyword}.
304 *
305 * **When to use**
306 *
307 * Use to identify the AST node for the TypeScript `object` keyword when
308 * inspecting or transforming a Schema AST.
309 *
310 * @see {@link ObjectKeyword} for the AST node matched by this guard
311 * @see {@link objectKeyword} for the singleton `ObjectKeyword` AST instance
312 * @see {@link isObjects} for struct and record AST nodes
313 *
314 * @category guards
315 * @since 3.10.0
316 */
317export const isObjectKeyword = makeGuard("ObjectKeyword")
318
319/**
320 * Narrows an {@link AST} to {@link Enum}.
321 *
322 * **When to use**
323 *
324 * Use to recognize enum AST nodes before reading enum cases or running
325 * enum-specific handling.
326 *
327 * @see {@link Enum} for the AST node type narrowed by this guard
328 *
329 * @category guards
330 * @since 4.0.0
331 */
332export const isEnum = makeGuard("Enum")
333
334/**
335 * Narrows an {@link AST} to {@link TemplateLiteral}.
336 *
337 * @category guards
338 * @since 3.10.0
339 */
340export const isTemplateLiteral = makeGuard("TemplateLiteral")
341
342/**
343 * Narrows an {@link AST} to {@link Arrays}.
344 *
345 * **When to use**
346 *
347 * Use to recognize array-like AST nodes before reading their element, rest, or
348 * mutability metadata.
349 *
350 * @see {@link Arrays} for the AST node type narrowed by this guard
351 *
352 * @category guards
353 * @since 4.0.0
354 */
355export const isArrays = makeGuard("Arrays")
356
357/**
358 * Narrows an {@link AST} to {@link Objects}.
359 *
360 * @category guards
361 * @since 4.0.0
362 */
363export const isObjects = makeGuard("Objects")
364
365/**
366 * Narrows an {@link AST} to {@link Union}.
367 *
368 * @category guards
369 * @since 3.10.0
370 */
371export const isUnion = makeGuard("Union")
372
373/**
374 * Narrows an {@link AST} to {@link Suspend}.
375 *
376 * @category guards
377 * @since 3.10.0
378 */
379export const isSuspend = makeGuard("Suspend")
380
381/**
382 * Represents a single step in an {@link Encoding} chain.
383 *
384 * **Details**
385 *
386 * A link pairs a target {@link AST} with a `Transformation` or `Middleware`
387 * that converts values between the current node and the target.
388 *
389 * - `to` — the AST node on the other side of this transformation step.
390 * - `transformation` — the bidirectional conversion logic (decode/encode).
391 *
392 * Links are composed into a non-empty array ({@link Encoding}) attached to
393 * AST nodes that have a different encoded representation.
394 *
395 * @see {@link Encoding}
396 * @see {@link decodeTo}
397 * @category models
398 * @since 4.0.0
399 */
400export interface Link {
401 readonly to: AST
402 readonly transformation:
403 | SchemaTransformation.Transformation<any, any, any, any>
404 | SchemaTransformation.Middleware<any, any, any, any, any, any>
405}
406
407/**
408 * Constructs a {@link Link}.
409 *
410 * @category constructors
411 * @since 4.0.0
412 */
413export const Link: new(
414 to: AST,
415 transformation:
416 | SchemaTransformation.Transformation<any, any, any, any>
417 | SchemaTransformation.Middleware<any, any, any, any, any, any>
418) => Link = class {
419 readonly to: AST
420 readonly transformation:
421 | SchemaTransformation.Transformation<any, any, any, any>
422 | SchemaTransformation.Middleware<any, any, any, any, any, any>
423
424 constructor(
425 to: AST,
426 transformation:
427 | SchemaTransformation.Transformation<any, any, any, any>
428 | SchemaTransformation.Middleware<any, any, any, any, any, any>
429 ) {
430 this.to = to
431 this.transformation = transformation
432 }
433}
434
435/**
436 * A non-empty chain of {@link Link} values representing the transformation
437 * steps between a schema's decoded (type) form and its encoded (wire) form.
438 *
439 * **Details**
440 *
441 * Stored on an AST node's `encoding` field. When `undefined`, the node has no
442 * encoding transformation (type and encoded forms are identical).
443 *
444 * @see {@link Link}
445 * @see {@link toEncoded}
446 * @category models
447 * @since 4.0.0
448 */
449export type Encoding = readonly [Link, ...Array<Link>]
450
451/**
452 * Options that control schema parsing, validation, transformation, and output behavior.
453 *
454 * **Details**
455 *
456 * Pass to `Schema.decodeUnknown`, `Schema.encode`, and related APIs to customize
457 * error reporting, excess property handling, check execution, and product
458 * concurrency. Options apply throughout the parse; schema annotations do not
459 * override them.
460 *
461 * - `errors` — `"first"` (default) stops at the first error; `"all"` collects
462 * every error.
463 * - `onExcessProperty` — `"ignore"` (default) strips unknown object keys;
464 * `"error"` fails.
465 * - `disableChecks` — skips validation checks while still applying defaults and
466 * transformations.
467 * - `concurrency` — controls concurrent parsing of tuple elements, array
468 * elements, struct fields, and record entries using the same semantics as
469 * `Effect.forEach`; the default is sequential.
470 * - `reportInput` — includes rejected input values in value-bearing schema
471 * issues.
472 *
473 * Object property order is unspecified, including in values passed to checks.
474 * Decoding and encoding do not guarantee preservation of input key order.
475 *
476 * @category options
477 * @since 3.10.0
478 */
479export interface ParseOptions {
480 /**
481 * Controls how many parsing errors are reported.
482 *
483 * **Details**
484 *
485 * The default, `"first"`, stops at the first error. Set the option to `"all"`
486 * to collect every parsing error, which can help with debugging or with
487 * presenting more complete error messages to a user.
488 *
489 * @default "first"
490 */
491 readonly errors?: "first" | "all" | undefined
492
493 /**
494 * Controls how object parsing handles keys that are not declared by the schema.
495 *
496 * **Details**
497 *
498 * The default, `"ignore"`, strips unspecified properties from the output. Use
499 * `"error"` to fail when an excess property is present. A key is covered by a
500 * declared property or any index signature selecting that key. This applies
501 * to structs, records, and structs with rest. Values must satisfy every
502 * applicable index signature. Empty structs keep their non-nullish behavior.
503 *
504 * An excess property is an enumerable own property that is not covered by a
505 * declared property or an index signature. Non-enumerable own properties,
506 * such as `Error#stack`, are ignored unless explicitly declared by the
507 * schema. Index signatures likewise select only enumerable own properties.
508 *
509 * @default "ignore"
510 */
511 readonly onExcessProperty?: "ignore" | "error" | undefined
512
513 /**
514 * Whether to disable checks while still applying defaults and
515 * transformations.
516 */
517 readonly disableChecks?: boolean | undefined
518
519 /**
520 * Controls how many children of a product schema may parse concurrently.
521 *
522 * **Details**
523 *
524 * This option has the same concurrency semantics as {@link Effect.forEach}.
525 * It applies to tuples, arrays, structs, records, and structs with rest. Each
526 * nested product applies the limit independently. Union members remain
527 * sequential, though products inside the current union member still receive
528 * the option.
529 *
530 * Array and tuple outputs retain their element order. Other observable work,
531 * including issue accumulation and colliding transformed record keys, follows
532 * effect completion order when concurrency is greater than `1`.
533 *
534 * **Gotchas**
535 *
536 * With `errors: "first"`, the first observed child failure interrupts the
537 * remaining children and may not belong to the earliest product position.
538 * Concurrent children may perform effects before another child fails.
539 *
540 * @default undefined
541 */
542 readonly concurrency?: Types.Concurrency | undefined
543
544 /**
545 * Whether schema issues should retain and report rejected input values.
546 *
547 * **Details**
548 *
549 * When enabled, value-bearing issues created by the parser expose an `input`
550 * field. Built-in formatters may include reported input in default messages.
551 * The input is retained by reference rather than copied.
552 *
553 * **Gotchas**
554 *
555 * Enabling this option can retain or disclose secrets, personally
556 * identifiable information, and large object graphs. The `input` field is
557 * enumerable and may be included by object enumeration, spread, or
558 * serialization. Issues returned directly by user-defined declarations,
559 * checks, transformations, and middleware are not modified; their authors
560 * decide whether to retain an input. To respect this option, pass the
561 * callback's input and parse options directly to a value-bearing issue
562 * constructor. Custom messages and annotations remain the caller's
563 * responsibility regardless of this option.
564 * Formatting an issue with `SchemaIssue.makeFormatterDefault()`, reading
565 * `SchemaError.message`, or formatting a Standard Schema failure can disclose
566 * retained input.
567 *
568 * @default false
569 */
570 readonly reportInput?: boolean | undefined
571}
572
573/** @internal */
574export const defaultParseOptions: ParseOptions = {}
575
576/**
577 * Represents per-property metadata attached to an AST node's `context` field.
578 *
579 * **Details**
580 *
581 * Tracks whether a property key is optional, mutable, has a constructor
582 * default, or carries key-level annotations. Typically set by helpers like
583 * {@link optionalKey} and `Schema.mutableKey`.
584 *
585 * - `isOptional` — the property key may be absent from the input.
586 * - `isMutable` — the property is `readonly` when `false`.
587 * - `constructorDefault` — an effect evaluated during construction to supply
588 * missing values.
589 * - `annotations` — key-level annotations (e.g. description of the key
590 * itself).
591 *
592 * @see `Schema.optionalKey`
593 * @see {@link isOptional}
594 * @category models
595 * @since 4.0.0
596 */
597export interface Context {
598 readonly isOptional: boolean
599 readonly isMutable: boolean
600 /** Used for constructor default values (e.g. `withConstructorDefault` API) */
601 readonly constructorDefault: Effect.Effect<unknown, SchemaIssue.Issue> | undefined
602 readonly annotations: Schema.Annotations.Key<unknown> | undefined
603}
604
605/**
606 * Constructs a {@link Context}.
607 *
608 * @category constructors
609 * @since 4.0.0
610 */
611export const Context: new(
612 isOptional: boolean,
613 isMutable: boolean, /** Used for constructor default values (e.g. `withConstructorDefault` API) */
614 constructorDefault?: Effect.Effect<unknown, SchemaIssue.Issue> | undefined,
615 annotations?: Schema.Annotations.Key<unknown> | undefined
616) => Context = class {
617 readonly isOptional: boolean
618 readonly isMutable: boolean
619 /** Used for constructor default values (e.g. `withConstructorDefault` API) */
620 readonly constructorDefault: Effect.Effect<unknown, SchemaIssue.Issue> | undefined
621 readonly annotations: Schema.Annotations.Key<unknown> | undefined
622
623 constructor(
624 isOptional: boolean,
625 isMutable: boolean,
626 /** Used for constructor default values (e.g. `withConstructorDefault` API) */
627 constructorDefault: Effect.Effect<unknown, SchemaIssue.Issue> | undefined = undefined,
628 annotations: Schema.Annotations.Key<unknown> | undefined = undefined
629 ) {
630 this.isOptional = isOptional
631 this.isMutable = isMutable
632 this.constructorDefault = constructorDefault
633 this.annotations = annotations
634 }
635}
636
637/**
638 * Non-empty array of validation {@link Check} values attached to an AST node's
639 * `checks` field.
640 *
641 * **Details**
642 *
643 * Checks are run after basic type matching succeeds. They represent
644 * refinements like `minLength`, `pattern`, `int`, etc.
645 *
646 * @see {@link Check}
647 * @see {@link Filter}
648 * @see {@link FilterGroup}
649 * @category models
650 * @since 4.0.0
651 */
652export type Checks = readonly [Check<any>, ...Array<Check<any>>]
653
654const TypeId = "~effect/Schema"
655interface ASTNode {
656 readonly [TypeId]: typeof TypeId
657 readonly _tag: string
658 readonly annotations: Schema.Annotations.Annotations | undefined
659 readonly checks: Checks | undefined
660 readonly encoding: Encoding | undefined
661 readonly context: Context | undefined
662 toString(): string
663}
664
665abstract class ASTNodeImpl implements ASTNode {
666 readonly [TypeId] = TypeId
667 abstract readonly _tag: string
668 readonly annotations: Schema.Annotations.Annotations | undefined
669 readonly checks: Checks | undefined
670 readonly encoding: Encoding | undefined
671 readonly context: Context | undefined
672
673 constructor(
674 annotations: Schema.Annotations.Annotations | undefined = undefined,
675 checks: Checks | undefined = undefined,
676 encoding: Encoding | undefined = undefined,
677 context: Context | undefined = undefined
678 ) {
679 this.annotations = annotations
680 this.checks = checks
681 this.encoding = encoding
682 this.context = context
683 }
684 toString() {
685 return `<${this._tag}>`
686 }
687}
688
689type DeclarationRun = (
690 typeParameters: ReadonlyArray<AST>
691) => (input: unknown, self: Declaration, options: ParseOptions) => Effect.Effect<any, SchemaIssue.Issue>
692
693/**
694 * AST node for user-defined opaque types with custom parsing logic.
695 *
696 * **When to use**
697 *
698 * Use when you need a custom schema AST node because none of the built-in
699 * nodes fit.
700 *
701 * **Details**
702 *
703 * - `typeParameters` — inner schemas this declaration is parameterized over
704 * (e.g. the element type for a custom collection).
705 * - `run` — factory that receives `typeParameters` and returns a parser that
706 * validates or transforms raw input. The `Effect` returned by the parser must
707 * complete synchronously.
708 *
709 * @see {@link isDeclaration}
710 * @category models
711 * @since 4.0.0
712 */
713export interface Declaration extends ASTNode {
714 readonly _tag: "Declaration"
715 readonly typeParameters: ReadonlyArray<AST>
716 readonly run: DeclarationRun
717 readonly encodingChecks: Checks | undefined
718 /**
719 * Parser factory {@link flip} swaps in, so a declaration can behave
720 * differently when encoding. `undefined` reuses `run`.
721 */
722 readonly encodingRun: DeclarationRun | undefined
723 /** @internal */
724
725 getParser(): SchemaParser.Parser
726 /** @internal */
727
728 recur(recur: (ast: AST) => AST): Declaration
729 /** @internal */
730
731 flip(recur: (ast: AST) => AST): Declaration
732 /** @internal */
733
734 getExpected(): string
735}
736
737/**
738 * Constructs a {@link Declaration}.
739 *
740 * @category constructors
741 * @since 4.0.0
742 */
743export const Declaration: new(
744 typeParameters: ReadonlyArray<AST>,
745 run: DeclarationRun,
746 annotations?: Schema.Annotations.Annotations,
747 checks?: Checks,
748 encoding?: Encoding,
749 context?: Context,
750 encodingChecks?: Checks,
751 encodingRun?: DeclarationRun
752) => Declaration = class extends ASTNodeImpl {
753 readonly _tag = "Declaration"
754 readonly typeParameters: ReadonlyArray<AST>
755 readonly run: DeclarationRun
756 readonly encodingChecks: Checks | undefined
757 /**
758 * Parser factory {@link flip} swaps in, so a declaration can behave
759 * differently when encoding. `undefined` reuses `run`.
760 */
761 readonly encodingRun: DeclarationRun | undefined
762
763 constructor(
764 typeParameters: ReadonlyArray<AST>,
765 run: DeclarationRun,
766 annotations?: Schema.Annotations.Annotations,
767 checks?: Checks,
768 encoding?: Encoding,
769 context?: Context,
770 encodingChecks?: Checks,
771 encodingRun?: DeclarationRun
772 ) {
773 super(annotations, checks, encoding, context)
774 this.typeParameters = typeParameters
775 this.run = run
776 this.encodingChecks = encodingChecks
777 this.encodingRun = encodingRun
778 }
779 /** @internal */
780 getParser(): SchemaParser.Parser {
781 let run: ReturnType<typeof this.run>
782 return (input, options) => {
783 if (input === InternalParser.missing) return InternalParser.missingExit
784 return (run ??= this.run(this.typeParameters))(input, this, options)
785 }
786 }
787 private _rebuild(
788 recur: (ast: AST) => AST,
789 checks: Checks | undefined,
790 encodingChecks: Checks | undefined,
791 run: DeclarationRun,
792 encodingRun: DeclarationRun | undefined
793 ) {
794 const tps = mapOrSame(this.typeParameters, recur)
795 return tps === this.typeParameters && checks === this.checks && encodingChecks === this.encodingChecks &&
796 run === this.run && encodingRun === this.encodingRun ?
797 this :
798 new Declaration(tps, run, this.annotations, checks, undefined, this.context, encodingChecks, encodingRun)
799 }
800 /** @internal */
801 recur(recur: (ast: AST) => AST) {
802 return this._rebuild(recur, this.checks, this.encodingChecks, this.run, this.encodingRun)
803 }
804 /** @internal */
805 flip(recur: (ast: AST) => AST) {
806 return this._rebuild(recur, this.encodingChecks, this.checks, this.encodingRun ?? this.run, this.run)
807 }
808 /** @internal */
809 getExpected(): string {
810 const expected = this.annotations?.expected
811 if (typeof expected === "string") return expected
812 return "<Declaration>"
813 }
814}
815
816/**
817 * AST node matching the `null` literal value.
818 *
819 * **Details**
820 *
821 * Parsing succeeds only when the input is exactly `null`.
822 *
823 * @see {@link null_ null}
824 * @see {@link isNull}
825 * @category models
826 * @since 4.0.0
827 */
828export interface Null extends ASTNode {
829 readonly _tag: "Null"
830 /** @internal */
831
832 getParser(): SchemaParser.Parser
833 /** @internal */
834
835 getExpected(): string
836}
837
838/**
839 * Constructs a {@link Null}.
840 *
841 * @category constructors
842 * @since 4.0.0
843 */
844export const Null: new(
845 annotations?: Schema.Annotations.Annotations | undefined,
846 checks?: Checks | undefined,
847 encoding?: Encoding | undefined,
848 context?: Context | undefined
849) => Null = class extends ASTNodeImpl {
850 readonly _tag = "Null"
851 /** @internal */
852 getParser() {
853 return fromConst(this, null)
854 }
855 /** @internal */
856 getExpected(): string {
857 return "null"
858 }
859}
860
861const null_ = new Null()
862export {
863 /**
864 * Provides the singleton {@link Null} AST instance.
865 *
866 * **When to use**
867 *
868 * Use when you need the shared AST node for exact null values while inspecting
869 * or constructing schema ASTs.
870 *
871 * @category constants
872 * @since 4.0.0
873 */
874 null_ as null
875}
876
877/**
878 * AST node matching the `undefined` value.
879 *
880 * **Details**
881 *
882 * Parsing succeeds only when the input is exactly `undefined`.
883 *
884 * @see {@link undefined}
885 * @see {@link isUndefined}
886 * @category models
887 * @since 4.0.0
888 */
889export interface Undefined extends ASTNode {
890 readonly _tag: "Undefined"
891 /** @internal */
892
893 getParser(): SchemaParser.Parser
894 /** @internal */
895
896 toCodecJson(): AST
897 /** @internal */
898
899 getExpected(): string
900}
901
902/**
903 * Constructs a {@link Undefined}.
904 *
905 * @category constructors
906 * @since 4.0.0
907 */
908export const Undefined: new(
909 annotations?: Schema.Annotations.Annotations | undefined,
910 checks?: Checks | undefined,
911 encoding?: Encoding | undefined,
912 context?: Context | undefined
913) => Undefined = class extends ASTNodeImpl {
914 readonly _tag = "Undefined"
915 /** @internal */
916 getParser() {
917 return fromConst(this, undefined)
918 }
919 /** @internal */
920 toCodecJson(): AST {
921 return replaceEncoding(this, [undefinedToNull])
922 }
923 /** @internal */
924 getExpected(): string {
925 return "undefined"
926 }
927}
928
929const undefinedToNull = new Link(
930 null_,
931 new SchemaTransformation.Transformation(
932 SchemaGetter.transform(() => undefined),
933 SchemaGetter.transform(() => null)
934 )
935)
936
937const undefined_ = new Undefined()
938export {
939 /**
940 * Provides the singleton {@link Undefined} AST instance.
941 *
942 * **When to use**
943 *
944 * Use when you need the shared AST node for exact undefined values while
945 * inspecting or constructing schema ASTs.
946 *
947 * @category constants
948 * @since 4.0.0
949 */
950 undefined_ as undefined
951}
952
953/**
954 * AST node matching TypeScript `void` return-value semantics.
955 *
956 * **When to use**
957 *
958 * Use when you need an AST node for a value whose result is intentionally
959 * ignored.
960 *
961 * **Details**
962 *
963 * Parsers built from this node accept any present runtime input and map it to
964 * `undefined`. Public schemas built from it may still expose `void` as their
965 * typed decoded and encoded representation.
966 *
967 * @see {@link undefined} for the AST singleton that matches only exact `undefined`
968 * @see {@link void_ void}
969 * @see {@link isVoid}
970 * @category models
971 * @since 4.0.0
972 */
973export interface Void extends ASTNode {
974 readonly _tag: "Void"
975 /** @internal */
976
977 getParser(): (
978 input: unknown
979 ) => InternalParser.Success<undefined, never> | InternalParser.Success<typeof InternalParser.missing, never>
980 /** @internal */
981
982 toCodecJson(): AST
983 /** @internal */
984
985 getExpected(): string
986}
987
988/**
989 * Constructs a {@link Void}.
990 *
991 * @category constructors
992 * @since 4.0.0
993 */
994export const Void: new(
995 annotations?: Schema.Annotations.Annotations | undefined,
996 checks?: Checks | undefined,
997 encoding?: Encoding | undefined,
998 context?: Context | undefined
999) => Void = class extends ASTNodeImpl {
1000 readonly _tag = "Void"
1001 /** @internal */
1002 getParser() {
1003 const succeed = InternalParser.succeed(undefined)
1004 return (input: unknown) => input === InternalParser.missing ? InternalParser.missingExit : succeed
1005 }
1006 /** @internal */
1007 toCodecJson(): AST {
1008 return replaceEncoding(this, [undefinedToNull])
1009 }
1010 /** @internal */
1011 getExpected(): string {
1012 return "void"
1013 }
1014}
1015
1016const void_ = new Void()
1017export {
1018 /**
1019 * Provides the singleton {@link Void} AST instance.
1020 *
1021 * **When to use**
1022 *
1023 * Use when constructing or comparing AST nodes for TypeScript `void` return
1024 * values whose result is intentionally ignored.
1025 *
1026 * **Details**
1027 *
1028 * The node parses any present runtime value as `undefined`; schemas may still
1029 * expose `void` on their typed decoded and encoded sides.
1030 *
1031 * @see {@link Void} for the AST node class
1032 * @see {@link undefined} for the sibling AST singleton that matches exactly `undefined`
1033 * @see {@link isVoid} for narrowing an AST to a `Void` node
1034 *
1035 * @category constructors
1036 * @since 4.0.0
1037 */
1038 void_ as void
1039}
1040
1041/**
1042 * AST node representing the `never` type — no value matches.
1043 *
1044 * **Details**
1045 *
1046 * Parsing always fails. Useful as a placeholder in unions or as the result
1047 * of narrowing that eliminates all options.
1048 *
1049 * @see {@link never}
1050 * @see {@link isNever}
1051 * @category models
1052 * @since 4.0.0
1053 */
1054export interface Never extends ASTNode {
1055 readonly _tag: "Never"
1056 /** @internal */
1057
1058 getParser(): SchemaParser.Parser
1059 /** @internal */
1060
1061 getExpected(): string
1062}
1063
1064/**
1065 * Constructs a {@link Never}.
1066 *
1067 * @category constructors
1068 * @since 4.0.0
1069 */
1070export const Never: new(
1071 annotations?: Schema.Annotations.Annotations | undefined,
1072 checks?: Checks | undefined,
1073 encoding?: Encoding | undefined,
1074 context?: Context | undefined
1075) => Never = class extends ASTNodeImpl {
1076 readonly _tag = "Never"
1077 /** @internal */
1078 getParser() {
1079 return fromRefinement(this, Predicate.isNever)
1080 }
1081 /** @internal */
1082 getExpected(): string {
1083 return "never"
1084 }
1085}
1086
1087/**
1088 * Provides the singleton {@link Never} AST instance.
1089 *
1090 * **When to use**
1091 *
1092 * Use to reuse the canonical bottom-type AST node when constructing,
1093 * comparing, or returning ASTs.
1094 *
1095 * @see {@link Never} for the AST node class
1096 * @see {@link isNever} for narrowing an AST to a `Never` node
1097 *
1098 * @category constructors
1099 * @since 4.0.0
1100 */
1101export const never = new Never()
1102
1103/**
1104 * AST node representing the `any` type — every value matches.
1105 *
1106 * @see {@link any}
1107 * @see {@link isAny}
1108 *
1109 * @category models
1110 * @since 4.0.0
1111 */
1112export interface Any extends ASTNode {
1113 readonly _tag: "Any"
1114 /** @internal */
1115
1116 getParser(): SchemaParser.Parser
1117 /** @internal */
1118
1119 getExpected(): string
1120}
1121
1122/**
1123 * Constructs a {@link Any}.
1124 *
1125 * @category constructors
1126 * @since 4.0.0
1127 */
1128export const Any: new(
1129 annotations?: Schema.Annotations.Annotations | undefined,
1130 checks?: Checks | undefined,
1131 encoding?: Encoding | undefined,
1132 context?: Context | undefined
1133) => Any = class extends ASTNodeImpl {
1134 readonly _tag = "Any"
1135 /** @internal */
1136 getParser() {
1137 return fromRefinement(this, Predicate.isUnknown)
1138 }
1139 /** @internal */
1140 getExpected(): string {
1141 return "any"
1142 }
1143}
1144
1145/**
1146 * Provides the singleton {@link Any} AST instance.
1147 *
1148 * **When to use**
1149 *
1150 * Use when you need the singleton AST node for the TypeScript `any` type and
1151 * intentionally want parsing to accept every input value.
1152 *
1153 * @see {@link unknown} for the sibling AST singleton that also accepts every value while preserving the safer `unknown` type
1154 *
1155 * @category constructors
1156 * @since 4.0.0
1157 */
1158export const any = new Any()
1159
1160/**
1161 * AST node representing the `unknown` type — every value matches.
1162 *
1163 * **Details**
1164 *
1165 * Unlike {@link Any}, this is type-safe: the parsed result is typed as
1166 * `unknown` rather than `any`.
1167 *
1168 * @see {@link unknown}
1169 * @see {@link isUnknown}
1170 * @category models
1171 * @since 4.0.0
1172 */
1173export interface Unknown extends ASTNode {
1174 readonly _tag: "Unknown"
1175 /** @internal */
1176
1177 getParser(): SchemaParser.Parser
1178 /** @internal */
1179
1180 getExpected(): string
1181}
1182
1183/**
1184 * Constructs a {@link Unknown}.
1185 *
1186 * @category constructors
1187 * @since 4.0.0
1188 */
1189export const Unknown: new(
1190 annotations?: Schema.Annotations.Annotations | undefined,
1191 checks?: Checks | undefined,
1192 encoding?: Encoding | undefined,
1193 context?: Context | undefined
1194) => Unknown = class extends ASTNodeImpl {
1195 readonly _tag = "Unknown"
1196 /** @internal */
1197 getParser() {
1198 return fromRefinement(this, Predicate.isUnknown)
1199 }
1200 /** @internal */
1201 getExpected(): string {
1202 return "unknown"
1203 }
1204}
1205
1206/**
1207 * Provides the singleton {@link Unknown} AST instance.
1208 *
1209 * **When to use**
1210 *
1211 * Use when you need the reusable AST singleton for a schema node that accepts
1212 * every value while keeping parsed values opaque.
1213 *
1214 * @see {@link any} for the singleton that accepts every value as `any`
1215 *
1216 * @category constructors
1217 * @since 4.0.0
1218 */
1219export const unknown = new Unknown()
1220
1221/**
1222 * AST node matching the TypeScript `object` type — accepts objects, arrays,
1223 * and functions (anything non-primitive and non-null).
1224 *
1225 * @see {@link objectKeyword}
1226 * @see {@link isObjectKeyword}
1227 *
1228 * @category models
1229 * @since 4.0.0
1230 */
1231export interface ObjectKeyword extends ASTNode {
1232 readonly _tag: "ObjectKeyword"
1233 /** @internal */
1234
1235 getParser(): SchemaParser.Parser
1236 /** @internal */
1237
1238 getExpected(): string
1239}
1240
1241/**
1242 * Constructs a {@link ObjectKeyword}.
1243 *
1244 * @category constructors
1245 * @since 4.0.0
1246 */
1247export const ObjectKeyword: new(
1248 annotations?: Schema.Annotations.Annotations | undefined,
1249 checks?: Checks | undefined,
1250 encoding?: Encoding | undefined,
1251 context?: Context | undefined
1252) => ObjectKeyword = class extends ASTNodeImpl {
1253 readonly _tag = "ObjectKeyword"
1254 /** @internal */
1255 getParser() {
1256 return fromRefinement(this, Predicate.isObjectKeyword)
1257 }
1258 /** @internal */
1259 getExpected(): string {
1260 return "object | array | function"
1261 }
1262}
1263
1264/**
1265 * Provides the singleton {@link ObjectKeyword} AST instance.
1266 *
1267 * **When to use**
1268 *
1269 * Use to reuse the canonical AST node for the TypeScript `object` keyword when
1270 * building or comparing `SchemaAST` values directly.
1271 *
1272 * @see {@link ObjectKeyword} for the AST node class
1273 * @see {@link isObjectKeyword} for narrowing an AST to an `ObjectKeyword` node
1274 *
1275 * @category constructors
1276 * @since 3.10.0
1277 */
1278export const objectKeyword = new ObjectKeyword()
1279
1280/**
1281 * AST node representing a TypeScript `enum`.
1282 *
1283 * **Details**
1284 *
1285 * Holds `enums` as an array of `[name, value]` pairs where values are
1286 * `string | number`. Parsing succeeds when the input matches any enum value.
1287 *
1288 * @see {@link isEnum}
1289 * @category models
1290 * @since 4.0.0
1291 */
1292export interface Enum extends ASTNode {
1293 readonly _tag: "Enum"
1294 readonly enums: ReadonlyArray<
1295 readonly [
1296 string,
1297 string | number
1298 ]
1299 >
1300 /** @internal */
1301
1302 getParser(): SchemaParser.Parser
1303 /** @internal */
1304
1305 toCodecStringTree(): AST
1306 /** @internal */
1307
1308 getExpected(): string
1309}
1310
1311/**
1312 * Constructs a {@link Enum}. Numeric values must be finite.
1313 *
1314 * @category constructors
1315 * @since 4.0.0
1316 */
1317export const Enum: new(
1318 enums: ReadonlyArray<
1319 readonly [
1320 string,
1321 string | number
1322 ]
1323 >,
1324 annotations?: Schema.Annotations.Annotations,
1325 checks?: Checks,
1326 encoding?: Encoding,
1327 context?: Context
1328) => Enum = class extends ASTNodeImpl {
1329 readonly _tag = "Enum"
1330 readonly enums: ReadonlyArray<readonly [string, string | number]>
1331
1332 constructor(
1333 enums: ReadonlyArray<readonly [string, string | number]>,
1334 annotations?: Schema.Annotations.Annotations,
1335 checks?: Checks,
1336 encoding?: Encoding,
1337 context?: Context
1338 ) {
1339 super(annotations, checks, encoding, context)
1340 for (const [, value] of enums) {
1341 if (typeof value === "number" && !globalThis.Number.isFinite(value)) {
1342 throw new Error(`A numeric enum value must be finite, got ${format(value)}`)
1343 }
1344 }
1345 this.enums = enums
1346 }
1347 /** @internal */
1348 getParser() {
1349 const values = new Set<unknown>(this.enums.map(([, v]) => v))
1350 return fromRefinement(
1351 this,
1352 (input): input is typeof this.enums[number][1] => values.has(input)
1353 )
1354 }
1355 /** @internal */
1356 toCodecStringTree(): AST {
1357 if (this.enums.some(([_, v]) => typeof v === "number")) {
1358 const coercions = Object.fromEntries(this.enums.map(([_, v]) => [globalThis.String(v), v]))
1359 return replaceEncoding(this, [
1360 new Link(
1361 new Union(Object.keys(coercions).map((k) => new Literal(k))),
1362 new SchemaTransformation.Transformation(
1363 SchemaGetter.transform((s) => coercions[s]),
1364 SchemaGetter.String()
1365 )
1366 )
1367 ])
1368 }
1369 return this
1370 }
1371 /** @internal */
1372 getExpected(): string {
1373 return this.enums.map(([_, value]) => JSON.stringify(value)).join(" | ")
1374 }
1375}
1376
1377type TemplateLiteralPart =
1378 | String
1379 | Number
1380 | BigInt
1381 | Literal
1382 | TemplateLiteral
1383 | Union<TemplateLiteralPart>
1384
1385function formatTemplateLiteralPath(path: string | number): string {
1386 return typeof path === "number" ? `parts[${path}]` : path
1387}
1388
1389function isTemplateLiteralPart(
1390 ast: AST,
1391 path: string | number,
1392 validated: Set<AST>
1393): ast is TemplateLiteralPart {
1394 if (validated.has(ast)) return true
1395 if (ast.encoding) {
1396 throw new Error(`TemplateLiteral parts cannot have an encoding at ${formatTemplateLiteralPath(path)}`)
1397 }
1398 let valid: boolean
1399 switch (ast._tag) {
1400 case "String":
1401 case "Number":
1402 case "BigInt":
1403 valid = true
1404 break
1405 case "Literal":
1406 valid = !ast.checks
1407 break
1408 case "TemplateLiteral": {
1409 const currentPath = formatTemplateLiteralPath(path)
1410 valid = !ast.checks &&
1411 ast.parts.every((part, index) => isTemplateLiteralPart(part, `${currentPath}.parts[${index}]`, validated))
1412 break
1413 }
1414 case "Union": {
1415 const currentPath = formatTemplateLiteralPath(path)
1416 valid = !ast.checks &&
1417 ast.types.every((part, index) => isTemplateLiteralPart(part, `${currentPath}.types[${index}]`, validated))
1418 break
1419 }
1420 default:
1421 return false
1422 }
1423 if (valid) validated.add(ast)
1424 return valid
1425}
1426
1427/**
1428 * AST node representing a TypeScript template literal type
1429 * (e.g. `` `user_${string}` ``).
1430 *
1431 * **Details**
1432 *
1433 * `parts` is an array of AST nodes; each part contributes to matching
1434 * strings at runtime.
1435 *
1436 * @see {@link isTemplateLiteral}
1437 * @category models
1438 * @since 4.0.0
1439 */
1440export interface TemplateLiteral extends ASTNode {
1441 readonly _tag: "TemplateLiteral"
1442 readonly parts: ReadonlyArray<AST>
1443 /** @internal */
1444 readonly encodedParts: ReadonlyArray<TemplateLiteralPart>
1445 /** @internal */
1446 readonly literals: ReadonlyArray<string | undefined>
1447 /** @internal */
1448 readonly suffixLengths: ReadonlyArray<number>
1449 /** @internal */
1450
1451 getParser(compile: SchemaParser.Compiler): SchemaParser.Parser
1452 /** @internal */
1453
1454 getExpected(): string
1455 /** @internal */
1456
1457 matchPart(s: string, options: ParseOptions): string | undefined
1458}
1459
1460/**
1461 * Constructs a {@link TemplateLiteral}.
1462 *
1463 * **Gotchas**
1464 *
1465 * Throws if a part contains an encoding, including inside unions or nested
1466 * template literals. Parts must describe their values without transformations.
1467 *
1468 * @category constructors
1469 * @since 4.0.0
1470 */
1471export const TemplateLiteral: new(
1472 parts: ReadonlyArray<AST>,
1473 annotations?: Schema.Annotations.Annotations,
1474 checks?: Checks,
1475 encoding?: Encoding,
1476 context?: Context
1477) => TemplateLiteral = class extends ASTNodeImpl {
1478 readonly _tag = "TemplateLiteral"
1479 readonly parts: ReadonlyArray<AST>
1480 /** @internal */
1481 readonly encodedParts: ReadonlyArray<TemplateLiteralPart>
1482 /** @internal */
1483 readonly literals: ReadonlyArray<string | undefined>
1484 /** @internal */
1485 readonly suffixLengths: ReadonlyArray<number>
1486
1487 constructor(
1488 parts: ReadonlyArray<AST>,
1489 annotations?: Schema.Annotations.Annotations,
1490 checks?: Checks,
1491 encoding?: Encoding,
1492 context?: Context
1493 ) {
1494 super(annotations, checks, encoding, context)
1495 const encodedParts: Array<TemplateLiteralPart> = []
1496 const literals: Array<string | undefined> = []
1497 const validated = new Set<AST>()
1498 for (let index = 0; index < parts.length; index++) {
1499 const part = parts[index]
1500 if (!isTemplateLiteralPart(part, index, validated)) {
1501 throw new Error(`Invalid TemplateLiteral part ${part._tag}`)
1502 }
1503 encodedParts.push(part)
1504 literals.push(part._tag === "Literal" ? globalThis.String(part.literal) : undefined)
1505 }
1506 const suffixLengths = new Array<number>(encodedParts.length + 1)
1507 suffixLengths[encodedParts.length] = 0
1508 for (let i = encodedParts.length - 1; i >= 0; i--) {
1509 suffixLengths[i] = suffixLengths[i + 1] + (literals[i]?.length ?? 0)
1510 }
1511 this.parts = parts
1512 this.encodedParts = encodedParts
1513 this.literals = literals
1514 this.suffixLengths = suffixLengths
1515 }
1516 /** @internal */
1517 getParser(compile: SchemaParser.Compiler): SchemaParser.Parser {
1518 const tuple = new Arrays(false, this.parts.map(partFromString), [])
1519 const parser = compile(decodeTo(string, tuple, templateLiteralTransformation(this)))
1520 return (input, options) => {
1521 if (input === InternalParser.missing) return InternalParser.missingExit
1522 const result = parser(input, options)
1523 if ((result as Exit.Exit<unknown, unknown>)._tag === "Success") {
1524 return InternalParser.sameExit
1525 }
1526 return Effect.mapBothEager(result, {
1527 onSuccess: () => input,
1528 onFailure: (issue) => new SchemaIssue.Composite(this, [issue], input, options)
1529 })
1530 }
1531 }
1532 /** @internal */
1533 getExpected(): string {
1534 return "string"
1535 }
1536 /** @internal */
1537 matchPart(s: string, options: ParseOptions): string | undefined {
1538 return segmentTemplateLiteralParts(this, s, options) === undefined ? undefined : s
1539 }
1540}
1541
1542/** @internal */
1543export function templateLiteralParser(parts: ReadonlyArray<AST>): Arrays {
1544 // Encoded members can overlap even when decoding selects exactly one member.
1545 // Match their spellings here; the original tuple enforces the union's mode.
1546 const normalize = memoize((encoded: AST): AST => {
1547 if (encoded._tag !== "Union") return encoded
1548 const types = mapOrSame(encoded.types, normalize)
1549 return (encoded.options?.mode ?? "anyOf") === "anyOf" && types === encoded.types
1550 ? encoded
1551 : new Union(
1552 types,
1553 encoded.options === undefined ? undefined : { ...encoded.options, mode: "anyOf" },
1554 encoded.annotations,
1555 encoded.checks,
1556 undefined,
1557 encoded.context
1558 )
1559 })
1560 const template = new TemplateLiteral(parts.map((part) => normalize(toEncoded(part))))
1561 const tuple = new Arrays(false, parts.map(partFromString), [])
1562 return decodeTo(template, tuple, templateLiteralTransformation(template))
1563}
1564
1565function templateLiteralTransformation(template: TemplateLiteral) {
1566 return new SchemaTransformation.Transformation(
1567 SchemaGetter.transformEffect((s: string, options) => {
1568 const segments = segmentTemplateLiteralParts(template, s, options)
1569 if (segments) return Effect.succeed(segments)
1570 return Effect.fail(
1571 new SchemaIssue.InvalidValue(
1572 { expected: "a string matching template literal parts" },
1573 s,
1574 options
1575 )
1576 )
1577 }),
1578 SchemaGetter.transform((parts: ReadonlyArray<string>) => parts.join(""))
1579 )
1580}
1581
1582/**
1583 * AST node matching a specific `unique symbol` value.
1584 *
1585 * **Details**
1586 *
1587 * Parsing succeeds only when the input is reference-equal to the stored
1588 * `symbol`.
1589 *
1590 * @see {@link isUniqueSymbol}
1591 * @category models
1592 * @since 4.0.0
1593 */
1594export interface UniqueSymbol extends ASTNode {
1595 readonly _tag: "UniqueSymbol"
1596 readonly symbol: symbol
1597 /** @internal */
1598
1599 getParser(): SchemaParser.Parser
1600 /** @internal */
1601
1602 toCodecStringTree(): AST
1603 /** @internal */
1604
1605 getExpected(): string
1606}
1607
1608/**
1609 * Constructs a {@link UniqueSymbol}.
1610 *
1611 * @category constructors
1612 * @since 4.0.0
1613 */
1614export const UniqueSymbol: new(
1615 symbol: symbol,
1616 annotations?: Schema.Annotations.Annotations,
1617 checks?: Checks,
1618 encoding?: Encoding,
1619 context?: Context
1620) => UniqueSymbol = class extends ASTNodeImpl {
1621 readonly _tag = "UniqueSymbol"
1622 readonly symbol: symbol
1623
1624 constructor(
1625 symbol: symbol,
1626 annotations?: Schema.Annotations.Annotations,
1627 checks?: Checks,
1628 encoding?: Encoding,
1629 context?: Context
1630 ) {
1631 super(annotations, checks, encoding, context)
1632 this.symbol = symbol
1633 }
1634 /** @internal */
1635 getParser() {
1636 return fromConst(this, this.symbol)
1637 }
1638 /** @internal */
1639 toCodecStringTree(): AST {
1640 const key = globalThis.Symbol.keyFor(this.symbol)
1641 return replaceEncoding(this, [
1642 new Link(
1643 key === undefined ? never : new Literal(globalThis.String(this.symbol)),
1644 symbolToString.transformation
1645 )
1646 ])
1647 }
1648 /** @internal */
1649 getExpected(): string {
1650 return globalThis.String(this.symbol)
1651 }
1652}
1653
1654/**
1655 * The set of primitive types that can appear as a {@link Literal} value.
1656 *
1657 * @see {@link Literal}
1658 *
1659 * @category models
1660 * @since 3.10.0
1661 */
1662export type LiteralValue = string | number | boolean | bigint
1663
1664/**
1665 * AST node matching an exact primitive value (string, number, boolean, or
1666 * bigint).
1667 *
1668 * **Details**
1669 *
1670 * Parsing succeeds only when the input is strictly equal (`===`) to the
1671 * stored `literal`, preserving the input value. Both `0` and `-0` are accepted
1672 * by either zero literal, and parsing preserves the input's sign. Numeric
1673 * literals must be finite. `Infinity`, `-Infinity`, and `NaN` are rejected at
1674 * construction time.
1675 *
1676 * **Example** (Creating a literal AST)
1677 *
1678 * ```ts import.meta.vitest
1679 * import { SchemaAST } from "effect"
1680 *
1681 * const ast = new SchemaAST.Literal("active")
1682 * ast.literal // => "active"
1683 * ```
1684 *
1685 * @see {@link LiteralValue}
1686 * @see {@link isLiteral}
1687 * @category models
1688 * @since 4.0.0
1689 */
1690export interface Literal extends ASTNode {
1691 readonly _tag: "Literal"
1692 readonly literal: LiteralValue
1693 /** @internal */
1694
1695 getParser(): SchemaParser.Parser
1696 /** @internal */
1697
1698 matchPart(s: string, _options: ParseOptions): LiteralValue | undefined
1699 /** @internal */
1700
1701 toCodecJson(): AST
1702 /** @internal */
1703
1704 toCodecStringTree(): AST
1705 /** @internal */
1706
1707 getExpected(): string
1708}
1709
1710/**
1711 * Constructs a {@link Literal}.
1712 *
1713 * @category constructors
1714 * @since 4.0.0
1715 */
1716export const Literal: new(
1717 literal: LiteralValue,
1718 annotations?: Schema.Annotations.Annotations,
1719 checks?: Checks,
1720 encoding?: Encoding,
1721 context?: Context
1722) => Literal = class extends ASTNodeImpl {
1723 readonly _tag = "Literal"
1724 readonly literal: LiteralValue
1725
1726 constructor(
1727 literal: LiteralValue,
1728 annotations?: Schema.Annotations.Annotations,
1729 checks?: Checks,
1730 encoding?: Encoding,
1731 context?: Context
1732 ) {
1733 super(annotations, checks, encoding, context)
1734 if (typeof literal === "number" && !globalThis.Number.isFinite(literal)) {
1735 throw new Error(`A numeric literal must be finite, got ${format(literal)}`)
1736 }
1737 this.literal = literal
1738 }
1739 /** @internal */
1740 getParser() {
1741 return fromConst(this, this.literal)
1742 }
1743 /** @internal */
1744 matchPart(s: string, _options: ParseOptions): LiteralValue | undefined {
1745 return s === globalThis.String(this.literal) ? this.literal : undefined
1746 }
1747 /** @internal */
1748 toCodecJson(): AST {
1749 return typeof this.literal === "bigint" ? literalToString(this) : this
1750 }
1751 /** @internal */
1752 toCodecStringTree(): AST {
1753 return typeof this.literal === "string" ? this : literalToString(this)
1754 }
1755 /** @internal */
1756 getExpected(): string {
1757 return typeof this.literal === "string" ? JSON.stringify(this.literal) : globalThis.String(this.literal)
1758 }
1759}
1760
1761function literalToString(ast: Literal): Literal {
1762 const literalAsString = globalThis.String(ast.literal)
1763 return replaceEncoding(ast, [
1764 new Link(
1765 new Literal(literalAsString),
1766 new SchemaTransformation.Transformation(
1767 SchemaGetter.transform(() => ast.literal),
1768 SchemaGetter.transform(() => literalAsString)
1769 )
1770 )
1771 ])
1772}
1773
1774/**
1775 * AST node matching any `string` value.
1776 *
1777 * @see {@link string}
1778 * @see {@link isString}
1779 *
1780 * @category models
1781 * @since 4.0.0
1782 */
1783export interface String extends ASTNode {
1784 readonly _tag: "String"
1785 /** @internal */
1786
1787 getParser(): SchemaParser.Parser
1788 /** @internal */
1789
1790 matchPart(s: string, options: ParseOptions): string | undefined
1791 /** @internal */
1792
1793 getExpected(): string
1794}
1795
1796/**
1797 * Constructs a {@link String}.
1798 *
1799 * @category constructors
1800 * @since 4.0.0
1801 */
1802export const String: new(
1803 annotations?: Schema.Annotations.Annotations | undefined,
1804 checks?: Checks | undefined,
1805 encoding?: Encoding | undefined,
1806 context?: Context | undefined
1807) => String = class extends ASTNodeImpl {
1808 readonly _tag = "String"
1809 /** @internal */
1810 getParser() {
1811 return fromRefinement(this, Predicate.isString)
1812 }
1813 /** @internal */
1814 matchPart(s: string, options: ParseOptions): string | undefined {
1815 const checks = this.checks
1816 return checks && !options.disableChecks && collectIssues(checks, s, undefined, this, options) ? undefined : s
1817 }
1818 /** @internal */
1819 getExpected(): string {
1820 return "string"
1821 }
1822}
1823
1824/**
1825 * Provides the singleton {@link String} AST instance.
1826 *
1827 * **When to use**
1828 *
1829 * Use as the shared `SchemaAST` node for unconstrained JavaScript strings.
1830 *
1831 * @see {@link String} for the AST node class
1832 * @see {@link isString} for narrowing an AST to a string node
1833 *
1834 * @category constructors
1835 * @since 4.0.0
1836 */
1837export const string = new String()
1838
1839/**
1840 * AST node matching any `number` value (including `NaN`, `Infinity`,
1841 * `-Infinity`).
1842 *
1843 * **Details**
1844 *
1845 * Default JSON serialization:
1846 *
1847 * - Finite numbers are serialized as JSON numbers.
1848 * - `Infinity`, `-Infinity`, and `NaN` are serialized as JSON strings.
1849 *
1850 * If the node has an `isFinite` or `isInt` check, the string fallback is
1851 * skipped since non-finite values cannot occur.
1852 *
1853 * @see {@link number}
1854 * @see {@link isNumber}
1855 * @category models
1856 * @since 4.0.0
1857 */
1858export interface Number extends ASTNode {
1859 readonly _tag: "Number"
1860 /** @internal */
1861
1862 getParser(): SchemaParser.Parser
1863 /** @internal */
1864
1865 matchKey(s: string, options: ParseOptions): number | undefined
1866 /** @internal */
1867
1868 matchPart(s: string, options: ParseOptions): number | undefined
1869 /** @internal */
1870
1871 toCodecJson(): AST
1872 /** @internal */
1873
1874 toCodecStringTree(): AST
1875 /** @internal */
1876
1877 getExpected(): string
1878}
1879
1880/**
1881 * Constructs a {@link Number}.
1882 *
1883 * @category constructors
1884 * @since 4.0.0
1885 */
1886export const Number: new(
1887 annotations?: Schema.Annotations.Annotations | undefined,
1888 checks?: Checks | undefined,
1889 encoding?: Encoding | undefined,
1890 context?: Context | undefined
1891) => Number = class extends ASTNodeImpl {
1892 readonly _tag = "Number"
1893 /** @internal */
1894 getParser() {
1895 return fromRefinement(this, Predicate.isNumber)
1896 }
1897 /** @internal */
1898 matchKey(s: string, options: ParseOptions): number | undefined {
1899 return this._match(isStringNumberRegExp, s, options)
1900 }
1901 /** @internal */
1902 matchPart(s: string, options: ParseOptions): number | undefined {
1903 return this._match(isStringFiniteRegExp, s, options)
1904 }
1905 private _match(regexp: RegExp, s: string, options: ParseOptions): number | undefined {
1906 if (!regexp.test(s)) return undefined
1907 const value = globalThis.Number(s)
1908 if (options.disableChecks || !this.checks) return value
1909 return collectIssues(this.checks, value, undefined, this, options) ? undefined : value
1910 }
1911 /** @internal */
1912 toCodecJson(): AST {
1913 if (
1914 this.checks &&
1915 (hasCheck(this.checks, "effect/schema/isFinite") || hasCheck(this.checks, "effect/schema/isInt"))
1916 ) {
1917 return this
1918 }
1919 return replaceEncoding(this, [numberToJson])
1920 }
1921 /** @internal */
1922 toCodecStringTree(): AST {
1923 if (this.toCodecJson() === this) {
1924 return replaceEncoding(this, [finiteToString])
1925 }
1926 return replaceEncoding(this, [numberToString])
1927 }
1928 /** @internal */
1929 getExpected(): string {
1930 return "number"
1931 }
1932}
1933
1934function hasCheck(checks: ReadonlyArray<Check<unknown>>, id: string): boolean {
1935 return checks.some((check) =>
1936 check.annotations?.representation?.id === id ||
1937 (check._tag === "FilterGroup" && hasCheck(check.checks, id))
1938 )
1939}
1940
1941/**
1942 * Provides the singleton {@link Number} AST instance.
1943 *
1944 * **When to use**
1945 *
1946 * Use when you need the canonical `SchemaAST` node for schemas that accept any
1947 * JavaScript number value.
1948 *
1949 * @see {@link Number} for the AST node class and serialization behavior
1950 * @see {@link Literal} for exact finite numeric literal AST nodes
1951 *
1952 * @category constructors
1953 * @since 4.0.0
1954 */
1955export const number = new Number()
1956
1957/**
1958 * AST node matching any `boolean` value (`true` or `false`).
1959 *
1960 * @see {@link boolean}
1961 * @see {@link isBoolean}
1962 *
1963 * @category models
1964 * @since 4.0.0
1965 */
1966export interface Boolean extends ASTNode {
1967 readonly _tag: "Boolean"
1968 /** @internal */
1969
1970 getParser(): SchemaParser.Parser
1971 /** @internal */
1972
1973 getExpected(): string
1974}
1975
1976/**
1977 * Constructs a {@link Boolean}.
1978 *
1979 * @category constructors
1980 * @since 4.0.0
1981 */
1982export const Boolean: new(
1983 annotations?: Schema.Annotations.Annotations | undefined,
1984 checks?: Checks | undefined,
1985 encoding?: Encoding | undefined,
1986 context?: Context | undefined
1987) => Boolean = class extends ASTNodeImpl {
1988 readonly _tag = "Boolean"
1989 /** @internal */
1990 getParser() {
1991 return fromRefinement(this, Predicate.isBoolean)
1992 }
1993 /** @internal */
1994 getExpected(): string {
1995 return "boolean"
1996 }
1997}
1998
1999/**
2000 * Provides the singleton {@link Boolean} AST instance.
2001 *
2002 * **When to use**
2003 *
2004 * Use to reuse the standard AST node that accepts either `true` or `false` when
2005 * constructing schema ASTs directly.
2006 *
2007 * @see {@link Boolean} for the AST node class
2008 * @see {@link Literal} for exact boolean literal AST nodes
2009 *
2010 * @category constructors
2011 * @since 4.0.0
2012 */
2013export const boolean = new Boolean()
2014
2015/**
2016 * AST node matching any `symbol` value.
2017 *
2018 * **When to use**
2019 *
2020 * Use when you need the AST node class for schemas that match any JavaScript
2021 * symbol value.
2022 *
2023 * **Details**
2024 *
2025 * When serialized to a string-based codec, symbols are converted via
2026 * `Symbol.keyFor` and must be registered with `Symbol.for`.
2027 *
2028 * @see {@link symbol}
2029 * @see {@link isSymbol}
2030 * @category models
2031 * @since 4.0.0
2032 */
2033export interface Symbol extends ASTNode {
2034 readonly _tag: "Symbol"
2035 /** @internal */
2036
2037 getParser(): SchemaParser.Parser
2038 /** @internal */
2039
2040 matchKey(s: symbol, options: ParseOptions): symbol | undefined
2041 /** @internal */
2042
2043 toCodecStringTree(): AST
2044 /** @internal */
2045
2046 getExpected(): string
2047}
2048
2049/**
2050 * Constructs a {@link Symbol}.
2051 *
2052 * @category constructors
2053 * @since 4.0.0
2054 */
2055export const Symbol: new(
2056 annotations?: Schema.Annotations.Annotations | undefined,
2057 checks?: Checks | undefined,
2058 encoding?: Encoding | undefined,
2059 context?: Context | undefined
2060) => Symbol = class extends ASTNodeImpl {
2061 readonly _tag = "Symbol"
2062 /** @internal */
2063 getParser() {
2064 return fromRefinement(this, Predicate.isSymbol)
2065 }
2066 /** @internal */
2067 matchKey(s: symbol, options: ParseOptions): symbol | undefined {
2068 if (options.disableChecks || !this.checks) return s
2069 return collectIssues(this.checks, s, undefined, this, options) ? undefined : s
2070 }
2071 /** @internal */
2072 toCodecStringTree(): AST {
2073 return replaceEncoding(this, [symbolToString])
2074 }
2075 /** @internal */
2076 getExpected(): string {
2077 return "symbol"
2078 }
2079}
2080
2081/**
2082 * Provides the singleton {@link Symbol} AST instance.
2083 *
2084 * **When to use**
2085 *
2086 * Use to reuse the singleton AST node for schemas that match any JavaScript
2087 * symbol value.
2088 *
2089 * **Gotchas**
2090 *
2091 * String-based codecs can encode only symbols registered with `Symbol.for`,
2092 * because the implementation uses `Symbol.keyFor`.
2093 *
2094 * @see {@link UniqueSymbol} for an AST node that matches one specific symbol
2095 *
2096 * @category constructors
2097 * @since 4.0.0
2098 */
2099export const symbol = new Symbol()
2100
2101/**
2102 * AST node matching any `bigint` value.
2103 *
2104 * **Details**
2105 *
2106 * When serialized to a string-based codec, bigints are converted to/from
2107 * their decimal string representation.
2108 *
2109 * @see {@link bigInt}
2110 * @see {@link isBigInt}
2111 * @category models
2112 * @since 4.0.0
2113 */
2114export interface BigInt extends ASTNode {
2115 readonly _tag: "BigInt"
2116 /** @internal */
2117
2118 getParser(): SchemaParser.Parser
2119 /** @internal */
2120
2121 matchPart(s: string, options: ParseOptions): bigint | undefined
2122 /** @internal */
2123
2124 toCodecStringTree(): AST
2125 /** @internal */
2126
2127 getExpected(): string
2128}
2129
2130/**
2131 * Constructs a {@link BigInt}.
2132 *
2133 * @category constructors
2134 * @since 4.0.0
2135 */
2136export const BigInt: new(
2137 annotations?: Schema.Annotations.Annotations | undefined,
2138 checks?: Checks | undefined,
2139 encoding?: Encoding | undefined,
2140 context?: Context | undefined
2141) => BigInt = class extends ASTNodeImpl {
2142 readonly _tag = "BigInt"
2143 /** @internal */
2144 getParser() {
2145 return fromRefinement(this, Predicate.isBigInt)
2146 }
2147 /** @internal */
2148 matchPart(s: string, options: ParseOptions): bigint | undefined {
2149 if (!isStringBigIntRegExp.test(s)) return undefined
2150 const value = globalThis.BigInt(s)
2151 if (options.disableChecks || !this.checks) return value
2152 return collectIssues(this.checks, value, undefined, this, options) ? undefined : value
2153 }
2154 /** @internal */
2155 toCodecStringTree(): AST {
2156 return replaceEncoding(this, [bigIntToString])
2157 }
2158 /** @internal */
2159 getExpected(): string {
2160 return "bigint"
2161 }
2162}
2163
2164/**
2165 * Provides the singleton {@link BigInt} AST instance.
2166 *
2167 * **When to use**
2168 *
2169 * Use to reuse the canonical `BigInt` AST node when constructing, inspecting,
2170 * or transforming schemas at the AST level.
2171 *
2172 * @see {@link BigInt} for the AST node class and string-codec behavior
2173 * @see {@link isBigInt} for narrowing an AST to a `BigInt` node
2174 *
2175 * @category constructors
2176 * @since 4.0.0
2177 */
2178export const bigInt = new BigInt()
2179
2180/**
2181 * AST node for array-like types — both tuples and arrays.
2182 *
2183 * **When to use**
2184 *
2185 * Use when constructing or inspecting AST nodes for tuple or array-like schemas,
2186 * including rest elements.
2187 *
2188 * **Details**
2189 *
2190 * - `elements` — positional element types (tuple elements). An element is
2191 * optional if its context's `isOptional` field is `true`.
2192 * - `rest` — the rest/variadic element types. When non-empty, the first
2193 * entry is the "spread" type (e.g. `...Array<string>`), and subsequent
2194 * entries are trailing positional elements after the spread.
2195 * - `isMutable` — whether the resulting array is `readonly` (`false`) or
2196 * mutable (`true`).
2197 *
2198 * **Gotchas**
2199 *
2200 * Construction enforces TypeScript ordering rules: a required element
2201 * cannot follow an optional one, and an optional element cannot follow a
2202 * rest element.
2203 *
2204 * **Example** (Inspecting a tuple AST)
2205 *
2206 * ```ts import.meta.vitest
2207 * import { Schema, SchemaAST } from "effect"
2208 *
2209 * const schema = Schema.Tuple([Schema.String, Schema.Number])
2210 * const ast = schema.ast
2211 *
2212 * if (SchemaAST.isArrays(ast)) {
2213 * [ast.elements.length, ast.rest.length] // => [2, 0]
2214 * }
2215 * ```
2216 *
2217 * @see {@link isArrays}
2218 * @see {@link Objects}
2219 * @category models
2220 * @since 4.0.0
2221 */
2222export interface Arrays extends ASTNode {
2223 readonly _tag: "Arrays"
2224 readonly isMutable: boolean
2225 readonly elements: ReadonlyArray<AST>
2226 readonly rest: ReadonlyArray<AST>
2227 readonly encodingChecks: Checks | undefined
2228 /** @internal */
2229
2230 getParser(
2231 compile: SchemaParser.Compiler,
2232 compileField?: SchemaParser.Compiler
2233 ): SchemaParser.Parser
2234 /** @internal */
2235
2236 recur(recur: (ast: AST) => AST): Arrays
2237 /** @internal */
2238
2239 flip(recur: (ast: AST) => AST): Arrays
2240 /** @internal */
2241
2242 getExpected(): string
2243}
2244
2245/**
2246 * Constructs a {@link Arrays}.
2247 *
2248 * @category constructors
2249 * @since 4.0.0
2250 */
2251export const Arrays: new(
2252 isMutable: boolean,
2253 elements: ReadonlyArray<AST>,
2254 rest: ReadonlyArray<AST>,
2255 annotations?: Schema.Annotations.Annotations,
2256 checks?: Checks,
2257 encoding?: Encoding,
2258 context?: Context,
2259 encodingChecks?: Checks
2260) => Arrays = class extends ASTNodeImpl {
2261 readonly _tag = "Arrays"
2262 readonly isMutable: boolean
2263 readonly elements: ReadonlyArray<AST>
2264 readonly rest: ReadonlyArray<AST>
2265 readonly encodingChecks: Checks | undefined
2266
2267 constructor(
2268 isMutable: boolean,
2269 elements: ReadonlyArray<AST>,
2270 rest: ReadonlyArray<AST>,
2271 annotations?: Schema.Annotations.Annotations,
2272 checks?: Checks,
2273 encoding?: Encoding,
2274 context?: Context,
2275 encodingChecks?: Checks
2276 ) {
2277 super(annotations, checks, encoding, context)
2278 this.isMutable = isMutable
2279 this.elements = elements
2280 this.rest = rest
2281 this.encodingChecks = encodingChecks
2282
2283 let hasOptional = false
2284 for (let i = 0; i < elements.length; i++) {
2285 if (isOptional(elements[i])) {
2286 hasOptional = true
2287 } else if (hasOptional) {
2288 throw new Error("A required element cannot follow an optional element. ts(1257)")
2289 }
2290 }
2291 if (hasOptional && rest.length > 1) {
2292 throw new Error("A required element cannot follow an optional element. ts(1257)")
2293 }
2294
2295 // An optional element cannot follow a rest element.ts(1266)
2296 for (let i = 1; i < rest.length; i++) {
2297 if (isOptional(rest[i])) {
2298 throw new Error("An optional element cannot follow a rest element. ts(1266)")
2299 }
2300 }
2301 }
2302 /** @internal */
2303 getParser(
2304 compile: SchemaParser.Compiler,
2305 compileField: SchemaParser.Compiler = compile
2306 ): SchemaParser.Parser {
2307 // oxlint-disable-next-line @typescript-eslint/no-this-alias
2308 const ast = this
2309 type ElementParser = { readonly ast: AST; readonly parser: SchemaParser.Parser }
2310 let elements: Array<ElementParser> | undefined
2311 let rest: Array<ElementParser> | undefined
2312 const elementLen = ast.elements.length
2313 const tailLen = Math.max(0, ast.rest.length - 1)
2314
2315 function getParser(
2316 tailThreshold: number,
2317 index: number
2318 ): { readonly ast: AST; readonly parser: SchemaParser.Parser } {
2319 if (index < elementLen) {
2320 return elements![index]
2321 } else if (index >= tailThreshold) {
2322 return rest![index - tailThreshold + 1]
2323 }
2324 return rest![0]
2325 }
2326
2327 const finish = (state: ArrayParserState): Effect.Effect<unknown, SchemaIssue.Issue, any> => {
2328 const { input, len, options } = state
2329 if (ast.rest.length === 0 && len > elementLen) {
2330 for (let i = elementLen; i < len; i++) {
2331 const unexpected = new SchemaIssue.UnexpectedKey(ast, input[i], options)
2332 const issue = new SchemaIssue.Pointer([i], unexpected)
2333 if (options.errors === "all") {
2334 if (state.issues) state.issues.push(issue)
2335 else state.issues = [issue]
2336 } else {
2337 return Effect.fail(new SchemaIssue.Composite(ast, [issue], input, options))
2338 }
2339 }
2340 }
2341 if (state.issues) {
2342 return Effect.fail(new SchemaIssue.Composite(ast, state.issues, input, options))
2343 }
2344 return InternalParser.succeed(state.output)
2345 }
2346
2347 const parse = (
2348 input: ReadonlyArray<unknown>,
2349 options: ParseOptions
2350 ): Effect.Effect<unknown, SchemaIssue.Issue, any> => {
2351 const len = input.length
2352 const state: ArrayParserState = {
2353 ast,
2354 getParser,
2355 input,
2356 len,
2357 tailThreshold: Math.max(elementLen, len - tailLen),
2358 output: new globalThis.Array(len),
2359 issues: undefined,
2360 options
2361 }
2362 const end = ast.rest.length === 0 ? elementLen : Math.max(len, elementLen + tailLen)
2363 const concurrency = options.concurrency === undefined ? 1 : resolveConcurrency(options.concurrency)
2364 const eff = concurrency === 1
2365 ? parseArray(state, input, 0, end)
2366 : parseArrayConcurrent(state, input, { concurrency, end })
2367 if (!eff) return finish(state)
2368 if (effectIsExit(eff)) return Effect.flatMapEager(eff, () => finish(state))
2369 // Reparse on later runs to avoid reusing mutable traversal state.
2370 let first = true
2371 return Effect.suspend(() => {
2372 if (!first) return parse(input, options)
2373 first = false
2374 return Effect.flatMap(eff, () => finish(state))
2375 })
2376 }
2377
2378 return (input, options) => {
2379 if (input === InternalParser.missing) return InternalParser.missingExit
2380 try {
2381 if (!Array.isArray(input)) {
2382 return Effect.fail(new SchemaIssue.InvalidType(ast, input, options))
2383 }
2384 if (!elements) {
2385 elements = ast.elements.map((ast) => ({ ast, parser: compileField(ast) }))
2386 rest = ast.rest.map((ast) => ({ ast, parser: compileField(ast) }))
2387 }
2388 return parse(input, options)
2389 } catch (error) {
2390 return Effect.die(error)
2391 }
2392 }
2393 }
2394 private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) {
2395 const elements = mapOrSame(this.elements, recur)
2396 const rest = mapOrSame(this.rest, recur)
2397 return elements === this.elements && rest === this.rest && checks === this.checks &&
2398 encodingChecks === this.encodingChecks ?
2399 this :
2400 new Arrays(
2401 this.isMutable,
2402 elements,
2403 rest,
2404 this.annotations,
2405 checks,
2406 undefined,
2407 this.context,
2408 encodingChecks
2409 )
2410 }
2411 /** @internal */
2412 recur(recur: (ast: AST) => AST) {
2413 return this._rebuild(recur, this.checks, this.encodingChecks)
2414 }
2415 /** @internal */
2416 flip(recur: (ast: AST) => AST) {
2417 return this._rebuild(recur, this.encodingChecks, this.checks)
2418 }
2419 /** @internal */
2420 getExpected(): string {
2421 return "array"
2422 }
2423}
2424
2425type ArrayParserState = {
2426 readonly ast: AST
2427 readonly input: ReadonlyArray<unknown>
2428 readonly len: number
2429 readonly getParser: (
2430 tailThreshold: number,
2431 index: number
2432 ) => { readonly ast: AST; readonly parser: SchemaParser.Parser }
2433 readonly tailThreshold: number
2434 readonly options: ParseOptions
2435 readonly output: Array<unknown>
2436 issues: Arr.NonEmptyArray<SchemaIssue.Issue> | undefined
2437}
2438
2439/** @internal */
2440export function stepArray(
2441 s: ArrayParserState,
2442 item: unknown,
2443 exit: Exit.Exit<unknown, SchemaIssue.Issue>,
2444 i: number
2445) {
2446 if (exit._tag === "Failure") {
2447 return wrapPropertyKeyIssue(s, s.ast, i, exit)
2448 }
2449 const value = exit === InternalParser.sameExit
2450 ? item
2451 : (exit as InternalParser.Success<unknown, SchemaIssue.Issue>)[InternalParser.args]
2452 if (value !== InternalParser.missing) {
2453 s.output[i] = value
2454 } else {
2455 const p = s.getParser(s.tailThreshold, i)
2456 if (isOptional(p.ast)) return
2457 const issue = new SchemaIssue.Pointer([i], new SchemaIssue.MissingKey(p.ast.context?.annotations))
2458 if (s.options.errors === "all") {
2459 if (s.issues) s.issues.push(issue)
2460 else s.issues = [issue]
2461 } else {
2462 return Exit.fail(
2463 new SchemaIssue.Composite(s.ast, [issue], s.input, s.options)
2464 )
2465 }
2466 }
2467}
2468
2469const parseArrayOptions = {
2470 onItem(s: ArrayParserState, item: unknown, i: number) {
2471 const value = i < s.len ? item : InternalParser.missing
2472 return s.getParser(s.tailThreshold, i).parser(value, s.options)
2473 },
2474 step: stepArray
2475}
2476
2477/** @internal */
2478export const parseArray = iterateEager<ArrayParserState, unknown>()(parseArrayOptions)
2479const parseArrayConcurrent = iterateConcurrent<ArrayParserState, unknown>()(parseArrayOptions)
2480
2481const wrapPropertyKeyIssue = (
2482 s: {
2483 readonly input: unknown
2484 readonly options: ParseOptions
2485 issues: Array<SchemaIssue.Issue> | undefined
2486 },
2487 ast: AST,
2488 key: PropertyKey,
2489 exit: Exit.Failure<any, SchemaIssue.Issue>
2490) => {
2491 if (exit.cause.reasons.length === 0) {
2492 return exit
2493 }
2494 const issue = InternalSchemaCause.getSchemaIssue(exit.cause)
2495 if (issue === undefined) {
2496 return Exit.failCause(
2497 Cause.map(
2498 exit.cause,
2499 (issue) =>
2500 new SchemaIssue.Composite(
2501 ast,
2502 [new SchemaIssue.Pointer([key], issue)],
2503 s.input,
2504 s.options
2505 )
2506 )
2507 )
2508 }
2509 const pointer = new SchemaIssue.Pointer([key], issue)
2510 if (s.options.errors === "all") {
2511 if (s.issues) s.issues.push(pointer)
2512 else s.issues = [pointer]
2513 } else {
2514 return Exit.fail(
2515 new SchemaIssue.Composite(ast, [pointer], s.input, s.options)
2516 )
2517 }
2518}
2519
2520/**
2521 * floating point or integer, with optional exponent
2522 * @internal
2523 */
2524export const FINITE_PATTERN = "[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?"
2525
2526/**
2527 * Returns the object keys that match the index signature parameter schema.
2528 * @internal
2529 */
2530export function getIndexSignatureKeys(
2531 input: { readonly [x: PropertyKey]: unknown },
2532 parameter: IndexSignatureParameter,
2533 options: ParseOptions = defaultParseOptions
2534): ReadonlyArray<PropertyKey> {
2535 let stringKeys: ReadonlyArray<string> | undefined
2536 let symbolKeys: ReadonlyArray<symbol> | undefined
2537
2538 function go(parameter: AST): ReadonlyArray<PropertyKey> {
2539 switch (parameter._tag) {
2540 case "String":
2541 case "TemplateLiteral":
2542 return (stringKeys ??= Object.keys(input)).filter((k) => parameter.matchPart(k, options) !== undefined)
2543 case "Number":
2544 return (stringKeys ??= Object.keys(input)).filter((k) => parameter.matchKey(k, options) !== undefined)
2545 case "Symbol":
2546 return (symbolKeys ??= Object.getOwnPropertySymbols(input)).filter((k) =>
2547 Object.prototype.propertyIsEnumerable.call(input, k) && parameter.matchKey(k, options) !== undefined
2548 )
2549 case "Union":
2550 return [...new Set(parameter.types.flatMap(go))]
2551 default:
2552 return []
2553 }
2554 }
2555 return go(parameterFromPropertyKey(toEncoded(parameter)))
2556}
2557
2558/**
2559 * Represents a named property within an {@link Objects} node.
2560 *
2561 * **Details**
2562 *
2563 * Pairs a `name` (any `PropertyKey`) with a `type` ({@link AST}). The
2564 * property's optionality and mutability are determined by the `type`'s
2565 * {@link Context}.
2566 *
2567 * @see {@link Objects}
2568 * @category models
2569 * @since 4.0.0
2570 */
2571export interface PropertySignature {
2572 readonly name: PropertyKey
2573 readonly type: AST
2574}
2575
2576/**
2577 * Constructs a {@link PropertySignature}.
2578 *
2579 * @category constructors
2580 * @since 4.0.0
2581 */
2582export const PropertySignature: new(name: PropertyKey, type: AST) => PropertySignature = class {
2583 readonly name: PropertyKey
2584 readonly type: AST
2585
2586 constructor(
2587 name: PropertyKey,
2588 type: AST
2589 ) {
2590 this.name = name
2591 this.type = type
2592 }
2593}
2594
2595type IndexSignatureParameter =
2596 | String
2597 | Number
2598 | Symbol
2599 | TemplateLiteral
2600 | Union<IndexSignatureParameter>
2601
2602function isIndexSignatureParameterSide(ast: AST): ast is IndexSignatureParameter {
2603 switch (ast._tag) {
2604 case "String":
2605 case "Number":
2606 case "Symbol":
2607 case "TemplateLiteral":
2608 return true
2609 case "Union":
2610 return ast.types.every(isIndexSignatureParameterSide)
2611 default:
2612 return false
2613 }
2614}
2615
2616function isIndexSignatureParameterEncodedSide(ast: AST): boolean {
2617 const encoded = getLastEncoding(ast)
2618 switch (encoded._tag) {
2619 case "String":
2620 case "Number":
2621 case "Symbol":
2622 case "TemplateLiteral":
2623 return true
2624 case "Union":
2625 return encoded.types.every(isIndexSignatureParameterEncodedSide)
2626 default:
2627 return false
2628 }
2629}
2630
2631function isIndexSignatureParameter(ast: AST): ast is IndexSignatureParameter {
2632 return isIndexSignatureParameterSide(ast) && isIndexSignatureParameterEncodedSide(ast)
2633}
2634
2635/**
2636 * Represents an index signature entry within an {@link Objects} node.
2637 *
2638 * **When to use**
2639 *
2640 * Use when constructing or inspecting object AST entries for record-like keys
2641 * and values.
2642 *
2643 * **Details**
2644 *
2645 * - `parameter` — the key type AST (e.g. {@link String} for `string` keys,
2646 * {@link TemplateLiteral} for patterned keys).
2647 * - `type` — the value type SchemaAST.
2648 *
2649 * **Gotchas**
2650 *
2651 * Using `Schema.optionalKey` on the value type is not allowed for index
2652 * signatures (throws at construction); use `Schema.optional` instead.
2653 *
2654 * @see {@link Objects}
2655 * @see {@link PropertySignature}
2656 * @category models
2657 * @since 4.0.0
2658 */
2659export interface IndexSignature {
2660 readonly parameter: IndexSignatureParameter
2661 readonly type: AST
2662}
2663
2664/**
2665 * Constructs a {@link IndexSignature}.
2666 *
2667 * @category constructors
2668 * @since 4.0.0
2669 */
2670export const IndexSignature: new(parameter: AST, type: AST) => IndexSignature = class {
2671 readonly parameter: IndexSignatureParameter
2672 readonly type: AST
2673
2674 constructor(
2675 parameter: AST,
2676 type: AST
2677 ) {
2678 if (!isIndexSignatureParameter(parameter)) {
2679 throw new Error(`Invalid index signature parameter ${parameter._tag}`)
2680 }
2681 this.parameter = parameter
2682 this.type = type
2683 if (isOptional(type) && !containsUndefined(type)) {
2684 throw new Error("Cannot use `Schema.optionalKey` with index signatures, use `Schema.optional` instead.")
2685 }
2686 }
2687}
2688
2689/**
2690 * AST node for object-like schemas, including structs and records.
2691 *
2692 * **When to use**
2693 *
2694 * Use when constructing or inspecting AST nodes for structs or records rather
2695 * than array-like schemas.
2696 *
2697 * **Details**
2698 *
2699 * - `propertySignatures` — named properties with their types (struct fields).
2700 * - `indexSignatures` — index signature entries (record patterns), each with
2701 * a `parameter` AST for matching keys and a `type` AST for values.
2702 *
2703 * An `Objects` node with no properties and no index signatures performs only a
2704 * non-nullish check: it accepts any value except `null` and `undefined`,
2705 * including primitive values.
2706 *
2707 * **Gotchas**
2708 *
2709 * Duplicate property names throw at construction time.
2710 *
2711 * **Example** (Inspecting a struct AST)
2712 *
2713 * ```ts import.meta.vitest
2714 * import { Schema, SchemaAST } from "effect"
2715 *
2716 * const schema = Schema.Struct({ name: Schema.String })
2717 * const ast = schema.ast
2718 *
2719 * if (SchemaAST.isObjects(ast)) {
2720 * ast.propertySignatures.map((ps) => [ps.name, ps.type._tag]) // => [["name", "String"]]
2721 * }
2722 * ```
2723 *
2724 * @see {@link isObjects}
2725 * @see {@link PropertySignature}
2726 * @see {@link IndexSignature}
2727 * @see {@link Arrays}
2728 * @category models
2729 * @since 4.0.0
2730 */
2731export interface Objects extends ASTNode {
2732 readonly _tag: "Objects"
2733 readonly propertySignatures: ReadonlyArray<PropertySignature>
2734 readonly indexSignatures: ReadonlyArray<IndexSignature>
2735 readonly encodingChecks: Checks | undefined
2736 /** @internal */
2737
2738 getParser(
2739 compile: SchemaParser.Compiler,
2740 compileField?: SchemaParser.Compiler
2741 ): SchemaParser.Parser
2742 /** @internal */
2743
2744 flip(recur: (ast: AST) => AST): AST
2745 /** @internal */
2746
2747 recur(recur: (ast: AST) => AST, recurParameter?: (ast: AST) => AST): AST
2748 /** @internal */
2749
2750 getExpected(): string
2751}
2752
2753/**
2754 * Constructs a {@link Objects}.
2755 *
2756 * @category constructors
2757 * @since 4.0.0
2758 */
2759export const Objects: new(
2760 propertySignatures: ReadonlyArray<PropertySignature>,
2761 indexSignatures: ReadonlyArray<IndexSignature>,
2762 annotations?: Schema.Annotations.Annotations,
2763 checks?: Checks,
2764 encoding?: Encoding,
2765 context?: Context,
2766 encodingChecks?: Checks
2767) => Objects = class extends ASTNodeImpl {
2768 readonly _tag = "Objects"
2769 readonly propertySignatures: ReadonlyArray<PropertySignature>
2770 readonly indexSignatures: ReadonlyArray<IndexSignature>
2771 readonly encodingChecks: Checks | undefined
2772
2773 constructor(
2774 propertySignatures: ReadonlyArray<PropertySignature>,
2775 indexSignatures: ReadonlyArray<IndexSignature>,
2776 annotations?: Schema.Annotations.Annotations,
2777 checks?: Checks,
2778 encoding?: Encoding,
2779 context?: Context,
2780 encodingChecks?: Checks
2781 ) {
2782 super(annotations, checks, encoding, context)
2783 this.propertySignatures = propertySignatures
2784 this.indexSignatures = indexSignatures
2785 this.encodingChecks = encodingChecks
2786 }
2787 /** @internal */
2788 getParser(
2789 compile: SchemaParser.Compiler,
2790 compileField: SchemaParser.Compiler = compile
2791 ): SchemaParser.Parser {
2792 // oxlint-disable-next-line @typescript-eslint/no-this-alias
2793 const ast = this
2794 const hasProperties = ast.propertySignatures.length
2795 const indexCount = ast.indexSignatures.length
2796 // ---------------------------------------------
2797 // handle empty struct
2798 // ---------------------------------------------
2799 if (!hasProperties && !indexCount) {
2800 return fromRefinement(ast, Predicate.isNotNullish)
2801 }
2802
2803 let properties: Array<ParsedProperty> | undefined
2804 let indexes:
2805 | Array<{
2806 readonly is: IndexSignature
2807 readonly parserKey: SchemaParser.Parser
2808 readonly parserValue: SchemaParser.Parser
2809 }>
2810 | undefined
2811 type Index = NonNullable<typeof indexes>[number]
2812 const compileMembers = (): Array<ParsedProperty> => {
2813 if (!properties) {
2814 properties = ast.propertySignatures.map((ps) => ({
2815 parser: compileField(ps.type),
2816 name: ps.name,
2817 type: ps.type
2818 }))
2819 indexes = indexCount
2820 ? ast.indexSignatures.map((is) => ({
2821 is,
2822 parserKey: compile(parameterFromPropertyKey(is.parameter)),
2823 parserValue: compileField(is.type)
2824 }))
2825 : undefined
2826 }
2827 return properties
2828 }
2829
2830 const makeFallback = (): SchemaParser.Parser => {
2831 const expectedKeys = new Set<PropertyKey>(
2832 ast.propertySignatures.map((ps) => typeof ps.name === "number" ? globalThis.String(ps.name) : ps.name)
2833 )
2834 const finishIndex = (
2835 s: ObjectParserState,
2836 key: PropertyKey,
2837 k2: PropertyKey | typeof InternalParser.missing,
2838 inputValue: unknown,
2839 exitValue: Exit.Exit<unknown, SchemaIssue.Issue>
2840 ): Effect.Effect<void, SchemaIssue.Issue, any> => {
2841 if (exitValue._tag === "Failure") {
2842 return wrapPropertyKeyIssue(s, ast, key, exitValue) ?? Exit.void
2843 }
2844 const value = exitValue === InternalParser.sameExit
2845 ? inputValue
2846 : (exitValue as InternalParser.Success<unknown, SchemaIssue.Issue>)[InternalParser.args]
2847 if (k2 !== InternalParser.missing && value !== InternalParser.missing) {
2848 if (
2849 hasProperties &&
2850 (expectedKeys.has(key) || expectedKeys.has(typeof k2 === "number" ? globalThis.String(k2) : k2))
2851 ) return Exit.void
2852 InternalRecord.assignProperty(s.out, k2, value)
2853 }
2854 return Exit.void
2855 }
2856 const parseIndex = (
2857 s: ObjectParserState,
2858 key: PropertyKey,
2859 index: Index,
2860 exitKey?: Exit.Exit<unknown, SchemaIssue.Issue>
2861 ): Effect.Effect<void, SchemaIssue.Issue, any> => {
2862 if (!exitKey) {
2863 const eff = index.parserKey(key, s.options)
2864 if (!effectIsExit(eff)) {
2865 return Effect.flatMap(Effect.exit(eff), (exit) => parseIndex(s, key, index, exit))
2866 }
2867 exitKey = eff
2868 }
2869 if (exitKey._tag === "Failure") {
2870 return wrapPropertyKeyIssue(s, ast, key, exitKey) ?? Exit.void
2871 }
2872 const k2 = exitKey === InternalParser.sameExit
2873 ? key
2874 : (exitKey as InternalParser.Success<PropertyKey, SchemaIssue.Issue>)[InternalParser.args]
2875 const inputValue = s.input[key]
2876 const result = index.parserValue(inputValue, s.options)
2877 return effectIsExit(result)
2878 ? finishIndex(s, key, k2, inputValue, result)
2879 : Effect.flatMap(Effect.exit(result), (exit) => finishIndex(s, key, k2, inputValue, exit))
2880 }
2881 const parseStringIndex = (
2882 s: ObjectParserState,
2883 key: PropertyKey,
2884 index: Index
2885 ): Effect.Effect<void, SchemaIssue.Issue, any> => {
2886 const inputValue = s.input[key]
2887 const result = index.parserValue(inputValue, s.options)
2888 return effectIsExit(result)
2889 ? finishIndex(s, key, key, inputValue, result)
2890 : Effect.flatMap(Effect.exit(result), (exit) => finishIndex(s, key, key, inputValue, exit))
2891 }
2892 const parseIndexes = indexCount
2893 ? iterateConcurrent<ObjectParserState, readonly [key: PropertyKey, index: Index]>()({
2894 onItem: (s, [key, index]) =>
2895 index.is.parameter === string ? parseStringIndex(s, key, index) : parseIndex(s, key, index),
2896 step: (_s, _item, exit) => exit._tag === "Failure" ? exit : undefined
2897 })
2898 : undefined
2899 return Effect.fnUntracedEager(function*(input, options) {
2900 if (input === InternalParser.missing) {
2901 return InternalParser.missing
2902 }
2903
2904 // If the input is not a record, return early with an error
2905 if (!(typeof input === "object" && input !== null && !Array.isArray(input))) {
2906 return yield* Effect.fail(new SchemaIssue.InvalidType(ast, input, options))
2907 }
2908 compileMembers()
2909
2910 const record = input as Record<PropertyKey, unknown>
2911 const out: Record<PropertyKey, unknown> = {}
2912 const state = {
2913 ast,
2914 input: record,
2915 out,
2916 issues: undefined as Arr.NonEmptyArray<SchemaIssue.Issue> | undefined,
2917 options
2918 }
2919 const errorsAllOption = options.errors === "all"
2920 const onExcessPropertyError = options.onExcessProperty === "error"
2921 const concurrency = options.concurrency === undefined ? 1 : resolveConcurrency(options.concurrency)
2922
2923 // ---------------------------------------------
2924 // handle excess properties
2925 // ---------------------------------------------
2926 const indexKeys = indexCount && onExcessPropertyError
2927 ? ast.indexSignatures.map((index) => getIndexSignatureKeys(record, index.parameter, options))
2928 : undefined
2929 if (onExcessPropertyError) {
2930 const coveredKeys = indexKeys ? new Set(expectedKeys) : expectedKeys
2931 if (indexKeys) {
2932 for (const keys of indexKeys) {
2933 for (const key of keys) coveredKeys.add(key)
2934 }
2935 }
2936 // Only enumerable own properties can be excess. Declared fields are
2937 // parsed separately, regardless of their enumerability.
2938 const inputKeys = Reflect.ownKeys(record)
2939 for (let i = 0; i < inputKeys.length; i++) {
2940 const key = inputKeys[i]
2941 if (!coveredKeys.has(key) && Object.prototype.propertyIsEnumerable.call(record, key)) {
2942 // key is unexpected
2943 const unexpected = new SchemaIssue.UnexpectedKey(ast, record[key], options)
2944 const issue = new SchemaIssue.Pointer([key], unexpected)
2945 if (errorsAllOption) {
2946 if (state.issues) {
2947 state.issues.push(issue)
2948 } else {
2949 state.issues = [issue]
2950 }
2951 continue
2952 } else {
2953 return yield* Effect.fail(
2954 new SchemaIssue.Composite(ast, [issue], input, options)
2955 )
2956 }
2957 }
2958 }
2959 }
2960
2961 // ---------------------------------------------
2962 // handle property signatures
2963 // ---------------------------------------------
2964 if (hasProperties) {
2965 const eff = concurrency === 1
2966 ? parseProperties(state, properties!)
2967 : parsePropertiesConcurrent(state, properties!, { concurrency })
2968 if (eff) yield* eff
2969 }
2970
2971 // ---------------------------------------------
2972 // handle index signatures
2973 // ---------------------------------------------
2974 if (indexCount && concurrency === 1) {
2975 for (let i = 0; i < indexCount; i++) {
2976 const index = indexes![i]
2977 const parse = index.is.parameter === string ? parseStringIndex : parseIndex
2978 const keys = indexKeys?.[i] ?? (index.is.parameter === string
2979 ? Object.keys(record)
2980 : getIndexSignatureKeys(record, index.is.parameter, options))
2981 for (let j = 0; j < keys.length; j++) {
2982 const eff = parse(state, keys[j], index)
2983 if (!effectIsExit(eff)) yield* eff
2984 else if (eff._tag === "Failure") return yield* eff as Exit.Exit<never, SchemaIssue.Issue>
2985 }
2986 }
2987 } else if (parseIndexes) {
2988 const keyPairs = Arr.empty<readonly [PropertyKey, Index]>()
2989 for (let i = 0; i < indexCount; i++) {
2990 const index = indexes![i]
2991 const keys = indexKeys?.[i] ?? (index.is.parameter === string
2992 ? Object.keys(record)
2993 : getIndexSignatureKeys(record, index.is.parameter, options))
2994 for (let j = 0; j < keys.length; j++) {
2995 keyPairs.push([keys[j], index])
2996 }
2997 }
2998 const eff = parseIndexes(state, keyPairs, { concurrency })
2999 if (eff) yield* eff
3000 }
3001
3002 if (state.issues) {
3003 return yield* Effect.fail(
3004 new SchemaIssue.Composite(ast, state.issues, input, options)
3005 )
3006 }
3007 return out
3008 })
3009 }
3010
3011 if (indexCount) return makeFallback()
3012
3013 let fallback: SchemaParser.Parser | undefined
3014
3015 // Resumes at the property whose parser suspended, without replaying the
3016 // properties already parsed.
3017 const resume = (
3018 state: ObjectParserState,
3019 index: number,
3020 pending: Effect.Effect<unknown, SchemaIssue.Issue, any>
3021 ): Effect.Effect<unknown, SchemaIssue.Issue, any> => {
3022 const property = properties![index]
3023 return Effect.flatMap(Effect.exit(pending), (exit) => {
3024 const terminal = stepProperty(state, property, exit)
3025 if (terminal) return terminal
3026 const done = () => InternalParser.succeed(state.out)
3027 const eff = parseProperties(state, properties!.slice(index + 1))
3028 return eff ? Effect.flatMapEager(eff, done) : done()
3029 })
3030 }
3031
3032 // Fast path: a struct without index signatures, under the default parse
3033 // options, needs none of the generator the fallback runs per value.
3034 return (input, options) => {
3035 if (input === InternalParser.missing) return InternalParser.missingExit
3036 if (
3037 options.errors === "all" ||
3038 options.onExcessProperty !== undefined ||
3039 (options.concurrency !== undefined && resolveConcurrency(options.concurrency) !== 1)
3040 ) {
3041 return (fallback ??= makeFallback())(input, options)
3042 }
3043 if (!(typeof input === "object" && input !== null && !Array.isArray(input))) {
3044 return Effect.fail(new SchemaIssue.InvalidType(ast, input, options))
3045 }
3046 const props = compileMembers()
3047 const record = input as Record<PropertyKey, unknown>
3048 const out: Record<PropertyKey, unknown> = {}
3049 const state: ObjectParserState = { ast, input: record, out, issues: undefined, options }
3050 try {
3051 for (let index = 0; index < props.length; index++) {
3052 const property = props[index]
3053 const name = property.name
3054 const hasKey = hasPropertySignature(record, name)
3055 const value = hasKey ? record[name] : InternalParser.missing
3056 const exit = property.parser(value, options)
3057 if (!effectIsExit(exit)) {
3058 return resume(state, index, exit)
3059 }
3060 if (exit === InternalParser.sameExit) {
3061 if (hasKey) InternalRecord.assignProperty(out, name, value)
3062 continue
3063 }
3064 const terminal = stepProperty(state, property, exit)
3065 if (terminal) return terminal
3066 }
3067 } catch (error) {
3068 return Effect.die(error)
3069 }
3070 return InternalParser.succeed(out)
3071 }
3072 }
3073 private _rebuild(
3074 recur: (ast: AST) => AST,
3075 recurParameter: (ast: AST) => AST,
3076 checks: Checks | undefined,
3077 encodingChecks: Checks | undefined
3078 ): Objects {
3079 const props = mapOrSame(this.propertySignatures, (ps) => {
3080 const t = recur(ps.type)
3081 return t === ps.type ? ps : new PropertySignature(ps.name, t)
3082 })
3083
3084 const indexes = mapOrSame(this.indexSignatures, (is) => {
3085 const p = recurParameter(is.parameter)
3086 const t = recur(is.type)
3087 return p === is.parameter && t === is.type
3088 ? is
3089 : new IndexSignature(p, t)
3090 })
3091
3092 return props === this.propertySignatures && indexes === this.indexSignatures && checks === this.checks &&
3093 encodingChecks === this.encodingChecks
3094 ? this
3095 : new Objects(
3096 props,
3097 indexes,
3098 this.annotations,
3099 checks,
3100 undefined,
3101 this.context,
3102 encodingChecks
3103 )
3104 }
3105 /** @internal */
3106 flip(recur: (ast: AST) => AST): AST {
3107 return this._rebuild(recur, recur, this.encodingChecks, this.checks)
3108 }
3109 /** @internal */
3110 recur(recur: (ast: AST) => AST, recurParameter: (ast: AST) => AST = recur): AST {
3111 return this._rebuild(recur, recurParameter, this.checks, this.encodingChecks)
3112 }
3113 /** @internal */
3114 getExpected(): string {
3115 if (this.propertySignatures.length === 0 && this.indexSignatures.length === 0) return "object | array"
3116 return "object"
3117 }
3118}
3119
3120type ObjectParserState = {
3121 readonly ast: Objects
3122 readonly input: Record<PropertyKey, unknown>
3123 readonly options: ParseOptions
3124 readonly out: Record<PropertyKey, unknown>
3125 issues: Arr.NonEmptyArray<SchemaIssue.Issue> | undefined
3126}
3127
3128type ParsedProperty = {
3129 readonly parser: SchemaParser.Parser
3130 readonly name: PropertyKey
3131 readonly type: AST
3132}
3133
3134/** @internal */
3135export function stepProperty(
3136 s: ObjectParserState,
3137 p: ParsedProperty,
3138 exit: Exit.Exit<unknown, SchemaIssue.Issue>
3139): Exit.Exit<void, SchemaIssue.Issue> | void {
3140 if (exit._tag === "Failure") {
3141 return wrapPropertyKeyIssue(s, s.ast, p.name, exit)
3142 }
3143 if (exit === InternalParser.sameExit) return
3144 const value = (exit as InternalParser.Success<unknown, SchemaIssue.Issue>)[InternalParser.args]
3145 if (value !== InternalParser.missing) {
3146 InternalRecord.assignProperty(s.out, p.name, value)
3147 return
3148 }
3149 delete s.out[p.name]
3150 if (!isOptional(p.type)) {
3151 const issue = new SchemaIssue.Pointer([p.name], new SchemaIssue.MissingKey(p.type.context?.annotations))
3152 if (s.options.errors === "all") {
3153 if (s.issues) s.issues.push(issue)
3154 else s.issues = [issue]
3155 return
3156 } else {
3157 return Exit.fail(
3158 new SchemaIssue.Composite(s.ast, [issue], s.input, s.options)
3159 )
3160 }
3161 }
3162}
3163
3164const parsePropertiesOptions = {
3165 onItem(s: ObjectParserState, p: ParsedProperty) {
3166 if (!hasPropertySignature(s.input, p.name)) {
3167 return p.parser(InternalParser.missing, s.options)
3168 }
3169 const value = s.input[p.name]
3170 InternalRecord.assignProperty(s.out, p.name, value)
3171 return p.parser(value, s.options)
3172 },
3173 step: stepProperty
3174}
3175
3176/** @internal */
3177export const parseProperties = iterateEager<ObjectParserState, ParsedProperty>()(parsePropertiesOptions)
3178const parsePropertiesConcurrent = iterateConcurrent<ObjectParserState, ParsedProperty>()(parsePropertiesOptions)
3179
3180function combineChecks(a: Checks | undefined, b: Checks | undefined): Checks | undefined {
3181 if (!a) return b
3182 if (!b) return a
3183 return [...a, ...b]
3184}
3185
3186/** @internal */
3187export function struct<Fields extends Schema.Struct.Fields>(
3188 fields: Fields,
3189 checks: Checks | undefined,
3190 annotations?: Schema.Annotations.Annotations
3191): Objects {
3192 return new Objects(
3193 Reflect.ownKeys(fields).map((key) => {
3194 return new PropertySignature(key, fields[key].ast)
3195 }),
3196 [],
3197 annotations,
3198 checks
3199 )
3200}
3201
3202/** @internal */
3203export function getAST<S extends { readonly ast: AST }>(self: S): S["ast"] {
3204 return self.ast
3205}
3206
3207/** @internal */
3208export function tuple<Elements extends Schema.Tuple.Elements>(
3209 elements: Elements,
3210 checks: Checks | undefined = undefined
3211): Arrays {
3212 return new Arrays(false, elements.map((e) => e.ast), [], undefined, checks)
3213}
3214
3215/** @internal */
3216export function union<Members extends ReadonlyArray<{ readonly ast: AST }>>(
3217 members: Members,
3218 options: UnionOptions | undefined,
3219 checks: Checks | undefined
3220): Union<Members[number]["ast"]> {
3221 return new Union(members.map(getAST), options, undefined, checks)
3222}
3223
3224/** @internal */
3225export function structWithRest(ast: Objects, records: ReadonlyArray<Objects>): Objects {
3226 if (ast.encoding || records.some((r) => r.encoding)) {
3227 throw new Error("StructWithRest does not support encodings")
3228 }
3229 let propertySignatures = ast.propertySignatures
3230 let indexSignatures = ast.indexSignatures
3231 let checks = ast.checks
3232 for (const record of records) {
3233 for (const propertySignature of record.propertySignatures) {
3234 if (propertySignatures.some((ps) => ps.name === propertySignature.name)) {
3235 throw new Error(`Duplicate identifier: ${JSON.stringify(propertySignature.name)}. ts(2300)`)
3236 }
3237 }
3238 propertySignatures = propertySignatures.concat(record.propertySignatures)
3239 indexSignatures = indexSignatures.concat(record.indexSignatures)
3240 checks = combineChecks(checks, record.checks)
3241 }
3242 return new Objects(propertySignatures, indexSignatures, undefined, checks)
3243}
3244
3245/** @internal */
3246export function tupleWithRest(ast: Arrays, rest: ReadonlyArray<AST>): Arrays {
3247 if (ast.encoding) {
3248 throw new Error("TupleWithRest does not support encodings")
3249 }
3250 return new Arrays(ast.isMutable, ast.elements, rest, undefined, ast.checks)
3251}
3252
3253/** @internal */
3254export function mutable(ast: Arrays): Arrays {
3255 if (ast.encoding) {
3256 throw new Error("mutable does not support encodings")
3257 }
3258 return new Arrays(
3259 true,
3260 ast.elements,
3261 ast.rest,
3262 ast.annotations,
3263 ast.checks,
3264 undefined,
3265 ast.context,
3266 ast.encodingChecks
3267 )
3268}
3269
3270type Type =
3271 | "null"
3272 | "array"
3273 | "object"
3274 | "string"
3275 | "number"
3276 | "boolean"
3277 | "symbol"
3278 | "undefined"
3279 | "bigint"
3280 | "function"
3281
3282/** @internal */
3283export type Sentinel = {
3284 readonly key: PropertyKey
3285 readonly literal: LiteralValue | symbol
3286}
3287
3288const toCandidate = memoizeIdempotent((ast: AST): AST => {
3289 while (true) {
3290 if (isSuspend(ast)) return unknown
3291 const encoding = ast.encoding
3292 if (!encoding) {
3293 // Index signature parameters do not participate in union selection.
3294 return (ast as any).recur?.(toCandidate, identity) ?? ast
3295 }
3296 if (
3297 encoding.some((link) => link.transformation._tag === "Middleware" && link.transformation.decode !== identity)
3298 ) return unknown
3299 ast = encoding[encoding.length - 1].to
3300 }
3301})
3302
3303function getCandidateTypes(ast: AST): ReadonlyArray<Type> {
3304 switch (ast._tag) {
3305 case "Null":
3306 return ["null"]
3307 case "Undefined":
3308 return ["undefined"]
3309 case "String":
3310 case "TemplateLiteral":
3311 return ["string"]
3312 case "Number":
3313 return ["number"]
3314 case "Boolean":
3315 return ["boolean"]
3316 case "Symbol":
3317 case "UniqueSymbol":
3318 return ["symbol"]
3319 case "BigInt":
3320 return ["bigint"]
3321 case "Arrays":
3322 return ["array"]
3323 case "ObjectKeyword":
3324 return ["object", "array", "function"]
3325 case "Objects":
3326 return ast.propertySignatures.length || ast.indexSignatures.length
3327 ? ["object"]
3328 : ["string", "number", "boolean", "symbol", "bigint", "object", "array", "function"]
3329 case "Enum":
3330 return Array.from(new Set(ast.enums.map(([, v]) => typeof v)))
3331 case "Literal":
3332 return [typeof ast.literal]
3333 case "Union":
3334 return Array.from(new Set(ast.types.flatMap(getCandidateTypes)))
3335 default:
3336 return [
3337 "null",
3338 "undefined",
3339 "string",
3340 "number",
3341 "boolean",
3342 "symbol",
3343 "bigint",
3344 "object",
3345 "array",
3346 "function"
3347 ]
3348 }
3349}
3350
3351/** @internal */
3352export function collectSentinels(ast: AST): ReadonlyArray<Sentinel> {
3353 switch (ast._tag) {
3354 default:
3355 return []
3356 case "Declaration": {
3357 const s = ast.annotations?.[InternalAnnotations.SENTINELS_ANNOTATION_KEY]
3358 return Array.isArray(s) ? s : []
3359 }
3360 case "Objects":
3361 return ast.propertySignatures.flatMap((ps): Array<Sentinel> => {
3362 const type = ps.type
3363 if (!isOptional(type)) {
3364 if (isLiteral(type)) {
3365 return [{ key: ps.name, literal: type.literal }]
3366 }
3367 if (isUniqueSymbol(type)) {
3368 return [{ key: ps.name, literal: type.symbol }]
3369 }
3370 }
3371 return []
3372 })
3373 case "Arrays":
3374 return ast.elements.flatMap((e, i): Array<Sentinel> => {
3375 if (!isOptional(e)) {
3376 if (isLiteral(e)) {
3377 return [{ key: i, literal: e.literal }]
3378 }
3379 if (isUniqueSymbol(e)) {
3380 return [{ key: i, literal: e.symbol }]
3381 }
3382 }
3383 return []
3384 })
3385 case "Union": {
3386 if (ast.types.length === 0) return []
3387 const members = ast.types.map((type) => collectSentinels(toCandidate(type)))
3388 return members[0].filter((s) =>
3389 members.every((sentinels) => sentinels.some((o) => o.key === s.key && o.literal === s.literal))
3390 )
3391 }
3392 case "Suspend":
3393 return collectSentinels(ast.thunk())
3394 }
3395}
3396
3397/** @internal */
3398export type CandidateIndex = (input: any, isConstructor: boolean) => ReadonlyArray<number>
3399type SentinelEntry = readonly [
3400 byValue: Map<LiteralValue | symbol, Set<number>>,
3401 all: Set<number>
3402]
3403type SentinelIndex = Map<PropertyKey, SentinelEntry>
3404
3405const candidateIndexCache = new WeakMap<ReadonlyArray<AST>, CandidateIndex>()
3406const emptyCandidates: ReadonlyArray<never> = Object.freeze([])
3407
3408const getRuntimeType = (input: unknown): Type => input === null ? "null" : Array.isArray(input) ? "array" : typeof input
3409
3410const hasPropertySignature = (input: object, key: PropertyKey): boolean =>
3411 key === "__proto__" ? Object.hasOwn(input, key) : key in input
3412
3413/** @internal */
3414export function getCandidateIndex(types: ReadonlyArray<AST>): CandidateIndex {
3415 let index = candidateIndexCache.get(types)
3416 if (index) return index
3417
3418 let bySentinel: SentinelIndex | undefined
3419 let sentinelCandidateCount = 0
3420 let otherwise: { [K in Type]?: Array<number> } | undefined
3421 let literalCandidates: Map<LiteralValue | symbol, Array<number>> | undefined
3422 let onlyLiterals = true
3423 const literalOf: Array<LiteralValue | symbol | undefined> = []
3424 for (let i = 0; i < types.length; i++) {
3425 const a = types[i]
3426 const encoded = toCandidate(a)
3427 if (isNever(encoded)) continue
3428
3429 if (isLiteral(encoded) || isUniqueSymbol(encoded)) {
3430 literalCandidates ??= new Map()
3431 const literal = isLiteral(encoded) ? encoded.literal : encoded.symbol
3432 literalOf[i] = literal
3433 let arr = literalCandidates.get(literal)
3434 if (!arr) literalCandidates.set(literal, arr = [])
3435 arr.push(i)
3436 } else {
3437 onlyLiterals = false
3438 }
3439
3440 const sentinels = collectSentinels(encoded)
3441
3442 if (sentinels.length) { // discriminated variants
3443 bySentinel ??= new Map()
3444 sentinelCandidateCount++
3445 for (const { key, literal } of sentinels) {
3446 let entry = bySentinel.get(key)
3447 if (!entry) bySentinel.set(key, entry = [new Map(), new Set()])
3448 entry[1].add(i)
3449 let indexes = entry[0].get(literal)
3450 if (!indexes) entry[0].set(literal, indexes = new Set())
3451 indexes.add(i)
3452 }
3453 } else { // non-discriminated
3454 otherwise ??= {}
3455 const candidateTypes = getCandidateTypes(encoded)
3456 for (const t of candidateTypes) (otherwise[t] ??= []).push(i)
3457 }
3458 }
3459
3460 // Non-discriminated members are grouped by runtime type once and reused by every decode.
3461 const fallbacks: { [K in Type]?: ReadonlyArray<number> } = {}
3462 const getFallback = (type: Type): ReadonlyArray<number> =>
3463 fallbacks[type] ??= Object.freeze(otherwise?.[type] ?? emptyCandidates)
3464
3465 if (onlyLiterals && literalCandidates) {
3466 literalCandidates.forEach(Object.freeze)
3467 index = (input) => literalCandidates.get(input) ?? emptyCandidates
3468 } else if (bySentinel?.size === 1 && !otherwise) {
3469 const [key, [byValue]] = bySentinel.entries().next().value!
3470 const candidates = new Map<LiteralValue | symbol, ReadonlyArray<number>>()
3471 for (const [literal, indexes] of byValue) {
3472 candidates.set(literal, Object.freeze(Array.from(indexes)))
3473 }
3474 const all = Object.freeze(types.map((_, i) => i))
3475 index = (input, isConstructor) => {
3476 if (Predicate.isObjectKeyword(input)) {
3477 const value = hasPropertySignature(input, key) ? (input as any)[key] : undefined
3478 if (value !== undefined) return candidates.get(value) ?? emptyCandidates
3479 if (isConstructor) return all
3480 }
3481 return emptyCandidates
3482 }
3483 } else if (bySentinel) {
3484 // A key owned by every discriminated candidate is safe to use as the initial selector: no candidate can
3485 // be excluded merely because it uses a different sentinel key. Prefer the key with the most distinct values
3486 // to minimize the matching bucket.
3487 let commonSentinel: [PropertyKey, SentinelEntry] | undefined
3488 for (const entry of bySentinel) {
3489 if (
3490 (!commonSentinel || entry[1][0].size > commonSentinel[1][0].size) &&
3491 entry[1][1].size === sentinelCandidateCount
3492 ) {
3493 commonSentinel = entry
3494 }
3495 }
3496
3497 index = (input, isConstructor) => {
3498 const runtimeType = getRuntimeType(input)
3499 if (!Predicate.isObjectKeyword(input)) return getFallback(runtimeType)
3500
3501 // Non-discriminated candidates are runtime-type fallbacks and are never removed by sentinel checks.
3502 const selected = new Set(otherwise?.[runtimeType])
3503 let directKey: PropertyKey | undefined
3504 // An observed common key can seed the selection directly; an unknown value rules out every
3505 // discriminated candidate.
3506 if (commonSentinel) {
3507 const [key, [byValue]] = commonSentinel
3508 const hasKey = hasPropertySignature(input, key)
3509 const value = hasKey ? (input as any)[key] : undefined
3510 if (hasKey && (!isConstructor || value !== undefined)) {
3511 const match = byValue.get(value)
3512 if (!match) return getFallback(runtimeType)
3513 for (const i of match) selected.add(i)
3514 directKey = key
3515 }
3516 }
3517
3518 // Without an observed common key, collect positive matches from every sentinel. Constructor mode treats
3519 // absent and undefined keys as unconstrained and therefore selects every candidate that owns the key.
3520 if (directKey === undefined) {
3521 for (const [key, [byValue, all]] of bySentinel) {
3522 const hasKey = hasPropertySignature(input, key)
3523 const value = hasKey ? (input as any)[key] : undefined
3524 if (hasKey && (!isConstructor || value !== undefined)) {
3525 const match = byValue.get(value)
3526 if (match) {
3527 for (const i of match) selected.add(i)
3528 }
3529 } else if (isConstructor) {
3530 for (const i of all) selected.add(i)
3531 }
3532 }
3533 }
3534 // Missing keys are neutral. An observed key rejects only selected candidates that own it and do not match.
3535 for (const [key, [byValue, all]] of bySentinel) {
3536 if (key === directKey) continue
3537 const hasKey = hasPropertySignature(input, key)
3538 const value = hasKey ? (input as any)[key] : undefined
3539 if (hasKey && (!isConstructor || value !== undefined)) {
3540 const match = byValue.get(value)
3541 for (const i of selected) {
3542 if (all.has(i) && !match?.has(i)) selected.delete(i)
3543 }
3544 }
3545 }
3546 return Array.from(selected).sort((a, b) => a - b)
3547 }
3548 } else {
3549 index = (input) => {
3550 const fallback = getFallback(getRuntimeType(input))
3551 return literalCandidates ? fallback.filter((i) => literalOf[i] === undefined || literalOf[i] === input) : fallback
3552 }
3553 }
3554
3555 candidateIndexCache.set(types, index)
3556 return index
3557}
3558
3559/**
3560 * AST node representing a union of schemas.
3561 *
3562 * **Details**
3563 *
3564 * - `types` — the member AST nodes.
3565 * - `options.mode` selects matching behavior. `"anyOf"` succeeds on the first match;
3566 * `"oneOf"` requires exactly one member to match (fails if multiple do).
3567 *
3568 * During parsing, members are tried in order. An internal candidate index
3569 * narrows which members to try based on the runtime type of the input and
3570 * discriminant ("sentinel") fields, making large unions efficient.
3571 *
3572 * **Example** (Inspecting a union AST)
3573 *
3574 * ```ts import.meta.vitest
3575 * import { Schema, SchemaAST } from "effect"
3576 *
3577 * const schema = Schema.Union([Schema.String, Schema.Number])
3578 * const ast = schema.ast
3579 *
3580 * if (SchemaAST.isUnion(ast)) {
3581 * [ast.types.length, ast.options?.mode ?? "anyOf"] // => [2, "anyOf"]
3582 * }
3583 * ```
3584 *
3585 * @see {@link isUnion}
3586 * @category models
3587 * @since 4.0.0
3588 */
3589export interface Union<A extends AST = AST> extends ASTNode {
3590 readonly _tag: "Union"
3591 readonly types: ReadonlyArray<A>
3592 readonly options: UnionOptions | undefined
3593 readonly encodingChecks: Checks | undefined
3594 /** @internal */
3595
3596 getParser(compile: SchemaParser.Compiler, compileField?: SchemaParser.Compiler): SchemaParser.Parser
3597 /** @internal */
3598
3599 recur(recur: (ast: AST) => AST): Union<AST>
3600 /** @internal */
3601
3602 flip(recur: (ast: AST) => AST): Union<AST>
3603 /** @internal */
3604
3605 matchPart(s: string, options: ParseOptions): LiteralValue | undefined
3606 /** @internal */
3607
3608 getExpected(getExpected: (ast: AST) => string): string
3609}
3610
3611/**
3612 * Local matching options stored on {@link Union} nodes.
3613 *
3614 * @category options
3615 * @since 4.0.0
3616 */
3617export interface UnionOptions {
3618 /** Defaults to `"anyOf"`; `"oneOf"` requires exactly one matching member. */
3619 readonly mode?: "anyOf" | "oneOf" | undefined
3620}
3621
3622/**
3623 * Constructs a {@link Union}.
3624 *
3625 * @category constructors
3626 * @since 4.0.0
3627 */
3628export const Union: new<A extends AST = AST>(
3629 types: ReadonlyArray<A>,
3630 options?: UnionOptions,
3631 annotations?: Schema.Annotations.Annotations,
3632 checks?: Checks,
3633 encoding?: Encoding,
3634 context?: Context,
3635 encodingChecks?: Checks
3636) => Union<A> = class<A extends AST = AST> extends ASTNodeImpl {
3637 readonly _tag = "Union"
3638 readonly types: ReadonlyArray<A>
3639 readonly options: UnionOptions | undefined
3640 readonly encodingChecks: Checks | undefined
3641
3642 constructor(
3643 types: ReadonlyArray<A>,
3644 options?: UnionOptions,
3645 annotations?: Schema.Annotations.Annotations,
3646 checks?: Checks,
3647 encoding?: Encoding,
3648 context?: Context,
3649 encodingChecks?: Checks
3650 ) {
3651 super(annotations, checks, encoding, context)
3652 this.types = types
3653 this.options = options
3654 this.encodingChecks = encodingChecks
3655 }
3656 /** @internal */
3657 getParser(
3658 compile: SchemaParser.Compiler,
3659 compileField?: SchemaParser.Compiler
3660 ): SchemaParser.Parser {
3661 // oxlint-disable-next-line @typescript-eslint/no-this-alias
3662 const ast = this
3663 const isConstructor = compileField !== undefined
3664 const parsers: Array<SchemaParser.Parser> = []
3665 const parser = (i: number): SchemaParser.Parser => parsers[i] ??= compile(ast.types[i])
3666 let index: CandidateIndex | undefined
3667
3668 return (input, options) => {
3669 if (input === InternalParser.missing) {
3670 return InternalParser.missingExit
3671 }
3672 const candidates = (index ??= getCandidateIndex(ast.types))(input, isConstructor)
3673
3674 if (candidates.length === 0) {
3675 return Effect.fail(new SchemaIssue.AnyOf(ast, [], input, options))
3676 }
3677 if (candidates.length === 1) {
3678 const result = parser(candidates[0])(input, options)
3679 if ((result as Exit.Exit<unknown, SchemaIssue.Issue>)._tag === "Success") return result
3680 return effectIsExit(result)
3681 ? failSingleUnionCandidate(ast, (result as Exit.Failure<unknown, SchemaIssue.Issue>).cause, input, options)
3682 : catchSingleUnionCandidate(ast, result, input, options)
3683 }
3684 return parseUnionCandidates(ast, parser, candidates, input, options)
3685 }
3686 }
3687 private _rebuild(
3688 recur: (ast: AST) => AST,
3689 checks: Checks | undefined,
3690 encodingChecks: Checks | undefined
3691 ): Union<AST> {
3692 const types = mapOrSame(this.types, recur)
3693 return types === this.types && checks === this.checks && encodingChecks === this.encodingChecks ?
3694 this :
3695 new Union(types, this.options, this.annotations, checks, undefined, this.context, encodingChecks)
3696 }
3697 /** @internal */
3698 recur(recur: (ast: AST) => AST): Union<AST> {
3699 return this._rebuild(recur, this.checks, this.encodingChecks)
3700 }
3701 /** @internal */
3702 flip(recur: (ast: AST) => AST): Union<AST> {
3703 return this._rebuild(recur, this.encodingChecks, this.checks)
3704 }
3705 /** @internal */
3706 matchPart(s: string, options: ParseOptions): LiteralValue | undefined {
3707 for (const type of this.types) {
3708 const out = (type as TemplateLiteralPart).matchPart(s, options)
3709 if (out !== undefined) return out
3710 }
3711 return undefined
3712 }
3713 /** @internal */
3714 getExpected(getExpected: (ast: AST) => string): string {
3715 const expected = this.annotations?.expected
3716 if (typeof expected === "string") return expected
3717
3718 if (this.types.length === 0) return "never"
3719
3720 const types = this.types.map((type) => {
3721 const encoded = toEncoded(type)
3722 switch (encoded._tag) {
3723 case "Arrays": {
3724 const literals = encoded.elements.filter(isLiteral)
3725 if (literals.length > 0) {
3726 return `${formatIsMutable(encoded.isMutable)}[ ${
3727 literals.map((e) => getExpected(e) + formatIsOptional(e.context?.isOptional)).join(", ")
3728 }, ... ]`
3729 }
3730 break
3731 }
3732 case "Objects": {
3733 const literals = encoded.propertySignatures.filter((ps) => isLiteral(ps.type))
3734 if (literals.length > 0) {
3735 return `{ ${
3736 literals.map((ps) =>
3737 `${formatIsMutable(ps.type.context?.isMutable)}${formatPropertyKey(ps.name)}${
3738 formatIsOptional(ps.type.context?.isOptional)
3739 }: ${getExpected(ps.type)}`
3740 ).join(", ")
3741 }, ... }`
3742 }
3743 break
3744 }
3745 }
3746 return getExpected(encoded)
3747 })
3748 return Array.from(new Set(types)).join(" | ")
3749 }
3750}
3751
3752function failSingleUnionCandidate(
3753 ast: Union,
3754 cause: Cause.Cause<SchemaIssue.Issue>,
3755 input: unknown,
3756 options: ParseOptions
3757) {
3758 const issue = InternalSchemaCause.getSchemaIssue(cause)
3759 if (!issue) return Exit.failCause(cause)
3760 return Exit.fail(new SchemaIssue.AnyOf(ast, [issue], input, options))
3761}
3762
3763function catchSingleUnionCandidate(
3764 ast: Union,
3765 result: Effect.Effect<unknown, SchemaIssue.Issue, unknown>,
3766 input: unknown,
3767 options: ParseOptions
3768) {
3769 return Effect.catchCause(result, (cause) => failSingleUnionCandidate(ast, cause, input, options))
3770}
3771
3772type UnionParserState = {
3773 readonly parser: (i: number) => SchemaParser.Parser
3774 readonly ast: Union
3775 readonly input: unknown
3776 readonly options: ParseOptions
3777 out: Exit.Success<unknown, SchemaIssue.Issue> | undefined
3778 readonly successes: Array<AST> | undefined
3779 issues: Array<SchemaIssue.Issue> | undefined
3780}
3781
3782function parseUnionCandidates(
3783 ast: Union,
3784 parser: (i: number) => SchemaParser.Parser,
3785 candidates: ReadonlyArray<number>,
3786 input: unknown,
3787 options: ParseOptions
3788): Effect.Effect<unknown, SchemaIssue.Issue, any> {
3789 const state: UnionParserState = {
3790 ast,
3791 parser,
3792 input,
3793 out: undefined,
3794 successes: ast.options?.mode === "oneOf" ? [] : undefined,
3795 issues: undefined,
3796 options
3797 }
3798 const eff = parseUnion(state, candidates)
3799 if (!eff) {
3800 if (state.out) return state.out
3801 return Effect.fail(new SchemaIssue.AnyOf(ast, state.issues ?? [], input, options))
3802 }
3803 return resumeUnion(eff, state)
3804}
3805
3806function resumeUnion(
3807 eff: Effect.Effect<void, SchemaIssue.Issue, any>,
3808 state: UnionParserState
3809): Effect.Effect<unknown, SchemaIssue.Issue, any> {
3810 return Effect.flatMapEager(eff, (_) => {
3811 if (state.out === InternalParser.sameExit) return Effect.succeed(state.input)
3812 if (state.out) return state.out
3813 return Effect.fail(new SchemaIssue.AnyOf(state.ast, state.issues ?? [], state.input, state.options))
3814 })
3815}
3816
3817const parseUnion = iterateEager<UnionParserState, number>()({
3818 onItem(s, i) {
3819 return s.parser(i)(s.input, s.options)
3820 },
3821 step(s, i, exit) {
3822 if (exit._tag === "Failure") {
3823 const issue = InternalSchemaCause.getSchemaIssue(exit.cause)
3824 if (issue === undefined) {
3825 return exit
3826 }
3827 if (s.issues) s.issues.push(issue)
3828 else s.issues = [issue]
3829 } else {
3830 if (s.out && s.successes) {
3831 s.successes.push(s.ast.types[i])
3832 return Exit.fail(new SchemaIssue.OneOf(s.ast, s.successes, s.input, s.options))
3833 }
3834 s.out = exit
3835 if (s.successes) {
3836 s.successes.push(s.ast.types[i])
3837 } else {
3838 return Exit.void
3839 }
3840 }
3841 }
3842})
3843
3844const nonFiniteLiterals = new Union([
3845 new Literal("Infinity"),
3846 new Literal("-Infinity"),
3847 new Literal("NaN")
3848])
3849
3850function formatIsMutable(isMutable: boolean | undefined): string {
3851 return isMutable ? "" : "readonly "
3852}
3853
3854function formatIsOptional(isOptional: boolean | undefined): string {
3855 return isOptional ? "?" : ""
3856}
3857
3858/**
3859 * AST node for lazy/recursive schemas.
3860 *
3861 * **Details**
3862 *
3863 * Wraps a thunk (`() => AST`) that is memoized on first call. Use this to
3864 * define recursive or mutually recursive schemas without infinite loops at
3865 * construction time.
3866 *
3867 * **Example** (Defining recursive schema ASTs)
3868 *
3869 * ```ts import.meta.vitest
3870 * import { Schema, SchemaAST } from "effect"
3871 *
3872 * interface Category {
3873 * readonly name: string
3874 * readonly children: ReadonlyArray<Category>
3875 * }
3876 *
3877 * const Category = Schema.Struct({
3878 * name: Schema.String,
3879 * children: Schema.Array(Schema.suspend((): Schema.Codec<Category> => Category))
3880 * })
3881 *
3882 * SchemaAST.isObjects(Category.ast) // => true
3883 * ```
3884 *
3885 * @see {@link isSuspend}
3886 * @category models
3887 * @since 4.0.0
3888 */
3889export interface Suspend extends ASTNode {
3890 readonly _tag: "Suspend"
3891 readonly thunk: () => AST
3892 /** @internal */
3893
3894 getParser(compile: SchemaParser.Compiler): SchemaParser.Parser
3895 /** @internal */
3896
3897 recur(recur: (ast: AST) => AST): Suspend
3898 /** @internal */
3899
3900 getExpected(getExpected: (ast: AST) => string): string
3901}
3902
3903/**
3904 * Constructs a {@link Suspend}.
3905 *
3906 * @category constructors
3907 * @since 4.0.0
3908 */
3909export const Suspend: new(
3910 thunk: () => AST,
3911 annotations?: Schema.Annotations.Annotations,
3912 checks?: Checks,
3913 encoding?: Encoding,
3914 context?: Context
3915) => Suspend = class extends ASTNodeImpl {
3916 readonly _tag = "Suspend"
3917 readonly thunk: () => AST
3918
3919 constructor(
3920 thunk: () => AST,
3921 annotations?: Schema.Annotations.Annotations,
3922 checks?: Checks,
3923 encoding?: Encoding,
3924 context?: Context
3925 ) {
3926 if (checks) {
3927 throw new Error("Cannot add checks to Suspend")
3928 }
3929 super(annotations, undefined, encoding, context)
3930 let ast: AST
3931 this.thunk = () => ast ??= thunk()
3932 }
3933 /** @internal */
3934 getParser(compile: SchemaParser.Compiler): SchemaParser.Parser {
3935 let parser: SchemaParser.Parser
3936 return (input, options) => (parser ??= compile(this.thunk()))(input, options)
3937 }
3938 /** @internal */
3939 recur(recur: (ast: AST) => AST) {
3940 return new Suspend(
3941 () => recur(this.thunk()),
3942 this.annotations,
3943 undefined,
3944 undefined,
3945 this.context
3946 )
3947 }
3948 /** @internal */
3949 getExpected(getExpected: (ast: AST) => string): string {
3950 return getExpected(this.thunk())
3951 }
3952}
3953
3954// -----------------------------------------------------------------------------
3955// Checks
3956// -----------------------------------------------------------------------------
3957
3958/**
3959 * Represents a single validation check attached to an AST node.
3960 *
3961 * **Details**
3962 *
3963 * - `run` — the validation function. Returns `undefined` on success, or an
3964 * `Issue` on failure.
3965 * - `annotations` — optional filter-level annotations (expected message,
3966 * representation, arbitrary constraint hints).
3967 * - `aborted` — when `true`, parsing stops immediately after this filter
3968 * fails (no further checks run).
3969 *
3970 * Use `.annotate()` to add metadata and `.abort()` to mark as aborting.
3971 * Combine with another check via `.and()` to form a {@link FilterGroup}.
3972 *
3973 * @see {@link FilterGroup}
3974 * @see {@link Check}
3975 * @see {@link isPattern}
3976 * @category models
3977 * @since 4.0.0
3978 */
3979export interface Filter<in E> extends Pipeable.Pipeable {
3980 readonly _tag: "Filter"
3981 readonly run: (input: E, self: AST, options: ParseOptions) => SchemaIssue.Issue | undefined
3982 readonly annotations: Schema.Annotations.Filter | undefined
3983 /**
3984 * Whether the parsing process should be aborted after this check has failed.
3985 */
3986 readonly aborted: boolean
3987 annotate(annotations: Schema.Annotations.Filter): Filter<E>
3988 abort(): Filter<E>
3989 and(other: Check<E>, annotations?: Schema.Annotations.Filter): FilterGroup<E>
3990}
3991
3992/**
3993 * Constructs a {@link Filter}.
3994 *
3995 * @category constructors
3996 * @since 4.0.0
3997 */
3998export const Filter: new<E>(
3999 run: (input: E, self: AST, options: ParseOptions) => SchemaIssue.Issue | undefined,
4000 annotations?: Schema.Annotations.Filter | undefined, /**
4001 * Whether the parsing process should be aborted after this check has failed.
4002 */
4003 aborted?: boolean
4004) => Filter<E> = class<in E> extends Pipeable.Class {
4005 readonly _tag = "Filter"
4006 readonly run: (input: E, self: AST, options: ParseOptions) => SchemaIssue.Issue | undefined
4007 readonly annotations: Schema.Annotations.Filter | undefined
4008 /**
4009 * Whether the parsing process should be aborted after this check has failed.
4010 */
4011 readonly aborted: boolean
4012
4013 constructor(
4014 run: (input: E, self: AST, options: ParseOptions) => SchemaIssue.Issue | undefined,
4015 annotations: Schema.Annotations.Filter | undefined = undefined,
4016 /**
4017 * Whether the parsing process should be aborted after this check has failed.
4018 */
4019 aborted: boolean = false
4020 ) {
4021 super()
4022 this.run = run
4023 this.annotations = annotations
4024 this.aborted = aborted
4025 }
4026 annotate(annotations: Schema.Annotations.Filter): Filter<E> {
4027 return new Filter(this.run, { ...this.annotations, ...annotations }, this.aborted)
4028 }
4029 abort(): Filter<E> {
4030 return new Filter(this.run, this.annotations, true)
4031 }
4032 and(other: Check<E>, annotations?: Schema.Annotations.Filter): FilterGroup<E>
4033 and(other: Check<E>, annotations?: Schema.Annotations.Filter): FilterGroup<E> {
4034 return new FilterGroup([this, other], annotations)
4035 }
4036}
4037
4038/**
4039 * Represents a composite validation check grouping multiple {@link Check} values.
4040 *
4041 * **Details**
4042 *
4043 * Created by calling `.and()` on a {@link Filter} or another `FilterGroup`.
4044 * All inner checks are run; failures from aborted filters still stop
4045 * evaluation.
4046 *
4047 * @see {@link Filter}
4048 * @see {@link Check}
4049 * @category models
4050 * @since 4.0.0
4051 */
4052export interface FilterGroup<in E> extends Pipeable.Pipeable {
4053 readonly _tag: "FilterGroup"
4054 readonly checks: readonly [
4055 Check<E>,
4056 ...Array<Check<E>>
4057 ]
4058 readonly annotations: Schema.Annotations.Filter | undefined
4059 annotate(annotations: Schema.Annotations.Filter): FilterGroup<E>
4060 and(other: Check<E>, annotations?: Schema.Annotations.Filter): FilterGroup<E>
4061}
4062
4063/**
4064 * Constructs a {@link FilterGroup}.
4065 *
4066 * @category constructors
4067 * @since 4.0.0
4068 */
4069export const FilterGroup: new<E>(
4070 checks: readonly [
4071 Check<E>,
4072 ...Array<Check<E>>
4073 ],
4074 annotations?: Schema.Annotations.Filter | undefined
4075) => FilterGroup<E> = class<in E> extends Pipeable.Class {
4076 readonly _tag = "FilterGroup"
4077 readonly checks: readonly [Check<E>, ...Array<Check<E>>]
4078 readonly annotations: Schema.Annotations.Filter | undefined
4079
4080 constructor(
4081 checks: readonly [Check<E>, ...Array<Check<E>>],
4082 annotations: Schema.Annotations.Filter | undefined = undefined
4083 ) {
4084 super()
4085 this.checks = checks
4086 this.annotations = annotations
4087 }
4088 annotate(annotations: Schema.Annotations.Filter): FilterGroup<E> {
4089 return new FilterGroup(this.checks, { ...this.annotations, ...annotations })
4090 }
4091 and(other: Check<E>, annotations?: Schema.Annotations.Filter): FilterGroup<E>
4092 and(other: Check<E>, annotations?: Schema.Annotations.Filter): FilterGroup<E> {
4093 return new FilterGroup([this, other], annotations)
4094 }
4095}
4096
4097/**
4098 * A validation check — either a single {@link Filter} or a composite
4099 * {@link FilterGroup}.
4100 *
4101 * **Details**
4102 *
4103 * Stored in an AST node's {@link Checks} array.
4104 *
4105 * @see {@link Filter}
4106 * @see {@link FilterGroup}
4107 * @category models
4108 * @since 4.0.0
4109 */
4110export type Check<T> = Filter<T> | FilterGroup<T>
4111
4112/** @internal */
4113export function makeFilter<T>(
4114 filter: (input: T, ast: AST, options: ParseOptions) => Schema.FilterOutput,
4115 annotations?: Schema.Annotations.Filter | undefined,
4116 aborted: boolean = false
4117): Filter<T> {
4118 return new Filter(
4119 (input, ast, options) => SchemaIssue.normalizeFilterOutput(ast, filter(input, ast, options), input, options),
4120 annotations,
4121 aborted
4122 )
4123}
4124
4125/** @internal */
4126export function makeFilterByGuard<T extends E, E>(
4127 is: (value: E) => value is T,
4128 annotations?: Schema.Annotations.Filter
4129): Filter<any> {
4130 return new Filter(
4131 (input: E, _ast, options) => is(input) ? undefined : new SchemaIssue.InvalidValue(undefined, input, options),
4132 annotations,
4133 true // after a guard, we always want to abort
4134 )
4135}
4136
4137/** @internal */
4138export function isFinite(annotations?: Schema.Annotations.Filter) {
4139 return makeFilter(
4140 (n: number) => globalThis.Number.isFinite(n),
4141 {
4142 expected: "a finite number",
4143 representation: {
4144 id: "effect/schema/isFinite",
4145 payload: null
4146 },
4147 toJsonSchema: () => ({ type: "number" }),
4148 toCode: () => ({ runtime: "Schema.isFinite()" }),
4149 arbitraryConstraint: {
4150 number: "finite"
4151 },
4152 ...annotations
4153 }
4154 )
4155}
4156
4157/** @internal */
4158export const finite = appendChecks(number, [isFinite()])
4159
4160const numberToJson = new Link(
4161 new Union([finite, nonFiniteLiterals]),
4162 new SchemaTransformation.Transformation(
4163 SchemaGetter.Number(),
4164 SchemaGetter.transform((n) => globalThis.Number.isFinite(n) ? n : globalThis.String(n))
4165 )
4166)
4167
4168/**
4169 * Creates a {@link Filter} that validates strings by running `RegExp.test`.
4170 *
4171 * **When to use**
4172 *
4173 * Use when string validation should be represented as a schema `Filter` backed
4174 * by a regular expression.
4175 *
4176 * **Details**
4177 *
4178 * The filter can be used with `Schema.filter` or attached directly to a
4179 * `String` AST node through checks. The regular expression is cloned and its
4180 * `lastIndex` is reset before each test, so global and sticky expressions are
4181 * deterministic and the provided regular expression is not mutated. The
4182 * regular expression source is stored in annotations for serialization and
4183 * arbitrary generation.
4184 *
4185 * **Gotchas**
4186 *
4187 * Arbitrary metadata preserves both `regExp.source` and `regExp.flags`.
4188 * Implementations that cannot consume all flags may still use the source as a
4189 * generation hint because the Schema filter validates every generated value.
4190 * JSON Schema has no way to carry JavaScript regular-expression flags. Unless
4191 * annotations provide `toJsonSchema`, the RegExp constraint is omitted from
4192 * JSON Schema export.
4193 *
4194 * **Example** (Validating an email pattern)
4195 *
4196 * ```ts import.meta.vitest
4197 * import { SchemaAST } from "effect"
4198 *
4199 * const emailFilter = SchemaAST.isPattern(/^[^@]+@[^@]+$/)
4200 * emailFilter.run("alice@example.com", SchemaAST.string, {}) // => undefined
4201 * emailFilter.run("invalid", SchemaAST.string, {})?._tag // => "InvalidValue"
4202 * ```
4203 *
4204 * @see {@link Filter}
4205 * @category constructors
4206 * @since 4.0.0
4207 */
4208export function isPattern(regExp: globalThis.RegExp, annotations?: Schema.Annotations.Filter) {
4209 const copy = new globalThis.RegExp(regExp)
4210 const payload = { source: copy.source, flags: copy.flags }
4211 return makeFilter(
4212 (s: string) => {
4213 copy.lastIndex = 0
4214 return copy.test(s)
4215 },
4216 {
4217 expected: `a string matching the RegExp ${payload.source}`,
4218 representation: {
4219 id: "effect/schema/isPattern",
4220 payload
4221 },
4222 // Keep the filter exportable so its JSON Schema annotations are preserved
4223 // when the RegExp constraint itself is omitted.
4224 toJsonSchema: () => [{}, true],
4225 arbitraryConstraint: {
4226 patterns: [payload]
4227 },
4228 ...annotations
4229 }
4230 )
4231}
4232
4233// Copies that update only context or encoding share a body.
4234// All other fields describe the body itself, including child contexts.
4235const bodyOwners = new WeakMap<AST, AST>()
4236
4237function copy<A extends AST>(ast: A, changes: Partial<AST>): A {
4238 // AST copies preserve the prototype and enumerable values, not property descriptors.
4239 const out = Object.assign(Object.create(Object.getPrototypeOf(ast)), ast, changes) as A
4240 if (Reflect.ownKeys(changes).every((key) => key === "context" || key === "encoding")) {
4241 bodyOwners.set(out, getContextOwner(ast))
4242 }
4243 return out
4244}
4245
4246/** @internal */
4247export function getContextOwner<A extends AST>(ast: A): A {
4248 const existing = bodyOwners.get(ast)
4249 if (existing !== undefined) return existing as A
4250 if (ast.encoding === undefined) return ast
4251 // A body representative must not expose or retain the node's own codec.
4252 const owner = Object.assign(Object.create(Object.getPrototypeOf(ast)), ast, { encoding: undefined }) as A
4253 bodyOwners.set(ast, owner)
4254 return owner
4255}
4256
4257/** @internal */
4258export function replaceEncoding<A extends AST>(ast: A, encoding: Encoding | undefined): A {
4259 return ast.encoding === encoding ? ast : copy(ast, { encoding })
4260}
4261
4262/** @internal */
4263export function replaceContext<A extends AST>(ast: A, context: Context | undefined): A {
4264 if (ast.context === context) {
4265 return ast
4266 }
4267 const owner = getContextOwner(ast)
4268 if (owner.context === context && owner.encoding === ast.encoding) {
4269 return owner
4270 }
4271 return copy(ast, { context })
4272}
4273
4274/** @internal */
4275export function getLastEncoding(ast: AST): AST {
4276 return ast.encoding ? getLastEncoding(ast.encoding[ast.encoding.length - 1].to) : ast
4277}
4278
4279/** @internal */
4280export function annotate<A extends AST>(ast: A, annotations: Schema.Annotations.Annotations): A {
4281 if (ast.checks) {
4282 const last = ast.checks[ast.checks.length - 1]
4283 return replaceChecks(ast, Arr.append(ast.checks.slice(0, -1), last.annotate(annotations)))
4284 }
4285 return copy(ast, { annotations: { ...ast.annotations, ...annotations } })
4286}
4287
4288/** @internal */
4289export function replaceChecks<A extends AST>(ast: A, checks: Checks | undefined): A {
4290 if (ast._tag === "Suspend" && checks) {
4291 throw new Error("Cannot add checks to Suspend")
4292 }
4293 if (ast.checks === checks) {
4294 return ast
4295 }
4296 return copy(ast, { checks })
4297}
4298
4299/** @internal */
4300export function appendChecks<A extends AST>(ast: A, checks: Checks | undefined): A {
4301 return replaceChecks(ast, combineChecks(ast.checks, checks))
4302}
4303
4304/** @internal */
4305export function mapLink(link: Link, f: (ast: AST) => AST): Link {
4306 const to = f(link.to)
4307 return to === link.to ? link : new Link(to, link.transformation)
4308}
4309
4310function updateLastLink(encoding: Encoding, f: (ast: AST) => AST): Encoding {
4311 const links = encoding
4312 const last = links[links.length - 1]
4313 const out = mapLink(last, f)
4314 return out === last ? encoding : Arr.append(encoding.slice(0, encoding.length - 1), out)
4315}
4316
4317/** @internal */
4318export function applyToLastLink(f: (ast: AST) => AST) {
4319 return <A extends AST>(ast: A): A => ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, f)) : ast
4320}
4321
4322/** @internal */
4323export function replaceContextLastLink<A extends AST>(ast: A, context: Context): A {
4324 return applyToLastLink((ast) => replaceContext(ast, context))(ast)
4325}
4326
4327/** @internal */
4328export function applyToSelfOrLastLinkEncoding(f: (ast: AST) => AST) {
4329 function out(ast: AST): AST {
4330 return ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, out)) : f(ast)
4331 }
4332 return memoize(out)
4333}
4334
4335/** @internal */
4336export function applyToSelfOrLastLinkEncodingIdempotent(
4337 f: (ast: AST) => AST,
4338 options?: { readonly stopAt?: (link: Link) => boolean }
4339) {
4340 function out(ast: AST): AST {
4341 if (ast.encoding) {
4342 const last = ast.encoding[ast.encoding.length - 1]
4343 return options?.stopAt?.(last) ? ast : replaceEncoding(ast, updateLastLink(ast.encoding, out))
4344 }
4345 return f(ast)
4346 }
4347 return memoizeIdempotent(out)
4348}
4349
4350/** @internal */
4351export function middlewareDecoding(
4352 ast: AST,
4353 middleware: SchemaTransformation.Middleware<any, any, any, any, any, any>
4354): AST {
4355 return appendTransformation(ast, middleware, toType(ast))
4356}
4357
4358/** @internal */
4359export function middlewareEncoding(
4360 ast: AST,
4361 middleware: SchemaTransformation.Middleware<any, any, any, any, any, any>
4362): AST {
4363 return appendTransformation(toEncoded(ast), middleware, ast)
4364}
4365
4366function appendTransformation<A extends AST>(
4367 from: AST,
4368 transformation:
4369 | SchemaTransformation.Transformation<any, any, any, any>
4370 | SchemaTransformation.Middleware<any, any, any, any, any, any>,
4371 to: A
4372): A {
4373 const link = new Link(from, transformation)
4374 return replaceEncoding(to, to.encoding ? [...to.encoding, link] : [link])
4375}
4376
4377/**
4378 * Maps over the array but will return the original array if no changes occur.
4379 * @internal
4380 */
4381export function mapOrSame<A>(as: Arr.NonEmptyReadonlyArray<A>, f: (a: A) => A): Arr.NonEmptyReadonlyArray<A>
4382export function mapOrSame<A>(as: ReadonlyArray<A>, f: (a: A) => A): ReadonlyArray<A>
4383export function mapOrSame<A>(as: ReadonlyArray<A>, f: (a: A) => A): ReadonlyArray<A> {
4384 let out: Array<A> | undefined
4385 for (let i = 0; i < as.length; i++) {
4386 const a = as[i]
4387 const fa = f(a)
4388 if (out) {
4389 out[i] = fa
4390 } else if (fa !== a) {
4391 out = new Array(as.length)
4392 for (let j = 0; j < i; j++) {
4393 out[j] = as[j]
4394 }
4395 out[i] = fa
4396 }
4397 }
4398 return out ?? as
4399}
4400
4401/** @internal */
4402export function annotateKey<A extends AST>(ast: A, annotations: Schema.Annotations.Key<unknown>): A {
4403 const context = ast.context ?
4404 new Context(
4405 ast.context.isOptional,
4406 ast.context.isMutable,
4407 ast.context.constructorDefault,
4408 { ...ast.context.annotations, ...annotations }
4409 ) :
4410 new Context(false, false, undefined, annotations)
4411 return replaceContext(ast, context)
4412}
4413
4414/** @internal */
4415export const optionalKey: <A extends AST>(ast: A) => A = memoizeIdempotent(<A extends AST>(ast: A): A => {
4416 const context = ast.context ?
4417 ast.context.isOptional === false ?
4418 new Context(true, ast.context.isMutable, ast.context.constructorDefault, ast.context.annotations) :
4419 ast.context :
4420 new Context(true, false)
4421 return optionalKeyLastLink(replaceContext(ast, context))
4422})
4423
4424const optionalKeyLastLink = applyToLastLink(optionalKey)
4425
4426/** @internal */
4427export const optional = memoize(<A extends AST>(ast: A): Union<A | Undefined> =>
4428 optionalKey(new Union([ast, undefined_]))
4429)
4430
4431/** @internal */
4432export const mutableKey = memoizeIdempotent(<A extends AST>(ast: A): A => {
4433 const context = ast.context ?
4434 ast.context.isMutable === false ?
4435 new Context(ast.context.isOptional, true, ast.context.constructorDefault, ast.context.annotations) :
4436 ast.context :
4437 new Context(false, true)
4438 return mutableKeyLastLink(replaceContext(ast, context))
4439})
4440
4441const mutableKeyLastLink = applyToLastLink(mutableKey)
4442
4443/** @internal */
4444export function withConstructorDefault<A extends AST>(
4445 ast: A,
4446 defaultValue: Effect.Effect<unknown, SchemaIssue.Issue>
4447): A {
4448 const context = ast.context ?
4449 new Context(ast.context.isOptional, ast.context.isMutable, defaultValue, ast.context.annotations) :
4450 new Context(false, false, defaultValue)
4451 return replaceContext(ast, context)
4452}
4453
4454/**
4455 * Attaches a `Transformation` to the `to` AST, making it decode from the
4456 * `from` AST and encode back to it.
4457 *
4458 * **Details**
4459 *
4460 * This is the low-level primitive behind `Schema.decodeTo`. It appends a
4461 * {@link Link} to the `to` node's encoding chain.
4462 *
4463 * - Returns a new AST with the same type as `to`.
4464 *
4465 * @see {@link Link}
4466 * @see {@link Encoding}
4467 * @see {@link flip}
4468 * @category transforming
4469 * @since 4.0.0
4470 */
4471export function decodeTo<A extends AST>(
4472 from: AST,
4473 to: A,
4474 transformation: SchemaTransformation.Transformation<any, any, any, any>
4475): A {
4476 return appendTransformation(from, transformation, to)
4477}
4478
4479function parseParameter(ast: AST): {
4480 literals: ReadonlyArray<PropertyKey>
4481 parameters: ReadonlyArray<AST>
4482} {
4483 const literals: Array<PropertyKey> = []
4484 const parameters: Array<AST> = []
4485 function go(ast: AST) {
4486 switch (ast._tag) {
4487 case "Literal":
4488 if (Predicate.isPropertyKey(ast.literal) && !literals.includes(ast.literal)) {
4489 literals.push(ast.literal)
4490 }
4491 return
4492 case "UniqueSymbol":
4493 if (!literals.includes(ast.symbol)) {
4494 literals.push(ast.symbol)
4495 }
4496 return
4497 case "Never":
4498 return
4499 case "Union":
4500 for (let i = 0; i < ast.types.length; i++) {
4501 go(ast.types[i])
4502 }
4503 return
4504 default:
4505 parameters.push(ast)
4506 }
4507 }
4508 go(ast)
4509 return { literals, parameters }
4510}
4511
4512/** @internal */
4513export function record(key: AST, value: AST): Objects {
4514 const { literals, parameters: indexSignatures } = parseParameter(key)
4515 return new Objects(
4516 literals.map((literal) => new PropertySignature(literal, value)),
4517 indexSignatures.map((parameter) => new IndexSignature(parameter, value))
4518 )
4519}
4520
4521// -------------------------------------------------------------------------------------
4522// Public APIs
4523// -------------------------------------------------------------------------------------
4524
4525/**
4526 * Returns `true` if the AST node represents an optional property.
4527 *
4528 * **Details**
4529 *
4530 * Checks `ast.context?.isOptional`. Defaults to `false` when no
4531 * {@link Context} is set.
4532 *
4533 * @see `Schema.optionalKey`
4534 * @see {@link Context}
4535 * @category predicates
4536 * @since 4.0.0
4537 */
4538export function isOptional(ast: AST): boolean {
4539 return ast.context?.isOptional ?? false
4540}
4541
4542/** @internal */
4543export function isMutable(ast: AST): boolean {
4544 return ast.context?.isMutable ?? false
4545}
4546
4547function isStructuralCheck(check: Check<any>): boolean {
4548 return check.annotations?.[InternalAnnotations.STRUCTURAL_ANNOTATION_KEY] === true ||
4549 check._tag === "FilterGroup" && check.checks.every(isStructuralCheck)
4550}
4551
4552function extractStructuralChecks(checks: Checks): Checks | undefined {
4553 function extract(check: Check<any>): Array<Check<any>> {
4554 if (isStructuralCheck(check)) return [check]
4555 return check._tag === "FilterGroup" ? check.checks.flatMap(extract) : []
4556 }
4557 const out = checks.flatMap(extract)
4558 return Arr.isArrayNonEmpty(out) ? out : undefined
4559}
4560
4561function canPreserveEncodingChecks(ast: AST): boolean {
4562 let preserve = true
4563 function visit(child: AST): AST {
4564 preserve = preserve && !child.encoding && !isSuspend(child)
4565 if (preserve && "recur" in child) child.recur(visit)
4566 return child
4567 }
4568 if ("recur" in ast) ast.recur(visit)
4569 return preserve
4570}
4571
4572/**
4573 * Strips all encoding transformations from an AST, returning the decoded
4574 * (type-level) representation.
4575 *
4576 * **Details**
4577 *
4578 * - Memoized: same input reference → same output reference.
4579 * - Recursively walks into composite nodes ({@link Arrays}, {@link Objects},
4580 * {@link Union}, {@link Suspend}).
4581 *
4582 * **Example** (Getting the type AST)
4583 *
4584 * ```ts import.meta.vitest
4585 * import { Schema, SchemaAST } from "effect"
4586 *
4587 * const schema = Schema.NumberFromString
4588 * const typeAst = SchemaAST.toType(schema.ast)
4589 * typeAst._tag // => "Number"
4590 * ```
4591 *
4592 * @see {@link toEncoded}
4593 * @see {@link flip}
4594 * @category transforming
4595 * @since 4.0.0
4596 */
4597export const toType = memoizeIdempotent(<A extends AST>(ast: A): A => {
4598 const owner = getContextOwner(ast)
4599 if (owner !== ast) {
4600 const type = toType(owner)
4601 return type === owner && ast.encoding === undefined ? ast : replaceContext(type, ast.context)
4602 }
4603 const type = ("recur" in ast ? ast.recur(toType) : ast) as A
4604 if ("encodingChecks" in type && type.encodingChecks) {
4605 const checks = canPreserveEncodingChecks(ast)
4606 ? type.encodingChecks
4607 : isArrays(type) || isObjects(type) || isDeclaration(type) && type.typeParameters.length > 0
4608 ? extractStructuralChecks(type.encodingChecks)
4609 : undefined
4610 return copy(type, {
4611 encodingChecks: undefined,
4612 checks: combineChecks(type.checks, checks)
4613 })
4614 }
4615 return type
4616})
4617
4618/**
4619 * Returns the encoded (wire-format) AST by flipping and then stripping
4620 * encodings.
4621 *
4622 * **Details**
4623 *
4624 * Equivalent to `toType(flip(ast))`. This gives you the AST that describes
4625 * the shape of the serialized/encoded data.
4626 *
4627 * - Memoized: same input reference → same output reference.
4628 *
4629 * **Example** (Getting the encoded AST)
4630 *
4631 * ```ts import.meta.vitest
4632 * import { Schema, SchemaAST } from "effect"
4633 *
4634 * const schema = Schema.NumberFromString
4635 * const encodedAst = SchemaAST.toEncoded(schema.ast)
4636 * encodedAst._tag // => "String"
4637 * ```
4638 *
4639 * @see {@link toType}
4640 * @see {@link flip}
4641 * @category transforming
4642 * @since 4.0.0
4643 */
4644export const toEncoded = memoizeIdempotent((ast: AST): AST => {
4645 return toType(flip(ast))
4646})
4647
4648function flipEncoding(ast: AST, encoding: Encoding): AST {
4649 const links = encoding
4650 const len = links.length
4651 const last = links[len - 1]
4652 const ls: Arr.NonEmptyArray<Link> = [
4653 new Link(flip(replaceEncoding(ast, undefined)), links[0].transformation.flip())
4654 ]
4655 for (let i = 1; i < len; i++) {
4656 ls.unshift(new Link(flip(links[i - 1].to), links[i].transformation.flip()))
4657 }
4658 const to = flip(last.to)
4659 if (to.encoding) {
4660 return replaceEncoding(to, [...to.encoding, ...ls])
4661 } else {
4662 return replaceEncoding(to, ls)
4663 }
4664}
4665
4666/**
4667 * Swaps the decode and encode directions of an AST's {@link Encoding} chain.
4668 *
4669 * **Details**
4670 *
4671 * After flipping, what was decoding becomes encoding and vice versa. This is
4672 * the core operation behind `Schema.encode` — encoding a value is decoding
4673 * with a flipped SchemaAST.
4674 *
4675 * - Memoized: same input reference → same output reference.
4676 * - Recursively walks composite nodes.
4677 *
4678 * @see {@link toType}
4679 * @see {@link toEncoded}
4680 * @category transforming
4681 * @since 4.0.0
4682 */
4683export const flip = memoize((ast: AST): AST => {
4684 if (ast.encoding) {
4685 return flipEncoding(ast, ast.encoding)
4686 }
4687 const owner = getContextOwner(ast)
4688 if (owner !== ast) {
4689 const flipped = flip(owner)
4690 return flipped === owner ? ast : replaceContext(flipped, ast.context)
4691 }
4692 return "flip" in ast ? ast.flip(flip) : "recur" in ast ? ast.recur(flip) : ast
4693})
4694
4695/** @internal */
4696export function containsUndefined(ast: AST): boolean {
4697 switch (ast._tag) {
4698 case "Undefined":
4699 return true
4700 case "Union":
4701 return ast.types.some(containsUndefined)
4702 default:
4703 return false
4704 }
4705}
4706
4707function fromConst<const T>(
4708 ast: AST,
4709 value: T
4710): SchemaParser.Parser {
4711 const succeed = value === 0 ? InternalParser.sameExit : InternalParser.succeed(value)
4712 return (input, options) => {
4713 if (input === InternalParser.missing) return InternalParser.missingExit
4714 if (input === value) return succeed
4715 return Effect.fail(new SchemaIssue.InvalidType(ast, input, options))
4716 }
4717}
4718
4719function fromRefinement<T>(
4720 ast: AST,
4721 refinement: (input: unknown) => input is T
4722): SchemaParser.Parser {
4723 return (input, options) => {
4724 if (input === InternalParser.missing) return InternalParser.missingExit
4725 if (refinement(input)) return InternalParser.sameExit
4726 return Effect.fail(new SchemaIssue.InvalidType(ast, input, options))
4727 }
4728}
4729
4730function segmentTemplateLiteralParts(
4731 ast: TemplateLiteral,
4732 input: string,
4733 options: ParseOptions
4734): Array<string> | undefined {
4735 const parts = ast.encodedParts
4736 const literals = ast.literals
4737 const inputLength = input.length
4738 for (let i = 0; i < literals.length; i++) {
4739 const literal = literals[i]
4740 if (literal && !input.includes(literal)) return undefined
4741 }
4742 if (ast.suffixLengths[0] > inputLength) return undefined
4743
4744 const out = new Array<string>(parts.length)
4745 let failures: Set<number> | undefined
4746 function go(i: number, pos: number): boolean {
4747 if (i === parts.length) return pos === inputLength
4748 if (failures?.has(i * (inputLength + 1) + pos)) return false
4749 const part = parts[i]
4750 if (i === parts.length - 1) {
4751 const s = input.slice(pos)
4752 if (part.matchPart(s, options) !== undefined) {
4753 out[i] = s
4754 return true
4755 }
4756 } else if (part._tag === "Literal") {
4757 const s = literals[i]!
4758 if (input.startsWith(s, pos) && go(i + 1, pos + s.length)) {
4759 out[i] = s
4760 return true
4761 }
4762 } else {
4763 const maximumEnd = inputLength - ast.suffixLengths[i + 1]
4764 // Splits preceding a literal only need to consider occurrences of that literal.
4765 const anchor = literals[i + 1]
4766 let end = anchor === undefined ? maximumEnd : input.lastIndexOf(anchor, maximumEnd)
4767 while (end >= pos) {
4768 const s = input.slice(pos, end)
4769 if (part.matchPart(s, options) !== undefined && go(i + 1, end)) {
4770 out[i] = s
4771 return true
4772 }
4773 if (end === 0) break
4774 end = anchor === undefined ? end - 1 : input.lastIndexOf(anchor, end - 1)
4775 }
4776 }
4777 failures ??= new Set()
4778 failures.add(i * (inputLength + 1) + pos)
4779 return false
4780 }
4781 return go(0, 0) ? out : undefined
4782}
4783
4784/** @internal */
4785export const parameterFromPropertyKey = applyToSelfOrLastLinkEncodingIdempotent((ast) => {
4786 switch (ast._tag) {
4787 default:
4788 return ast
4789 case "Number":
4790 return ast.toCodecStringTree()
4791 case "Union":
4792 return ast.recur(parameterFromPropertyKey)
4793 }
4794})
4795
4796/** @internal */
4797export const parameterFromString = applyToSelfOrLastLinkEncodingIdempotent((ast) => {
4798 switch (ast._tag) {
4799 default:
4800 return ast
4801 case "Symbol":
4802 case "UniqueSymbol":
4803 return ast.toCodecStringTree()
4804 case "Union":
4805 return ast.recur(parameterFromString)
4806 }
4807})
4808
4809const partFromString = applyToSelfOrLastLinkEncodingIdempotent((ast) => {
4810 switch (ast._tag) {
4811 default:
4812 return ast
4813 case "Number":
4814 case "Literal":
4815 case "BigInt":
4816 return ast.toCodecStringTree()
4817 case "Union":
4818 return ast.recur(partFromString)
4819 }
4820})
4821
4822/**
4823 * any string, including newlines
4824 * @internal
4825 */
4826export const STRING_PATTERN = "[\\s\\S]*?"
4827
4828const isStringFiniteRegExp = new globalThis.RegExp(`^${FINITE_PATTERN}$`)
4829
4830const isStringNumberRegExp = new globalThis.RegExp(`^(?:${FINITE_PATTERN}|Infinity|-Infinity|NaN)$`)
4831
4832/** @internal */
4833export function isStringFinite(annotations?: Schema.Annotations.Filter) {
4834 return isPattern(
4835 isStringFiniteRegExp,
4836 {
4837 expected: "a string representing a finite number",
4838 representation: {
4839 id: "effect/schema/isStringFinite",
4840 payload: null
4841 },
4842 toJsonSchema: () => ({ pattern: isStringFiniteRegExp.source }),
4843 ...annotations
4844 }
4845 )
4846}
4847
4848const finiteString = appendChecks(string, [isStringFinite()])
4849
4850const finiteToString = new Link(
4851 finiteString,
4852 SchemaTransformation.numberFromString
4853)
4854
4855const numberToString = new Link(
4856 new Union([finiteString, nonFiniteLiterals]),
4857 SchemaTransformation.numberFromString
4858)
4859
4860/**
4861 * signed integer only (no leading "+" because TypeScript doesn't support it)
4862 */
4863const BIGINT_PATTERN = "-?\\d+"
4864
4865const isStringBigIntRegExp = new globalThis.RegExp(`^${BIGINT_PATTERN}$`)
4866
4867/** @internal */
4868export function isStringBigInt(annotations?: Schema.Annotations.Filter) {
4869 return isPattern(
4870 isStringBigIntRegExp,
4871 {
4872 expected: "a string representing a bigint",
4873 representation: {
4874 id: "effect/schema/isStringBigInt",
4875 payload: null
4876 },
4877 toJsonSchema: () => ({ pattern: isStringBigIntRegExp.source }),
4878 ...annotations
4879 }
4880 )
4881}
4882
4883/** @internal */
4884export const bigIntString = appendChecks(string, [isStringBigInt({
4885 expected: "a string representing a bigint"
4886})])
4887
4888const bigIntToString = new Link(
4889 bigIntString,
4890 SchemaTransformation.bigintFromString
4891)
4892
4893const REGEXP_PATTERN = "Symbol\\(([\\s\\S]*)\\)"
4894
4895const isStringSymbolRegExp = new globalThis.RegExp(`^${REGEXP_PATTERN}$`)
4896
4897/** @internal */
4898export const symbolString = appendChecks(string, [isStringSymbol()])
4899
4900/**
4901 * to distinguish between Symbol and String, we need to add a check to the string keyword
4902 */
4903const symbolToString = new Link(
4904 symbolString,
4905 new SchemaTransformation.Transformation(
4906 SchemaGetter.transform((description) => globalThis.Symbol.for(isStringSymbolRegExp.exec(description)![1])),
4907 SchemaGetter.transformEffect((sym: symbol, options) => {
4908 const key = globalThis.Symbol.keyFor(sym)
4909 if (key !== undefined) {
4910 return Effect.succeed(globalThis.String(sym))
4911 }
4912 return Effect.fail(
4913 new SchemaIssue.Forbidden(
4914 { message: "cannot serialize to string, Symbol is not registered" },
4915 sym,
4916 options
4917 )
4918 )
4919 })
4920 )
4921)
4922
4923/** @internal */
4924export function isStringSymbol(annotations?: Schema.Annotations.Filter) {
4925 return isPattern(
4926 isStringSymbolRegExp,
4927 {
4928 expected: "a string representing a symbol",
4929 representation: {
4930 id: "effect/schema/isStringSymbol",
4931 payload: null
4932 },
4933 toJsonSchema: () => ({ pattern: isStringSymbolRegExp.source }),
4934 ...annotations
4935 }
4936 )
4937}
4938
4939/** @internal */
4940export function collectIssues<T>(
4941 checks: ReadonlyArray<Check<T>>,
4942 value: T,
4943 issues: Arr.NonEmptyArray<SchemaIssue.Issue> | undefined,
4944 ast: AST,
4945 options: ParseOptions
4946): Arr.NonEmptyArray<SchemaIssue.Issue> | undefined {
4947 for (let i = 0; i < checks.length; i++) {
4948 const check = checks[i]
4949 if (check._tag === "FilterGroup") {
4950 issues = collectIssues(check.checks, value, issues, ast, options)
4951 if (
4952 issues &&
4953 (options.errors !== "all" || (issues[issues.length - 1] as SchemaIssue.Filter).filter.aborted)
4954 ) {
4955 return issues
4956 }
4957 } else {
4958 const issue = check.run(value, ast, options)
4959 if (issue) {
4960 const filter = new SchemaIssue.Filter(check, issue, value, options)
4961 if (issues) issues.push(filter)
4962 else issues = [filter]
4963 if (options.errors !== "all" || check.aborted) {
4964 return issues
4965 }
4966 }
4967 }
4968 }
4969 return issues
4970}
4971
4972/** @internal */
4973export function runChecks<T>(
4974 checks: readonly [Check<T>, ...Array<Check<T>>],
4975 s: T
4976): Result.Result<T, SchemaIssue.Issue> {
4977 const issues = collectIssues(checks, s, undefined, unknown, { errors: "all" })
4978 if (issues) {
4979 const issue = new SchemaIssue.Composite(unknown, issues)
4980 return Result.fail(issue)
4981 }
4982 return Result.succeed(s)
4983}
4984
4985/** @internal */
4986export interface ConstructorDescriptor {
4987 readonly isConstructed: Predicate.Predicate<unknown>
4988 readonly link: Link
4989}
4990
4991/** @internal */
4992export function getConstructorDescriptor(ast: AST): ConstructorDescriptor | undefined {
4993 if (!isDeclaration(ast)) return undefined
4994 const getDescriptor = ast.annotations?.[InternalAnnotations.CONSTRUCTOR_ANNOTATION_KEY]
4995 return Predicate.isFunction(getDescriptor) ? getDescriptor(ast.typeParameters) : undefined
4996}
4997
4998/**
4999 * Returns all annotations from the AST node.
5000 *
5001 * **Details**
5002 *
5003 * If the node has {@link Checks}, returns annotations from the last check
5004 * (which is where user-supplied annotations end up after `.pipe(Schema.annotations(...))`).
5005 * Otherwise returns the node's `annotations` directly.
5006 *
5007 * **Example** (Reading annotations)
5008 *
5009 * ```ts import.meta.vitest
5010 * import { Schema, SchemaAST } from "effect"
5011 *
5012 * const schema = Schema.String.annotate({ title: "Name" })
5013 * const annotations = SchemaAST.resolve(schema.ast)
5014 * annotations?.title // => "Name"
5015 * ```
5016 *
5017 * @see {@link resolveAt}
5018 * @see {@link resolveIdentifier}
5019 * @see {@link resolveTitle}
5020 * @see {@link resolveDescription}
5021 * @category annotations
5022 * @since 4.0.0
5023 */
5024export const resolve: (ast: AST) => Schema.Annotations.Annotations | undefined = InternalAnnotations.resolve
5025
5026/**
5027 * Returns a single annotation value by key from the AST node.
5028 *
5029 * **Details**
5030 *
5031 * Like {@link resolve}, reads from the last check's annotations when checks
5032 * are present. Returns `undefined` if the key is not found.
5033 *
5034 * @see {@link resolve}
5035 * @category annotations
5036 * @since 4.0.0
5037 */
5038export const resolveAt: <A>(key: string) => (ast: AST) => A | undefined = InternalAnnotations.resolveAt
5039
5040/**
5041 * Returns the `identifier` annotation from the AST node, if set.
5042 *
5043 * **Details**
5044 *
5045 * The identifier is typically set by `Schema.annotations({ identifier: "..." })`
5046 * and is used for error messages and schema identification.
5047 *
5048 * @see {@link resolve}
5049 * @see {@link resolveTitle}
5050 * @category annotations
5051 * @since 4.0.0
5052 */
5053export const resolveIdentifier: (ast: AST) => string | undefined = InternalAnnotations.resolveIdentifier
5054
5055/**
5056 * Returns the `title` annotation from the AST node, if set.
5057 *
5058 * @see {@link resolve}
5059 * @see {@link resolveIdentifier}
5060 * @see {@link resolveDescription}
5061 *
5062 * @category annotations
5063 * @since 4.0.0
5064 */
5065export const resolveTitle: (ast: AST) => string | undefined = InternalAnnotations.resolveTitle
5066
5067/**
5068 * Returns the `description` annotation from the AST node, if set.
5069 *
5070 * @see {@link resolve}
5071 * @see {@link resolveTitle}
5072 * @see {@link resolveIdentifier}
5073 *
5074 * @category annotations
5075 * @since 4.0.0
5076 */
5077export const resolveDescription: (ast: AST) => string | undefined = InternalAnnotations.resolveDescription
5078
5079type TreeFrame = {
5080 readonly value: object
5081 // Object keys or an array length snapshot.
5082 readonly keys: ReadonlyArray<string> | number
5083 index: number
5084}
5085
5086function isJsonLeaf(u: unknown): boolean {
5087 return u === null || typeof u === "string" || typeof u === "boolean" ||
5088 typeof u === "number" && globalThis.Number.isFinite(u)
5089}
5090
5091function isStringTreeLeaf(u: unknown): boolean {
5092 return u === undefined || typeof u === "string"
5093}
5094
5095function isTree(u: unknown, isLeaf: (u: unknown) => boolean): boolean {
5096 const cache = new WeakMap<object, boolean>()
5097 const stack: Array<TreeFrame> = []
5098 outer: while (true) {
5099 if (typeof u !== "object" || u === null) {
5100 if (!isLeaf(u)) {
5101 return false
5102 }
5103 } else {
5104 const value = u
5105 const cached = cache.get(value)
5106 // `false` marks a node on the current path, while `true` marks a fully
5107 // validated node that can be safely reused by a DAG.
5108 if (cached === false) {
5109 return false
5110 }
5111 if (cached === undefined) {
5112 const isArray = Array.isArray(value)
5113 if (!isArray) {
5114 const prototype = Object.getPrototypeOf(value)
5115 // A plain object from another realm has a different Object.prototype,
5116 // but that prototype still has a null prototype.
5117 if (
5118 prototype !== null &&
5119 prototype !== Object.prototype &&
5120 Object.getPrototypeOf(prototype) !== null
5121 ) {
5122 return false
5123 }
5124 }
5125 cache.set(value, false)
5126 stack.push({
5127 value,
5128 keys: isArray ? value.length : Object.keys(value),
5129 index: 0
5130 })
5131 }
5132 }
5133
5134 while (stack.length > 0) {
5135 const frame = stack[stack.length - 1]
5136 const keys = frame.keys
5137 if (typeof keys === "number") {
5138 if (frame.index < keys) {
5139 // A sparse slot is read as `undefined`; the leaf predicate determines
5140 // whether that is valid for the current tree.
5141 u = (frame.value as ReadonlyArray<unknown>)[frame.index++]
5142 continue outer
5143 }
5144 } else if (frame.index < keys.length) {
5145 u = (frame.value as Record<string, unknown>)[keys[frame.index++]]
5146 continue outer
5147 }
5148 cache.set(frame.value, true)
5149 stack.pop()
5150 }
5151 return true
5152 }
5153}
5154
5155/**
5156 * Returns true if the value is a JSON value.
5157 *
5158 * When a cyclic reference is detected, returns false.
5159 *
5160 * @internal
5161 */
5162export function isJson(u: unknown): u is Schema.Json {
5163 return isTree(u, isJsonLeaf)
5164}
5165
5166/** @internal */
5167export const Json = new Declaration(
5168 [],
5169 () => (input, ast, options) =>
5170 isJson(input) ?
5171 InternalParser.sameExit :
5172 Effect.fail(new SchemaIssue.InvalidType(ast, input, options)),
5173 {
5174 representation: {
5175 id: "effect/schema/Json",
5176 payload: null
5177 },
5178 expected: "JSON value",
5179 toCodecJson: () => undefined,
5180 toCodecStringTree: () => unknownToStringTree
5181 }
5182)
5183
5184/** @internal */
5185export const MutableJson = annotate(Json, {
5186 representation: {
5187 id: "effect/schema/MutableJson",
5188 payload: null
5189 }
5190})
5191
5192/** @internal */
5193export const unknownToJson = new Link(
5194 Json,
5195 SchemaTransformation.passthrough()
5196)
5197
5198/** @internal */
5199export const objectKeywordToJson = new Link(
5200 new Union([
5201 new Arrays(false, [], [Json]),
5202 new Objects([], [new IndexSignature(string, Json)])
5203 ]),
5204 SchemaTransformation.passthrough()
5205)
5206
5207/**
5208 * Returns true if the value is a StringTree value.
5209 *
5210 * When a cyclic reference is detected, returns false.
5211 *
5212 * @internal
5213 */
5214export function isStringTree(u: unknown): u is Schema.StringTree {
5215 return isTree(u, isStringTreeLeaf)
5216}
5217
5218const StringTree = new Declaration(
5219 [],
5220 () => (input, ast, options) =>
5221 isStringTree(input) ?
5222 InternalParser.sameExit :
5223 Effect.fail(new SchemaIssue.InvalidType(ast, input, options)),
5224 { expected: "StringTree", toCodecStringTree: () => undefined }
5225)
5226
5227/** @internal */
5228export const unknownToStringTree = new Link(
5229 StringTree,
5230 SchemaTransformation.passthrough()
5231)