1import { assert, describe, it, vi } from "@effect/vitest"
2import { Effect, Schema, SchemaAST, SchemaParser } from "effect"
3import { SchemaCompiler, SchemaJITCompiler } from "effect/schema"
4import { deepStrictEqual, strictEqual, throws } from "../utils/assert.ts"
5
6describe("SchemaCompiler", () => {
7 it("resolves a type guard on first invocation", () => {
8 const schema = Schema.String.annotate({ title: "lazy type guard" })
9 const ast = SchemaAST.toType(schema.ast)
10 const guard = SchemaParser.is(schema)
11 let calls = 0
12
13 SchemaCompiler.set(ast, {
14 is: () => {
15 calls++
16 return true
17 },
18 decodeEffect: Effect.succeed
19 })
20
21 strictEqual(guard(1), true)
22 strictEqual(guard(2), true)
23 strictEqual(calls, 2)
24
25 SchemaCompiler.set(ast, {
26 is: () => false,
27 decodeEffect: Effect.succeed
28 })
29
30 strictEqual(guard(3), true)
31 strictEqual(SchemaParser.is(schema)(3), false)
32 strictEqual(calls, 3)
33 })
34
35 it("reuses interpreted candidates inside a selectively compiled Union", () => {
36 const child = Schema.String.annotate({ title: "interpreted Union candidate" })
37 const schema = Schema.Union([child, Schema.Number])
38 const initialize = vi.spyOn(child.ast, "getParser")
39 try {
40 SchemaJITCompiler.enable(schema.ast)
41 const make = SchemaParser.make(schema)
42 strictEqual(make("first"), "first")
43 strictEqual(make("second"), "second")
44 strictEqual(initialize.mock.calls.length, 1)
45 } finally {
46 initialize.mockRestore()
47 }
48 })
49
50 it("retains a resolved Union member parser after replacement", () => {
51 const child = Schema.String.annotate({ title: "resolved Union candidate" })
52 const decode = SchemaParser.decodeUnknownSync(Schema.Union([child, Schema.Number]))
53
54 strictEqual(decode("original"), "original")
55
56 SchemaCompiler.set(child.ast, {
57 decode: () => "replacement",
58 decodeEffect: () => Effect.succeed("replacement")
59 })
60
61 strictEqual(decode("cached"), "cached")
62 strictEqual(SchemaParser.decodeUnknownSync(child)("new consumer"), "replacement")
63 })
64
65 it("uses a replacement for an unresolved Union member", () => {
66 const child = Schema.String.annotate({ title: "unresolved Union candidate" })
67 const decode = SchemaParser.decodeUnknownSync(Schema.Union([Schema.Number, child]))
68
69 strictEqual(decode(1), 1)
70
71 SchemaCompiler.set(child.ast, {
72 decode: () => "replacement",
73 decodeEffect: () => Effect.succeed("replacement")
74 })
75
76 strictEqual(decode("first visit"), "replacement")
77 })
78
79 it("installs a decoder in the shared registry", () => {
80 const schema = Schema.Struct({ value: Schema.String })
81 const early = SchemaParser.decodeUnknownSync(schema)
82 deepStrictEqual(early({ value: "interpreted" }), { value: "interpreted" })
83
84 let decodes = 0
85 SchemaCompiler.set(schema.ast, {
86 is: () => true,
87 decode: (_input, options) =>
88 options.reportInput === true
89 ? { value: "compiled" }
90 : SchemaCompiler.invalid,
91 decodeEffect: () => {
92 decodes++
93 return Effect.succeed({ value: "detailed" })
94 }
95 })
96
97 const late = SchemaParser.decodeUnknownSync(schema)
98 deepStrictEqual(late({ value: 1 }, { reportInput: true }), { value: "compiled" })
99 deepStrictEqual(late({ value: 1 }), { value: "detailed" })
100 strictEqual(decodes, 1)
101
102 // Parsers that resolved the old entry before set keep using it.
103 deepStrictEqual(early({ value: "interpreted" }), { value: "interpreted" })
104 })
105
106 it("uses is only for type guards", () => {
107 const schema = Schema.Struct({ value: Schema.String })
108 let validations = 0
109 SchemaCompiler.set(schema.ast, {
110 is: (input, options) => {
111 strictEqual(options, SchemaAST.defaultParseOptions)
112 return (input as { readonly value?: unknown }).value === "accepted"
113 },
114 decode: (input) => {
115 validations++
116 return input
117 },
118 decodeEffect: Effect.succeed
119 })
120
121 strictEqual(SchemaParser.is(schema)({ value: "accepted" }), true)
122 strictEqual(SchemaParser.is(schema)({ value: "rejected" }), false)
123 deepStrictEqual(SchemaParser.decodeUnknownSync(schema)({ value: "decoded" }), { value: "decoded" })
124 strictEqual(validations, 1)
125 })
126
127 it("retains one resolved entry across sync option paths", () => {
128 for (const direction of ["decode", "encode"]) {
129 for (const firstOptions of [undefined, { reportInput: true }]) {
130 const schema = Schema.Struct({ value: Schema.String })
131 const makeSync = () =>
132 direction === "decode"
133 ? SchemaParser.decodeUnknownSync(schema)
134 : SchemaParser.encodeUnknownSync(schema)
135 const decode = makeSync()
136 const input = { value: "original" }
137 deepStrictEqual(decode(input, firstOptions), input)
138
139 SchemaCompiler.set(schema.ast, {
140 decode: () => ({ value: "replacement" }),
141 decodeEffect: () => Effect.succeed({ value: "replacement" })
142 })
143
144 deepStrictEqual(decode(input), input)
145 deepStrictEqual(decode(input, { reportInput: true }), input)
146 deepStrictEqual(makeSync()(input), { value: "replacement" })
147 }
148 }
149 })
150
151 it("does not resolve child operations until the child is parsed", () => {
152 const child = Schema.String.annotate({ title: "lazy child" })
153 let reads = 0
154 SchemaCompiler.set(child.ast, {
155 get decode() {
156 reads++
157 return undefined
158 },
159 get decodeEffect() {
160 reads++
161 return Effect.succeed
162 }
163 })
164 const schema = Schema.Struct({ first: Schema.Number, child })
165 const decode = SchemaParser.decodeUnknownSync(schema)
166 throws(() => decode({ first: "invalid", child: "unreached" }))
167 strictEqual(reads, 0)
168 deepStrictEqual(decode({ first: 1, child: "reached" }), { first: 1, child: "reached" })
169 strictEqual(reads, 2)
170 deepStrictEqual(decode({ first: 2, child: "cached" }), { first: 2, child: "cached" })
171 strictEqual(reads, 2)
172 })
173
174 it("uses an installed child decoder from an interpreted Array", () => {
175 const child = Schema.String.annotate({ title: "installed array child" })
176 SchemaCompiler.set(child.ast, {
177 decode: (input) => typeof input === "string" ? `${input}!` : SchemaCompiler.invalid,
178 decodeEffect: (input) => Effect.succeed(`${input}!`)
179 })
180
181 deepStrictEqual(
182 SchemaParser.decodeUnknownSync(Schema.Array(child))(["a"]),
183 ["a!"]
184 )
185 })
186
187 it("exposes the canonical missing value to installed decoders", () => {
188 const schema = Schema.Struct({ value: Schema.optionalKey(Schema.String) })
189 assert(schema.ast._tag === "Objects")
190 const value = schema.ast.propertySignatures[0].type
191 let sawMissing = false
192 SchemaCompiler.set(value, {
193 decode: (input) => typeof input === "string" ? input : SchemaCompiler.invalid,
194 decodeEffect: (input) => {
195 sawMissing = input === SchemaCompiler.missing
196 return Effect.succeed(input)
197 }
198 })
199
200 deepStrictEqual(SchemaParser.decodeUnknownSync(schema)({}), {})
201 strictEqual(sawMissing, true)
202 })
203
204 it("installs encoders on the flipped AST", () => {
205 const schema = Schema.FiniteFromString
206 SchemaCompiler.set(SchemaAST.flip(schema.ast), {
207 decode: () => "aot",
208 decodeEffect: () => Effect.succeed("detailed")
209 })
210
211 strictEqual(SchemaParser.encodeUnknownSync(schema)(1), "aot")
212 })
213})
214
215describe("SchemaJITCompiler", () => {
216 it("reuses option-independent generated functions for explicit options", () => {
217 const schema = Schema.Struct({ nested: Schema.Struct({ value: Schema.String }) })
218 const input = { nested: { value: "valid" } }
219 const Function = globalThis.Function
220 let constructions = 0
221 try {
222 globalThis.Function = ((...args: ReadonlyArray<string>) => {
223 constructions++
224 return Function(...args)
225 }) as FunctionConstructor
226
227 SchemaJITCompiler.enable(schema.ast)
228 const decode = SchemaParser.decodeUnknownSync(schema)
229 deepStrictEqual(decode(input), input)
230 strictEqual(SchemaParser.is(schema)(input), true)
231 const initialized = constructions
232 assert(initialized > 1)
233 for (const options of [{}, { reportInput: true }, { errors: "all" }, { disableChecks: true }] as const) {
234 deepStrictEqual(decode(input, options), input)
235 strictEqual(SchemaParser.is(schema)(input), true)
236 strictEqual(constructions, initialized)
237 }
238 } finally {
239 globalThis.Function = Function
240 }
241 })
242
243 it("keeps generated operations lazy", () => {
244 const schema = Schema.Struct({ value: Schema.String })
245 const Function = globalThis.Function
246 let constructions = 0
247 try {
248 globalThis.Function = ((...args: ReadonlyArray<string>) => {
249 constructions++
250 return Function(...args)
251 }) as FunctionConstructor
252
253 SchemaJITCompiler.enable(schema.ast)
254 strictEqual(constructions, 1)
255 deepStrictEqual(SchemaParser.decodeUnknownSync(schema)({ value: "valid" }), { value: "valid" })
256 assert(constructions > 1)
257 } finally {
258 globalThis.Function = Function
259 }
260 })
261
262 it("replaces only the selected AST and leaves resolved parsers intact", () => {
263 const selected = Schema.Struct({ value: Schema.String })
264 const untouched = Schema.Struct({ value: Schema.String })
265 const early = SchemaParser.decodeUnknownSync(selected)
266 deepStrictEqual(early({ value: "valid" }), { value: "valid" })
267
268 SchemaJITCompiler.enable(selected.ast)
269
270 let earlyReads = 0
271 throws(() =>
272 early({
273 get value() {
274 earlyReads++
275 return 1
276 }
277 })
278 )
279 strictEqual(earlyReads, 1)
280
281 let selectedReads = 0
282 throws(() =>
283 SchemaParser.decodeUnknownSync(selected)({
284 get value() {
285 selectedReads++
286 return 1
287 }
288 })
289 )
290 strictEqual(selectedReads, 2)
291
292 let untouchedReads = 0
293 throws(() =>
294 SchemaParser.decodeUnknownSync(untouched)({
295 get value() {
296 untouchedReads++
297 return 1
298 }
299 })
300 )
301 strictEqual(untouchedReads, 1)
302 })
303
304 it("compiles descendants through an unsupported lazy root", () => {
305 const child = Schema.Struct({ value: Schema.String })
306 const schema = Schema.suspend(() => child)
307 SchemaJITCompiler.enable(schema.ast)
308
309 let reads = 0
310 throws(() =>
311 SchemaParser.decodeUnknownSync(schema)({
312 get value() {
313 reads++
314 return 1
315 }
316 })
317 )
318 strictEqual(reads, 2)
319 })
320
321 it("prepares declaration type parameters with the selective compiler on first use", () => {
322 const child = Schema.Struct({ value: Schema.String })
323 const schema = Schema.ReadonlySet(child)
324 SchemaJITCompiler.enable(schema.ast)
325 const decode = SchemaParser.decodeUnknownSync(schema)
326 deepStrictEqual(decode(new Set([{ value: "a", extra: true }])), new Set([{ value: "a" }]))
327 throws(() => decode(new Set([{ value: 1 }])))
328 })
329
330 it("does not initialize unused declaration type parameter operations", () => {
331 const child = Schema.Struct({ value: Schema.String })
332 let reads = 0
333 SchemaCompiler.set(child.ast, {
334 get decode() {
335 reads++
336 return undefined
337 },
338 get decodeEffect() {
339 reads++
340 return Effect.succeed
341 }
342 })
343 const schema = Schema.ReadonlySet(child)
344 SchemaJITCompiler.enable(schema.ast)
345 const decode = SchemaParser.decodeUnknownSync(schema)
346 strictEqual(reads, 0)
347 deepStrictEqual(decode(new Set()), new Set())
348 strictEqual(reads, 0)
349 deepStrictEqual(decode(new Set([{ value: "a" }])), new Set([{ value: "a" }]))
350 strictEqual(reads, 2)
351 })
352
353 it("preserves an installed decoder when dynamic code generation is unavailable", () => {
354 const schema = Schema.Struct({ value: Schema.String })
355 SchemaCompiler.set(schema.ast, {
356 decode: () => ({ value: "installed" }),
357 decodeEffect: () => Effect.succeed({ value: "installed" })
358 })
359 const Function = globalThis.Function
360 try {
361 globalThis.Function = (() => {
362 throw new Error("dynamic function generation unavailable")
363 }) as any
364 SchemaJITCompiler.enable(schema.ast)
365 } finally {
366 globalThis.Function = Function
367 }
368
369 deepStrictEqual(SchemaParser.decodeUnknownSync(schema)({ value: 1 }), { value: "installed" })
370 })
371})