| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { |
| 3 | Effect, |
| 4 | Exit, |
| 5 | Option, |
| 6 | Result, |
| 7 | Schema, |
| 8 | SchemaAST, |
| 9 | SchemaGetter, |
| 10 | SchemaParser, |
| 11 | SchemaTransformation |
| 12 | } from "effect" |
| 13 | import * as Codegen from "effect/internal/schema/codegen" |
| 14 | import { SchemaCompiler, SchemaJITCompiler } from "effect/schema" |
| 15 | import { deepStrictEqual, strictEqual } from "../utils/assert.ts" |
| 16 | |
| 17 | describe("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 | }) |