1import { assert, describe, it } from "@effect/vitest"
2import { Effect, Result, Schema, SchemaAST, SchemaGetter, SchemaParser, SchemaTransformation } from "effect"
3import * as Codegen from "effect/internal/schema/codegen"
4import * as Registry from "effect/internal/schema/compilerRegistry"
5import { SchemaJITCompiler } from "effect/schema"
6import * as SchemaAOTCompiler from "effect/schema/SchemaAOTCompiler"
7import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
8import { join } from "node:path"
9import { fileURLToPath, pathToFileURL } from "node:url"
10import { deepStrictEqual } from "../utils/assert.ts"
11
12type Options = Parameters<ReturnType<typeof SchemaParser.decodeUnknownResult>>[1]
13
14const optionSets: ReadonlyArray<Options> = [
15 undefined,
16 { reportInput: true },
17 { errors: "all", reportInput: true },
18 { onExcessProperty: "error", reportInput: true },
19 { disableChecks: true, reportInput: true }
20]
21
22const counters = { checks: 0, transforms: 0 }
23
24// Every case builds its own schemas: the compiler registry is global, so a
25// shared child AST enabled by an earlier case would make a later "interpreted"
26// snapshot use compiled children.
27const makeFixtures = () => {
28 const counted = <A, B>(f: (a: A) => B) => (a: A): B => {
29 counters.transforms++
30 return f(a)
31 }
32 // Every source check is counted.
33 const countedFilter = (predicate: (s: string) => boolean, expected: string) =>
34 Schema.makeFilter((s: string) => {
35 counters.checks++
36 return predicate(s)
37 }, { expected })
38 const Digits = Schema.String.check(countedFilter((s) => /^[0-9]{1,6}$/.test(s), "digits"))
39 const Short = Schema.String.check(
40 countedFilter((s) => s.length >= 1, "non-empty"),
41 countedFilter((s) => s.length <= 3, "short")
42 )
43 const Checked = Schema.String.check(Schema.isPattern(/^[a-z ]*$/))
44 // "13" transforms into a value that fails the target type.
45 const toNumber = {
46 decode: SchemaGetter.transform(counted((s: string) => s === "13" ? "thirteen" as unknown as number : Number(s))),
47 encode: SchemaGetter.transform((n: number) => String(n))
48 }
49 const prefixed = SchemaTransformation.transform({
50 decode: counted((s: string) => s === "7" ? 7 as unknown as string : `#${s}`),
51 encode: (s: string) => s.slice(1)
52 })
53 return {
54 Digits,
55 Checked,
56 OneOf: Schema.TemplateLiteral([Schema.Union([Schema.String, Schema.Literal("x")], { mode: "oneOf" })]),
57 identity: SchemaTransformation.transform({ decode: counted((s: string) => s), encode: (s: string) => s }),
58 DigitsToNumber: Digits.pipe(Schema.decodeTo(Schema.Number, toNumber)),
59 ShortToNumber: Short.pipe(Schema.decodeTo(Schema.Number, toNumber)),
60 StringToNumber: Schema.String.pipe(Schema.decodeTo(Schema.Number, toNumber)),
61 DigitsToString: Digits.pipe(Schema.decodeTo(Schema.String, prefixed)),
62 DigitsPassthrough: Digits.pipe(Schema.decodeTo(Schema.String)),
63 // Two links: Digits -> String (prefixed) -> Number (toNumber, after dropping the prefix).
64 TwoLinks: Digits.pipe(Schema.decodeTo(
65 Schema.String.pipe(Schema.decodeTo(
66 Schema.Number,
67 {
68 decode: SchemaGetter.transform(
69 counted((s: string) => s === "#13" ? "thirteen" as unknown as number : Number(s.slice(1)))
70 ),
71 encode: SchemaGetter.transform((n: number) => `#${n}`)
72 }
73 )),
74 prefixed
75 )),
76 countedFilter,
77 toNumber,
78 prefixed
79 }
80}
81type Fixtures = ReturnType<typeof makeFixtures>
82
83const values: ReadonlyArray<unknown> = ["1", "123456", "13", "7", "", "abc", "1234567", 1, null, undefined]
84
85interface Case {
86 readonly name: string
87 readonly schema: (f: Fixtures) => Schema.Codec<unknown, unknown>
88 readonly inputs: ReadonlyArray<unknown>
89 readonly make?: ReadonlyArray<unknown>
90 // Compiled parsers may run a failing check a second time in their
91 // diagnostic pass. That is only allowed on paths this change does not
92 // inline: "always" for schemas that keep their previous parser, "fallback"
93 // when a Struct delegates to its fallback (`errors: "all"` or
94 // `onExcessProperty`) and a field's own parser is not inlined. Everywhere
95 // else each check must run exactly as often as in the interpreter.
96 readonly replay?: "always" | "fallback" | undefined
97}
98
99const fallbackOptions = (options: Options): boolean =>
100 options?.errors === "all" || options?.onExcessProperty !== undefined
101
102// A field alone, in a Struct of its own, and in a Struct with a valid checked
103// sibling (per-property layout), so that no earlier failure hides the field.
104const inLayouts = (
105 name: string,
106 field: (f: Fixtures) => Schema.Codec<unknown, unknown>,
107 inputs: ReadonlyArray<unknown>,
108 options: Pick<Case, "replay"> & { readonly ownParserReplays?: boolean } = {}
109): ReadonlyArray<Case> => [
110 {
111 name,
112 schema: field,
113 inputs,
114 replay: options.replay ?? (options.ownParserReplays ? "always" : undefined)
115 },
116 {
117 name: `${name} in a Struct`,
118 schema: (f) => Schema.Struct({ n: field(f) }),
119 inputs: inputs.map((n) => ({ n })),
120 replay: options.replay ?? (options.ownParserReplays ? "fallback" : undefined)
121 },
122 {
123 name: `${name} next to a checked field`,
124 schema: (f) => Schema.Struct({ s: f.Checked, n: field(f) }),
125 inputs: inputs.map((n) => ({ s: "ok", n })),
126 replay: options.replay ?? (options.ownParserReplays ? "fallback" : undefined)
127 }
128]
129
130const cases: ReadonlyArray<Case> = [
131 ...inLayouts(
132 "TemplateLiteral with overlapping oneOf parts",
133 (f) => f.OneOf,
134 ["x", "y", "", 1]
135 ),
136 ...inLayouts(
137 "nested TemplateLiteral with overlapping oneOf parts",
138 (f) => Schema.TemplateLiteral(["nested:", f.OneOf]),
139 ["nested:x", "nested:y", "wrong", 1]
140 ),
141 ...inLayouts(
142 "TemplateLiteral with oneOf inside anyOf",
143 () =>
144 Schema.TemplateLiteral([Schema.Union([
145 Schema.Union([Schema.String, Schema.Literal("x")], { mode: "oneOf" }),
146 Schema.Literal("z")
147 ])]),
148 ["x", "y", "z", 1]
149 ),
150 ...inLayouts(
151 "TemplateLiteral with overlapping numeric oneOf parts",
152 () => Schema.TemplateLiteral([Schema.Union([Schema.Number, Schema.Literal("1")], { mode: "oneOf" })]),
153 ["1", "2", "wrong", 1]
154 ),
155 ...inLayouts(
156 "TemplateLiteral transformation target",
157 (f) =>
158 Schema.String.pipe(Schema.decodeTo(
159 Schema.TemplateLiteral(["x-", Schema.String]),
160 f.identity as SchemaTransformation.Transformation<`x-${string}`, string>
161 )),
162 ["x-a", "wrong", 1]
163 ),
164 ...inLayouts(
165 "TemplateLiteral transformation target with overlapping oneOf parts",
166 (f) => Schema.String.pipe(Schema.decodeTo(f.OneOf, f.identity)),
167 ["x", "y", 1]
168 ),
169 ...inLayouts(
170 "checked source with a oneOf TemplateLiteral target",
171 (f) => f.Checked.pipe(Schema.decodeTo(f.OneOf, f.identity)),
172 ["x", "y", "INVALID", 1]
173 ),
174 ...inLayouts(
175 "TemplateLiteral transformation source",
176 (f) =>
177 Schema.TemplateLiteral(["x-", Schema.String]).pipe(Schema.decodeTo(
178 Schema.String,
179 f.identity as SchemaTransformation.Transformation<string, `x-${string}`>
180 )),
181 ["x-a", "wrong", 1]
182 ),
183 ...inLayouts(
184 "TemplateLiteral intermediate transformation",
185 (f) =>
186 Schema.String.pipe(Schema.decodeTo(
187 Schema.TemplateLiteral(["x-", Schema.String]).pipe(Schema.decodeTo(
188 Schema.String,
189 f.identity as SchemaTransformation.Transformation<string, `x-${string}`>
190 )),
191 f.identity as SchemaTransformation.Transformation<`x-${string}`, string>
192 )),
193 ["x-a", "wrong", 1]
194 ),
195 ...inLayouts("checked source", (f) => f.DigitsToNumber, values),
196 ...inLayouts("several source checks", (f) => f.ShortToNumber, values),
197 ...inLayouts("string target", (f) => f.DigitsToString, values),
198 // emitEncoding only compiles a single Transform link, so these fields keep
199 // their previous parser on their own; inside a Struct they are inlined.
200 ...inLayouts("passthrough", (f) => f.DigitsPassthrough, values, { ownParserReplays: true }),
201 ...inLayouts("two-link chain", (f) => f.TwoLinks, values, { ownParserReplays: true }),
202 {
203 name: "Struct with only transformed fields",
204 // Passthrough fields have their own layout cases, which allow their
205 // fallback replay; here every field is inlined, so counts are exact.
206 schema: (f) => Schema.Struct({ a: f.DigitsToNumber, b: f.StringToNumber, c: f.DigitsToString }),
207 inputs: [
208 ...values.map((a) => ({ a, b: "2", c: "3" })),
209 ...values.map((c) => ({ a: "1", b: "2", c })),
210 { b: "2", c: "3" },
211 { a: "1", c: "3" },
212 { a: "abc", b: 2, c: "3" },
213 { a: "1", b: "2", c: "3", d: "4" },
214 null,
215 []
216 ]
217 },
218 {
219 name: "Struct mixing checked and transformed fields",
220 schema: (f) => Schema.Struct({ s: f.Checked, n: f.DigitsToNumber, m: f.ShortToNumber }),
221 inputs: [
222 ...values.map((n) => ({ s: "ok", n, m: "12" })),
223 ...values.map((m) => ({ s: "ok", n: "1", m })),
224 { s: "NOT OK", n: "abc", m: "1234" },
225 { s: "ok", m: "12" },
226 { s: "ok", n: "1", m: "12", extra: true }
227 ]
228 },
229 {
230 name: "Struct with several checked transformations",
231 schema: (f) => Schema.Struct({ a: f.DigitsToNumber, b: f.DigitsToNumber, c: f.DigitsToString }),
232 inputs: [
233 { a: "1", b: "2", c: "3" },
234 { a: "x", b: "y", c: "z" },
235 { a: "1", b: "13", c: "3" },
236 { a: "1", b: "2", c: "7" },
237 { a: "1", b: "2", c: 3 }
238 ]
239 },
240 {
241 name: "optional keys",
242 schema: (f) => Schema.Struct({ a: Schema.optionalKey(f.DigitsToNumber), b: Schema.optional(f.DigitsToNumber) }),
243 inputs: [{}, { a: "1" }, { a: "x" }, { b: undefined }, { b: "1" }, { b: "x" }, { a: undefined }]
244 },
245 {
246 name: "constructor defaults",
247 schema: (f) => Schema.Struct({ n: f.DigitsToNumber.pipe(Schema.withConstructorDefault(Effect.succeed(0))) }),
248 inputs: [{}, { n: undefined }, { n: "1" }, { n: "x" }],
249 make: [{}, { n: undefined }, { n: 5 }, { n: "x" }]
250 },
251 {
252 name: "__proto__ key",
253 schema: (f) => Schema.Struct({ ["__proto__"]: f.DigitsToNumber, a: f.DigitsToNumber }),
254 inputs: [
255 JSON.parse(`{"__proto__":"1","a":"2"}`),
256 JSON.parse(`{"__proto__":"x","a":"2"}`),
257 JSON.parse(`{"a":"2"}`),
258 { a: "2" }
259 ]
260 },
261 {
262 name: "symbol key",
263 schema: (f) => Schema.Struct({ [Symbol.for("n")]: f.DigitsToNumber, a: f.DigitsToNumber }),
264 inputs: [{ [Symbol.for("n")]: "1", a: "2" }, { [Symbol.for("n")]: "x", a: "2" }, { a: "2" }]
265 },
266 {
267 name: "nested Struct",
268 schema: (f) => Schema.Struct({ outer: Schema.Struct({ s: f.Checked, n: f.DigitsToNumber }), n: f.DigitsToNumber }),
269 inputs: [
270 { outer: { s: "ok", n: "1" }, n: "2" },
271 { outer: { s: "ok", n: "x" }, n: "2" },
272 { outer: { s: "ok", n: "1" }, n: "x" },
273 { outer: { s: "OK", n: "x" }, n: "x" }
274 ]
275 },
276 {
277 name: "Array of Structs",
278 schema: (f) => Schema.Array(Schema.Struct({ s: f.Checked, n: f.DigitsToNumber })),
279 inputs: [[{ s: "ok", n: "1" }, { s: "ok", n: "2" }], [{ s: "ok", n: "1" }, { s: "ok", n: "x" }, {
280 s: "OK",
281 n: "y"
282 }]]
283 },
284 {
285 name: "target checks",
286 schema: (f) =>
287 Schema.Struct({ n: f.Digits.pipe(Schema.decodeTo(Schema.Number.check(Schema.isLessThan(100)), f.toNumber)) }),
288 inputs: [{ n: "1" }, { n: "100" }, { n: "x" }]
289 },
290 // TemplateLiteral chains retain their parsers for structured diagnostics.
291 ...inLayouts(
292 "checked TemplateLiteral source",
293 (f) =>
294 Schema.TemplateLiteral(["x-", Schema.String]).check(f.countedFilter((s) => s.length <= 4, "short")).pipe(
295 Schema.decodeTo(
296 Schema.String,
297 f.prefixed as unknown as SchemaTransformation.Transformation<string, `x-${string}`>
298 )
299 ),
300 ["x-a", "x-abc", "wrong", 1],
301 { replay: "always" }
302 ),
303 ...inLayouts(
304 "checked source with a TemplateLiteral target",
305 (f) =>
306 f.Digits.pipe(Schema.decodeTo(
307 Schema.TemplateLiteral(["#", Schema.String]),
308 f.prefixed as unknown as SchemaTransformation.Transformation<`#${string}`, string>
309 )),
310 ["1", "7", "x"],
311 { replay: "always" }
312 ),
313 ...inLayouts(
314 "checked TemplateLiteral source with a oneOf part",
315 (f) =>
316 f.OneOf
317 .check(f.countedFilter((s) => s.length >= 1, "non-empty")).pipe(Schema.decodeTo(Schema.String, f.prefixed)),
318 ["x", "y"],
319 { replay: "always" }
320 )
321]
322
323// Objects that belong to the schema (AST nodes, filters) or to the inputs are
324// compared by identity; issues and objects built while parsing (for example the
325// ASTs a TemplateLiteral builds for its diagnostics) are compared by structure.
326const collectKnown = (root: unknown, inputs: ReadonlyArray<unknown>): Set<unknown> => {
327 const known = new Set<unknown>()
328 const visit = (value: unknown): void => {
329 if ((typeof value !== "object" && typeof value !== "function") || value === null || known.has(value)) return
330 known.add(value)
331 for (const key of Reflect.ownKeys(value)) visit((value as any)[key])
332 }
333 visit(root)
334 inputs.forEach(visit)
335 return known
336}
337
338const makeDescriber = (known: Set<unknown>) => {
339 const identities = new Map<unknown, string>()
340 const identity = (value: unknown): string => {
341 let id = identities.get(value)
342 if (id === undefined) {
343 id = `#${identities.size}`
344 identities.set(value, id)
345 }
346 return id
347 }
348 const describeValue = (value: unknown, depth = 0): unknown => {
349 if (known.has(value)) return identity(value)
350 if (typeof value === "function") return "<function>"
351 if (typeof value !== "object" || value === null) {
352 return typeof value === "number" && Number.isNaN(value) ? "NaN" : value
353 }
354 if (depth > 32) return "<deep>"
355 if (Array.isArray(value)) return value.map((item) => describeValue(item, depth + 1))
356 return Reflect.ownKeys(value).map((key) => [describeKey(key), describeValue((value as any)[key], depth + 1)])
357 }
358 // Symbol keys keep their identity, so that they differ from their string form
359 // and from other symbols with the same description.
360 const describeKey = (key: PropertyKey) => typeof key === "symbol" ? { symbol: identity(key) } : key
361 const describeOutput = (value: unknown): unknown =>
362 Array.isArray(value)
363 ? value.map(describeOutput)
364 : typeof value === "object" && value !== null && !known.has(value)
365 ? Reflect.ownKeys(value).map((key) => [describeKey(key), describeOutput((value as any)[key])])
366 : describeValue(value)
367 const describeResult = (result: Result.Result<unknown, unknown>) =>
368 Result.isSuccess(result) ? { success: describeOutput(result.success) } : { failure: describeValue(result.failure) }
369 return { describeResult, describeOutput }
370}
371
372const snapshot = (
373 schema: Schema.Codec<unknown, unknown>,
374 { inputs, make = [] }: Case,
375 describeResult: ReturnType<typeof makeDescriber>["describeResult"],
376 checks: Array<{ readonly count: number; readonly fallback: boolean }>
377) => {
378 const decode = SchemaParser.decodeUnknownResult(schema)
379 const construct = SchemaParser.make(schema as Schema.Codec<unknown, unknown> & Schema.Top)
380 const run = (fallback: boolean, f: () => Result.Result<unknown, unknown>) => {
381 counters.checks = 0
382 counters.transforms = 0
383 const result = describeResult(f())
384 checks.push({ count: counters.checks, fallback })
385 return { result, transforms: counters.transforms }
386 }
387 return [
388 ...optionSets.flatMap((options) =>
389 inputs.map((input) => run(fallbackOptions(options), () => decode(input, options)))
390 ),
391 ...make.map((input) =>
392 run(false, () => {
393 try {
394 return Result.succeed(construct(input as never))
395 } catch (error) {
396 // Construction throws an Error whose cause is the schema issue.
397 const cause = error instanceof Error ? error.cause : undefined
398 if (typeof cause !== "object" || cause === null || !("~effect/SchemaIssue/Issue" in cause)) throw error
399 return Result.fail(cause)
400 }
401 })
402 )
403 ]
404}
405
406const install = {
407 jit: async (schema: Schema.Top) => {
408 SchemaJITCompiler.enable(schema.ast)
409 SchemaJITCompiler.enable(SchemaAST.toType(schema.ast))
410 },
411 aot: async (schema: Schema.Top) => {
412 const asts = [schema.ast, SchemaAST.toType(schema.ast)]
413 const directory = mkdtempSync(fileURLToPath(new URL("../../.schema-aot-checked-sources-", import.meta.url)))
414 try {
415 const file = join(directory, "module.mjs")
416 writeFileSync(file, SchemaAOTCompiler.compile(asts.map((ast) => ({ ast, operations: ["decode", "is", "make"] }))))
417 const generated = await import(pathToFileURL(file).href)
418 generated.install(asts)
419 } finally {
420 rmSync(directory, { recursive: true, force: true })
421 }
422 }
423}
424
425describe("Schema compilers: transformations with checked sources", { concurrent: false }, () => {
426 for (const compiler of ["jit", "aot"] as const) {
427 it(`${compiler} preserves oneOf TemplateLiteral guards and constructors`, async () => {
428 const shapes: ReadonlyArray<(f: Fixtures) => readonly [Schema.Top, (value: string) => unknown]> = [
429 (f) => [f.OneOf, (value) => value],
430 (f) => [Schema.Struct({ value: f.OneOf }), (value) => ({ value })],
431 (f) => [Schema.Array(f.OneOf), (value) => [value]],
432 (f) => [Schema.TemplateLiteral(["nested:", f.OneOf]), (value) => `nested:${value}`]
433 ]
434 for (const shape of shapes) {
435 const [schema, input] = shape(makeFixtures())
436 for (const compiled of [false, true]) {
437 if (compiled) await install[compiler](schema)
438 const is = SchemaParser.is(schema)
439 const make = SchemaParser.make(schema)
440 assert.isFalse(is(input("x")))
441 assert.isTrue(is(input("y")))
442 assert.throws(() => make(input("x") as never))
443 deepStrictEqual(make(input("y") as never), input("y"))
444 }
445 }
446 })
447
448 for (const testCase of cases) {
449 it(`${compiler} matches the interpreter: ${testCase.name}`, async () => {
450 const schema = testCase.schema(makeFixtures()) as Schema.Codec<unknown, unknown> & Schema.Top
451 const { describeResult } = makeDescriber(
452 collectKnown([schema.ast, SchemaAST.toType(schema.ast)], [...testCase.inputs, ...testCase.make ?? []])
453 )
454 assert.isUndefined(Registry.resolve(schema.ast).compiled)
455 const interpretedChecks: Array<{ readonly count: number; readonly fallback: boolean }> = []
456 const compiledChecks: Array<{ readonly count: number; readonly fallback: boolean }> = []
457 const interpreted = snapshot(schema, testCase, describeResult, interpretedChecks)
458 await install[compiler](schema)
459 assert.isDefined(Registry.resolve(schema.ast).compiled)
460 const compiled = snapshot(schema, testCase, describeResult, compiledChecks)
461 deepStrictEqual(compiled, interpreted)
462 compiledChecks.forEach(({ count, fallback }, index) => {
463 const expected = interpretedChecks[index].count
464 if (testCase.replay === "always" || testCase.replay === "fallback" && fallback) {
465 assert.isAtLeast(count, expected)
466 assert.isAtMost(count, 2 * expected)
467 } else {
468 assert.strictEqual(count, expected, `check runs for decode #${index}`)
469 }
470 })
471 })
472 }
473 }
474
475 it("inlines a transformation whose source has checks", () => {
476 const f = makeFixtures()
477 const field = Codegen.generate(f.DigitsToNumber.ast, "decodeEffect")
478 assert.isDefined(field)
479 assert.notInclude(field, "R.decode(")
480 assert.include(field, "R.getCheckIssues(")
481
482 const struct = Codegen.generate(Schema.Struct({ a: f.DigitsToNumber, b: f.StringToNumber }).ast, "decodeEffect")
483 assert.isDefined(struct)
484 // Every property is inlined, so the properties are resolved lazily.
485 assert.include(struct, "const p=i=>getProperties()[i]")
486 assert.include(struct, "invalidSource(")
487
488 const mixed = Codegen.generate(Schema.Struct({ s: f.Checked, n: f.TwoLinks }).ast, "decodeEffect")
489 assert.isDefined(mixed)
490 assert.include(mixed, "R.invalidEncodingChecks(p1.type,v1,")
491 })
492
493 for (const compiler of ["jit", "aot"] as const) {
494 it(`${compiler} runs inlined source checks once`, async () => {
495 const shapes: ReadonlyArray<(f: Fixtures) => readonly [Schema.Top, (n: string) => unknown]> = [
496 (f) => [f.DigitsToNumber, (n) => n],
497 (f) => [Schema.Struct({ n: f.DigitsToNumber }), (n) => ({ n })],
498 (f) => [Schema.Struct({ s: f.Checked, n: f.DigitsToNumber }), (n) => ({ s: "ok", n })],
499 (f) => [Schema.Struct({ n: f.TwoLinks }), (n) => ({ n })],
500 (f) => [Schema.Struct({ s: f.Checked, n: f.TwoLinks }), (n) => ({ s: "ok", n })],
501 (f) => [Schema.Struct({ n: f.DigitsPassthrough }), (n) => ({ n })]
502 ]
503 for (const shape of shapes) {
504 const [schema, input] = shape(makeFixtures())
505 await install[compiler](schema)
506 const decode = SchemaParser.decodeUnknownResult(schema as Schema.Codec<unknown>)
507 for (const [value, succeeds] of [["1", true], ["x", false]] as const) {
508 counters.checks = 0
509 assert.strictEqual(Result.isSuccess(decode(input(value))), succeeds)
510 assert.strictEqual(counters.checks, 1)
511 }
512 }
513 })
514 }
515
516 it("distinguishes symbol keys in snapshots", () => {
517 const { describeOutput } = makeDescriber(new Set())
518 const a = Symbol("n")
519 const b = Symbol("n")
520 assert.notDeepEqual(describeOutput({ [Symbol.for("n")]: 1 }), describeOutput({ "Symbol(n)": 1 }))
521 assert.notDeepEqual(describeOutput({ [a]: 1 }), describeOutput({ [b]: 1 }))
522 assert.deepEqual(describeOutput({ [a]: 1 }), describeOutput({ [a]: 1 }))
523 })
524
525 it("uses TemplateLiteral parsers for transformation diagnostics", () => {
526 const f = makeFixtures()
527 const source = Schema.TemplateLiteral(["x-", Schema.String]).check(Schema.isMaxLength(4)).pipe(
528 Schema.decodeTo(
529 Schema.String,
530 f.prefixed as unknown as SchemaTransformation.Transformation<string, `x-${string}`>
531 )
532 )
533 const target = f.Digits.pipe(Schema.decodeTo(
534 Schema.TemplateLiteral(["#", Schema.String]),
535 f.prefixed as unknown as SchemaTransformation.Transformation<`#${string}`, string>
536 ))
537 const unchecked = Schema.String.pipe(Schema.decodeTo(
538 Schema.TemplateLiteral(["#", Schema.String]),
539 f.prefixed as SchemaTransformation.Transformation<`#${string}`, string>
540 ))
541 for (const schema of [source, target, unchecked]) {
542 assert.include(Codegen.generate(schema.ast, "decodeEffect"), "R.decode(")
543 assert.notInclude(Codegen.generate(Schema.Struct({ n: schema }).ast, "decodeEffect"), "R.getCheckIssues(")
544 }
545 })
546})