| 1 | import { assert, describe, it, vi } from "@effect/vitest" |
| 2 | import { Cause, Schema, SchemaParser, SchemaTransformation } from "effect" |
| 3 | import * as Codegen from "effect/internal/schema/codegen" |
| 4 | // oxlint-disable-next-line no-unassigned-import |
| 5 | import "effect/schema/SchemaJITCompiler/enable" |
| 6 | import { assertSchemaIssueError, deepStrictEqual, strictEqual, throws } from "../utils/assert.ts" |
| 7 | |
| 8 | describe("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 | }) |