1import { assert, describe, it } from "@effect/vitest"
2import {
3 Effect,
4 Exit,
5 Option,
6 Result,
7 Schema,
8 SchemaAST,
9 SchemaGetter,
10 SchemaParser,
11 SchemaTransformation
12} from "effect"
13import * as Codegen from "effect/internal/schema/codegen"
14import { SchemaCompiler, SchemaJITCompiler } from "effect/schema"
15import { deepStrictEqual, strictEqual } from "../utils/assert.ts"
16
17describe("compiler regression contracts", () => {
18 it("preserves template literal issues after compilation", () => {
19 const schema = Schema.TemplateLiteral(["count:", Schema.Int.check(Schema.isGreaterThan(0))])
20 const inputs = ["count:1", "count:0", "count:1.5", "invalid", null]
21 const snapshot = () => {
22 const decode = SchemaParser.decodeUnknownResult(schema)
23 // Diagnostic ASTs contain freshly constructed transformation functions.
24 return inputs.map((input) => Result.mapError(decode(input), (issue) => JSON.stringify(issue)))
25 }
26 const interpreted = snapshot()
27 SchemaJITCompiler.enable(schema.ast)
28 deepStrictEqual(snapshot(), interpreted)
29 })
30
31 it.effect("preserves missing and present undefined through eager and suspended transformations", () =>
32 Effect.gen(function*() {
33 for (const suspended of [false, true]) {
34 const seen: Array<Option.Option<unknown>> = []
35 const schema = Schema.Struct({
36 value: Schema.Unknown.pipe(
37 Schema.decode({
38 decode: SchemaGetter.transformOptionalEffect<unknown, unknown>((input) => {
39 seen.push(input)
40 const output = Option.isNone(input) || input.value === "omit" ? Option.none() : Option.some(undefined)
41 return suspended ? Effect.sync(() => output) : Effect.succeed(output)
42 }),
43 encode: SchemaGetter.passthrough()
44 }),
45 Schema.optionalKey
46 )
47 })
48 for (const compiled of [false, true]) {
49 if (compiled) SchemaJITCompiler.enable(schema.ast)
50 seen.length = 0
51 const decode = SchemaParser.decodeUnknownEffect(schema)
52 deepStrictEqual(yield* decode({}), {})
53 deepStrictEqual(yield* decode({ value: "omit" }), {})
54 deepStrictEqual(yield* decode({ value: "present" }), { value: undefined })
55 deepStrictEqual(seen, [Option.none(), Option.some("omit"), Option.some("present")])
56 }
57 }
58 }))
59
60 it.effect("continues encoding checkpoints after middleware recovery without replaying transformations", () =>
61 Effect.gen(function*() {
62 for (const suspended of [false, true]) {
63 const events: Array<string> = []
64 const schema = Schema.String.pipe(
65 Schema.decodeTo(
66 Schema.Number.check(Schema.isGreaterThan(0)),
67 SchemaTransformation.transform({
68 decode: (input) => {
69 events.push("first")
70 return Number(input)
71 },
72 encode: String
73 })
74 ),
75 Schema.middlewareDecoding((effect) =>
76 Effect.catchEager(effect, () => {
77 events.push("recover")
78 return suspended ? Effect.sync(() => Option.some(1)) : Effect.succeed(Option.some(1))
79 })
80 ),
81 Schema.decodeTo(
82 Schema.String,
83 SchemaTransformation.transform({
84 decode: (input) => {
85 events.push("last")
86 return String(input)
87 },
88 encode: Number
89 })
90 )
91 )
92 for (const compiled of [false, true]) {
93 if (compiled) SchemaJITCompiler.enable(schema.ast)
94 const decode = SchemaParser.decodeUnknownEffect(schema)
95 events.length = 0
96 strictEqual(yield* decode("2"), "2")
97 deepStrictEqual(events, ["first", "last"])
98 events.length = 0
99 strictEqual(yield* decode("-1"), "1")
100 deepStrictEqual(events, ["first", "recover", "last"])
101 events.length = 0
102 strictEqual(yield* decode(false), "1")
103 deepStrictEqual(events, ["recover", "last"])
104 }
105 }
106 }))
107
108 it.effect("resolved parsers return Effects containing their actual output", () =>
109 Effect.gen(function*() {
110 const object = { value: "a" }
111 const cases: ReadonlyArray<readonly [Schema.Codec<unknown>, unknown, unknown]> = [
112 [Schema.String, "a", "a"],
113 [Schema.Number, -0, -0],
114 [Schema.Literal(0), -0, -0],
115 [Schema.Undefined, undefined, undefined],
116 [Schema.ObjectKeyword, object, object],
117 [Schema.Json, object, object],
118 [Schema.Struct({}), 1, 1],
119 [Schema.TemplateLiteral(["a"]), "a", "a"],
120 [Schema.FiniteFromString, "1", 1]
121 ]
122 for (const [schema, input, expected] of cases) {
123 for (const compiled of [false, true]) {
124 if (compiled) SchemaJITCompiler.enable(schema.ast)
125 const parser = SchemaParser.decodeUnknownEffect(schema)
126 const effect = parser(input, SchemaAST.defaultParseOptions)
127 strictEqual(Effect.isEffect(effect), true)
128 const output = yield* Effect.map(effect, (value) => value)
129 strictEqual(Object.is(output, expected), true)
130 const publicEffect = SchemaParser.decodeUnknownEffect(schema)(input)
131 strictEqual(Effect.isEffect(publicEffect), true)
132 strictEqual(Object.is(yield* publicEffect, expected), true)
133 }
134 }
135 }))
136
137 it.effect("retains public success values across subsequent and reentrant parser calls", () =>
138 Effect.gen(function*() {
139 let reenter: (input: unknown) => string
140 const schema = Schema.String.check(
141 Schema.makeFilter((value) => value !== "first" || reenter("nested") === "nested")
142 )
143 for (const compiled of [false, true]) {
144 if (compiled) SchemaJITCompiler.enable(schema.ast)
145 const decode = SchemaParser.decodeUnknownEffect(schema)
146 const decodeSync = SchemaParser.decodeUnknownSync(schema)
147 reenter = decodeSync
148 const first = decode("first")
149 const second = decode("second")
150 strictEqual(
151 yield* Effect.map(first, (value) => {
152 strictEqual(decodeSync("nested"), "nested")
153 return value
154 }),
155 "first"
156 )
157 strictEqual(yield* second, "second")
158 strictEqual(yield* first, "first")
159 }
160 }))
161
162 it.effect("preserves unchanged fields before and after asynchronous transformations", () =>
163 Effect.gen(function*() {
164 const number = Schema.String.pipe(Schema.decodeTo(Schema.Number, {
165 decode: SchemaGetter.transformOptionalEffect((input) =>
166 Effect.yieldNow.pipe(Effect.as(Option.map(input, Number)))
167 ),
168 encode: SchemaGetter.transform(String)
169 }))
170 const tuple = Schema.Tuple([Schema.String, number, Schema.Undefined]).check(
171 Schema.makeFilter((value) => value[0] === "before" && value[1] === 42 && value[2] === undefined)
172 )
173 const struct = Schema.Struct({ before: Schema.String, middle: number, after: Schema.Undefined }).check(
174 Schema.makeFilter((value) => value.before === "before" && value.middle === 42 && value.after === undefined)
175 )
176 for (const compiled of [false, true]) {
177 if (compiled) {
178 SchemaJITCompiler.enable(tuple.ast)
179 SchemaJITCompiler.enable(struct.ast)
180 }
181 deepStrictEqual(
182 yield* SchemaParser.decodeUnknownEffect(tuple)(["before", "42", undefined]),
183 ["before", 42, undefined]
184 )
185 deepStrictEqual(
186 yield* SchemaParser.decodeUnknownEffect(struct)({ before: "before", middle: "42", after: undefined }),
187 { before: "before", middle: 42, after: undefined }
188 )
189 }
190 }))
191
192 it("calls installed operations with options without inspecting extra function properties", () => {
193 const schema = Schema.Struct({ value: Schema.String })
194 const seen: Array<SchemaAST.ParseOptions> = []
195 const is: SchemaCompiler.Is = (_input, options) => {
196 seen.push(options)
197 return true
198 }
199 const decode: SchemaCompiler.Decode = (input, options) => {
200 seen.push(options)
201 return input
202 }
203 for (const operation of [is, decode]) {
204 Object.defineProperty(operation, "default", {
205 get() {
206 throw new Error("Not part of the compiled decoder contract")
207 }
208 })
209 }
210 SchemaCompiler.set(schema.ast, { is, decode, decodeEffect: Effect.succeed })
211 const input = { value: "a" }
212 strictEqual(SchemaParser.is(schema)(input), true)
213 strictEqual(SchemaParser.decodeUnknownSync(schema)(input), input)
214 const options = { reportInput: true }
215 strictEqual(SchemaParser.decodeUnknownSync(schema, options)(input), input)
216 deepStrictEqual(seen, [SchemaAST.defaultParseOptions, SchemaAST.defaultParseOptions, options])
217 })
218
219 it("bounds inlining of shared subgraphs", () => {
220 let schema: Schema.Codec<unknown> = Schema.Struct({ value: Schema.optionalKey(Schema.String) })
221 let valid: unknown = { value: "value" }
222 let invalid: unknown = { value: 1 }
223 for (let i = 0; i < 16; i++) {
224 schema = Schema.Struct({
225 left: Schema.optionalKey(schema),
226 right: Schema.optionalKey(schema)
227 })
228 valid = { left: valid }
229 invalid = { left: invalid }
230 }
231 const cases = [
232 { schema, valid, invalid },
233 { schema, valid: { left: { right: {} } }, invalid: { left: { right: 1 } } },
234 { schema: Schema.Array(schema), valid: [{}], invalid: [1] },
235 { schema: Schema.Union([schema, Schema.String]), valid: {}, invalid: 1 }
236 ]
237 for (const { schema, valid, invalid } of cases) {
238 const expected = SchemaParser.decodeUnknownResult(schema)(invalid)
239 assert(Result.isFailure(expected))
240 SchemaJITCompiler.enable(schema.ast)
241 deepStrictEqual(SchemaParser.decodeUnknownSync(schema)(valid), valid)
242 strictEqual(SchemaParser.is(schema)(valid), true)
243 strictEqual(SchemaParser.is(schema)(invalid), false)
244 deepStrictEqual(SchemaParser.decodeUnknownResult(schema)(invalid), expected)
245 }
246 })
247
248 it("bounds generated composed parsers for wide objects", () => {
249 const property = Schema.optionalKey(Schema.String)
250 const schema = Schema.Struct(Object.fromEntries(
251 Array.from({ length: 4096 }, (_, i) => [`key${i}`, property])
252 ))
253 SchemaJITCompiler.enable(schema.ast)
254 const input = { key4095: "last" }
255 deepStrictEqual(SchemaParser.decodeUnknownSync(schema)(input), input)
256 strictEqual(SchemaParser.is(schema)(input), true)
257 strictEqual(SchemaParser.is(schema)({ key4095: 1 }), false)
258 })
259
260 it("applies generation limits to every occurrence of a reused schema", () => {
261 const compiles = (schema: Schema.Top) => Codegen.generate(schema.ast, "decode") !== undefined
262 const nest = (schema: Schema.Top, depth: number): Schema.Top => {
263 for (let i = 0; i < depth; i++) schema = Schema.Struct({ n: schema })
264 return schema
265 }
266 assert.isTrue(compiles(nest(Schema.String, 256)))
267 assert.isFalse(compiles(nest(Schema.String, 257)))
268 const inner = nest(Schema.String, 200)
269 assert.isFalse(compiles(nest(inner, 57)))
270 assert.isTrue(compiles(nest(inner, 56)))
271 assert.isFalse(compiles(nest(inner, 57)))
272
273 const field = Schema.Struct(Object.fromEntries(Array.from({ length: 600 }, (_, i) => [`f${i}`, Schema.String])))
274 assert.isFalse(compiles(Schema.Struct({ a: field, b: field, c: field, d: field })))
275 assert.isTrue(compiles(Schema.Struct({ a: field, b: field, c: field })))
276 })
277
278 it("does not generate code for schemas nesting a declaration or suspension", () => {
279 const Recursive: Schema.Top = Schema.Struct({ next: Schema.optionalKey(Schema.suspend(() => Recursive)) })
280 for (const field of [Schema.Date, Recursive]) {
281 assert.isUndefined(Codegen.generate(Schema.Struct({ a: Schema.Struct({ b: field }) }).ast, "decode"))
282 }
283 })
284
285 it("does not reanalyse a nested schema that cannot be generated for each enclosing schema", () => {
286 let reads = 0
287 const date: SchemaAST.AST = new Proxy(Schema.Date.ast, {
288 get(target, key, receiver) {
289 reads++
290 return Reflect.get(target, key, receiver)
291 }
292 })
293 const levels: Array<SchemaAST.AST> = [date]
294 for (let i = 0; i < 50; i++) {
295 levels.push(new SchemaAST.Objects([new SchemaAST.PropertySignature("n", levels[i])], []))
296 }
297 for (const ast of levels.slice(1).reverse()) {
298 assert.isUndefined(Codegen.generate(ast, "decode"))
299 }
300 assert.isBelow(reads, 10)
301 })
302
303 it("preserves repeated Union members when selecting generated decoders", () => {
304 const member = Schema.Struct({ kind: Schema.Literal("a"), value: Schema.Number })
305 for (const mode of ["anyOf", "oneOf"] as const) {
306 const schema = Schema.Union([
307 member,
308 Schema.Struct({ kind: Schema.Literal("b"), value: Schema.String }),
309 member,
310 Schema.Never
311 ], { mode })
312 SchemaJITCompiler.enable(schema.ast)
313 const decode = SchemaParser.decodeUnknownResult(schema)
314 const is = SchemaParser.is(schema)
315 const input = { kind: "a", value: 1 } as const
316 strictEqual(is(input), mode === "anyOf")
317 const result = decode(input)
318 if (mode === "anyOf") {
319 deepStrictEqual(result, Result.succeed(input))
320 } else {
321 assert(Result.isFailure(result))
322 strictEqual(result.failure._tag, "OneOf")
323 }
324 const other = { kind: "b", value: "value" } as const
325 strictEqual(is(other), true)
326 deepStrictEqual(decode(other), Result.succeed(other))
327 }
328 })
329
330 it("stops oneOf after its second successful candidate", () => {
331 const schema = Schema.Union([
332 Schema.String.check(Schema.isMinLength(1)),
333 Schema.String.check(Schema.isMaxLength(10)),
334 Schema.String.check(Schema.makeFilter(() => {
335 throw new Error("The third candidate must not be evaluated")
336 }))
337 ], { mode: "oneOf" })
338 for (const compiled of [false, true]) {
339 if (compiled) SchemaJITCompiler.enable(schema.ast)
340 strictEqual(SchemaParser.is(schema)("hello"), false)
341 for (const options of [undefined, { errors: "all" }] as const) {
342 const result = SchemaParser.decodeUnknownResult(schema, options)("hello")
343 assert(Result.isFailure(result))
344 strictEqual(result.failure._tag, "OneOf")
345 }
346 }
347 })
348
349 it.effect("accepts both zero signs and preserves the input across parser adapters", () =>
350 Effect.gen(function*() {
351 const options: Array<SchemaAST.ParseOptions | undefined> = [
352 undefined,
353 { errors: "all" },
354 { reportInput: true }
355 ]
356 for (const literal of [0, -0]) {
357 const schemas = [
358 Schema.Literal(literal),
359 Schema.Union([Schema.Literal(literal), Schema.Literal(1)]),
360 Schema.Literal(literal).check(Schema.makeFilter((n) => Object.is(n, -0)))
361 ]
362 for (const schema of schemas) {
363 const nested = Schema.Struct({ values: Schema.Array(schema) })
364 for (const compiled of [false, true]) {
365 if (compiled) {
366 SchemaJITCompiler.enable(schema.ast)
367 SchemaJITCompiler.enable(nested.ast)
368 }
369 for (const input of [0, -0]) {
370 if (schema.ast.checks && !Object.is(input, -0)) {
371 strictEqual(SchemaParser.is(schema)(input), false)
372 assert(Result.isFailure(SchemaParser.decodeUnknownResult(schema)(input)))
373 continue
374 }
375 strictEqual(SchemaParser.is(schema)(input), true)
376 for (const option of options) {
377 strictEqual(Object.is(SchemaParser.decodeUnknownSync(schema, option)(input), input), true)
378 strictEqual(Object.is(SchemaParser.encodeUnknownSync(schema, option)(input), input), true)
379 const result = SchemaParser.decodeUnknownResult(schema, option)(input)
380 assert(Result.isSuccess(result))
381 strictEqual(Object.is(result.success, input), true)
382 const exit = SchemaParser.decodeUnknownExit(schema, option)(input)
383 assert(Exit.isSuccess(exit))
384 strictEqual(Object.is(exit.value, input), true)
385 const optional = SchemaParser.decodeUnknownOption(schema, option)(input)
386 assert(Option.isSome(optional))
387 strictEqual(Object.is(optional.value, input), true)
388 const output = yield* SchemaParser.decodeUnknownEffect(schema, option)(input)
389 strictEqual(Object.is(output, input), true)
390 const decoded = SchemaParser.decodeUnknownSync(nested, option)({ values: [input] })
391 strictEqual(Object.is(decoded.values[0], input), true)
392 }
393 }
394 }
395 }
396 }
397 }))
398
399 it("retains the original encoding AST in local checks", () => {
400 const schema = Schema.NumberFromString.check(
401 Schema.makeFilter((_value, ast) => ast === schema.ast && ast.encoding !== undefined)
402 )
403 strictEqual(SchemaParser.decodeUnknownSync(schema)("1"), 1)
404 SchemaJITCompiler.enable(schema.ast)
405 strictEqual(SchemaParser.decodeUnknownSync(schema)("1"), 1)
406 })
407
408 it("retains the original encoding AST in structural issues", () => {
409 const schema = Schema.String.pipe(Schema.decodeTo(
410 Schema.Number,
411 SchemaTransformation.transform({ decode: () => "invalid" as any, encode: String })
412 ))
413 for (const compiled of [false, true]) {
414 if (compiled) SchemaJITCompiler.enable(schema.ast)
415 const result = SchemaParser.decodeUnknownResult(schema)("input")
416 assert(Result.isFailure(result))
417 assert(result.failure._tag === "InvalidType")
418 strictEqual(result.failure.ast, schema.ast)
419 }
420 })
421
422 it("installs accessors without evaluating them and reads only the selected operation once", () => {
423 const schema = Schema.Struct({ value: Schema.String })
424 const reads: Array<string> = []
425 const decoder = {
426 get is() {
427 strictEqual(this, decoder)
428 reads.push("is")
429 return (_input: unknown) => true
430 },
431 get decode() {
432 strictEqual(this, decoder)
433 reads.push("decode")
434 return (input: unknown) => input
435 },
436 get decodeEffect() {
437 strictEqual(this, decoder)
438 reads.push("decodeEffect")
439 return Effect.succeed
440 }
441 }
442 SchemaCompiler.set(schema.ast, decoder)
443 deepStrictEqual(reads, [])
444 strictEqual(SchemaParser.is(schema)({ value: "a" }), true)
445 strictEqual(SchemaParser.is(schema)({ value: "b" }), true)
446 deepStrictEqual(reads, ["is"])
447 const input = { value: "a" }
448 strictEqual(SchemaParser.decodeUnknownSync(schema)(input), input)
449 strictEqual(SchemaParser.decodeUnknownSync(schema)(input), input)
450 deepStrictEqual(reads, ["is", "decode"])
451 })
452
453 it("memoizes an absent optional operation", () => {
454 const schema = Schema.Struct({ value: Schema.String })
455 let reads = 0
456 SchemaCompiler.set(schema.ast, {
457 get is() {
458 reads++
459 return undefined
460 },
461 decode: (input) => input,
462 decodeEffect: Effect.succeed
463 })
464 strictEqual(SchemaParser.is(schema)({ value: "a" }), true)
465 strictEqual(SchemaParser.is(schema)({ value: "b" }), true)
466 strictEqual(reads, 1)
467 })
468
469 it("shares lazy detailed decoding across public adapters without mutating the supplied decoder", () => {
470 const schema = Schema.Struct({ value: Schema.String })
471 const reads: Array<string> = []
472 const decoder = Object.freeze({
473 get decode() {
474 strictEqual(this, decoder)
475 reads.push("decode")
476 return () => SchemaCompiler.invalid
477 },
478 get decodeEffect() {
479 strictEqual(this, decoder)
480 reads.push("decodeEffect")
481 return Effect.succeed
482 }
483 })
484 SchemaCompiler.set(schema.ast, decoder)
485 const input = { value: "a" }
486 strictEqual(SchemaParser.decodeUnknownSync(schema)(input), input)
487 deepStrictEqual(SchemaParser.decodeUnknownResult(schema)(input), Result.succeed(input))
488 strictEqual(SchemaParser.decodeUnknownSync(schema, { reportInput: true })(input), input)
489 deepStrictEqual(reads, ["decode", "decodeEffect"])
490 })
491
492 it("does not restart validation inside the detailed decoder", () => {
493 const schema = Schema.Struct({ values: Schema.Array(Schema.Struct({ value: Schema.String })) })
494 SchemaJITCompiler.enable(schema.ast)
495 let reads = 0
496 const result = SchemaParser.decodeUnknownResult(schema)({
497 values: [{
498 get value() {
499 reads++
500 return 1
501 }
502 }]
503 })
504 assert(Result.isFailure(result))
505 strictEqual(reads, 2)
506 })
507
508 it("uses the interpreter after selective JIT generation fails", () => {
509 const schema = Schema.Struct({ value: Schema.String })
510 const original = globalThis.Function
511 const defect = new SyntaxError("generated source defect")
512 let attempts = 0
513 try {
514 globalThis.Function = ((...parameters: Array<string>) => {
515 if (parameters.length === 1 && parameters[0] === "return true") return original(...parameters)
516 attempts++
517 throw defect
518 }) as FunctionConstructor
519 SchemaJITCompiler.enable(schema.ast)
520 const decode = SchemaParser.decodeUnknownSync(schema)
521 deepStrictEqual(decode({ value: "a", extra: true }), { value: "a" })
522 assert(Result.isFailure(SchemaParser.decodeUnknownResult(schema)({ value: 1 })))
523 deepStrictEqual(decode({ value: "b" }), { value: "b" })
524 strictEqual(attempts, 1)
525 } finally {
526 globalThis.Function = original
527 }
528 })
529})