1import { assert, describe, it, vi } from "@effect/vitest"
2import { Cause, Schema, SchemaParser, SchemaTransformation } from "effect"
3import * as Codegen from "effect/internal/schema/codegen"
4// oxlint-disable-next-line no-unassigned-import
5import "effect/schema/SchemaJITCompiler/enable"
6import { assertSchemaIssueError, deepStrictEqual, strictEqual, throws } from "../utils/assert.ts"
7
8describe("Schema JIT compilation fallback", () => {
9 for (const phase of ["construction", "factory"] as const) {
10 for (const firstOperation of ["decode", "is"] as const) {
11 it(`recovers ${phase} failure during ${firstOperation} without retrying`, () => {
12 const schema = Schema.Struct({ value: Schema.String })
13 const getParser = vi.spyOn(schema.ast, "getParser")
14 const Function = globalThis.Function
15 let attempts = 0
16 try {
17 globalThis.Function = ((...args: Array<string>) => {
18 if (args.length === 1 && args[0] === "return true") return Function(...args)
19 attempts++
20 if (phase === "construction") throw new SyntaxError("invalid generated source")
21 return () => {
22 throw new Error("generated factory failed")
23 }
24 }) as FunctionConstructor
25
26 const decode = SchemaParser.decodeUnknownSync(schema)
27 const is = SchemaParser.is(schema)
28 const input = { value: "valid", extra: true }
29 if (firstOperation === "is") strictEqual(is(input), true)
30 deepStrictEqual(decode(input), { value: "valid" })
31 strictEqual(is(input), true)
32 strictEqual(is({ value: 1 }), false)
33 throws(() => decode({ value: 1 }), (error) => {
34 assertSchemaIssueError(error, `Expected string\n at ["value"]`)
35 })
36 throws(() => decode(input, { onExcessProperty: "error" }), (error) => {
37 assertSchemaIssueError(error, `Expected no excess property\n at ["extra"]`)
38 })
39 deepStrictEqual(SchemaParser.decodeUnknownSync(schema)(input), { value: "valid" })
40 strictEqual(attempts, 1)
41 strictEqual(getParser.mock.calls.length, 1)
42 } finally {
43 globalThis.Function = Function
44 getParser.mockRestore()
45 }
46 })
47 }
48 }
49
50 for (const phase of ["generate"] as const) {
51 it(`recovers errors in ${phase} without disabling other schemas`, () => {
52 const failure = vi.spyOn(Codegen, phase).mockImplementationOnce(() => {
53 throw new Error("compiler failed")
54 })
55 const schema = Schema.Struct({ value: Schema.String })
56 const getParser = vi.spyOn(schema.ast, "getParser")
57 try {
58 const decode = SchemaParser.decodeUnknownSync(schema)
59 deepStrictEqual(decode({ value: "valid" }), { value: "valid" })
60 strictEqual(getParser.mock.calls.length, 1)
61 const attempts = failure.mock.calls.length
62 deepStrictEqual(decode({ value: "again" }), { value: "again" })
63 strictEqual(failure.mock.calls.length, attempts)
64
65 const other = Schema.Struct({ value: Schema.String })
66 const otherParser = vi.spyOn(other.ast, "getParser")
67 try {
68 deepStrictEqual(SchemaParser.decodeUnknownSync(other)({ value: "compiled" }), { value: "compiled" })
69 strictEqual(otherParser.mock.calls.length, 0)
70 } finally {
71 otherParser.mockRestore()
72 }
73 } finally {
74 failure.mockRestore()
75 getParser.mockRestore()
76 }
77 })
78 }
79
80 it("recovers composed decoder generation without repeating transformations", () => {
81 let transformations = 0
82 const field = Schema.String.pipe(Schema.decodeTo(
83 Schema.String,
84 SchemaTransformation.transform({
85 decode: (value) => {
86 transformations++
87 return value.trim()
88 },
89 encode: (value) => value
90 })
91 ))
92 const schema = Schema.Struct({ value: field })
93 const generate = Codegen.generate
94 const failure = vi.spyOn(Codegen, "generate").mockImplementation((ast, operation) => {
95 if (ast === schema.ast) throw new Error("composed decoder generation failed")
96 return generate(ast, operation)
97 })
98 try {
99 const decode = SchemaParser.decodeUnknownSync(schema)
100 deepStrictEqual(decode({ value: " a " }), { value: "a" })
101 deepStrictEqual(decode({ value: " b " }), { value: "b" })
102 strictEqual(transformations, 2)
103 strictEqual(failure.mock.calls.filter(([ast]) => ast === schema.ast).length, 1)
104 } finally {
105 failure.mockRestore()
106 }
107 })
108
109 it("recovers decoder generation without replaying its encoding or middleware", () => {
110 let transformations = 0
111 let middlewareRuns = 0
112 const schema = Schema.String.pipe(
113 Schema.decodeTo(
114 Schema.Struct({ value: Schema.String.check(Schema.isMinLength(2)) }),
115 SchemaTransformation.transform({
116 decode: (value) => {
117 transformations++
118 return { value: value.trim() }
119 },
120 encode: (value) => value.value
121 })
122 ),
123 Schema.middlewareDecoding((effect) => {
124 middlewareRuns++
125 return effect
126 })
127 )
128 const emit = Codegen.generate
129 let attempts = 0
130 let transformationsAtFailure = 0
131 const failure = vi.spyOn(Codegen, "generate").mockImplementation((ast, operation) => {
132 if (ast === schema.ast) {
133 attempts++
134 transformationsAtFailure = transformations
135 throw new Error("local checkpoint generation failed")
136 }
137 return emit(ast, operation)
138 })
139 try {
140 const decode = SchemaParser.decodeUnknownSync(schema)
141 deepStrictEqual(decode(" valid "), { value: "valid" })
142 strictEqual(transformationsAtFailure, 0)
143 strictEqual(transformations, 1)
144 strictEqual(middlewareRuns, 1)
145 throws(() => decode(" x "))
146 strictEqual(transformations, 2)
147 strictEqual(middlewareRuns, 2)
148 strictEqual(attempts, 1)
149 } finally {
150 failure.mockRestore()
151 }
152 })
153
154 it("propagates errors from executing generated code without interpreting the input", () => {
155 const schema = Schema.Struct({ value: Schema.String })
156 const getParser = vi.spyOn(schema.ast, "getParser")
157 const Function = globalThis.Function
158 let executions = 0
159 const defect = new Error("generated parser failed")
160 try {
161 globalThis.Function = ((...args: Array<string>) => {
162 if (args.length === 1 && args[0] === "return true") return Function(...args)
163 return () => () => {
164 executions++
165 throw defect
166 }
167 }) as FunctionConstructor
168 const result = SchemaParser.decodeUnknownExit(schema)({ value: "valid" })
169 assert(result._tag === "Failure")
170 assert(Cause.hasDies(result.cause))
171 strictEqual(executions, 1)
172 strictEqual(getParser.mock.calls.length, 0)
173 } finally {
174 globalThis.Function = Function
175 getParser.mockRestore()
176 }
177 })
178})