1import { assert, describe, it } from "@effect/vitest"
2import {
3 Cause,
4 Deferred,
5 Effect,
6 Fiber,
7 Result,
8 Schema,
9 SchemaGetter,
10 SchemaIssue,
11 SchemaParser,
12 SchemaTransformation
13} from "effect"
14import { SchemaCompiler } from "effect/schema"
15// oxlint-disable-next-line no-unassigned-import
16import "effect/schema/SchemaJITCompiler/enable"
17import { assertSchemaIssueError, deepStrictEqual, strictEqual, throws } from "../utils/assert.ts"
18
19const schema = Schema.Struct({
20 name: Schema.String,
21 count: Schema.Number,
22 active: Schema.Boolean,
23 nested: Schema.Struct({ value: Schema.String })
24})
25
26const decode = SchemaParser.decodeUnknownSync(schema)
27const is = SchemaParser.is(schema)
28
29describe("SchemaJITCompiler", () => {
30 it.effect("preserves product concurrency", () =>
31 Effect.gen(function*() {
32 const started = yield* Effect.forEach([0, 1, 2], () => Deferred.make<void>())
33 const releases = yield* Effect.forEach([0, 1, 2], () => Deferred.make<void>())
34 const item = Schema.String.pipe(Schema.decode({
35 decode: SchemaGetter.transformEffect((value) => {
36 const index = value.charCodeAt(0) - 97
37 return Deferred.succeed(started[index], undefined).pipe(
38 Effect.andThen(Deferred.await(releases[index])),
39 Effect.as(value)
40 )
41 }),
42 encode: SchemaGetter.passthrough()
43 }))
44 const schema = Schema.Struct({ a: item, b: item, c: item })
45 const fiber = yield* SchemaParser.decodeUnknownEffect(schema)(
46 { a: "a", b: "b", c: "c" },
47 { concurrency: 2 }
48 ).pipe(Effect.forkChild)
49
50 yield* Deferred.await(started[0])
51 yield* Deferred.await(started[1])
52 strictEqual(yield* Deferred.isDone(started[2]), false)
53
54 yield* Deferred.succeed(releases[0], undefined)
55 yield* Deferred.await(started[2])
56 yield* Deferred.succeed(releases[1], undefined)
57 yield* Deferred.succeed(releases[2], undefined)
58
59 deepStrictEqual(yield* Fiber.join(fiber), { a: "a", b: "b", c: "c" })
60 }))
61
62 it("compiles a decoder lazily after import", () => {
63 const input = {
64 name: "a",
65 count: 1,
66 active: true,
67 nested: { value: "b", extra: true },
68 extra: true
69 }
70 const output = decode(input)
71
72 deepStrictEqual(output, {
73 name: "a",
74 count: 1,
75 active: true,
76 nested: { value: "b" }
77 })
78 assert.notStrictEqual(output, input)
79 assert.notStrictEqual(output.nested, input.nested)
80 })
81
82 it("compiles type guards", () => {
83 strictEqual(
84 is({
85 name: "a",
86 count: 1,
87 active: true,
88 nested: { value: "b" }
89 }),
90 true
91 )
92 strictEqual(
93 is({
94 name: 1,
95 count: 1,
96 active: true,
97 nested: { value: "b" }
98 }),
99 false
100 )
101
102 const defect = new Error("boom")
103 let reads = 0
104 throws(() =>
105 is({
106 get name(): string {
107 reads++
108 throw defect
109 },
110 count: 1,
111 active: true,
112 nested: { value: "b" }
113 }), (error) => {
114 assert(error instanceof Error)
115 strictEqual(error.message, "Type guard adapter can only return false for schema issues")
116 assert(Cause.hasDies(error.cause as Cause.Cause<never>))
117 })
118 strictEqual(reads, 1)
119 })
120
121 it("does not confuse a valid value with the invalid sentinel", () => {
122 const schemas = [
123 Schema.Union([Schema.Symbol, Schema.String]),
124 Schema.Union([Schema.UniqueSymbol(SchemaCompiler.invalid), Schema.Literal("valid")])
125 ]
126 for (const schema of schemas) {
127 strictEqual(SchemaParser.is(schema)(SchemaCompiler.invalid), true)
128 strictEqual(SchemaParser.decodeUnknownSync(schema)(SchemaCompiler.invalid), SchemaCompiler.invalid)
129 }
130 })
131
132 it("uses default options in compiled type guards", () => {
133 const structural = Schema.Struct({ value: Schema.String })
134 const is = SchemaParser.is(structural)
135 strictEqual(is({ value: "a" }), true)
136 strictEqual(is({ value: "a", extra: true }), true)
137 strictEqual(is({ value: 1 }), false)
138
139 const checked = Schema.Struct({ a: Schema.String, b: Schema.String }).check(
140 Schema.makeFilter((value, _ast, options) => options.reportInput === true && !Object.hasOwn(value, "extra"))
141 )
142 const input = { b: "b", a: "a", extra: true }
143 strictEqual(SchemaParser.is(checked)(input), false)
144 deepStrictEqual(SchemaParser.decodeUnknownSync(checked)(input, { reportInput: true }), { a: "a", b: "b" })
145 deepStrictEqual(SchemaParser.decodeUnknownSync(checked)(input, { disableChecks: true }), { a: "a", b: "b" })
146 strictEqual(SchemaParser.is(checked)(input), false)
147 })
148
149 it("accepts the invalid sentinel inside a checked composite", () => {
150 const schema = Schema.Struct({ value: Schema.Union([Schema.Symbol, Schema.String]) }).check(
151 Schema.makeFilter(() => true)
152 )
153 const input = { value: SchemaCompiler.invalid }
154 strictEqual(SchemaParser.is(schema)(input), true)
155 deepStrictEqual(SchemaParser.decodeUnknownSync(schema)(input), input)
156 })
157
158 it("keeps runtime options for nested checks, template parts and record keys", () => {
159 const checked = Schema.Struct({
160 nested: Schema.Struct({ value: Schema.String }).check(
161 Schema.makeFilter((_value, _ast, options) => options.reportInput === true)
162 )
163 })
164 const value = { nested: { value: "valid" } }
165 strictEqual(SchemaParser.is(checked)(value), false)
166 deepStrictEqual(SchemaParser.decodeUnknownSync(checked)(value, { reportInput: true }), value)
167
168 const template = Schema.Struct({
169 value: Schema.TemplateLiteral(["prefix-", Schema.String.check(Schema.isMinLength(2))])
170 })
171 strictEqual(SchemaParser.is(template)({ value: "prefix-a" }), false)
172 deepStrictEqual(SchemaParser.decodeUnknownSync(template)({ value: "prefix-a" }, { disableChecks: true }), {
173 value: "prefix-a"
174 })
175 strictEqual(SchemaParser.is(template)({ value: "prefix-a" }), false)
176
177 const record = Schema.Record(Schema.String.check(Schema.isStartingWith("x")), Schema.Number)
178 const decode = SchemaParser.decodeUnknownSync(record)
179 deepStrictEqual(decode({ x: 1, y: 2 }), { x: 1 })
180 deepStrictEqual(decode({ x: 1, y: 2 }, { disableChecks: true }), { x: 1, y: 2 })
181 })
182
183 it("runs the diagnostic phase after fast validation fails", () => {
184 let reads = 0
185 const input = {
186 get name() {
187 reads++
188 return 1
189 },
190 count: 1,
191 active: true,
192 nested: { value: "b" }
193 }
194
195 throws(() => decode(input), (error) => {
196 assertSchemaIssueError(error, `Expected string\n at ["name"]`)
197 })
198 strictEqual(reads, 2)
199 })
200
201 it("runs one diagnostic pass for a nested failure", () => {
202 let checks = 0
203 const decode = SchemaParser.decodeUnknownSync(Schema.Struct({
204 nested: Schema.Struct({
205 value: Schema.String.check(Schema.makeFilter(() => {
206 checks++
207 return false
208 }))
209 })
210 }))
211
212 throws(() => decode({ nested: { value: "invalid" } }), (error) => {
213 assertSchemaIssueError(
214 error,
215 `Expected <filter>
216 at ["nested"]["value"]`
217 )
218 })
219 strictEqual(checks, 2)
220 })
221
222 it("preserves diagnostics with shared parser helpers", () => {
223 const schema = Schema.Struct({ value: Schema.String })
224 const decode = SchemaParser.decodeUnknownSync(schema)
225
226 deepStrictEqual(decode({ value: "a", extra: true }), { value: "a" })
227 throws(() => decode({ value: 1 }), (error) => {
228 assertSchemaIssueError(
229 error,
230 `Expected string
231 at ["value"]`
232 )
233 })
234 throws(() => decode({ value: "a", extra: true }, { onExcessProperty: "error" }), (error) => {
235 assertSchemaIssueError(
236 error,
237 `Expected no excess property
238 at ["extra"]`
239 )
240 })
241 })
242
243 it("executes transformations and middleware once", () => {
244 let transformations = 0
245 const transformed = Schema.String.pipe(
246 Schema.decodeTo(
247 Schema.String.check(Schema.isMinLength(2)),
248 SchemaTransformation.transform({
249 decode: (value) => {
250 transformations++
251 return value.trim()
252 },
253 encode: (value) => value
254 })
255 )
256 )
257 const decodeTransformed = SchemaParser.decodeUnknownSync(transformed)
258
259 strictEqual(decodeTransformed(" valid "), "valid")
260 strictEqual(transformations, 1)
261 throws(() => decodeTransformed(" x "))
262 strictEqual(transformations, 2)
263
264 let middlewareRuns = 0
265 const middleware = Schema.Struct({ value: Schema.String }).pipe(
266 Schema.middlewareDecoding((effect) => {
267 middlewareRuns++
268 return effect
269 })
270 )
271
272 deepStrictEqual(SchemaParser.decodeUnknownSync(middleware)({ value: "valid" }), { value: "valid" })
273 strictEqual(middlewareRuns, 1)
274 })
275
276 it("decodes transformed Struct properties with runtime options", () => {
277 const schema = Schema.Struct({
278 first: Schema.FiniteFromString,
279 second: Schema.FiniteFromString
280 })
281
282 const decode = SchemaParser.decodeUnknownSync(schema)
283 for (
284 const options of [
285 undefined,
286 {},
287 { errors: "first" },
288 { onExcessProperty: "ignore" },
289 { reportInput: true },
290 { disableChecks: true }
291 ] as const
292 ) {
293 deepStrictEqual(decode({ first: "1", second: "2" }, options), { first: 1, second: 2 })
294 }
295 })
296
297 it("does not replay inlined Struct transformations", () => {
298 let transformations = 0
299 const transformed = Schema.String.pipe(
300 Schema.decodeTo(
301 Schema.Number,
302 SchemaTransformation.transform({
303 decode: (value) => {
304 transformations++
305 return value === "invalid" ? "invalid" as any : Number(value)
306 },
307 encode: String
308 })
309 )
310 )
311 const decode = SchemaParser.decodeUnknownSync(Schema.Struct({ value: transformed }))
312
313 deepStrictEqual(decode({ value: "1" }), { value: 1 })
314 strictEqual(transformations, 1)
315 throws(() => decode({ value: false }))
316 strictEqual(transformations, 1)
317 throws(() => decode({ value: "invalid" }))
318 strictEqual(transformations, 2)
319 })
320
321 it("applies Struct output and encoding checks after compiled fields without replay", () => {
322 let transformations = 0
323 let checks = 0
324 const schema = Schema.Struct({
325 value: Schema.String.pipe(Schema.decodeTo(
326 Schema.Number,
327 SchemaTransformation.transform({
328 decode: (value) => {
329 transformations++
330 return Number(value)
331 },
332 encode: String
333 })
334 ))
335 }).check(Schema.makeFilter((output) => {
336 checks++
337 deepStrictEqual(Object.keys(output), ["value"])
338 return output.value > 0
339 })).pipe(
340 Schema.flip,
341 Schema.check(Schema.makeFilter((input) => input.value !== "01")),
342 Schema.flip
343 )
344 const decode = SchemaParser.decodeUnknownSync(schema)
345 deepStrictEqual(decode({ value: "1", extra: true }), { value: 1 })
346 throws(() => decode({ value: "-1", extra: true }))
347 strictEqual(transformations, 2)
348 strictEqual(checks, 2)
349
350 deepStrictEqual(decode({ value: "-1" }, { disableChecks: true }), { value: -1 })
351 strictEqual(checks, 2)
352 throws(() => decode({ value: "-1" }, { errors: "all" }))
353 strictEqual(transformations, 4)
354 strictEqual(checks, 3)
355 throws(() => decode({ value: "01" }))
356 strictEqual(transformations, 5)
357 strictEqual(checks, 3)
358 })
359
360 it("replaces the parser used by the other decoding adapters", () => {
361 let checks = 0
362 const schema = Schema.String.check(Schema.makeFilter(() => {
363 checks++
364 return false
365 }))
366
367 assert(Result.isFailure(SchemaParser.decodeUnknownResult(schema)("value")))
368 strictEqual(checks, 2)
369 })
370
371 it("replaces the parser used by encoding adapters", () => {
372 let checks = 0
373 const schema = Schema.String.check(Schema.makeFilter(() => {
374 checks++
375 return false
376 }))
377
378 assert(Result.isFailure(SchemaParser.encodeUnknownResult(schema)("value")))
379 strictEqual(checks, 2)
380 })
381
382 it("does not replay defects", () => {
383 const defect = new Error("boom")
384 let reads = 0
385 const input = {
386 get name(): string {
387 reads++
388 throw defect
389 },
390 count: 1,
391 active: true,
392 nested: { value: "b" }
393 }
394
395 throws(() => decode(input), (error) => {
396 assert(error instanceof Error)
397 strictEqual(error.message, "Sync adapter can only throw schema issues")
398 assert(Cause.hasDies(error.cause as Cause.Cause<never>))
399 })
400 strictEqual(reads, 1)
401 })
402
403 it("honors explicit ParseOptions in the compiled diagnostic phase", () => {
404 throws(() =>
405 decode({
406 name: "a",
407 count: 1,
408 active: true,
409 nested: { value: "b", nestedExtra: true },
410 extra: true
411 }, { onExcessProperty: "error", errors: "all" }), (error) => {
412 assertSchemaIssueError(
413 error,
414 `Expected no excess property
415 at ["extra"]
416Expected no excess property
417 at ["nested"]["nestedExtra"]`
418 )
419 })
420 })
421
422 it("compiles symbol-keyed Struct properties", () => {
423 const key = Symbol("key")
424 const decode = SchemaParser.decodeUnknownSync(Schema.Struct({
425 text: Schema.String,
426 [key]: Schema.Number
427 }))
428 const output = decode({ text: "value", [key]: 1, extra: true })
429
430 strictEqual(output.text, "value")
431 strictEqual(output[key], 1)
432 deepStrictEqual(Reflect.ownKeys(output), ["text", key])
433 })
434
435 it("uses the interpreter for unsupported schemas", () => {
436 const date = new Date(0)
437 const decode = SchemaParser.decodeUnknownSync(Schema.instanceOf(Date))
438 strictEqual(decode(date), date)
439 })
440
441 it("uses the interpreter when dynamic function generation is unavailable", () => {
442 const Function = globalThis.Function
443 try {
444 globalThis.Function = (() => {
445 throw new Error("dynamic function generation unavailable")
446 }) as any
447 const schema = Schema.Struct({ value: Schema.String })
448 const getParser = schema.ast.getParser.bind(schema.ast)
449 let interpreterConstructions = 0
450 Object.defineProperty(schema.ast, "getParser", {
451 configurable: true,
452 value(...args: Parameters<typeof getParser>) {
453 interpreterConstructions++
454 return getParser(...args)
455 }
456 })
457 const decode = SchemaParser.decodeUnknownSync(schema)
458
459 deepStrictEqual(decode({ value: "a" }), { value: "a" })
460 strictEqual(interpreterConstructions, 1)
461 throws(() => decode({ value: 1 }), (error) => {
462 assertSchemaIssueError(
463 error,
464 `Expected string
465 at ["value"]`
466 )
467 })
468 } finally {
469 globalThis.Function = Function
470 }
471 })
472
473 it("compiles primitive leaves without confusing undefined with a missing key", () => {
474 const decode = SchemaParser.decodeUnknownSync(Schema.Struct({
475 undefined: Schema.Undefined,
476 unknown: Schema.Unknown,
477 bigint: Schema.BigInt,
478 symbol: Schema.Symbol,
479 literal: Schema.Literal("a")
480 }))
481 const symbol = globalThis.Symbol("a")
482
483 deepStrictEqual(
484 decode({ undefined, unknown: undefined, bigint: 1n, symbol, literal: "a", extra: true }),
485 { undefined, unknown: undefined, bigint: 1n, symbol, literal: "a" }
486 )
487 throws(() => decode({ unknown: undefined, bigint: 1n, symbol, literal: "a" }), (error) => {
488 assertSchemaIssueError(error, `Missing key\n at ["undefined"]`)
489 })
490 })
491
492 it("compiles arrays and tuples with rest and tail elements", () => {
493 const decodeArray = SchemaParser.decodeUnknownSync(Schema.Array(Schema.String))
494 deepStrictEqual(decodeArray(["a", "b"]), ["a", "b"])
495
496 const decodeTuple = SchemaParser.decodeUnknownSync(
497 Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean])
498 )
499 deepStrictEqual(decodeTuple(["a", 1, 2, true]), ["a", 1, 2, true])
500 throws(() => decodeTuple(["a", 1, 2]), (error) => {
501 assertSchemaIssueError(error, `Expected boolean\n at [2]`)
502 })
503 })
504
505 it("compiles optional tuple elements", () => {
506 const schema = Schema.Tuple([Schema.optionalKey(Schema.String)])
507 const decode = SchemaParser.decodeUnknownSync(schema)
508 const is = SchemaParser.is(schema)
509
510 deepStrictEqual(decode([]), [])
511 deepStrictEqual(decode(["a"]), ["a"])
512 strictEqual(is([]), true)
513 strictEqual(is(["a"]), true)
514 strictEqual(is([1]), false)
515 })
516
517 it("preserves the input sign for signed-zero literals", () => {
518 const decode = SchemaParser.decodeUnknownSync(Schema.Struct({
519 negative: Schema.Literal(-0),
520 positive: Schema.Union([Schema.Literal(0), Schema.Literal(1)]),
521 union: Schema.Union([Schema.Literal(-0), Schema.Literal(1)])
522 }))
523 const output = decode({ negative: 0, positive: -0, union: 0 })
524
525 strictEqual(Object.is(output.negative, 0), true)
526 strictEqual(Object.is(output.positive, -0), true)
527 strictEqual(Object.is(output.union, 0), true)
528 })
529
530 it("compiles primitive anyOf and oneOf unions", () => {
531 const decodeAnyOf = SchemaParser.decodeUnknownSync(
532 Schema.Union([Schema.Literal("a"), Schema.Literal("b"), Schema.Number])
533 )
534 strictEqual(decodeAnyOf("b"), "b")
535 strictEqual(decodeAnyOf(1), 1)
536 throws(() => decodeAnyOf(true), (error) => {
537 assertSchemaIssueError(error, "Expected \"a\" | \"b\" | number")
538 })
539
540 const decodeOneOf = SchemaParser.decodeUnknownSync(
541 Schema.Union([Schema.String, Schema.Literal("a")], { mode: "oneOf" })
542 )
543 strictEqual(decodeOneOf("b"), "b")
544 throws(() => decodeOneOf("a"), (error) => {
545 assertSchemaIssueError(error, "Expected exactly one member to match")
546 })
547 })
548
549 it("compiles structural unions through canonical candidate selection", () => {
550 const decodeTagged = SchemaParser.decodeUnknownSync(Schema.Union([
551 Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String }),
552 Schema.Struct({ kind: Schema.Literal("b"), value: Schema.Number })
553 ]))
554 deepStrictEqual(
555 decodeTagged({ kind: "b", value: 1, extra: true }),
556 { kind: "b", value: 1 }
557 )
558
559 const decodeUntagged = SchemaParser.decodeUnknownSync(Schema.Union([
560 Schema.Struct({ first: Schema.String }),
561 Schema.Struct({ second: Schema.String })
562 ]))
563 deepStrictEqual(
564 decodeUntagged({ first: "a", second: "b" }),
565 { first: "a" }
566 )
567
568 const decodeOneOf = SchemaParser.decodeUnknownSync(Schema.Union([
569 Schema.Struct({ first: Schema.String }),
570 Schema.Struct({ second: Schema.String })
571 ], { mode: "oneOf" }))
572 throws(() => decodeOneOf({ first: "a", second: "b" }), (error) => {
573 assertSchemaIssueError(error, "Expected exactly one member to match")
574 })
575 })
576
577 it("compiles optional object properties", () => {
578 const decode = SchemaParser.decodeUnknownSync(Schema.Struct({
579 required: Schema.String,
580 optional: Schema.optionalKey(Schema.Number),
581 undefined: Schema.optionalKey(Schema.Undefined)
582 }))
583
584 deepStrictEqual(decode({ required: "a" }), { required: "a" })
585 deepStrictEqual(
586 decode({ required: "a", optional: 1, undefined, extra: true }),
587 { required: "a", optional: 1, undefined }
588 )
589 throws(() => decode({ required: "a", optional: "invalid" }), (error) => {
590 assertSchemaIssueError(error, `Expected number\n at ["optional"]`)
591 })
592 })
593
594 it("compiles string records", () => {
595 const decode = SchemaParser.decodeUnknownSync(
596 Schema.Record(Schema.String, Schema.Struct({ count: Schema.Number }))
597 )
598 deepStrictEqual(
599 decode({ a: { count: 1 }, b: { count: 2 }, extra: { count: 3, ignored: true } }),
600 { a: { count: 1 }, b: { count: 2 }, extra: { count: 3 } }
601 )
602 throws(() => decode({ a: { count: "invalid" } }), (error) => {
603 assertSchemaIssueError(error, `Expected number\n at ["a"]["count"]`)
604 })
605 })
606
607 it("compiles symbol and template-literal records", () => {
608 const symbol = Symbol("key")
609 const decodeSymbols = SchemaParser.decodeUnknownSync(Schema.Record(Schema.Symbol, Schema.Number))
610 const symbolOutput = decodeSymbols({ text: "ignored", [symbol]: 1 })
611 strictEqual(symbolOutput[symbol], 1)
612 deepStrictEqual(Reflect.ownKeys(symbolOutput), [symbol])
613
614 const decodeTemplates = SchemaParser.decodeUnknownSync(
615 Schema.Record(Schema.TemplateLiteral(["data-", Schema.String]), Schema.Number)
616 )
617 deepStrictEqual(
618 decodeTemplates({ "data-a": 1, ignored: 2, "data-b": 3 }),
619 { "data-a": 1, "data-b": 3 }
620 )
621 })
622
623 it("compiles fixed properties with index signatures", () => {
624 const decode = SchemaParser.decodeUnknownSync(
625 Schema.StructWithRest(
626 Schema.Struct({ fixed: Schema.Trim }),
627 [Schema.Record(Schema.String, Schema.String)]
628 )
629 )
630 deepStrictEqual(decode({ fixed: " value ", other: "other" }), { fixed: "value", other: "other" })
631 })
632
633 it("compiles decoded index keys", () => {
634 const decodeNumbers = SchemaParser.decodeUnknownSync(Schema.Record(Schema.Number, Schema.Number))
635 deepStrictEqual(decodeNumbers({ 1: 1, other: "ignored" }), { 1: 1 })
636
637 const decodeCamelCase = SchemaParser.decodeUnknownSync(
638 Schema.Record(
639 Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.toUpperCase())),
640 Schema.Number
641 )
642 )
643 deepStrictEqual(decodeCamelCase({ a: 1, b: 2 }), { A: 1, B: 2 })
644 })
645
646 it("compiles encoding checks", () => {
647 const checked = Schema.Struct({ value: Schema.String }).pipe(
648 Schema.flip,
649 Schema.check(Schema.makeFilter((input) => input.value.length > 1)),
650 Schema.flip
651 )
652 const decode = SchemaParser.decodeUnknownSync(checked)
653 deepStrictEqual(decode({ value: "valid" }), { value: "valid" })
654 throws(() => decode({ value: "" }))
655 })
656
657 it("runs checks against decoded output", () => {
658 const decodeString = SchemaParser.decodeUnknownSync(
659 Schema.String.check(Schema.isMinLength(2))
660 )
661 strictEqual(decodeString("ab"), "ab")
662 throws(() => decodeString("a"), (error) => {
663 assertSchemaIssueError(error, "Expected a value with a length of at least 2")
664 })
665
666 const decodeObject = SchemaParser.decodeUnknownSync(
667 Schema.Struct({ value: Schema.String }).check(Schema.isMaxProperties(1))
668 )
669 deepStrictEqual(decodeObject({ value: "a", extra: true }), { value: "a" })
670 })
671
672 it("runs compiled type guard checks against decoded output", () => {
673 const checked = Schema.Struct({ value: Schema.String }).check(Schema.isMaxProperties(1))
674 const isRoot = SchemaParser.is(checked)
675 const isNested = SchemaParser.is(Schema.Struct({ nested: checked }))
676
677 strictEqual(isRoot({ value: "a", extra: true }), true)
678 strictEqual(isNested({ nested: { value: "a", extra: true }, extra: true }), true)
679 strictEqual(isNested({ nested: { value: 1 } }), false)
680 })
681
682 it("uses compiled checkpoints around interpreted declarations and transformations", () => {
683 const date = new Date(0)
684 const decode = SchemaParser.decodeUnknownSync(Schema.Struct({
685 date: Schema.instanceOf(Date),
686 count: Schema.FiniteFromString
687 }))
688
689 const output = decode({ date, count: "1", extra: true })
690 strictEqual(output.date, date)
691 strictEqual(output.count, 1)
692 deepStrictEqual(Reflect.ownKeys(output), ["date", "count"])
693 })
694
695 it("uses compiled checkpoints inside root encoding chains", () => {
696 const decodeNumber = SchemaParser.decodeUnknownSync(Schema.FiniteFromString)
697 strictEqual(decodeNumber("1"), 1)
698 throws(() => decodeNumber("invalid"), (error) => {
699 assertSchemaIssueError(error, "Expected a finite number")
700 })
701
702 const decodeJson = SchemaParser.decodeUnknownSync(
703 Schema.fromJsonString(Schema.Struct({ value: Schema.Number }))
704 )
705 deepStrictEqual(decodeJson("{\"value\":1,\"extra\":true}"), { value: 1 })
706 })
707
708 it("does not replay transformations when a compiled checkpoint fails", () => {
709 let sourceChecks = 0
710 let firstTransformations = 0
711 let secondTransformations = 0
712 let checks = 0
713 const schema = Schema.Struct({
714 value: Schema.String.check(Schema.makeFilter((value) => {
715 sourceChecks++
716 return value !== "blocked"
717 })).pipe(
718 Schema.decodeTo(
719 Schema.Number.check(Schema.makeFilter((value) => {
720 checks++
721 return value > 0
722 })),
723 SchemaTransformation.transform({
724 decode: (value) => {
725 firstTransformations++
726 return Number(value)
727 },
728 encode: String
729 })
730 ),
731 Schema.decodeTo(
732 Schema.String,
733 SchemaTransformation.transform({
734 decode: (value) => {
735 secondTransformations++
736 return String(value)
737 },
738 encode: Number
739 })
740 )
741 )
742 })
743
744 throws(() => SchemaParser.decodeUnknownSync(schema)({ value: "blocked" }))
745 strictEqual(sourceChecks, 1)
746 strictEqual(firstTransformations, 0)
747 strictEqual(secondTransformations, 0)
748
749 sourceChecks = 0
750 throws(() => SchemaParser.decodeUnknownSync(schema)({ value: "-1" }))
751 strictEqual(sourceChecks, 1)
752 strictEqual(firstTransformations, 1)
753 strictEqual(secondTransformations, 0)
754 strictEqual(checks, 1)
755 })
756
757 it("preserves mixed causes from interpreted encoding chains", () => {
758 const cause = Cause.combine(
759 Cause.fail(new SchemaIssue.InvalidValue({ message: "schema issue" })),
760 Cause.die(new Error("defect"))
761 )
762 const schema = Schema.String.pipe(Schema.decode({
763 decode: SchemaGetter.transformOptionalEffect(() => Effect.failCause(cause)),
764 encode: SchemaGetter.passthrough()
765 }))
766
767 throws(() => SchemaParser.decodeUnknownSync(schema)("value"), (error) => {
768 assert(error instanceof Error)
769 strictEqual(error.message, "Sync adapter can only throw schema issues")
770 const issue = Cause.findError(error.cause as Cause.Cause<SchemaIssue.Issue>)
771 assert(Result.isSuccess(issue))
772 strictEqual(issue.success._tag, "Encoding")
773 assert(Cause.hasDies(error.cause as Cause.Cause<never>))
774 })
775 })
776
777 it("keeps Suspend parsers lazy", () => {
778 interface Category {
779 readonly value: string
780 readonly children: ReadonlyArray<Category>
781 }
782 let evaluations = 0
783 const schema: Schema.Codec<Category> = Schema.Struct({
784 value: Schema.String,
785 children: Schema.Array(Schema.suspend((): Schema.Codec<Category> => {
786 evaluations++
787 return schema
788 }))
789 })
790 const decode = SchemaParser.decodeUnknownSync(schema)
791
792 deepStrictEqual(decode({ value: "root", children: [] }), { value: "root", children: [] })
793 strictEqual(evaluations, 0)
794 deepStrictEqual(
795 decode({ value: "root", children: [{ value: "child", children: [] }] }),
796 { value: "root", children: [{ value: "child", children: [] }] }
797 )
798 strictEqual(evaluations, 1)
799 })
800
801 it("does not replay declaration defects", () => {
802 const defect = new Error("declaration defect")
803 let runs = 0
804 const declaration = Schema.declareConstructor<unknown>()([], () => () => {
805 runs++
806 return Effect.die(defect)
807 })
808 const decode = SchemaParser.decodeUnknownSync(Schema.Struct({ declaration }))
809
810 throws(() => decode({ declaration: "value" }), (error) => {
811 assert(error instanceof Error)
812 strictEqual(error.message, "Sync adapter can only throw schema issues")
813 assert(Cause.hasDies(error.cause as Cause.Cause<never>))
814 })
815 strictEqual(runs, 1)
816 })
817})