1/**
2 * Runs schemas against real values.
3 *
4 * Schema parsers construct values from schema input, check whether a value
5 * matches a schema, decode encoded input, and encode decoded values back to
6 * their external form. This module exposes those operations through several
7 * result styles, including `Effect`, `Promise`, `Exit`, `Option`, `Result`, and
8 * synchronous functions that throw. It also contains the lower-level runner that
9 * walks a schema AST and reports schema failures as `SchemaIssue.Issue` values.
10 *
11 * @since 4.0.0
12 */
13import * as Cause from "./Cause.ts"
14import * as Effect from "./Effect.ts"
15import * as Exit from "./Exit.ts"
16import { effectIsExit } from "./internal/effect.ts"
17import * as InternalSchemaCause from "./internal/schema/cause.ts"
18import * as CompilerRegistry from "./internal/schema/compilerRegistry.ts"
19import * as InternalParser from "./internal/schema/parser.ts"
20import * as Option from "./Option.ts"
21import * as Result from "./Result.ts"
22import type * as Schema from "./Schema.ts"
23import * as SchemaAST from "./SchemaAST.ts"
24import * as SchemaIssue from "./SchemaIssue.ts"
25
26/**
27 * Creates an effectful maker for the schema's decoded type side.
28 *
29 * **When to use**
30 *
31 * Use to construct decoded schema values in `Effect` while preserving
32 * construction failures as `SchemaIssue.Issue` values in the error channel.
33 *
34 * **Details**
35 *
36 * The returned function accepts constructor input, applies constructor defaults,
37 * runs type-side validation unless checks are disabled, and fails with a
38 * `SchemaIssue.Issue` when construction fails.
39 *
40 * @category constructors
41 * @since 4.0.0
42 */
43export function makeEffect<S extends Schema.Constraint>(schema: S) {
44 const ast = schema.ast
45 let parser: ReturnType<typeof runWithCompiler<S["Type"], never>>
46 return (input: S["~type.make.in"], options?: Schema.MakeOptions): Effect.Effect<S["Type"], SchemaIssue.Issue> => {
47 return (parser ??= runWithCompiler<S["Type"], never>(constructorCompiler, SchemaAST.toType(ast)))(
48 input,
49 options?.disableChecks
50 ? options?.parseOptions ? { ...options.parseOptions, disableChecks: true } : { disableChecks: true }
51 : options?.parseOptions
52 )
53 }
54}
55
56/**
57 * Creates a synchronous maker that returns `Option.some` with the constructed
58 * value on success, or `Option.none` when construction fails with schema issues.
59 *
60 * **When to use**
61 *
62 * Use when you need to validate schema constructor input and only care whether
63 * construction succeeds, without exposing `SchemaIssue.Issue` details.
64 *
65 * **Gotchas**
66 *
67 * Only causes made entirely of schema issues are converted to `Option.none`.
68 * Causes that contain defects, interruptions, or asynchronous work at this
69 * synchronous boundary throw an `Error` whose cause is the underlying `Cause`.
70 *
71 * @category constructors
72 * @since 4.0.0
73 */
74export function makeOption<S extends Schema.Constraint>(schema: S) {
75 const parser = makeEffect(schema)
76 return (input: S["~type.make.in"], options?: Schema.MakeOptions): Option.Option<S["Type"]> => {
77 const exit = Effect.runSyncExit(parser(input, options))
78 if (Exit.isSuccess(exit)) {
79 return Option.some(exit.value)
80 }
81 InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Option adapter can only return none for schema issues")
82 return Option.none()
83 }
84}
85
86/**
87 * Creates a synchronous maker for the schema's decoded type side.
88 *
89 * **When to use**
90 *
91 * Use to construct decoded schema values synchronously when invalid input
92 * should throw an `Error` whose cause is `SchemaIssue.Issue`.
93 *
94 * **Details**
95 *
96 * The returned function constructs a value from constructor input and throws an
97 * `Error` with the `SchemaIssue.Issue` in its `cause` when construction fails.
98 * Schema validation failures use the generic message `"Schema validation failed"`.
99 * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when
100 * human-readable details are needed.
101 *
102 * **Gotchas**
103 *
104 * Causes that contain defects, interruptions, or asynchronous work at this
105 * synchronous boundary throw an `Error` whose cause is the underlying `Cause`,
106 * instead of being converted to a schema validation error.
107 *
108 * @category constructors
109 * @since 4.0.0
110 */
111export function make<S extends Schema.Constraint>(schema: S) {
112 return makeConstructorSync<S["Type"], S["~type.make.in"]>(SchemaAST.toType(schema.ast))
113}
114
115/**
116 * Creates a type guard that checks whether an input satisfies the schema's decoded
117 * type side.
118 *
119 * **When to use**
120 *
121 * Use to build a type guard for checking the decoded side of a schema without
122 * exposing issue details.
123 *
124 * **Details**
125 *
126 * The guard returns `true` on successful validation and `false` when validation
127 * fails only with schema issues, without exposing issue details.
128 *
129 * **Gotchas**
130 *
131 * Only causes made entirely of schema issues are converted to `false`. Causes
132 * that contain defects, interruptions, or asynchronous work at this synchronous
133 * boundary throw an `Error` whose cause is the underlying `Cause`.
134 *
135 * @category guards
136 * @since 3.10.0
137 */
138export function is<S extends Schema.Constraint>(schema: S): <I>(input: I) => input is I & S["Type"] {
139 return _is<S["Type"]>(schema.ast)
140}
141
142function makeIs<T>(ast: SchemaAST.AST): <I>(input: I) => input is I & T {
143 if (!CompilerRegistry.compilerAdaptersEnabled) {
144 const parser = asExit(run<T, never>(ast))
145 return <I>(input: I): input is I & T => {
146 const exit = parser(input, SchemaAST.defaultParseOptions)
147 if (Exit.isSuccess(exit)) return true
148 InternalSchemaCause.getSchemaIssueOrThrow(
149 exit.cause,
150 "Type guard adapter can only return false for schema issues"
151 )
152 return false
153 }
154 }
155 const entry = CompilerRegistry.resolve(ast)
156 const guard = entry.is
157 if (guard !== undefined) {
158 return <I>(input: I): input is I & T => {
159 try {
160 return guard(input, SchemaAST.defaultParseOptions)
161 } catch (error) {
162 InternalSchemaCause.getSchemaIssueOrThrow(
163 Cause.die(error),
164 "Type guard adapter can only return false for schema issues"
165 )
166 return false
167 }
168 }
169 }
170 const parser = entry.parser
171 return <I>(input: I): input is I & T => {
172 const exit = Effect.runSyncExit(parserResult<T, never>(parser(input, SchemaAST.defaultParseOptions), input))
173 if (Exit.isSuccess(exit)) return true
174 InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Type guard adapter can only return false for schema issues")
175 return false
176 }
177}
178
179/** @internal */
180export function _is<T>(ast: SchemaAST.AST) {
181 const typeAST = SchemaAST.toType(ast)
182 let guard: <I>(input: I) => input is I & T = <I>(input: I): input is I & T => {
183 guard = makeIs<T>(typeAST)
184 return guard(input)
185 }
186 return <I>(input: I): input is I & T => {
187 return guard(input)
188 }
189}
190
191/** @internal */
192export function _issue<T>(ast: SchemaAST.AST) {
193 const parser = run<T, never>(ast)
194 return (input: unknown, options: SchemaAST.ParseOptions): SchemaIssue.Issue | undefined => {
195 const exit = Effect.runSyncExit(parser(input, options))
196 if (Exit.isSuccess(exit)) {
197 return undefined
198 }
199 return InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Issue adapter can only return schema issues")
200 }
201}
202
203/**
204 * Asserts that an input satisfies the schema's decoded type side.
205 *
206 * **When to use**
207 *
208 * Use to assert that an input satisfies the decoded side of a schema when schema
209 * validation failures should throw an `Error` whose cause is `SchemaIssue.Issue`.
210 *
211 * **Details**
212 *
213 * The assertion returns normally when validation succeeds. When the input does
214 * not satisfy the schema with a schema-only failure, it throws an `Error` with
215 * the `SchemaIssue.Issue` in its `cause`.
216 * Schema validation failures use the generic message `"Schema validation failed"`.
217 * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when
218 * human-readable details are needed.
219 *
220 * **Gotchas**
221 *
222 * Causes that contain defects, interruptions, or asynchronous work at this
223 * synchronous boundary throw an `Error` whose cause is the underlying `Cause`,
224 * instead of being converted to a schema validation error.
225 *
226 * @category guards
227 * @since 4.0.0
228 */
229export function asserts<S extends Schema.Constraint, I>(schema: S, input: I): asserts input is I & S["Type"] {
230 const ast = SchemaAST.toType(schema.ast)
231 const result = CompilerRegistry.resolve(ast).parser(input, SchemaAST.defaultParseOptions)
232 const exit = Effect.runSyncExit(parserResult<S["Type"], never>(result, input))
233 if (Exit.isFailure(exit)) {
234 const issue = InternalSchemaCause.getSchemaIssueOrThrow(
235 exit.cause,
236 "Assertion adapter can only throw schema issues"
237 )
238 throw new Error("Schema validation failed", { cause: issue })
239 }
240}
241
242/**
243 * Creates an effectful decoder for `unknown` input.
244 *
245 * **When to use**
246 *
247 * Use when you need to decode untyped boundary input in an `Effect` whose
248 * failure channel is `SchemaIssue.Issue`, while preserving transformations
249 * and service requirements.
250 *
251 * **Details**
252 *
253 * The returned function succeeds with the schema's decoded `Type` or fails with a
254 * `SchemaIssue.Issue`. Decoding service requirements are preserved in the returned
255 * `Effect`. Parse options may be provided when creating the decoder and overridden
256 * when applying it.
257 *
258 * @see {@link decodeEffect} for input already typed as the schema's `Encoded` type
259 *
260 * @category decoding
261 * @since 4.0.0
262 */
263export function decodeUnknownEffect<S extends Schema.Constraint>(
264 schema: S,
265 options?: SchemaAST.ParseOptions
266): (
267 input: unknown,
268 options?: SchemaAST.ParseOptions
269) => Effect.Effect<S["Type"], SchemaIssue.Issue, S["DecodingServices"]> {
270 const parser = run<S["Type"], S["DecodingServices"]>(schema.ast)
271 return options === undefined
272 ? parser
273 : (input, overrideOptions) => parser(input, mergeParseOptions(options, overrideOptions))
274}
275
276/**
277 * Creates an effectful decoder for input already typed as the schema's `Encoded`
278 * type.
279 *
280 * **When to use**
281 *
282 * Use when you already have input typed as the schema's `Encoded` type and
283 * need an `Effect` whose failure channel is `SchemaIssue.Issue`, while
284 * preserving decoding service requirements.
285 *
286 * **Details**
287 *
288 * The returned function succeeds with the decoded `Type` or fails with a
289 * `SchemaIssue.Issue`, preserving any decoding service requirements in the
290 * returned `Effect`.
291 *
292 * @see {@link decodeUnknownEffect} for untyped boundary input
293 * @see {@link encodeEffect} for the opposite direction
294 *
295 * @category decoding
296 * @since 4.0.0
297 */
298export const decodeEffect: <S extends Schema.Constraint>(
299 schema: S,
300 options?: SchemaAST.ParseOptions
301) => (
302 input: S["Encoded"],
303 options?: SchemaAST.ParseOptions
304) => Effect.Effect<S["Type"], SchemaIssue.Issue, S["DecodingServices"]> = decodeUnknownEffect
305
306/**
307 * Creates a Promise-based decoder for `unknown` input.
308 *
309 * **When to use**
310 *
311 * Use when you need to decode untyped input with a service-free schema and
312 * return a JavaScript `Promise`.
313 *
314 * **Details**
315 *
316 * The returned function resolves with the decoded `Type` on success and rejects
317 * with an `Error` whose cause is a `SchemaIssue.Issue` on decoding failure.
318 * Schema validation failures use the generic message `"Schema validation failed"`.
319 * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when
320 * human-readable details are needed.
321 *
322 * **Gotchas**
323 *
324 * Causes that contain defects, interruptions, or other non-schema reasons reject
325 * with an `Error` whose cause is the underlying `Cause`.
326 *
327 * @see {@link decodePromise} for input already typed as the schema's `Encoded` type
328 * @see {@link decodeUnknownEffect} for schemas that require decoding services or when failures should remain in `Effect`
329 *
330 * @category decoding
331 * @since 3.10.0
332 */
333export function decodeUnknownPromise<S extends Schema.ConstraintDecoder<unknown>>(
334 schema: S,
335 options?: SchemaAST.ParseOptions
336): (input: unknown, options?: SchemaAST.ParseOptions) => Promise<S["Type"]> {
337 return asPromise(decodeUnknownEffect(schema, options))
338}
339
340/**
341 * Creates a Promise-based decoder for input already typed as the schema's
342 * `Encoded` type.
343 *
344 * **When to use**
345 *
346 * Use when you already have input typed as the schema's `Encoded` type and need
347 * decoding to return a JavaScript `Promise`.
348 *
349 * **Details**
350 *
351 * The returned function resolves with the decoded `Type` on success and rejects
352 * with an `Error` whose cause is a `SchemaIssue.Issue` on decoding failure.
353 * Schema validation failures use the generic message `"Schema validation failed"`.
354 * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when
355 * human-readable details are needed.
356 *
357 * **Gotchas**
358 *
359 * Causes that contain defects, interruptions, or other non-schema reasons reject
360 * with an `Error` whose cause is the underlying `Cause`.
361 *
362 * @see {@link decodeUnknownPromise} for untyped input returning a JavaScript `Promise`
363 * @see {@link decodeEffect} for preserving decoding services and failures in `Effect`
364 *
365 * @category decoding
366 * @since 3.10.0
367 */
368export function decodePromise<S extends Schema.ConstraintDecoder<unknown>>(
369 schema: S,
370 options?: SchemaAST.ParseOptions
371): (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Promise<S["Type"]> {
372 return asPromise(decodeEffect(schema, options))
373}
374
375/**
376 * Creates a synchronous decoder for `unknown` input that reports failure safely
377 * as an `Exit`.
378 *
379 * **When to use**
380 *
381 * Use when you need to decode unknown input synchronously into an `Exit` whose
382 * failure contains `SchemaIssue.Issue`.
383 *
384 * **Details**
385 *
386 * The returned function produces `Exit.Success` with the decoded `Type`.
387 * Schema issues are represented by an `Exit.Failure` cause containing a
388 * `SchemaIssue.Issue`.
389 *
390 * **Gotchas**
391 *
392 * Because this adapter runs synchronously, async decoding work can produce an
393 * `Exit.Failure` with a defect cause. When the cause contains both schema
394 * issues and non-schema reasons, all reasons remain in the returned `Cause`.
395 *
396 * @see {@link decodeExit} for input already typed as the schema's `Encoded` type
397 * @see {@link decodeUnknownEffect} for preserving decoding services and failures in `Effect`
398 * @see {@link decodeUnknownResult} for returning schema issues as data
399 * @see {@link decodeUnknownSync} for throwing on decoding failure
400 *
401 * @category decoding
402 * @since 4.0.0
403 */
404export function decodeUnknownExit<S extends Schema.ConstraintDecoder<unknown>>(
405 schema: S,
406 options?: SchemaAST.ParseOptions
407): (input: unknown, options?: SchemaAST.ParseOptions) => Exit.Exit<S["Type"], SchemaIssue.Issue> {
408 return asExit(decodeUnknownEffect(schema, options))
409}
410
411/**
412 * Creates a synchronous decoder for input already typed as the schema's `Encoded`
413 * type, reporting failure safely as an `Exit`.
414 *
415 * **When to use**
416 *
417 * Use when you need synchronous decoding of already typed `Encoded` input into
418 * an `Exit` whose failure contains `SchemaIssue.Issue`.
419 *
420 * **Details**
421 *
422 * The returned function produces `Exit.Success` with the decoded `Type` or
423 * `Exit.Failure` with a `SchemaIssue.Issue`.
424 *
425 * **Gotchas**
426 *
427 * Because this adapter runs synchronously, async decoding work can produce an
428 * `Exit.Failure` with a defect cause. When the cause contains both schema
429 * issues and non-schema reasons, all reasons remain in the returned `Cause`.
430 *
431 * @see {@link decodeUnknownExit} for untyped input with the same `Exit` result shape
432 * @see {@link decodeEffect} for preserving decoding services and failures in `Effect`
433 *
434 * @category decoding
435 * @since 4.0.0
436 */
437export const decodeExit: <S extends Schema.ConstraintDecoder<unknown>>(
438 schema: S,
439 options?: SchemaAST.ParseOptions
440) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Exit.Exit<S["Type"], SchemaIssue.Issue> =
441 decodeUnknownExit
442
443/** @internal */
444export function decodeUnknownOption<S extends Schema.ConstraintDecoder<unknown>>(
445 schema: S,
446 options?: SchemaAST.ParseOptions
447): (input: unknown, options?: SchemaAST.ParseOptions) => Option.Option<S["Type"]> {
448 return asOption(decodeUnknownEffect(schema, options))
449}
450
451/** @internal */
452export const decodeOption: <S extends Schema.ConstraintDecoder<unknown>>(
453 schema: S,
454 options?: SchemaAST.ParseOptions
455) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Option.Option<S["Type"]> = decodeUnknownOption
456
457/**
458 * Creates a decoder for `unknown` input that reports failure safely as a
459 * `Result`.
460 *
461 * **When to use**
462 *
463 * Use when decoding untyped boundary input and you want `SchemaIssue.Issue`
464 * failures returned as data in a `Result`.
465 *
466 * **Details**
467 *
468 * The returned function produces `Result.succeed` with the decoded `Type` on
469 * success or `Result.fail` with a `SchemaIssue.Issue` on decoding failure.
470 *
471 * **Gotchas**
472 *
473 * This adapter runs synchronously. Causes made entirely of schema issues become
474 * `Result.fail`, but causes that contain defects, interruptions, or asynchronous
475 * work at this synchronous boundary throw instead.
476 *
477 * @see {@link decodeResult} for input already typed as the schema's `Encoded` type
478 * @see {@link decodeUnknownEffect} for effectful or service-requiring decoding
479 *
480 * @category decoding
481 * @since 4.0.0
482 */
483export function decodeUnknownResult<S extends Schema.ConstraintDecoder<unknown>>(
484 schema: S,
485 options?: SchemaAST.ParseOptions
486): (input: unknown, options?: SchemaAST.ParseOptions) => Result.Result<S["Type"], SchemaIssue.Issue> {
487 return asResult(decodeUnknownEffect(schema, options))
488}
489
490/**
491 * Creates a decoder for input already typed as the schema's `Encoded` type,
492 * reporting failure safely as a `Result`.
493 *
494 * **When to use**
495 *
496 * Use when you already have input typed as the schema's `Encoded` type and want
497 * schema decoding failures represented as `Result.fail` with `SchemaIssue.Issue`.
498 *
499 * **Details**
500 *
501 * The returned function produces `Result.succeed` with the decoded `Type` on
502 * success or `Result.fail` with a `SchemaIssue.Issue` on decoding failure.
503 *
504 * **Gotchas**
505 *
506 * This synchronous adapter returns `Result.fail` for causes made entirely of
507 * schema issues, but causes that contain defects, interruptions, or other
508 * non-schema reasons throw instead.
509 *
510 * @see {@link decodeUnknownResult} for untyped input with the same `Result` shape
511 * @see {@link decodeEffect} for effectful or service-requiring decoding
512 *
513 * @category decoding
514 * @since 4.0.0
515 */
516export const decodeResult: <S extends Schema.ConstraintDecoder<unknown>>(
517 schema: S,
518 options?: SchemaAST.ParseOptions
519) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Result.Result<S["Type"], SchemaIssue.Issue> =
520 decodeUnknownResult
521
522/**
523 * Creates a synchronous decoder for `unknown` input.
524 *
525 * **When to use**
526 *
527 * Use to decode untrusted or dynamically typed input at a synchronous boundary
528 * where invalid data should throw an `Error` whose cause is `SchemaIssue.Issue`.
529 *
530 * **Details**
531 *
532 * The returned function returns the decoded `Type` on success and throws an
533 * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure.
534 * Schema validation failures use the generic message `"Schema validation failed"`.
535 * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when
536 * human-readable details are needed.
537 *
538 * **Gotchas**
539 *
540 * Causes that contain defects, interruptions, or asynchronous work at this
541 * synchronous boundary throw an `Error` whose cause is the underlying `Cause`,
542 * instead of being converted to a schema validation error.
543 *
544 * @see {@link decodeSync} for input already typed as the schema's `Encoded` type
545 * @see {@link decodeUnknownEffect} for preserving decoding failures in `Effect`
546 * @see {@link decodeUnknownResult} for returning schema issues as data
547 *
548 * @category decoding
549 * @since 3.10.0
550 */
551export function decodeUnknownSync<S extends Schema.ConstraintDecoder<unknown>>(
552 schema: S,
553 options?: SchemaAST.ParseOptions
554): (input: unknown, options?: SchemaAST.ParseOptions) => S["Type"] {
555 return CompilerRegistry.compilerAdaptersEnabled
556 ? makeSync(schema.ast, options)
557 : asSync(decodeUnknownEffect(schema, options))
558}
559
560/**
561 * Creates a synchronous decoder for input already typed as the schema's `Encoded`
562 * type.
563 *
564 * **When to use**
565 *
566 * Use to decode values already typed as the schema's `Encoded` input when
567 * decoding failure should throw an `Error` whose cause is `SchemaIssue.Issue`.
568 *
569 * **Details**
570 *
571 * The returned function returns the decoded `Type` on success and throws an
572 * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure.
573 * Schema validation failures use the generic message `"Schema validation failed"`.
574 * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when
575 * human-readable details are needed.
576 *
577 * **Gotchas**
578 *
579 * Causes that contain defects, interruptions, or asynchronous work at this
580 * synchronous boundary throw an `Error` whose cause is the underlying `Cause`,
581 * instead of being converted to a schema validation error.
582 *
583 * @see {@link decodeUnknownSync} for untrusted or dynamically typed input
584 * @see {@link decodeResult} for returning schema issues as data
585 * @see {@link decodeEffect} for preserving decoding failures in `Effect`
586 *
587 * @category decoding
588 * @since 3.10.0
589 */
590export const decodeSync: <S extends Schema.ConstraintDecoder<unknown>>(
591 schema: S,
592 options?: SchemaAST.ParseOptions
593) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => S["Type"] = decodeUnknownSync
594
595/**
596 * Creates an effectful encoder for `unknown` input.
597 *
598 * **When to use**
599 *
600 * Use when you need to encode untyped boundary input in an `Effect` whose
601 * failure channel is `SchemaIssue.Issue`, while preserving service
602 * requirements.
603 *
604 * **Details**
605 *
606 * The returned function succeeds with the schema's `Encoded` value or fails with a
607 * `SchemaIssue.Issue`. Encoding service requirements are preserved in the returned
608 * `Effect`. Parse options may be provided when creating the encoder and overridden
609 * when applying it.
610 *
611 * @see {@link encodeEffect} for the typed-input variant when the value is already typed as the schema's decoded `Type`
612 *
613 * @category encoding
614 * @since 4.0.0
615 */
616export function encodeUnknownEffect<S extends Schema.Constraint>(
617 schema: S,
618 options?: SchemaAST.ParseOptions
619): (
620 input: unknown,
621 options?: SchemaAST.ParseOptions
622) => Effect.Effect<S["Encoded"], SchemaIssue.Issue, S["EncodingServices"]> {
623 const parser = run<S["Encoded"], S["EncodingServices"]>(SchemaAST.flip(schema.ast))
624 return options === undefined
625 ? parser
626 : (input, overrideOptions) => parser(input, mergeParseOptions(options, overrideOptions))
627}
628
629/**
630 * Creates an effectful encoder for input already typed as the schema's decoded
631 * `Type`.
632 *
633 * **When to use**
634 *
635 * Use when you need to encode values already typed as the schema's decoded
636 * `Type` in an `Effect` whose failure channel is `SchemaIssue.Issue`, while
637 * preserving service requirements.
638 *
639 * **Details**
640 *
641 * The returned function succeeds with the schema's `Encoded` value or fails with a
642 * `SchemaIssue.Issue`, preserving any encoding service requirements in the
643 * returned `Effect`.
644 *
645 * @see {@link encodeUnknownEffect} for encoding unknown input before the value is statically typed as the schema's `Type`
646 *
647 * @category encoding
648 * @since 4.0.0
649 */
650export const encodeEffect: <S extends Schema.Constraint>(
651 schema: S,
652 options?: SchemaAST.ParseOptions
653) => (
654 input: S["Type"],
655 options?: SchemaAST.ParseOptions
656) => Effect.Effect<S["Encoded"], SchemaIssue.Issue, S["EncodingServices"]> = encodeUnknownEffect
657
658/**
659 * Creates a Promise-based encoder for `unknown` input.
660 *
661 * **When to use**
662 *
663 * Use when you need to encode untrusted or dynamically typed values with a
664 * service-free schema and return a JavaScript `Promise`.
665 *
666 * **Details**
667 *
668 * The returned function resolves with the schema's `Encoded` value on success and
669 * rejects with an `Error` whose cause is a `SchemaIssue.Issue` on encoding failure.
670 * Schema validation failures use the generic message `"Schema validation failed"`.
671 * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when
672 * human-readable details are needed.
673 *
674 * **Gotchas**
675 *
676 * Causes that contain defects, interruptions, or other non-schema reasons reject
677 * with an `Error` whose cause is the underlying `Cause`.
678 *
679 * @see {@link encodePromise} for input already typed as the schema's decoded `Type`
680 * @see {@link encodeUnknownEffect} for schemas that require encoding services or when failures should remain in `Effect`
681 *
682 * @category encoding
683 * @since 3.10.0
684 */
685export const encodeUnknownPromise = <S extends Schema.ConstraintEncoder<unknown>>(
686 schema: S,
687 options?: SchemaAST.ParseOptions
688): (input: unknown, options?: SchemaAST.ParseOptions) => Promise<S["Encoded"]> =>
689 asPromise(encodeUnknownEffect(schema, options))
690
691/**
692 * Creates a Promise-based encoder for input already typed as the schema's decoded
693 * `Type`.
694 *
695 * **When to use**
696 *
697 * Use when you already have values typed as the schema's decoded `Type` and
698 * need encoding to return a JavaScript `Promise`.
699 *
700 * **Details**
701 *
702 * The returned function resolves with the schema's `Encoded` value on success and
703 * rejects with an `Error` whose cause is a `SchemaIssue.Issue` on encoding failure.
704 * Schema validation failures use the generic message `"Schema validation failed"`.
705 * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when
706 * human-readable details are needed.
707 *
708 * **Gotchas**
709 *
710 * Causes that contain defects, interruptions, or other non-schema reasons reject
711 * with an `Error` whose cause is the underlying `Cause`.
712 *
713 * @see {@link encodeUnknownPromise} for encoding untyped input
714 * @see {@link encodeEffect} for effectful encoding or schemas with encoding service requirements
715 *
716 * @category encoding
717 * @since 3.10.0
718 */
719export const encodePromise: <S extends Schema.ConstraintEncoder<unknown>>(
720 schema: S,
721 options?: SchemaAST.ParseOptions
722) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Promise<S["Encoded"]> = encodeUnknownPromise
723
724/**
725 * Creates a synchronous encoder for `unknown` input that reports failure safely
726 * as an `Exit`.
727 *
728 * **When to use**
729 *
730 * Use when you need synchronous encoding of unknown input into an `Exit` whose
731 * failure contains `SchemaIssue.Issue`.
732 *
733 * **Details**
734 *
735 * The returned function produces `Exit.Success` with the schema's `Encoded` value
736 * or `Exit.Failure` with a `SchemaIssue.Issue`.
737 *
738 * **Gotchas**
739 *
740 * Because this adapter runs synchronously, async encoding work can produce an
741 * `Exit.Failure` with a defect cause. When the cause contains both schema
742 * issues and non-schema reasons, all reasons remain in the returned `Cause`.
743 *
744 * @see {@link encodeExit} for input already typed as the schema's decoded `Type`
745 * @see {@link encodeUnknownEffect} for effectful encoding that preserves service requirements
746 *
747 * @category encoding
748 * @since 4.0.0
749 */
750export function encodeUnknownExit<S extends Schema.ConstraintEncoder<unknown>>(
751 schema: S,
752 options?: SchemaAST.ParseOptions
753): (input: unknown, options?: SchemaAST.ParseOptions) => Exit.Exit<S["Encoded"], SchemaIssue.Issue> {
754 return asExit(encodeUnknownEffect(schema, options))
755}
756
757/**
758 * Creates a synchronous encoder for input already typed as the schema's decoded
759 * `Type`, reporting failure safely as an `Exit`.
760 *
761 * **When to use**
762 *
763 * Use when you need synchronous encoding of already typed schema values into
764 * an `Exit` whose failure contains `SchemaIssue.Issue`.
765 *
766 * **Details**
767 *
768 * The returned function produces `Exit.Success` with the schema's `Encoded` value
769 * or `Exit.Failure` with a `SchemaIssue.Issue`.
770 *
771 * **Gotchas**
772 *
773 * Because this adapter runs synchronously, async encoding work can produce an
774 * `Exit.Failure` with a defect cause. When the cause contains both schema
775 * issues and non-schema reasons, all reasons remain in the returned `Cause`.
776 *
777 * @see {@link encodeUnknownExit} for unknown input with the same `Exit` result shape
778 * @see {@link encodeEffect} for effectful encoding that preserves service requirements
779 *
780 * @category encoding
781 * @since 4.0.0
782 */
783export const encodeExit: <S extends Schema.ConstraintEncoder<unknown>>(
784 schema: S,
785 options?: SchemaAST.ParseOptions
786) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Exit.Exit<S["Encoded"], SchemaIssue.Issue> =
787 encodeUnknownExit
788
789/** @internal */
790export function encodeUnknownOption<S extends Schema.ConstraintEncoder<unknown>>(
791 schema: S,
792 options?: SchemaAST.ParseOptions
793): (input: unknown, options?: SchemaAST.ParseOptions) => Option.Option<S["Encoded"]> {
794 return asOption(encodeUnknownEffect(schema, options))
795}
796
797/** @internal */
798export const encodeOption: <S extends Schema.ConstraintEncoder<unknown>>(
799 schema: S,
800 options?: SchemaAST.ParseOptions
801) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Option.Option<S["Encoded"]> = encodeUnknownOption
802
803/**
804 * Creates an encoder for `unknown` input that reports failure safely as a
805 * `Result`.
806 *
807 * **When to use**
808 *
809 * Use when encoding values from an unknown or dynamically typed boundary
810 * synchronously, and you want `SchemaIssue.Issue` failures returned as `Result`
811 * data.
812 *
813 * **Details**
814 *
815 * The returned function produces `Result.succeed` with the schema's `Encoded`
816 * value on success or `Result.fail` with a `SchemaIssue.Issue` on encoding
817 * failure.
818 *
819 * **Gotchas**
820 *
821 * This adapter runs synchronously. Causes made entirely of schema issues become
822 * `Result.fail`, but causes that contain defects, interruptions, or asynchronous
823 * work at this synchronous boundary throw instead.
824 *
825 * @see {@link encodeResult} for input already typed as the schema's decoded `Type`
826 * @see {@link encodeUnknownEffect} for effectful encoding, including schemas with encoding service requirements
827 *
828 * @category encoding
829 * @since 4.0.0
830 */
831export function encodeUnknownResult<S extends Schema.ConstraintEncoder<unknown>>(
832 schema: S,
833 options?: SchemaAST.ParseOptions
834): (input: unknown, options?: SchemaAST.ParseOptions) => Result.Result<S["Encoded"], SchemaIssue.Issue> {
835 return asResult(encodeUnknownEffect(schema, options))
836}
837
838/**
839 * Creates an encoder for input already typed as the schema's decoded `Type`,
840 * reporting failure safely as a `Result`.
841 *
842 * **When to use**
843 *
844 * Use when you already have input typed as the schema's decoded `Type` and want
845 * encoding failures returned as `Result.fail` with `SchemaIssue.Issue`.
846 *
847 * **Details**
848 *
849 * The returned function produces `Result.succeed` with the schema's `Encoded`
850 * value on success or `Result.fail` with a `SchemaIssue.Issue` on encoding
851 * failure.
852 *
853 * **Gotchas**
854 *
855 * This synchronous adapter returns `Result.fail` for causes made entirely of
856 * schema issues, but causes that contain defects, interruptions, or other
857 * non-schema reasons throw instead.
858 *
859 * @see {@link encodeUnknownResult} for the same `Result` shape when the input is not already typed
860 *
861 * @category encoding
862 * @since 4.0.0
863 */
864export const encodeResult: <S extends Schema.ConstraintEncoder<unknown>>(
865 schema: S,
866 options?: SchemaAST.ParseOptions
867) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Result.Result<S["Encoded"], SchemaIssue.Issue> =
868 encodeUnknownResult
869
870/**
871 * Creates a synchronous encoder for `unknown` input.
872 *
873 * **When to use**
874 *
875 * Use when you need to encode values from untyped input in synchronous code and
876 * want encoding failures to throw an `Error` whose cause is `SchemaIssue.Issue`.
877 *
878 * **Details**
879 *
880 * The returned function returns the schema's `Encoded` value on success and throws
881 * an `Error` with the `SchemaIssue.Issue` in its `cause` on encoding failure.
882 * Schema validation failures use the generic message `"Schema validation failed"`.
883 * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when
884 * human-readable details are needed.
885 *
886 * **Gotchas**
887 *
888 * Causes that contain defects, interruptions, or asynchronous work at this
889 * synchronous boundary throw an `Error` whose cause is the underlying `Cause`,
890 * instead of being converted to a schema validation error.
891 *
892 * @see {@link encodeSync} for input already typed as the schema's decoded `Type`
893 * @see {@link encodeUnknownEffect} for effectful encoding that preserves service requirements
894 *
895 * @category encoding
896 * @since 3.10.0
897 */
898export function encodeUnknownSync<S extends Schema.ConstraintEncoder<unknown>>(
899 schema: S,
900 options?: SchemaAST.ParseOptions
901): (input: unknown, options?: SchemaAST.ParseOptions) => S["Encoded"] {
902 return CompilerRegistry.compilerAdaptersEnabled
903 ? makeSync(SchemaAST.flip(schema.ast), options)
904 : asSync(encodeUnknownEffect(schema, options))
905}
906
907/**
908 * Creates a synchronous encoder for input already typed as the schema's decoded
909 * `Type`.
910 *
911 * **When to use**
912 *
913 * Use to encode already typed schema values synchronously when encoding failure
914 * should throw an `Error` whose cause is `SchemaIssue.Issue`.
915 *
916 * **Details**
917 *
918 * The returned function returns the schema's `Encoded` value on success and throws
919 * an `Error` with the `SchemaIssue.Issue` in its `cause` on encoding failure.
920 * Schema validation failures use the generic message `"Schema validation failed"`.
921 * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when
922 * human-readable details are needed.
923 *
924 * **Gotchas**
925 *
926 * Causes that contain defects, interruptions, or asynchronous work at this
927 * synchronous boundary throw an `Error` whose cause is the underlying `Cause`,
928 * instead of being converted to a schema validation error.
929 *
930 * @see {@link encodeUnknownSync} for unknown input with the same throwing boundary
931 * @see {@link encodeResult} for returning schema issues as data
932 * @see {@link encodeEffect} for effectful encoding that preserves service requirements
933 *
934 * @category encoding
935 * @since 3.10.0
936 */
937export const encodeSync: <S extends Schema.ConstraintEncoder<unknown>>(
938 schema: S,
939 options?: SchemaAST.ParseOptions
940) => (input: S["Type"], options?: SchemaAST.ParseOptions) => S["Encoded"] = encodeUnknownSync
941
942const mergeParseOptions = (
943 options: SchemaAST.ParseOptions,
944 overrideOptions: SchemaAST.ParseOptions | undefined
945): SchemaAST.ParseOptions => overrideOptions ? { ...options, ...overrideOptions } : options
946
947const getValue = (value: unknown): Effect.Effect<any, SchemaIssue.Issue> => {
948 if (value === InternalParser.missing) {
949 return Effect.fail(new SchemaIssue.InvalidValue())
950 }
951 return Effect.succeed(value)
952}
953
954/** @internal */
955export function run<T, R>(ast: SchemaAST.AST) {
956 return runWithCompiler<T, R>(normalCompiler, ast)
957}
958
959function parserResult<T, R>(
960 result: Effect.Effect<unknown, SchemaIssue.Issue, any>,
961 input: unknown
962): Effect.Effect<T, SchemaIssue.Issue, R> {
963 if (result === InternalParser.sameExit) {
964 return Effect.succeed(input) as Effect.Effect<T, SchemaIssue.Issue, R>
965 }
966 if (!effectIsExit(result)) {
967 return Effect.flatMapEager(result, getValue)
968 }
969 return (result as InternalParser.Success<unknown, SchemaIssue.Issue>)[InternalParser.args] ===
970 InternalParser.missing
971 ? getValue(InternalParser.missing)
972 : result as Effect.Effect<T, SchemaIssue.Issue, R>
973}
974
975function runWithCompiler<T, R>(compiler: Compiler, ast: SchemaAST.AST) {
976 let parser: Parser
977 return (input: unknown, options?: SchemaAST.ParseOptions): Effect.Effect<T, SchemaIssue.Issue, R> => {
978 const result = (parser ??= compiler(ast))(
979 input,
980 options ?? SchemaAST.defaultParseOptions
981 )
982 if (result === InternalParser.sameExit) {
983 return Effect.succeed(input) as Effect.Effect<T, SchemaIssue.Issue, R>
984 }
985 if (!effectIsExit(result)) {
986 return Effect.flatMapEager(result, getValue)
987 }
988 return (result as InternalParser.Success<unknown, SchemaIssue.Issue>)[InternalParser.args] ===
989 InternalParser.missing
990 ? getValue(InternalParser.missing)
991 : result as Effect.Effect<T, SchemaIssue.Issue, R>
992 }
993}
994
995function asPromise<T, E>(
996 parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect<T, SchemaIssue.Issue>
997): (input: E, options?: SchemaAST.ParseOptions) => Promise<T> {
998 return (input: E, options?: SchemaAST.ParseOptions) =>
999 Effect.runPromiseExit(parser(input, options)).then((exit) => {
1000 if (Exit.isSuccess(exit)) {
1001 return exit.value
1002 }
1003 const issue = InternalSchemaCause.getSchemaIssueOrThrow(
1004 exit.cause,
1005 "Promise adapter can only reject schema issues"
1006 )
1007 throw new Error("Schema validation failed", { cause: issue })
1008 })
1009}
1010
1011function asExit<T, E, R>(
1012 parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect<T, SchemaIssue.Issue, R>
1013): (input: E, options?: SchemaAST.ParseOptions) => Exit.Exit<T, SchemaIssue.Issue> {
1014 return (input: E, options?: SchemaAST.ParseOptions) => Effect.runSyncExit(parser(input, options) as any)
1015}
1016
1017/** @internal */
1018export function asOption<T, E, R>(
1019 parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect<T, SchemaIssue.Issue, R>
1020): (input: E, options?: SchemaAST.ParseOptions) => Option.Option<T> {
1021 const parserExit = asExit(parser)
1022 return (input: E, options?: SchemaAST.ParseOptions) => {
1023 const exit = parserExit(input, options)
1024 if (Exit.isSuccess(exit)) {
1025 return Option.some(exit.value)
1026 }
1027 InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Option adapter can only return none for schema issues")
1028 return Option.none()
1029 }
1030}
1031
1032function asResult<T, E, R>(
1033 parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect<T, SchemaIssue.Issue, R>
1034): (input: E, options?: SchemaAST.ParseOptions) => Result.Result<T, SchemaIssue.Issue> {
1035 const parserExit = asExit(parser)
1036 return (input: E, options?: SchemaAST.ParseOptions) => {
1037 const exit = parserExit(input, options)
1038 if (Exit.isSuccess(exit)) {
1039 return Result.succeed(exit.value)
1040 }
1041 return Result.fail(
1042 InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Result adapter can only return schema issues")
1043 )
1044 }
1045}
1046
1047function makeSync<T>(
1048 ast: SchemaAST.AST,
1049 options?: SchemaAST.ParseOptions
1050): (input: unknown, options?: SchemaAST.ParseOptions) => T {
1051 let run: ((input: unknown, options?: SchemaAST.ParseOptions) => T) | undefined
1052 return (input, overrideOptions) =>
1053 (run ??= makeSyncEntry<T>(CompilerRegistry.resolve(ast), options))(input, overrideOptions)
1054}
1055
1056function makeSyncEntry<T>(
1057 entry: CompilerRegistry.Entry,
1058 options?: SchemaAST.ParseOptions
1059): (input: unknown, options?: SchemaAST.ParseOptions) => T {
1060 const decode = entry.decode
1061 let detailed: ((input: unknown, options?: SchemaAST.ParseOptions) => T) | undefined
1062 const run = decode === undefined
1063 ? (input: unknown, parseOptions = SchemaAST.defaultParseOptions): T =>
1064 (detailed ??= makeDetailedSync<T>(entry))(input, parseOptions)
1065 : (input: unknown, parseOptions = SchemaAST.defaultParseOptions): T => {
1066 if (input === InternalParser.missing) {
1067 return (detailed ??= makeDetailedSync<T>(entry))(input, parseOptions)
1068 }
1069 let output: unknown
1070 try {
1071 output = decode(input, parseOptions)
1072 } catch (error) {
1073 InternalSchemaCause.getSchemaIssueOrThrow(Cause.die(error), "Sync adapter can only throw schema issues")
1074 throw error
1075 }
1076 if (output !== CompilerRegistry.invalid) return output as T
1077 return (detailed ??= makeDetailedSync<T>(entry))(input, parseOptions)
1078 }
1079 return options === undefined
1080 ? run
1081 : (input, overrideOptions) => run(input, mergeParseOptions(options, overrideOptions))
1082}
1083
1084function makeDetailedSync<T>(
1085 entry: CompilerRegistry.Entry
1086): (input: unknown, options?: SchemaAST.ParseOptions) => T {
1087 return asSync(runWithCompiler<T, never>(() => entry.decodeEffect, entry.ast))
1088}
1089
1090function asSync<T, E>(
1091 parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect<T, SchemaIssue.Issue>
1092): (input: E, options?: SchemaAST.ParseOptions) => T {
1093 const parserExit = asExit(parser)
1094 return (input: E, options?: SchemaAST.ParseOptions) => {
1095 const exit = parserExit(input, options)
1096 if (Exit.isSuccess(exit)) {
1097 return exit.value
1098 }
1099 const issue = InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Sync adapter can only throw schema issues")
1100 throw new Error("Schema validation failed", { cause: issue })
1101 }
1102}
1103
1104function runSync<T>(effect: Effect.Effect<T, SchemaIssue.Issue>, message: string): T {
1105 const exit = Effect.runSyncExit(effect)
1106 if (Exit.isSuccess(exit)) {
1107 return exit.value
1108 }
1109 const issue = InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, message)
1110 throw new Error("Schema validation failed", { cause: issue })
1111}
1112
1113function makeConstructorSync<T, E>(
1114 ast: SchemaAST.AST
1115): (input: E, options?: Schema.MakeOptions) => T {
1116 let entry: CompilerRegistry.Entry | undefined
1117 let parser: Parser | undefined
1118 return (input, options) => {
1119 entry ??= CompilerRegistry.resolve(ast)
1120 const parseOptions = options?.disableChecks
1121 ? options.parseOptions ? { ...options.parseOptions, disableChecks: true } : { disableChecks: true }
1122 : options?.parseOptions ?? SchemaAST.defaultParseOptions
1123 const make = entry.make
1124 if (make !== undefined && input !== InternalParser.missing) {
1125 let output: unknown
1126 try {
1127 output = make(input, parseOptions)
1128 } catch (error) {
1129 InternalSchemaCause.getSchemaIssueOrThrow(Cause.die(error), "Constructor adapter can only throw schema issues")
1130 throw error
1131 }
1132 if (output !== CompilerRegistry.invalid && output !== InternalParser.missing) return output as T
1133 }
1134 const result = (parser ??= entry.makeEffect)(input, parseOptions)
1135 return runSync(parserResult<T, never>(result, input), "Constructor adapter can only throw schema issues")
1136 }
1137}
1138
1139/** @internal */
1140export interface Parser {
1141 (
1142 input: unknown,
1143 options: SchemaAST.ParseOptions
1144 ): Effect.Effect<unknown, SchemaIssue.Issue, any>
1145}
1146
1147/** @internal */
1148export interface Compiler {
1149 (ast: SchemaAST.AST): Parser
1150}
1151
1152const normalCompiler: Compiler = (ast) => CompilerRegistry.resolve(ast).parser
1153const constructorCompiler: Compiler = (ast) => CompilerRegistry.resolve(ast).makeEffect