1import { assert, describe, it, vi } from "@effect/vitest"
2import { Effect, Option, Schema, SchemaAST, SchemaParser } from "effect"
3import * as Codegen from "effect/internal/schema/codegen"
4import * as Registry from "effect/internal/schema/compilerRegistry"
5import { SchemaCompiler, SchemaJITCompiler } from "effect/schema"
6import { constructionCases, constructionEvents, constructionOptions } from "./fixtures/construction.ts"
7
8describe("Schema compiler construction", { concurrent: false }, () => {
9 it.effect("initializes and caches makers on first access", () =>
10 Effect.gen(function*() {
11 const schema = Schema.Struct({ value: Schema.NumberFromString })
12 assert.isFalse(Object.hasOwn(schema, "make"))
13 assert.isFalse(Object.hasOwn(schema, "makeEffect"))
14 assert.isFalse(Object.hasOwn(schema, "makeOption"))
15
16 const { make, makeEffect, makeOption } = schema
17 assert.strictEqual(schema.make, make)
18 assert.strictEqual(schema.makeEffect, makeEffect)
19 assert.strictEqual(schema.makeOption, makeOption)
20 assert.deepStrictEqual(make({ value: 0 }), { value: 0 })
21 assert.deepStrictEqual(yield* makeEffect({ value: 1 }), { value: 1 })
22 assert.deepStrictEqual(makeOption({ value: 2 }), Option.some({ value: 2 }))
23 }))
24
25 it.effect("matches interpreted construction, effects and options", () =>
26 Effect.gen(function*() {
27 const fixtures = Object.entries(constructionCases)
28 const snapshot = Effect.fnUntraced(function*(fixture: typeof fixtures[number][1]) {
29 const out = []
30 const make = SchemaParser.makeEffect(fixture.schema)
31 for (const parseOptions of constructionOptions) {
32 for (const input of fixture.inputs) {
33 constructionEvents.length = 0
34 const result = yield* Effect.result(make(input as never, { parseOptions }))
35 out.push({ result, events: [...constructionEvents] })
36 }
37 }
38 return out
39 })
40 // Capture every interpreted result before installing shared child ASTs.
41 const interpreted = yield* Effect.forEach(fixtures, ([, fixture]) => snapshot(fixture))
42 for (const [index, [name, fixture]] of fixtures.entries()) {
43 SchemaJITCompiler.enable(SchemaAST.toType(fixture.schema.ast))
44 assert.deepStrictEqual(yield* snapshot(fixture), interpreted[index], name)
45 }
46 }))
47
48 it("keeps selective compilation below an interpreted Suspend", () => {
49 let forced = 0
50 const child = Schema.Struct({ value: Schema.String })
51 const schema = Schema.suspend(() => {
52 forced++
53 return child
54 })
55 SchemaJITCompiler.enable(SchemaAST.toType(schema.ast))
56 assert.strictEqual(forced, 0)
57 assert.deepStrictEqual(SchemaParser.make(schema)({ value: "a" }), { value: "a" })
58 assert.strictEqual(forced, 1)
59 })
60
61 it("compiles only the raw constructor when construction succeeds", () => {
62 const schema = Schema.Struct({ a: Schema.String })
63 const emit = vi.spyOn(Codegen, "generate")
64 try {
65 SchemaJITCompiler.enable(schema.ast)
66 assert.deepStrictEqual(SchemaParser.make(schema)({ a: "a" }), { a: "a" })
67 assert.strictEqual(
68 emit.mock.calls.filter(([, operation]) => operation === "decode" || operation === "is").length,
69 0
70 )
71 assert.strictEqual(emit.mock.calls.filter(([, operation]) => operation === "make").length, 1)
72 assert.strictEqual(emit.mock.calls.filter(([, operation]) => operation === "makeEffect").length, 0)
73 } finally {
74 emit.mockRestore()
75 }
76 })
77
78 it("does not compile construction when only decoding is used", () => {
79 const schema = Schema.Struct({ a: Schema.String })
80 const emit = vi.spyOn(Codegen, "generate")
81 try {
82 SchemaJITCompiler.enable(schema.ast)
83 assert.deepStrictEqual(SchemaParser.decodeUnknownSync(schema)({ a: "a" }), { a: "a" })
84 assert.strictEqual(emit.mock.calls.filter(([, operation]) => operation === "makeEffect").length, 0)
85 assert.deepStrictEqual(SchemaParser.make(schema)({ a: "a" }), { a: "a" })
86 assert.strictEqual(emit.mock.calls.filter(([, operation]) => operation === "make").length, 1)
87 assert.strictEqual(emit.mock.calls.filter(([, operation]) => operation === "makeEffect").length, 0)
88 } finally {
89 emit.mockRestore()
90 }
91 })
92
93 for (const compiled of [false, true]) {
94 it(`initializes only reached constructor children, compiled=${compiled}`, () => {
95 const child = Schema.String.annotate({ title: "lazy constructor child" })
96 let reads = 0
97 const decoder: SchemaCompiler.CompiledDecoder = Object.freeze({
98 decodeEffect: Effect.succeed,
99 get makeEffect() {
100 assert.strictEqual<unknown>(this, decoder)
101 reads++
102 return Effect.succeed
103 }
104 })
105 SchemaCompiler.set(child.ast, decoder)
106 const schema = Schema.Struct({ first: Schema.Number, child })
107 if (compiled) SchemaJITCompiler.enable(schema.ast)
108 const make = SchemaParser.make(schema)
109 assert.throws(() => make({ first: "invalid", child: "unreached" } as never))
110 assert.strictEqual(reads, 0)
111 assert.deepStrictEqual(make({ first: 1, child: "a" }), { first: 1, child: "a" })
112 assert.deepStrictEqual(make({ first: 2, child: "b" }), { first: 2, child: "b" })
113 assert.strictEqual(reads, 1)
114 })
115 }
116
117 it("prepares selective Declaration parameters for public decoders in the callback", () => {
118 const child = Schema.Struct({ value: Schema.String })
119 const schema = Schema.ReadonlySet(child)
120 SchemaJITCompiler.enable(SchemaAST.toType(schema.ast))
121 assert.deepStrictEqual(SchemaParser.make(schema)(new Set([{ value: "a" }])), new Set([{ value: "a" }]))
122 assert.strictEqual(Registry.resolve(child.ast).compiled !== undefined, true)
123 })
124
125 it("does not restart a parent if compilation fails after a default", () => {
126 let defaults = 0
127 const child = Schema.Struct({ value: Schema.String })
128 const schema = Schema.Struct({
129 child: child.pipe(Schema.withConstructorDefault(Effect.sync(() => {
130 defaults++
131 return { value: "default" }
132 })))
133 })
134 const childAST = schema.fields.child.ast
135 const emit = Codegen.generate
136 const failure = vi.spyOn(Codegen, "generate").mockImplementation((ast, operation) => {
137 if (ast === childAST && operation === "makeEffect") {
138 assert.strictEqual(defaults, 1)
139 throw new Error("child compile failed")
140 }
141 return emit(ast, operation)
142 })
143 try {
144 SchemaJITCompiler.enable(schema.ast)
145 assert.deepStrictEqual(SchemaParser.make(schema)({}), { child: { value: "default" } })
146 assert.strictEqual(defaults, 1)
147 } finally {
148 failure.mockRestore()
149 }
150 })
151
152 for (const operation of ["make", "decode"] as const) {
153 it(`keeps the other operation compiled after ${operation} compilation fails`, () => {
154 const schema = Schema.Struct({ a: Schema.String })
155 SchemaJITCompiler.enable(schema.ast)
156 const generate = Codegen.generate
157 const failed = vi.spyOn(Codegen, "generate").mockImplementation((ast, key) => {
158 if (ast === schema.ast && key === (operation === "make" ? "make" : "decode")) {
159 throw new Error("compile failed")
160 }
161 return generate(ast, key)
162 })
163 try {
164 const first = operation === "make" ? SchemaParser.make(schema) : SchemaParser.decodeUnknownSync(schema)
165 assert.deepStrictEqual(first({ a: "a" }), { a: "a" })
166 const second = operation === "make" ? SchemaParser.decodeUnknownSync(schema) : SchemaParser.make(schema)
167 assert.deepStrictEqual(second({ a: "a" }), { a: "a" })
168 assert(
169 failed.mock.calls.some(([ast, key]) =>
170 ast === schema.ast && key === (operation === "make" ? "decode" : "make")
171 )
172 )
173 } finally {
174 failed.mockRestore()
175 }
176 })
177 }
178 it("resolves installed construction lazily and independently from decoding", () => {
179 const schema = Schema.Struct({ a: Schema.String })
180 let reads = 0
181 let calls = 0
182 SchemaCompiler.set(schema.ast, {
183 get decodeEffect(): SchemaCompiler.DecodeEffect {
184 throw new Error("unused decoder")
185 },
186 get decode(): SchemaCompiler.Decode {
187 throw new Error("unused fast decoder")
188 },
189 get is(): SchemaCompiler.Is {
190 throw new Error("unused guard")
191 },
192 get makeEffect() {
193 reads++
194 return (input: unknown) => {
195 calls++
196 return Effect.succeed(input)
197 }
198 }
199 })
200 const make = SchemaParser.make(schema)
201 assert.strictEqual(reads, 0)
202 assert.deepStrictEqual(make({ a: "a" }), { a: "a" })
203 assert.deepStrictEqual(SchemaParser.make(schema)({ a: "b" }), { a: "b" })
204 assert.strictEqual(reads, 1)
205 assert.strictEqual(calls, 2)
206 })
207
208 it("uses an installed synchronous constructor without resolving makeEffect", () => {
209 const schema = Schema.Struct({ a: Schema.String })
210 let calls = 0
211 SchemaCompiler.set(schema.ast, {
212 decodeEffect: Effect.succeed,
213 make: (input, options) => {
214 calls++
215 assert.strictEqual(options, SchemaAST.defaultParseOptions)
216 return { a: `${(input as { readonly a: string }).a}!` }
217 },
218 get makeEffect(): SchemaCompiler.MakeEffect {
219 throw new Error("unused detailed constructor")
220 }
221 })
222 assert.deepStrictEqual(SchemaParser.make(schema)({ a: "a" }), { a: "a!" })
223 assert.strictEqual(calls, 1)
224 })
225
226 it("falls back to makeEffect after an installed synchronous constructor fails", () => {
227 const schema = Schema.Struct({ a: Schema.String })
228 let fast = 0
229 let detailed = 0
230 SchemaCompiler.set(schema.ast, {
231 decodeEffect: Effect.succeed,
232 make: () => {
233 fast++
234 return SchemaCompiler.invalid
235 },
236 makeEffect: (input) => {
237 detailed++
238 return Effect.succeed(input)
239 }
240 })
241 assert.deepStrictEqual(SchemaParser.make(schema)({ a: "a" }), { a: "a" })
242 assert.strictEqual(fast, 1)
243 assert.strictEqual(detailed, 1)
244 })
245
246 it("does not return a missing result from an installed synchronous constructor", () => {
247 const schema = Schema.Struct({ a: Schema.String })
248 SchemaCompiler.set(schema.ast, {
249 decodeEffect: Effect.succeed,
250 make: () => SchemaCompiler.missing,
251 makeEffect: () => Effect.succeed(SchemaCompiler.missing)
252 })
253 assert.throws(() => SchemaParser.make(schema)({ a: "a" }), /Schema validation failed/)
254 })
255
256 it("composes an installed synchronous child constructor in a compiled Array", () => {
257 const child = Schema.Struct({ a: Schema.String })
258 SchemaCompiler.set(child.ast, {
259 decodeEffect: Effect.succeed,
260 make: (input) => ({ a: `${(input as { readonly a: string }).a}!` }),
261 makeEffect: () => {
262 throw new Error("unused detailed child constructor")
263 }
264 })
265 const schema = Schema.Array(child)
266 SchemaJITCompiler.enable(schema.ast)
267 assert.deepStrictEqual(SchemaParser.make(schema)([{ a: "a" }, { a: "b" }]), [{ a: "a!" }, { a: "b!" }])
268 })
269
270 it("composes a compiled synchronous child constructor in a compiled Array", () => {
271 const child = Schema.Struct({ a: Schema.String })
272 SchemaJITCompiler.enable(child.ast)
273 const childEntry = Registry.resolve(child.ast)
274 const schema = Schema.Array(child)
275 SchemaJITCompiler.enable(schema.ast)
276 assert.deepStrictEqual(SchemaParser.make(schema)([{ a: "a" }]), [{ a: "a" }])
277 assert.isTrue(Object.hasOwn(childEntry, "make"))
278 })
279
280 it("falls back to detailed construction for an invalid compiled Array", () => {
281 const schema = Schema.Array(Schema.Struct({ a: Schema.String }))
282 SchemaJITCompiler.enable(schema.ast)
283 const make = SchemaParser.make(schema)
284 assert.deepStrictEqual(make([{ a: "a" }]), [{ a: "a" }])
285 assert.throws(() => make([{ a: 1 } as never]), /Schema validation failed/)
286 })
287
288 it("caches interpreted construction when an installed bundle omits it", () => {
289 const schema = Schema.Struct({ a: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(1))) })
290 SchemaCompiler.set(schema.ast, {
291 decodeEffect: () => {
292 throw new Error("not a constructor")
293 }
294 })
295 const entry = Registry.resolve(schema.ast)
296 const make = entry.makeEffect
297 assert.strictEqual(entry.makeEffect, make)
298 assert.deepStrictEqual(SchemaParser.make(schema)({}), { a: 1 })
299 })
300
301 it("keeps previously captured constructors after whole-entry replacement", () => {
302 const schema = Schema.Struct({ a: Schema.String })
303 const make = SchemaParser.make(schema)
304 assert.deepStrictEqual(make({ a: "a" }), { a: "a" })
305 SchemaCompiler.set(schema.ast, {
306 decodeEffect: Effect.succeed,
307 makeEffect: () => Effect.succeed({ a: "installed" })
308 })
309 assert.deepStrictEqual(make({ a: "a" }), { a: "a" })
310 assert.deepStrictEqual(SchemaParser.make(schema)({ a: "a" }), { a: "installed" })
311 SchemaCompiler.set(schema.ast, { decodeEffect: Effect.succeed })
312 assert.deepStrictEqual(SchemaParser.make(schema)({ a: "a" }), { a: "a" })
313 })
314
315 it("uses type-side identity without applying root defaults", () => {
316 const schema = Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(1)))
317 const ast = SchemaAST.toType(schema.ast)
318 SchemaCompiler.set(ast, { decodeEffect: Effect.succeed, makeEffect: Effect.succeed })
319 assert.strictEqual(SchemaParser.make(schema)(2), 2)
320 const number = Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(1)))
321 SchemaJITCompiler.enable(number.ast)
322 assert.throws(() => SchemaParser.make(number)(undefined as never))
323 assert.deepStrictEqual(SchemaParser.make(Schema.Struct({ number }))({}), { number: 1 })
324 })
325
326 it("preserves missing separately from undefined for installed children", () => {
327 const child = Schema.optionalKey(Schema.Undefined)
328 const inputs: Array<unknown> = []
329 SchemaCompiler.set(child.ast, {
330 decodeEffect: Effect.succeed,
331 makeEffect: (input) => {
332 inputs.push(input)
333 return Effect.succeed(input)
334 }
335 })
336 const schema = Schema.Struct({ child })
337 assert.deepStrictEqual(SchemaParser.make(schema)({}), {})
338 assert.deepStrictEqual(SchemaParser.make(schema)({ child: undefined }), { child: undefined })
339 assert.deepStrictEqual(inputs, [SchemaCompiler.missing, undefined])
340 })
341
342 it("lets parents and public roots handle missing constructor outputs", () => {
343 const required = Schema.String.annotate({ title: "Required construction output" })
344 const optional = Schema.optionalKey(required)
345 for (const schema of [required, optional]) {
346 SchemaCompiler.set(schema.ast, {
347 decodeEffect: Effect.succeed,
348 makeEffect: () => Effect.succeed(SchemaCompiler.missing)
349 })
350 }
351 const schema = Schema.Struct({ required, optional })
352 SchemaJITCompiler.enable(schema.ast)
353 assert.throws(() => SchemaParser.make(schema)({ required: "a" }), /Schema validation failed/)
354 assert.deepStrictEqual(SchemaParser.make(Schema.Struct({ optional }))({ optional: "a" }), {})
355 assert.throws(() => SchemaParser.make(required)("a"), /Schema validation failed/)
356 })
357
358 it("compiles construction only without nested records, unions or constructor defaults", () => {
359 const compilesMake = (schema: Schema.Top) => Codegen.generate(schema.ast, "make") !== undefined
360 assert.isTrue(compilesMake(Schema.Struct({ a: Schema.Struct({ b: Schema.String }) })))
361 assert.isFalse(compilesMake(Schema.Struct({ a: Schema.Record(Schema.String, Schema.Number) })))
362 assert.isFalse(compilesMake(Schema.Struct({ a: Schema.Union([Schema.String, Schema.Number]) })))
363 assert.isFalse(compilesMake(Schema.Struct({
364 a: Schema.Struct({ b: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("b"))) })
365 })))
366 })
367
368 it.effect("executes async defaults once, including on later failure", () =>
369 Effect.gen(function*() {
370 let defaults = 0
371 const schema = Schema.Struct({
372 a: Schema.String.pipe(Schema.withConstructorDefault(Effect.gen(function*() {
373 yield* Effect.yieldNow
374 defaults++
375 return "default"
376 }))),
377 b: Schema.Number
378 })
379 SchemaJITCompiler.enable(schema.ast)
380 const make = SchemaParser.makeEffect(schema)
381 assert.deepStrictEqual(yield* make({ b: 1 }), { a: "default", b: 1 })
382 assert.strictEqual((yield* Effect.exit(make({ b: "bad" } as never)))._tag, "Failure")
383 assert.strictEqual(defaults, 2)
384 }))
385})