1import { describe, it } from "@effect/vitest"
2import {
3 Cause,
4 Deferred,
5 Effect,
6 Exit,
7 Fiber,
8 Option,
9 Ref,
10 Result,
11 Schema,
12 type SchemaAST,
13 SchemaGetter,
14 SchemaIssue,
15 SchemaParser,
16 SchemaTransformation
17} from "effect"
18import {
19 assertFalse,
20 assertSchemaIssueError,
21 assertTrue,
22 deepStrictEqual,
23 strictEqual,
24 throws
25} from "../utils/assert.ts"
26
27describe("SchemaParser", () => {
28 describe("sequential parsing", () => {
29 const cases = [
30 {
31 name: "Struct",
32 make: (item: Schema.Codec<string>) => Schema.Struct({ a: item, b: item }),
33 input: { a: "a", b: "b" }
34 },
35 { name: "Tuple", make: (item: Schema.Codec<string>) => Schema.Tuple([item, item]), input: ["a", "b"] },
36 { name: "Array", make: (item: Schema.Codec<string>) => Schema.Array(item), input: ["a", "b"] },
37 {
38 name: "Record",
39 make: (item: Schema.Codec<string>) => Schema.Record(Schema.String, item),
40 input: { a: "a", b: "b" }
41 }
42 ]
43 for (const { input, make, name } of cases) {
44 for (const errors of ["first", "all"] as const) {
45 it.effect(`${name} decodes and encodes asynchronous children sequentially (${errors})`, () =>
46 Effect.gen(function*() {
47 const calls: Array<string> = []
48 const getter = SchemaGetter.transformEffect<string, string>((value) => {
49 calls.push(`start ${value}`)
50 return Effect.gen(function*() {
51 yield* Effect.yieldNow
52 calls.push(`end ${value}`)
53 return value
54 })
55 })
56 const schema = make(Schema.String.pipe(Schema.decode({ decode: getter, encode: getter })))
57 deepStrictEqual(yield* SchemaParser.decodeUnknownEffect(schema)(input, { errors }), input)
58 deepStrictEqual(calls, ["start a", "end a", "start b", "end b"])
59 calls.length = 0
60 deepStrictEqual(yield* SchemaParser.encodeUnknownEffect(schema)(input, { errors }), input)
61 deepStrictEqual(calls, ["start a", "end a", "start b", "end b"])
62 }))
63 }
64 }
65 })
66
67 describe("product concurrency", () => {
68 const cases = [
69 {
70 name: "Struct",
71 make: (item: Schema.Codec<string>) => Schema.Struct({ a: item, b: item, c: item }),
72 input: { a: "a", b: "b", c: "c" }
73 },
74 { name: "Tuple", make: (item: Schema.Codec<string>) => Schema.Tuple([item, item, item]), input: ["a", "b", "c"] },
75 { name: "Array", make: (item: Schema.Codec<string>) => Schema.Array(item), input: ["a", "b", "c"] },
76 {
77 name: "Record",
78 make: (item: Schema.Codec<string>) => Schema.Record(Schema.String, item),
79 input: { a: "a", b: "b", c: "c" }
80 }
81 ]
82
83 for (const { input, make, name } of cases) {
84 it.effect(`${name} uses Effect.forEach bounded concurrency when decoding and encoding`, () =>
85 Effect.gen(function*() {
86 for (const operation of ["decode", "encode"] as const) {
87 const started = yield* Effect.forEach([0, 1, 2], () => Deferred.make<void>())
88 const releases = yield* Effect.forEach([0, 1, 2], () => Deferred.make<void>())
89 const getter = SchemaGetter.transformEffect<string, string>((value) => {
90 const index = value.charCodeAt(0) - 97
91 return Deferred.succeed(started[index], undefined).pipe(
92 Effect.andThen(Deferred.await(releases[index])),
93 Effect.as(value)
94 )
95 })
96 const schema = make(Schema.String.pipe(Schema.decode({ decode: getter, encode: getter })))
97 const parse = operation === "decode"
98 ? SchemaParser.decodeUnknownEffect(schema)
99 : SchemaParser.encodeUnknownEffect(schema)
100 const fiber = yield* parse(input, { concurrency: 2 }).pipe(Effect.forkChild)
101
102 yield* Deferred.await(started[0])
103 yield* Deferred.await(started[1])
104 assertFalse(yield* Deferred.isDone(started[2]))
105
106 yield* Deferred.succeed(releases[0], undefined)
107 yield* Deferred.await(started[2])
108 yield* Deferred.succeed(releases[1], undefined)
109 yield* Deferred.succeed(releases[2], undefined)
110
111 deepStrictEqual(yield* Fiber.join(fiber), input)
112 }
113 }))
114 }
115
116 it.effect(`supports concurrency: "unbounded"`, () =>
117 Effect.gen(function*() {
118 const started = yield* Effect.forEach([0, 1, 2], () => Deferred.make<void>())
119 const release = yield* Deferred.make<void>()
120 const getter = SchemaGetter.transformEffect<string, string>((value) => {
121 const index = value.charCodeAt(0) - 97
122 return Deferred.succeed(started[index], undefined).pipe(
123 Effect.andThen(Deferred.await(release)),
124 Effect.as(value)
125 )
126 })
127 const schema = Schema.Array(Schema.String.pipe(Schema.decode({ decode: getter, encode: getter })))
128 const fiber = yield* SchemaParser.decodeUnknownEffect(schema)(["a", "b", "c"], {
129 concurrency: "unbounded"
130 }).pipe(Effect.forkChild)
131
132 yield* Effect.forEach(started, Deferred.await, { discard: true })
133 yield* Deferred.succeed(release, undefined)
134 deepStrictEqual(yield* Fiber.join(fiber), ["a", "b", "c"])
135 }))
136
137 it.effect("uses the first concurrent failure to complete and interrupts pending children", () =>
138 Effect.gen(function*() {
139 const firstStarted = yield* Deferred.make<void>()
140 const firstInterrupted = yield* Deferred.make<void>()
141 const first = Schema.String.pipe(Schema.decode({
142 decode: SchemaGetter.transformEffect(() =>
143 Deferred.succeed(firstStarted, undefined).pipe(
144 Effect.andThen(Effect.never),
145 Effect.onInterrupt(() => Deferred.succeed(firstInterrupted, undefined).pipe(Effect.asVoid))
146 )
147 ),
148 encode: SchemaGetter.passthrough()
149 }))
150 const second = Schema.String.pipe(Schema.decode({
151 decode: SchemaGetter.transformEffect(() =>
152 Deferred.await(firstStarted).pipe(
153 Effect.andThen(Effect.fail(new SchemaIssue.InvalidValue({ message: "second failed" })))
154 )
155 ),
156 encode: SchemaGetter.passthrough()
157 }))
158
159 const exit = yield* SchemaParser.decodeUnknownEffect(Schema.Tuple([first, second]))(["a", "b"], {
160 concurrency: 2
161 }).pipe(Effect.exit)
162
163 assertTrue(Exit.isFailure(exit))
164 assertTrue(yield* Deferred.isDone(firstInterrupted))
165 if (Exit.isFailure(exit)) {
166 const error = Cause.findError(exit.cause)
167 assertTrue(Result.isSuccess(error))
168 if (Result.isSuccess(error)) {
169 strictEqual(error.success._tag, "Composite")
170 if (error.success._tag === "Composite") {
171 strictEqual(error.success.issues[0]._tag, "Pointer")
172 if (error.success.issues[0]._tag === "Pointer") {
173 deepStrictEqual(error.success.issues[0].path, [1])
174 }
175 }
176 }
177 }
178 }))
179
180 it.effect(`accumulates concurrent errors in completion order with errors: "all"`, () =>
181 Effect.gen(function*() {
182 const releaseFirst = yield* Deferred.make<void>()
183 const secondCompleted = yield* Deferred.make<void>()
184 const failing = (effect: Effect.Effect<void>) =>
185 Schema.String.pipe(Schema.decode({
186 decode: SchemaGetter.transformEffect(() =>
187 effect.pipe(Effect.andThen(Effect.fail(new SchemaIssue.InvalidValue())))
188 ),
189 encode: SchemaGetter.passthrough()
190 }))
191 const schema = Schema.Tuple([
192 failing(Deferred.await(releaseFirst)),
193 failing(Deferred.succeed(secondCompleted, undefined).pipe(Effect.asVoid))
194 ])
195 const fiber = yield* SchemaParser.decodeUnknownEffect(schema)(["a", "b"], {
196 concurrency: 2,
197 errors: "all"
198 }).pipe(Effect.exit, Effect.forkChild)
199
200 yield* Deferred.await(secondCompleted)
201 yield* Effect.yieldNow
202 yield* Deferred.succeed(releaseFirst, undefined)
203 const exit = yield* Fiber.join(fiber)
204
205 assertTrue(Exit.isFailure(exit))
206 if (Exit.isFailure(exit)) {
207 const error = Cause.findError(exit.cause)
208 assertTrue(Result.isSuccess(error))
209 if (Result.isSuccess(error)) {
210 strictEqual(error.success._tag, "Composite")
211 if (error.success._tag === "Composite") {
212 deepStrictEqual(
213 error.success.issues.map((issue) => issue._tag === "Pointer" ? issue.path : []),
214 [[1], [0]]
215 )
216 }
217 }
218 }
219 }))
220
221 it.effect("uses completion order for colliding transformed Record keys", () =>
222 Effect.gen(function*() {
223 const releaseFirst = yield* Deferred.make<void>()
224 const secondCompleted = yield* Deferred.make<void>()
225 const value = Schema.String.pipe(Schema.decode({
226 decode: SchemaGetter.transformEffect((value) =>
227 value === "first"
228 ? Deferred.await(releaseFirst).pipe(Effect.as(value))
229 : Deferred.succeed(secondCompleted, undefined).pipe(Effect.as(value))
230 ),
231 encode: SchemaGetter.passthrough()
232 }))
233 const schema = Schema.Record(
234 Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel())),
235 value
236 )
237 const fiber = yield* SchemaParser.decodeUnknownEffect(schema)({ a_b: "first", aB: "second" }, {
238 concurrency: 2
239 }).pipe(Effect.forkChild)
240
241 yield* Deferred.await(secondCompleted)
242 yield* Effect.yieldNow
243 yield* Deferred.succeed(releaseFirst, undefined)
244 deepStrictEqual(yield* Fiber.join(fiber), { aB: "first" })
245 }))
246
247 it.effect("applies the concurrency limit independently to nested products", () =>
248 Effect.gen(function*() {
249 const started = yield* Ref.make(0)
250 const allStarted = yield* Deferred.make<void>()
251 const release = yield* Deferred.make<void>()
252 const item = Schema.String.pipe(Schema.decode({
253 decode: SchemaGetter.transformEffect((value) =>
254 Ref.updateAndGet(started, (n) => n + 1).pipe(
255 Effect.tap((n) => n === 4 ? Deferred.succeed(allStarted, undefined) : Effect.void),
256 Effect.andThen(Deferred.await(release)),
257 Effect.as(value)
258 )
259 ),
260 encode: SchemaGetter.passthrough()
261 }))
262 const schema = Schema.Array(Schema.Array(item))
263 const input = [["a", "b"], ["c", "d"]]
264 const fiber = yield* SchemaParser.decodeUnknownEffect(schema)(input, { concurrency: 2 }).pipe(Effect.forkChild)
265
266 yield* Deferred.await(allStarted)
267 strictEqual(yield* Ref.get(started), 4)
268 yield* Deferred.succeed(release, undefined)
269 deepStrictEqual(yield* Fiber.join(fiber), input)
270 }))
271
272 it.effect("keeps Union member evaluation sequential", () =>
273 Effect.gen(function*() {
274 const firstStarted = yield* Deferred.make<void>()
275 const releaseFirst = yield* Deferred.make<void>()
276 const secondStarted = yield* Deferred.make<void>()
277 const first = Schema.String.pipe(Schema.decode({
278 decode: SchemaGetter.transformEffect(() =>
279 Deferred.succeed(firstStarted, undefined).pipe(
280 Effect.andThen(Deferred.await(releaseFirst)),
281 Effect.as("first")
282 )
283 ),
284 encode: SchemaGetter.passthrough()
285 }))
286 const second = Schema.String.pipe(Schema.decode({
287 decode: SchemaGetter.transformEffect(() =>
288 Deferred.succeed(secondStarted, undefined).pipe(Effect.as("second"))
289 ),
290 encode: SchemaGetter.passthrough()
291 }))
292 const fiber = yield* SchemaParser.decodeUnknownEffect(Schema.Union([first, second]))("value", {
293 concurrency: "unbounded"
294 }).pipe(Effect.forkChild)
295
296 yield* Deferred.await(firstStarted)
297 yield* Effect.yieldNow
298 assertFalse(yield* Deferred.isDone(secondStarted))
299 yield* Deferred.succeed(releaseFirst, undefined)
300 strictEqual(yield* Fiber.join(fiber), "first")
301 assertFalse(yield* Deferred.isDone(secondStarted))
302 }))
303
304 it("keeps synchronous product parsers eager", () => {
305 const result = SchemaParser.decodeUnknownEffect(Schema.Array(Schema.String))(["a", "b"], {
306 concurrency: "unbounded"
307 })
308 assertTrue(Exit.isExit(result))
309 if (Exit.isSuccess(result)) {
310 deepStrictEqual(result.value, ["a", "b"])
311 }
312 })
313
314 it.effect("applies concurrency to constructor defaults", () =>
315 Effect.gen(function*() {
316 const firstStarted = yield* Deferred.make<void>()
317 const secondStarted = yield* Deferred.make<void>()
318 const release = yield* Deferred.make<void>()
319 const field = (value: string, started: Deferred.Deferred<void>) =>
320 Schema.String.pipe(Schema.withConstructorDefault(
321 Deferred.succeed(started, undefined).pipe(
322 Effect.andThen(Deferred.await(release)),
323 Effect.as(value)
324 )
325 ))
326 const schema = Schema.Struct({
327 a: field("a", firstStarted),
328 b: field("b", secondStarted)
329 })
330 const fiber = yield* SchemaParser.makeEffect(schema)({}, {
331 parseOptions: { concurrency: 2 }
332 }).pipe(Effect.forkChild)
333
334 yield* Deferred.await(firstStarted)
335 yield* Deferred.await(secondStarted)
336 yield* Deferred.succeed(release, undefined)
337 deepStrictEqual(yield* Fiber.join(fiber), { a: "a", b: "b" })
338 }))
339 })
340
341 describe("makeEffect", () => {
342 it.effect("defers deriving the type AST until the maker is called and derives it once", () =>
343 Effect.gen(function*() {
344 let encodingReads = 0
345 const ast = new Proxy(Schema.String.ast, {
346 get(target, property, receiver) {
347 if (property === "encoding") {
348 encodingReads++
349 }
350 return Reflect.get(target, property, receiver)
351 }
352 })
353 const schema: { ast: SchemaAST.AST } = { ast }
354 const make = SchemaParser.makeEffect(schema as Schema.Codec<string>)
355 schema.ast = Schema.Number.ast
356
357 strictEqual(encodingReads, 0)
358 const effect = make("a")
359 assertTrue(encodingReads > 0)
360 strictEqual(yield* effect, "a")
361 const reads = encodingReads
362 strictEqual(yield* make("b"), "b")
363 strictEqual(encodingReads, reads)
364 }))
365 })
366
367 const makeMixedCause = () =>
368 Cause.combine(
369 Cause.fail(new SchemaIssue.InvalidValue({ message: "schema issue" })),
370 Cause.die(new Error("defect"))
371 )
372 describe("make", () => {
373 it("should throw an error when the input is invalid", () => {
374 const schema = Schema.String
375 throws(() => SchemaParser.make(schema)(null as any), (e) => {
376 assertSchemaIssueError(e, "Expected string")
377 })
378 })
379
380 it("should throw an error when the cause contains both an Issue and a defect", () => {
381 const schema = Schema.Struct({
382 a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(makeMixedCause())))
383 })
384
385 throws(() => SchemaParser.make(schema)({}), (e) => {
386 assertTrue(e instanceof Error)
387 strictEqual(e.message, "Constructor adapter can only throw schema issues")
388 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
389 })
390 })
391 })
392
393 describe("makeOption", () => {
394 it("should throw an error when the cause is not an Issue", () => {
395 const schema = Schema.Struct({
396 a: Schema.String.pipe(Schema.withConstructorDefault(Effect.die(new Error("make defect"))))
397 })
398
399 throws(() => SchemaParser.makeOption(schema)({}), (e) => {
400 assertTrue(e instanceof Error)
401 strictEqual(e.message, "Option adapter can only return none for schema issues")
402 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
403 })
404 })
405
406 it("should throw an error when the cause contains both an Issue and a defect", () => {
407 const schema = Schema.Struct({
408 a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(makeMixedCause())))
409 })
410
411 throws(() => SchemaParser.makeOption(schema)({}), (e) => {
412 assertTrue(e instanceof Error)
413 strictEqual(e.message, "Option adapter can only return none for schema issues")
414 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
415 })
416 })
417 })
418
419 describe("decodeUnknownSync / encodeUnknownSync", () => {
420 it("decodes inherited struct properties", () => {
421 const schema = Schema.Struct({
422 required: Schema.String,
423 optional: Schema.optionalKey(Schema.String),
424 defaulted: Schema.String.pipe(Schema.withDecodingDefaultKey(Effect.succeed("default"))),
425 own: Schema.String
426 })
427 const input = Object.assign(
428 Object.create({
429 required: "required",
430 optional: "optional",
431 defaulted: "inherited"
432 }),
433 { own: "own" }
434 )
435
436 const output = SchemaParser.decodeUnknownSync(schema)(input)
437
438 deepStrictEqual(output, {
439 required: "required",
440 optional: "optional",
441 defaulted: "inherited",
442 own: "own"
443 })
444 for (const key of ["required", "optional", "defaulted", "own"]) {
445 assertTrue(Object.hasOwn(output, key))
446 }
447
448 const encoded = SchemaParser.encodeUnknownSync(Schema.Struct({ required: Schema.String }))(
449 Object.create({ required: "required" })
450 )
451 deepStrictEqual(encoded, { required: "required" })
452 assertTrue(Object.hasOwn(encoded, "required"))
453
454 deepStrictEqual(SchemaParser.decodeUnknownSync(schema)({ required: "required", own: "own" }), {
455 required: "required",
456 defaulted: "default",
457 own: "own"
458 })
459 })
460
461 it("decodes inherited string, number, and symbol fields", () => {
462 const symbol = Symbol("field")
463 const schema = Schema.Struct({
464 text: Schema.String,
465 1: Schema.Number,
466 [symbol]: Schema.Boolean
467 })
468 const output = SchemaParser.decodeUnknownSync(schema)(Object.create({ text: "a", 1: 1, [symbol]: true }))
469
470 deepStrictEqual(output, { text: "a", 1: 1, [symbol]: true })
471 for (const key of ["text", "1", symbol]) assertTrue(Object.hasOwn(output, key))
472 })
473
474 it("distinguishes an inherited undefined property from a missing property", () => {
475 const schema = Schema.Struct({
476 value: Schema.String.pipe(Schema.withDecodingDefaultKey(Effect.succeed("default")))
477 })
478 const decode = SchemaParser.decodeUnknownSync(schema)
479
480 deepStrictEqual(decode({}), { value: "default" })
481 throws(() => decode(Object.create({ value: undefined })), (error) => {
482 assertSchemaIssueError(error, `Expected string\n at ["value"]`)
483 })
484 })
485
486 it("selects union members using inherited discriminants", () => {
487 const schema = Schema.Union([
488 Schema.Struct({ _tag: Schema.Literal("A"), value: Schema.String }),
489 Schema.Struct({ _tag: Schema.Literal("B"), value: Schema.Number })
490 ])
491 const input = Object.assign(Object.create({ _tag: "A" }), { value: "value" })
492 const output = SchemaParser.decodeUnknownSync(schema)(input)
493
494 deepStrictEqual(output, { _tag: "A", value: "value" })
495 assertTrue(Object.hasOwn(output, "_tag"))
496 })
497
498 it("decodes inherited Object.prototype properties", () => {
499 const output = SchemaParser.decodeUnknownSync(Schema.Struct({ toString: Schema.Unknown }))({})
500
501 strictEqual(output.toString, Object.prototype.toString)
502 assertTrue(Object.hasOwn(output, "toString"))
503 })
504
505 it("requires __proto__ to be an own property", () => {
506 const schema = Schema.Struct({ ["__proto__"]: Schema.String })
507 throws(() => SchemaParser.decodeUnknownSync(schema)(Object.create({ __proto__: "value" })))
508 deepStrictEqual(SchemaParser.decodeUnknownSync(schema)({ ["__proto__"]: "value" }), {
509 ["__proto__"]: "value"
510 })
511 })
512
513 it("keeps dynamic record properties own-only while finite record keys are declared", () => {
514 const input = Object.assign(Object.create({ inherited: 1 }), { own: 2 })
515 deepStrictEqual(SchemaParser.decodeUnknownSync(Schema.Record(Schema.String, Schema.Number))(input), { own: 2 })
516
517 const finite = Schema.Record(Schema.Literal("inherited"), Schema.Number)
518 deepStrictEqual(SchemaParser.decodeUnknownSync(finite)(input), { inherited: 1 })
519 })
520
521 it("should throw an error when the input is invalid", () => {
522 const schema = Schema.String
523 throws(() => SchemaParser.decodeUnknownSync(schema)(null), (e) => {
524 assertSchemaIssueError(e, "Expected string")
525 })
526 throws(() => SchemaParser.encodeUnknownSync(schema)(null), (e) => {
527 assertSchemaIssueError(e, "Expected string")
528 })
529 })
530
531 it("should throw an error when the cause contains both an Issue and a defect", () => {
532 const decodeSchema = Schema.String.pipe(Schema.decode({
533 decode: SchemaGetter.transformOptionalEffect(() => Effect.failCause(makeMixedCause())),
534 encode: SchemaGetter.passthrough()
535 }))
536 const encodeSchema = Schema.String.pipe(Schema.encode({
537 decode: SchemaGetter.passthrough(),
538 encode: SchemaGetter.transformOptionalEffect(() => Effect.failCause(makeMixedCause()))
539 }))
540
541 throws(() => SchemaParser.decodeUnknownSync(decodeSchema)("a"), (e) => {
542 assertTrue(e instanceof Error)
543 strictEqual(e.message, "Sync adapter can only throw schema issues")
544 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
545 })
546 throws(() => SchemaParser.encodeUnknownSync(encodeSchema)("a"), (e) => {
547 assertTrue(e instanceof Error)
548 strictEqual(e.message, "Sync adapter can only throw schema issues")
549 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
550 })
551 })
552 })
553
554 describe("decodeUnknownPromise / encodeUnknownPromise", () => {
555 it("should reject with an error when the input is invalid", async () => {
556 const schema = Schema.String
557 const r1 = await SchemaParser.decodeUnknownPromise(schema)(null).then(Result.succeed, Result.fail)
558 assertTrue(Result.isFailure(r1))
559 assertSchemaIssueError(r1.failure, "Expected string")
560 const r2 = await SchemaParser.encodeUnknownPromise(schema)(null).then(Result.succeed, Result.fail)
561 assertTrue(Result.isFailure(r2))
562 assertSchemaIssueError(r2.failure, "Expected string")
563 })
564
565 it("should reject with an error when the cause contains both an Issue and a defect", async () => {
566 const decodeSchema = Schema.String.pipe(Schema.decode({
567 decode: SchemaGetter.transformOptionalEffect(() => Effect.failCause(makeMixedCause())),
568 encode: SchemaGetter.passthrough()
569 }))
570 const encodeSchema = Schema.String.pipe(Schema.encode({
571 decode: SchemaGetter.passthrough(),
572 encode: SchemaGetter.transformOptionalEffect(() => Effect.failCause(makeMixedCause()))
573 }))
574
575 const r1 = await SchemaParser.decodeUnknownPromise(decodeSchema)("a").then(Result.succeed, Result.fail)
576 assertTrue(Result.isFailure(r1))
577 assertTrue(r1.failure instanceof Error)
578 strictEqual(r1.failure.message, "Promise adapter can only reject schema issues")
579 assertTrue(Cause.hasDies(r1.failure.cause as Cause.Cause<never>))
580
581 const r2 = await SchemaParser.encodeUnknownPromise(encodeSchema)("a").then(Result.succeed, Result.fail)
582 assertTrue(Result.isFailure(r2))
583 assertTrue(r2.failure instanceof Error)
584 strictEqual(r2.failure.message, "Promise adapter can only reject schema issues")
585 assertTrue(Cause.hasDies(r2.failure.cause as Cause.Cause<never>))
586 })
587 })
588
589 describe("decodeUnknownOption / encodeUnknownOption", () => {
590 it("should return none when the input is invalid", () => {
591 const schema = Schema.String
592 assertTrue(Option.isSome(SchemaParser.decodeUnknownOption(schema)("a")))
593 assertTrue(Option.isNone(SchemaParser.decodeUnknownOption(schema)(null)))
594 assertTrue(Option.isSome(SchemaParser.encodeUnknownOption(schema)("a")))
595 assertTrue(Option.isNone(SchemaParser.encodeUnknownOption(schema)(null)))
596 })
597
598 it("should throw an error when the cause is not an Issue", () => {
599 const decodeSchema = Schema.String.pipe(Schema.decode({
600 decode: SchemaGetter.transformOptionalEffect(() => Effect.die(new Error("decode defect"))),
601 encode: SchemaGetter.passthrough()
602 }))
603 const encodeSchema = Schema.String.pipe(Schema.encode({
604 decode: SchemaGetter.passthrough(),
605 encode: SchemaGetter.transformOptionalEffect(() => Effect.die(new Error("encode defect")))
606 }))
607
608 throws(() => SchemaParser.decodeUnknownOption(decodeSchema)("a"), (e) => {
609 assertTrue(e instanceof Error)
610 strictEqual(e.message, "Option adapter can only return none for schema issues")
611 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
612 })
613 throws(() => SchemaParser.encodeUnknownOption(encodeSchema)("a"), (e) => {
614 assertTrue(e instanceof Error)
615 strictEqual(e.message, "Option adapter can only return none for schema issues")
616 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
617 })
618 })
619
620 it("should throw an error when the cause contains both an Issue and a defect", () => {
621 const decodeSchema = Schema.String.pipe(Schema.decode({
622 decode: SchemaGetter.transformOptionalEffect(() => Effect.failCause(makeMixedCause())),
623 encode: SchemaGetter.passthrough()
624 }))
625 const encodeSchema = Schema.String.pipe(Schema.encode({
626 decode: SchemaGetter.passthrough(),
627 encode: SchemaGetter.transformOptionalEffect(() => Effect.failCause(makeMixedCause()))
628 }))
629
630 throws(() => SchemaParser.decodeUnknownOption(decodeSchema)("a"), (e) => {
631 assertTrue(e instanceof Error)
632 strictEqual(e.message, "Option adapter can only return none for schema issues")
633 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
634 })
635 throws(() => SchemaParser.encodeUnknownOption(encodeSchema)("a"), (e) => {
636 assertTrue(e instanceof Error)
637 strictEqual(e.message, "Option adapter can only return none for schema issues")
638 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
639 })
640 })
641 })
642
643 describe("is", () => {
644 it("should return false when the input is invalid", () => {
645 const is = SchemaParser.is(Schema.String)
646 strictEqual(is("a"), true)
647 strictEqual(is(null), false)
648 })
649
650 it("should throw an error when the cause is not an Issue", () => {
651 const schema = Schema.declareConstructor<string>()(
652 [],
653 () => () => Effect.die(new Error("is defect"))
654 )
655
656 throws(() => SchemaParser.is(schema)("a"), (e) => {
657 assertTrue(e instanceof Error)
658 strictEqual(e.message, "Type guard adapter can only return false for schema issues")
659 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
660 })
661 })
662
663 it("should throw an error when the cause contains both an Issue and a defect", () => {
664 const schema = Schema.declareConstructor<string>()(
665 [],
666 () => () => Effect.failCause(makeMixedCause())
667 )
668
669 throws(() => SchemaParser.is(schema)("a"), (e) => {
670 assertTrue(e instanceof Error)
671 strictEqual(e.message, "Type guard adapter can only return false for schema issues")
672 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
673 })
674 })
675 })
676
677 describe("asserts", () => {
678 it("should use the default parse options", () => {
679 const schema = Schema.Struct({ a: Schema.String, b: Schema.String })
680
681 // `errors: "all"` would report both keys
682 throws(() => SchemaParser.asserts(schema, { a: 1, b: 2 }), (e) => {
683 assertSchemaIssueError(e, `Expected string\n at ["a"]`)
684 })
685
686 // `onExcessProperty: "error"` would reject the excess key
687 SchemaParser.asserts(schema, { a: "a", b: "b", c: 1 })
688 })
689
690 it("should assert the type side of the schema", () => {
691 const schema = Schema.Struct({ a: Schema.FiniteFromString })
692
693 SchemaParser.asserts(schema, { a: 1 })
694
695 throws(() => SchemaParser.asserts(schema, { a: "1" }), (e) => {
696 assertSchemaIssueError(e, `Expected number\n at ["a"]`)
697 })
698 })
699
700 it("should throw an error when the cause is not an Issue", () => {
701 const schema = Schema.declareConstructor<string>()(
702 [],
703 () => () => Effect.die(new Error("assert defect"))
704 )
705
706 throws(() => SchemaParser.asserts(schema, "a"), (e) => {
707 assertTrue(e instanceof Error)
708 strictEqual(e.message, "Assertion adapter can only throw schema issues")
709 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
710 })
711 })
712
713 it("should throw an error when the cause contains both an Issue and a defect", () => {
714 const schema = Schema.declareConstructor<string>()(
715 [],
716 () => () => Effect.failCause(makeMixedCause())
717 )
718
719 throws(() => SchemaParser.asserts(schema, "a"), (e) => {
720 assertTrue(e instanceof Error)
721 strictEqual(e.message, "Assertion adapter can only throw schema issues")
722 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
723 })
724 })
725 })
726
727 describe("decodeUnknownResult / encodeUnknownResult", () => {
728 it("should throw an error when the cause is not an Issue", () => {
729 const decodeSchema = Schema.String.pipe(Schema.decode({
730 decode: SchemaGetter.transformOptionalEffect(() => Effect.die(new Error("decode defect"))),
731 encode: SchemaGetter.passthrough()
732 }))
733 const encodeSchema = Schema.String.pipe(Schema.encode({
734 decode: SchemaGetter.passthrough(),
735 encode: SchemaGetter.transformOptionalEffect(() => Effect.die(new Error("encode defect")))
736 }))
737
738 throws(() => SchemaParser.decodeUnknownResult(decodeSchema)("a"), (e) => {
739 assertTrue(e instanceof Error)
740 strictEqual(e.message, "Result adapter can only return schema issues")
741 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
742 })
743 throws(() => SchemaParser.encodeUnknownResult(encodeSchema)("a"), (e) => {
744 assertTrue(e instanceof Error)
745 strictEqual(e.message, "Result adapter can only return schema issues")
746 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
747 })
748 })
749
750 it("should throw an error when the cause contains both an Issue and a defect", () => {
751 const decodeSchema = Schema.String.pipe(Schema.decode({
752 decode: SchemaGetter.transformOptionalEffect(() => Effect.failCause(makeMixedCause())),
753 encode: SchemaGetter.passthrough()
754 }))
755 const encodeSchema = Schema.String.pipe(Schema.encode({
756 decode: SchemaGetter.passthrough(),
757 encode: SchemaGetter.transformOptionalEffect(() => Effect.failCause(makeMixedCause()))
758 }))
759
760 throws(() => SchemaParser.decodeUnknownResult(decodeSchema)("a"), (e) => {
761 assertTrue(e instanceof Error)
762 strictEqual(e.message, "Result adapter can only return schema issues")
763 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
764 })
765 throws(() => SchemaParser.encodeUnknownResult(encodeSchema)("a"), (e) => {
766 assertTrue(e instanceof Error)
767 strictEqual(e.message, "Result adapter can only return schema issues")
768 assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
769 })
770 })
771 })
772
773 describe("decodeUnknownExit", () => {
774 it("preserves values that use Effect data protocols", () => {
775 let executions = 0
776 const suspended = Effect.sync(() => {
777 executions++
778 return "a"
779 })
780 const decode = SchemaParser.decodeUnknownExit(Schema.Unknown)
781 const values = [Option.none(), Option.some("a"), Exit.succeed("a"), Exit.fail("a"), suspended]
782
783 for (const value of values) {
784 const result = decode(value)
785 assertTrue(Exit.isSuccess(result))
786 strictEqual(result.value, value)
787 }
788 strictEqual(executions, 0)
789
790 const failure = Exit.fail("a")
791 const unionResult = SchemaParser.decodeUnknownExit(Schema.Union([Schema.Unknown]))(failure)
792 assertTrue(Exit.isSuccess(unionResult))
793 strictEqual(unionResult.value, failure)
794 })
795
796 it("distinguishes missing tuple elements from undefined", () => {
797 const decode = SchemaParser.decodeUnknownExit(
798 Schema.Tuple([Schema.optionalKey(Schema.Undefined)])
799 )
800
801 const empty = decode([])
802 assertTrue(Exit.isSuccess(empty))
803 strictEqual(empty.value.length, 0)
804
805 const present = decode([undefined])
806 assertTrue(Exit.isSuccess(present))
807 strictEqual(present.value.length, 1)
808 assertTrue(Object.hasOwn(present.value, 0))
809
810 const sparse = decode(new Array(1))
811 assertTrue(Exit.isSuccess(sparse))
812 strictEqual(sparse.value.length, 1)
813 assertTrue(Object.hasOwn(sparse.value, 0))
814 })
815
816 it("preserves missing optional template literals", () => {
817 const template = Schema.optionalKey(Schema.TemplateLiteral(["a", Schema.String]))
818 const decodeTuple = SchemaParser.decodeUnknownExit(Schema.Tuple([template]))
819 const decodeStruct = SchemaParser.decodeUnknownExit(Schema.Struct({ value: template }))
820
821 const tuple = decodeTuple([])
822 assertTrue(Exit.isSuccess(tuple))
823 strictEqual(tuple.value.length, 0)
824
825 const struct = decodeStruct({})
826 assertTrue(Exit.isSuccess(struct))
827 assertTrue(!Object.hasOwn(struct.value, "value"))
828 })
829
830 it("reads property and index values once", () => {
831 let propertyReads = 0
832 let indexReads = 0
833 const propertyInput = {
834 get value(): unknown {
835 propertyReads++
836 return propertyReads === 1 ? "a" : 1
837 }
838 }
839 const indexInput = {
840 get value(): unknown {
841 indexReads++
842 return indexReads === 1 ? "a" : 1
843 }
844 }
845
846 const property = SchemaParser.decodeUnknownExit(Schema.Struct({ value: Schema.String }))(propertyInput)
847 assertTrue(Exit.isSuccess(property))
848 strictEqual(property.value.value, "a")
849 strictEqual(propertyReads, 1)
850
851 const index = SchemaParser.decodeUnknownExit(Schema.Record(Schema.String, Schema.String))(indexInput)
852 assertTrue(Exit.isSuccess(index))
853 strictEqual(index.value.value, "a")
854 strictEqual(indexReads, 1)
855
856 let rejectedReads = 0
857 const rejectedKey = Schema.String.pipe(Schema.decode({
858 decode: SchemaGetter.transformOptionalEffect(() => Effect.fail(new SchemaIssue.InvalidValue())),
859 encode: SchemaGetter.passthrough()
860 }))
861 const rejectedInput = {
862 get value(): unknown {
863 rejectedReads++
864 return "a"
865 }
866 }
867 const rejected = SchemaParser.decodeUnknownExit(Schema.Record(rejectedKey, Schema.String))(rejectedInput)
868 assertTrue(Exit.isFailure(rejected))
869 strictEqual(rejectedReads, 0)
870 })
871
872 it("keeps synchronous transformations eager", () => {
873 const effect = SchemaParser.decodeUnknownEffect(Schema.NumberFromString)("1")
874 assertTrue(Exit.isExit(effect))
875 assertTrue(Exit.isSuccess(effect))
876 strictEqual(effect.value, 1)
877 })
878
879 it("rejects a missing root output", () => {
880 const schema = Schema.String.pipe(Schema.decode({
881 decode: SchemaGetter.transformOptionalEffect(() => Effect.succeedNone),
882 encode: SchemaGetter.passthrough()
883 }))
884 const result = SchemaParser.decodeUnknownExit(schema)("value")
885
886 assertTrue(Exit.isFailure(result))
887 })
888
889 it("rejects a missing root output after parsing structural schemas", () => {
890 const missing = SchemaGetter.transformOptionalEffect<never, unknown>(() => Effect.succeedNone)
891 const array = Schema.Array(Schema.String).pipe(Schema.decode({
892 decode: missing,
893 encode: missing
894 }))
895 const struct = Schema.Struct({ value: Schema.String }).pipe(Schema.decode({
896 decode: missing,
897 encode: missing
898 }))
899
900 assertTrue(Exit.isFailure(SchemaParser.decodeUnknownExit(array)(["value"])))
901 assertTrue(Exit.isFailure(SchemaParser.encodeUnknownExit(array)(["value"])))
902 assertTrue(Exit.isFailure(SchemaParser.decodeUnknownExit(struct)({ value: "value" })))
903 assertTrue(Exit.isFailure(SchemaParser.encodeUnknownExit(struct)({ value: "value" })))
904 })
905
906 it("rejects an invalid root without compiling deeply nested structural parsers", () => {
907 let array: Schema.Codec<unknown> = Schema.String
908 let struct: Schema.Codec<unknown> = Schema.String
909 for (let i = 0; i < 5_000; i++) {
910 array = Schema.Array(array)
911 struct = Schema.Struct({ value: struct })
912 }
913
914 assertTrue(Exit.isFailure(SchemaParser.decodeUnknownExit(array)(null)))
915 assertTrue(Exit.isFailure(SchemaParser.decodeUnknownExit(struct)(null)))
916 })
917
918 it("stops outer checks after an aborting check in a FilterGroup", () => {
919 let innerRuns = 0
920 let outerRuns = 0
921 const group = Schema.makeFilter(() => false).abort().and(
922 Schema.makeFilter(() => {
923 innerRuns++
924 return false
925 })
926 )
927 const schema = Schema.String.check(
928 group,
929 Schema.makeFilter(() => {
930 outerRuns++
931 return false
932 })
933 )
934
935 const result = SchemaParser.decodeUnknownExit(schema, { errors: "all" })("value")
936
937 assertTrue(Exit.isFailure(result))
938 strictEqual(innerRuns, 0)
939 strictEqual(outerRuns, 0)
940 })
941
942 it.effect("preserves unchanged values through asynchronous middleware", () =>
943 Effect.gen(function*() {
944 const schema = Schema.String.pipe(
945 Schema.middlewareDecoding((effect) => Effect.yieldNow.pipe(Effect.andThen(effect)))
946 )
947
948 strictEqual(yield* SchemaParser.decodeUnknownEffect(schema)("value"), "value")
949 }))
950
951 it.effect("wraps an asynchronous failure from a uniquely selected union member", () =>
952 Effect.gen(function*() {
953 const failing = Schema.String.pipe(Schema.decode({
954 decode: SchemaGetter.transformOptionalEffect(() =>
955 Effect.yieldNow.pipe(
956 Effect.andThen(Effect.fail(new SchemaIssue.InvalidValue()))
957 )
958 ),
959 encode: SchemaGetter.passthrough()
960 }))
961 const schema = Schema.Union([
962 Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String }),
963 Schema.Struct({ kind: Schema.Literal("b"), value: failing })
964 ])
965
966 const exit = yield* Effect.exit(
967 SchemaParser.decodeUnknownEffect(schema)({ kind: "b", value: "value" })
968 )
969 assertTrue(Exit.isFailure(exit))
970 const error = Cause.findError(exit.cause)
971 assertTrue(Result.isSuccess(error))
972 strictEqual(error.success._tag, "AnyOf")
973 }))
974
975 it.effect("resolves an unchanged union candidate after an asynchronous failure", () =>
976 Effect.gen(function*() {
977 const delayedFailure = Schema.String.pipe(Schema.decode({
978 decode: SchemaGetter.transformOptionalEffect(() =>
979 Effect.yieldNow.pipe(
980 Effect.andThen(Effect.fail(new SchemaIssue.InvalidValue()))
981 )
982 ),
983 encode: SchemaGetter.passthrough()
984 }))
985 const schema = Schema.Union([delayedFailure, Schema.String])
986
987 strictEqual(
988 yield* SchemaParser.decodeUnknownEffect(schema)("value"),
989 "value"
990 )
991 }))
992
993 it("does not replay eager fields after encountering a suspended transformation", () => {
994 const calls: Array<string> = []
995 const field = (name: string, suspended = false) =>
996 Schema.String.pipe(Schema.decode({
997 decode: SchemaGetter.transformOptionalEffect((input) => {
998 calls.push(name)
999 return suspended ? Effect.suspend(() => Effect.succeed(input)) : Effect.succeed(input)
1000 }),
1001 encode: SchemaGetter.passthrough()
1002 }))
1003 const schema = Schema.Struct({
1004 a: field("a"),
1005 b: field("b", true),
1006 c: field("c")
1007 })
1008
1009 const exit = SchemaParser.decodeUnknownExit(schema)({ a: "a", b: "b", c: "c" })
1010
1011 assertTrue(Exit.isSuccess(exit))
1012 strictEqual(calls.join(","), "a,b,c")
1013 })
1014
1015 it("converts synchronous property parser throws into defects", () => {
1016 const field = Schema.declareConstructor<string>()([], () => () => {
1017 throw new Error("property defect")
1018 })
1019 const schema = Schema.Struct({ field })
1020
1021 const exit = SchemaParser.decodeUnknownExit(schema)({ field: "value" })
1022
1023 assertTrue(Exit.isFailure(exit))
1024 assertTrue(Cause.hasDies(exit.cause))
1025 })
1026
1027 it("keeps encoding-link parser compilation lazy for Suspend", () => {
1028 let evaluations = 0
1029 const schema = Schema.suspend(() => {
1030 evaluations++
1031 return Schema.String
1032 }).pipe(Schema.decode({
1033 decode: SchemaGetter.passthrough(),
1034 encode: SchemaGetter.passthrough()
1035 }))
1036
1037 const decode = SchemaParser.decodeUnknownExit(schema)
1038 strictEqual(evaluations, 0)
1039 strictEqual(decode("a")._tag, "Success")
1040 strictEqual(evaluations, 1)
1041 strictEqual(decode("b")._tag, "Success")
1042 strictEqual(evaluations, 1)
1043 })
1044
1045 it("keeps direct Suspend parser compilation lazy", () => {
1046 let evaluations = 0
1047 const schema = Schema.suspend(() => {
1048 evaluations++
1049 return Schema.String
1050 })
1051
1052 const decode = SchemaParser.decodeUnknownExit(schema)
1053 strictEqual(evaluations, 0)
1054 strictEqual(decode("a")._tag, "Success")
1055 strictEqual(evaluations, 1)
1056 strictEqual(decode("b")._tag, "Success")
1057 strictEqual(evaluations, 1)
1058 })
1059
1060 it("supports mutually recursive Suspend parsers", () => {
1061 interface A {
1062 readonly _tag: "A"
1063 readonly next: string | B
1064 }
1065 interface B {
1066 readonly _tag: "B"
1067 readonly next: string | A
1068 }
1069 const A = Schema.Struct({
1070 _tag: Schema.Literal("A"),
1071 next: Schema.Union([Schema.String, Schema.suspend((): Schema.Codec<B> => B)])
1072 })
1073 const B = Schema.Struct({
1074 _tag: Schema.Literal("B"),
1075 next: Schema.Union([Schema.String, Schema.suspend((): Schema.Codec<A> => A)])
1076 })
1077 const decode = SchemaParser.decodeUnknownExit(A)
1078
1079 strictEqual(decode({ _tag: "A", next: { _tag: "B", next: { _tag: "A", next: "end" } } })._tag, "Success")
1080 })
1081
1082 it("keeps Declaration parser compilation lazy and shared by AST identity", () => {
1083 let evaluations = 0
1084 const schema = Schema.declareConstructor<unknown>()([], () => {
1085 evaluations++
1086 return (input) => Effect.succeed(input)
1087 })
1088
1089 const decode = SchemaParser.decodeUnknownExit(schema)
1090 const decodeAgain = SchemaParser.decodeUnknownExit(schema)
1091 strictEqual(evaluations, 0)
1092 strictEqual(decode("a")._tag, "Success")
1093 strictEqual(evaluations, 1)
1094 strictEqual(decodeAgain("b")._tag, "Success")
1095 strictEqual(evaluations, 1)
1096 })
1097
1098 it("keeps untried Declaration union members lazy", () => {
1099 let evaluations = 0
1100 const declaration = Schema.declareConstructor<unknown>()([], () => {
1101 evaluations++
1102 return (input) => Effect.succeed(input)
1103 })
1104 const decode = SchemaParser.decodeUnknownExit(Schema.Union([Schema.Literal("a"), declaration]))
1105
1106 strictEqual(decode("a")._tag, "Success")
1107 strictEqual(evaluations, 0)
1108 strictEqual(decode("b")._tag, "Success")
1109 strictEqual(evaluations, 1)
1110 })
1111
1112 it("keeps encode-side Declaration and Suspend parser compilation lazy", () => {
1113 let declarationEvaluations = 0
1114 let suspendEvaluations = 0
1115 const declaration = Schema.declareConstructor<unknown>()([], () => {
1116 declarationEvaluations++
1117 return (input) => Effect.succeed(input)
1118 })
1119 const suspend = Schema.suspend(() => {
1120 suspendEvaluations++
1121 return Schema.String
1122 })
1123
1124 const encodeDeclaration = SchemaParser.encodeUnknownExit(declaration)
1125 const encodeSuspend = SchemaParser.encodeUnknownExit(suspend)
1126 strictEqual(declarationEvaluations, 0)
1127 strictEqual(suspendEvaluations, 0)
1128 strictEqual(encodeDeclaration("a")._tag, "Success")
1129 strictEqual(encodeSuspend("a")._tag, "Success")
1130 strictEqual(declarationEvaluations, 1)
1131 strictEqual(suspendEvaluations, 1)
1132 strictEqual(encodeDeclaration("b")._tag, "Success")
1133 strictEqual(encodeSuspend("b")._tag, "Success")
1134 strictEqual(declarationEvaluations, 1)
1135 strictEqual(suspendEvaluations, 1)
1136 })
1137
1138 it("retries lazy parser compilation after a synchronous factory failure", () => {
1139 let evaluations = 0
1140 const schema = Schema.declareConstructor<unknown>()([], () => {
1141 if (++evaluations === 1) {
1142 throw new Error("factory failure")
1143 }
1144 return (input) => Effect.succeed(input)
1145 })
1146 const decode = SchemaParser.decodeUnknownExit(schema)
1147
1148 throws(() => decode("a"), (error) => {
1149 assertTrue(error instanceof Error)
1150 strictEqual(error.message, "factory failure")
1151 })
1152 strictEqual(decode("b")._tag, "Success")
1153 strictEqual(decode("c")._tag, "Success")
1154 strictEqual(evaluations, 2)
1155 })
1156
1157 it("retries lazy Suspend parser compilation after a synchronous thunk failure", () => {
1158 let evaluations = 0
1159 const schema = Schema.suspend(() => {
1160 if (++evaluations === 1) {
1161 throw new Error("thunk failure")
1162 }
1163 return Schema.String
1164 })
1165 const decode = SchemaParser.decodeUnknownExit(schema)
1166
1167 throws(() => decode("a"), (error) => {
1168 assertTrue(error instanceof Error)
1169 strictEqual(error.message, "thunk failure")
1170 })
1171 strictEqual(decode("b")._tag, "Success")
1172 strictEqual(decode("c")._tag, "Success")
1173 strictEqual(evaluations, 2)
1174 })
1175
1176 it("should preserve mixed causes in union candidates instead of trying later candidates", () => {
1177 const failure = Schema.String.pipe(Schema.decode({
1178 decode: SchemaGetter.transformOptionalEffect(() => Effect.failCause(makeMixedCause())),
1179 encode: SchemaGetter.passthrough()
1180 }))
1181 const schema = Schema.Union([
1182 failure,
1183 Schema.Literal("a")
1184 ])
1185 const taggedSchema = Schema.Union([
1186 Schema.Struct({ kind: Schema.Literal("a"), value: failure }),
1187 Schema.Struct({ kind: Schema.Literal("b"), value: Schema.String })
1188 ])
1189
1190 for (
1191 const exit of [
1192 SchemaParser.decodeUnknownExit(schema)("a"),
1193 SchemaParser.decodeUnknownExit(taggedSchema)({ kind: "a", value: "a" })
1194 ] as ReadonlyArray<Exit.Exit<unknown, SchemaIssue.Issue>>
1195 ) {
1196 assertTrue(Exit.isFailure(exit))
1197 assertTrue(Exit.hasDies(exit))
1198 const error = Cause.findError(exit.cause)
1199 assertTrue(Result.isSuccess(error))
1200 assertTrue(SchemaIssue.isIssue(error.success))
1201 }
1202 })
1203 })
1204})