1import { assert, describe, it } from "@effect/vitest"
2import { Schema, SchemaRepresentation } from "effect"
3import { assertInclude, throws } from "../../utils/assert.ts"
4
5function expectError(thunk: () => void, expected: string | Error): void {
6 if (typeof expected === "string") {
7 throws(thunk, expected)
8 } else {
9 throws(thunk, (error: unknown) => {
10 assert.strictEqual(error, expected)
11 return undefined
12 })
13 }
14}
15
16const StringRepresentation: SchemaRepresentation.Representation = {
17 _tag: "String",
18 checks: []
19}
20
21const NumberRepresentation: SchemaRepresentation.Representation = {
22 _tag: "Number",
23 checks: []
24}
25
26const EmptyUnionRepresentation: SchemaRepresentation.Representation = {
27 _tag: "Union",
28 types: [],
29 options: { mode: "anyOf" },
30 checks: []
31}
32
33describe("SchemaRepresentation.toCodeDocument annotations", () => {
34 it("preserves explicit options on an empty union", () => {
35 assert.deepStrictEqual(
36 SchemaRepresentation.toCodeDocument({
37 representations: [EmptyUnionRepresentation],
38 references: {}
39 }).codes,
40 [{ runtime: `Schema.Union([], { mode: "anyOf" })`, Type: "never" }]
41 )
42 })
43
44 it("compiles the isPattern and Option vertical slice", () => {
45 const document = SchemaRepresentation.toRepresentations([
46 Schema.String.check(Schema.isPattern(/^a+$/)).ast,
47 Schema.Option(Schema.String).ast
48 ])
49 const output = SchemaRepresentation.toCodeDocument(document)
50
51 assert.deepStrictEqual(output.codes, [
52 {
53 runtime:
54 `Schema.String.check(Schema.isPattern(new RegExp("^a+$")).annotate({ "expected": "a string matching the RegExp ^a+$" }))`,
55 Type: "string"
56 },
57 {
58 runtime: `Schema.Option(Schema.String).annotate({ "expected": "Option" })`,
59 Type: "Option.Option<string>"
60 }
61 ])
62 assert.deepStrictEqual(output.artifacts, [{
63 _tag: "Import",
64 importDeclaration: `import * as Option from "effect/Option"`
65 }])
66 })
67
68 it("passes compiled dependencies to checks and deduplicates imports", () => {
69 const check = (name: string): SchemaRepresentation.Filter => ({
70 _tag: "Filter",
71 aborted: false,
72 representation: {
73 id: `acme/schema/${name}`,
74 payload: null,
75 schemas: [StringRepresentation]
76 },
77 annotations: {
78 toCode: ({ schemas }: SchemaRepresentation.Generation.CheckInput) => ({
79 runtime: `Custom.${name}(${schemas[0].runtime})`,
80 importDeclarations: [`import * as Custom from "acme/Custom"`]
81 })
82 }
83 })
84 const document: SchemaRepresentation.MultiDocument = {
85 representations: [{ _tag: "String", checks: [check("first"), check("second")] }],
86 references: {}
87 }
88
89 const output = SchemaRepresentation.toCodeDocument(document)
90 assert.strictEqual(
91 output.codes[0].runtime,
92 "Schema.String.check(Custom.first(Schema.String)).check(Custom.second(Schema.String))"
93 )
94 assert.deepStrictEqual(output.artifacts, [{
95 _tag: "Import",
96 importDeclaration: `import * as Custom from "acme/Custom"`
97 }])
98 })
99
100 it("emits supported annotation trees atomically", () => {
101 const document = SchemaRepresentation.toRepresentations([
102 Schema.String.annotate({
103 emitted: {
104 bigint: 1n,
105 symbol: Symbol.for("shared"),
106 nan: NaN,
107 positive: Infinity,
108 negative: -Infinity
109 },
110 omitted: { value: 1, callback: () => 2 }
111 }).ast
112 ])
113
114 const runtime = SchemaRepresentation.toCodeDocument(document).codes[0].runtime
115 assertInclude(runtime, `"bigint": 1n`)
116 assertInclude(runtime, `"symbol": Symbol.for("shared")`)
117 assertInclude(runtime, `"nan": NaN`)
118 assertInclude(runtime, `"positive": Infinity`)
119 assertInclude(runtime, `"negative": -Infinity`)
120 assert.isFalse(runtime.includes("omitted"))
121 assert.isFalse(runtime.includes("callback"))
122 })
123
124 it("preserves fallback identifiers", () => {
125 const output = SchemaRepresentation.toCodeDocument({
126 representations: [{
127 _tag: "String",
128 checks: [],
129 annotations: { "~identifier": "Person" }
130 }],
131 references: {}
132 })
133
134 assertInclude(output.codes[0].runtime, `.annotate({ "~identifier": "Person" })`)
135 })
136
137 it("emits tuple element and property annotations", () => {
138 const document: SchemaRepresentation.MultiDocument = {
139 representations: [
140 {
141 _tag: "Arrays",
142 elements: [{
143 isOptional: false,
144 type: StringRepresentation,
145 annotations: {
146 element: { value: 1 },
147 omitted: { callback: () => 1 }
148 }
149 }],
150 rest: [],
151 checks: []
152 },
153 {
154 _tag: "Objects",
155 propertySignatures: [{
156 name: "value",
157 type: NumberRepresentation,
158 isOptional: false,
159 isMutable: false,
160 annotations: { property: true }
161 }],
162 indexSignatures: [],
163 checks: []
164 }
165 ],
166 references: {}
167 }
168
169 assert.deepStrictEqual(SchemaRepresentation.toCodeDocument(document).codes, [
170 {
171 runtime: `Schema.Tuple([Schema.String.annotateKey({ "element": { "value": 1 } })])`,
172 Type: "readonly [string]"
173 },
174 {
175 runtime: `Schema.Struct({ "value": Schema.Number.annotateKey({ "property": true }) })`,
176 Type: `{ readonly "value": number }`
177 }
178 ])
179 })
180
181 it("uses group overrides without visiting children and preserves abort", () => {
182 let visits = 0
183 const child: SchemaRepresentation.Filter = {
184 _tag: "Filter",
185 aborted: true,
186 annotations: {
187 toCode: () => {
188 visits++
189 return { runtime: "Custom.child()" }
190 }
191 }
192 }
193 const document: SchemaRepresentation.MultiDocument = {
194 representations: [
195 {
196 _tag: "String",
197 checks: [{
198 _tag: "FilterGroup",
199 checks: [child],
200 annotations: { toCode: () => ({ runtime: "Custom.group()" }) }
201 }]
202 },
203 {
204 _tag: "String",
205 checks: [{ _tag: "FilterGroup", checks: [child] }]
206 }
207 ],
208 references: {}
209 }
210
211 const output = SchemaRepresentation.toCodeDocument(document)
212 assert.strictEqual(visits, 1)
213 assert.strictEqual(output.codes[0].runtime, "Schema.String.check(Custom.group())")
214 assert.strictEqual(
215 output.codes[1].runtime,
216 "Schema.String.check(Schema.makeFilterGroup([Custom.child().abort()]))"
217 )
218 })
219
220 it("passes type parameters to declaration callbacks", () => {
221 const declaration: SchemaRepresentation.Representation = {
222 _tag: "Declaration",
223 typeParameters: [StringRepresentation],
224 checks: [],
225 representation: {
226 id: "acme/schema/Box",
227 payload: null
228 },
229 annotations: {
230 toCode: ({ typeParameters }: SchemaRepresentation.Generation.DeclarationInput) => ({
231 runtime: `Custom.box(${typeParameters[0].runtime})`,
232 Type: `Custom.Box<${typeParameters[0].Type}>`,
233 importDeclarations: [`import * as Custom from "acme/Custom"`]
234 })
235 }
236 }
237 const output = SchemaRepresentation.toCodeDocument({
238 representations: [declaration],
239 references: {}
240 })
241
242 assert.deepStrictEqual(output.codes, [{
243 runtime: "Custom.box(Schema.String)",
244 Type: "Custom.Box<string>"
245 }])
246 })
247
248 it("reports missing toCode callbacks and preserves callback exceptions", () => {
249 const missing: SchemaRepresentation.MultiDocument = {
250 representations: [{
251 _tag: "String",
252 checks: [{ _tag: "Filter", aborted: false }]
253 }],
254 references: {}
255 }
256 expectError(
257 () => SchemaRepresentation.toCodeDocument(missing),
258 `Missing toCode callback\n at ["representations"][0]["checks"][0]["annotations"]["toCode"]`
259 )
260
261 const cause = new Error("toCode callback")
262 const throwing: SchemaRepresentation.MultiDocument = {
263 representations: [{
264 _tag: "String",
265 checks: [{
266 _tag: "Filter",
267 aborted: false,
268 annotations: {
269 toCode: () => {
270 throw cause
271 }
272 }
273 }]
274 }],
275 references: {}
276 }
277 expectError(
278 () => SchemaRepresentation.toCodeDocument(throwing),
279 cause
280 )
281 })
282
283 it("generates content media types, optional pre-rest elements and numeric properties", () => {
284 const output = SchemaRepresentation.toCodeDocument({
285 representations: [
286 {
287 _tag: "String",
288 annotations: { contentMediaType: "text/plain" },
289 checks: []
290 },
291 {
292 _tag: "Arrays",
293 elements: [{
294 type: StringRepresentation,
295 isOptional: true
296 }],
297 rest: [NumberRepresentation],
298 checks: []
299 },
300 {
301 _tag: "Objects",
302 propertySignatures: [{
303 name: 1,
304 type: { _tag: "Boolean", checks: [] },
305 isOptional: false,
306 isMutable: false
307 }],
308 indexSignatures: [],
309 checks: []
310 }
311 ],
312 references: {}
313 })
314
315 assert.deepStrictEqual(output.codes, [
316 {
317 runtime: `Schema.String.annotate({ "contentMediaType": "text/plain" })`,
318 Type: "string"
319 },
320 {
321 runtime: `Schema.TupleWithRest(Schema.Tuple([Schema.optionalKey(Schema.String)]), [Schema.Number])`,
322 Type: `readonly [(string)?, ...Array<number>]`
323 },
324 {
325 runtime: `Schema.Struct({ 1: Schema.Boolean })`,
326 Type: `{ readonly 1: boolean }`
327 }
328 ])
329 assert.deepStrictEqual(output.artifacts, [])
330 })
331
332 it("emits references that are not reachable from a root", () => {
333 const document: SchemaRepresentation.MultiDocument = {
334 representations: [StringRepresentation],
335 references: {
336 Unused: NumberRepresentation
337 }
338 }
339 assert.deepStrictEqual(SchemaRepresentation.toCodeDocument(document).references, {
340 nonRecursives: [{
341 $ref: "Unused",
342 code: { runtime: "Schema.Number", Type: "number" }
343 }],
344 recursives: {}
345 })
346 })
347
348 it("capitalizes a lowercase reference identifier", () => {
349 const output = SchemaRepresentation.toCodeDocument({
350 representations: [StringRepresentation],
351 references: { abc: NumberRepresentation }
352 })
353 assert.strictEqual(output.references.nonRecursives[0].$ref, "Abc")
354 })
355
356 it("preserves a lowercase identifier annotation", () => {
357 const schema = Schema.Struct({ a: Schema.String }).annotate({ identifier: "hello" })
358 const output = SchemaRepresentation.toCodeDocument(
359 SchemaRepresentation.toMultiDocument(Schema.toRepresentation(schema))
360 )
361 assert.deepStrictEqual(output.references.nonRecursives[0], {
362 $ref: "Hello",
363 code: {
364 runtime: `Schema.Struct({ "a": Schema.String }).annotate({ "identifier": "hello" })`,
365 Type: `{ readonly "a": string }`
366 }
367 })
368 })
369
370 it("preserves an uppercase reference identifier", () => {
371 const output = SchemaRepresentation.toCodeDocument({
372 representations: [StringRepresentation],
373 references: { Abc: NumberRepresentation }
374 })
375 assert.strictEqual(output.references.nonRecursives[0].$ref, "Abc")
376 })
377
378 it("prefixes a numeric reference identifier", () => {
379 const output = SchemaRepresentation.toCodeDocument({
380 representations: [StringRepresentation],
381 references: { "1a": NumberRepresentation }
382 })
383 assert.strictEqual(output.references.nonRecursives[0].$ref, "_1a")
384 })
385
386 it("replaces punctuation in a reference identifier", () => {
387 const output = SchemaRepresentation.toCodeDocument({
388 representations: [StringRepresentation],
389 references: { "a-b": NumberRepresentation }
390 })
391 assert.strictEqual(output.references.nonRecursives[0].$ref, "A_b")
392 })
393
394 it("replaces non-ASCII characters in a reference identifier", () => {
395 const output = SchemaRepresentation.toCodeDocument({
396 representations: [StringRepresentation],
397 references: { café: NumberRepresentation }
398 })
399 assert.strictEqual(output.references.nonRecursives[0].$ref, "Caf_")
400 })
401
402 it("replaces an emoji reference identifier", () => {
403 const output = SchemaRepresentation.toCodeDocument({
404 representations: [StringRepresentation],
405 references: { "🤖": NumberRepresentation }
406 })
407 assert.strictEqual(output.references.nonRecursives[0].$ref, "_")
408 })
409
410 it("uses an underscore for an empty reference identifier", () => {
411 const output = SchemaRepresentation.toCodeDocument({
412 representations: [StringRepresentation],
413 references: { "": NumberRepresentation }
414 })
415 assert.strictEqual(output.references.nonRecursives[0].$ref, "_")
416 })
417
418 it("makes colliding sanitized reference identifiers unique", () => {
419 const output = SchemaRepresentation.toCodeDocument({
420 representations: [StringRepresentation],
421 references: {
422 "a-b": NumberRepresentation,
423 a_b: StringRepresentation
424 }
425 })
426 assert.deepStrictEqual(
427 output.references.nonRecursives.map((reference) => reference.$ref),
428 ["A_b", "A_b1"]
429 )
430 })
431
432 it("orders definitions after dependencies found in every representation position", () => {
433 const reference = ($ref: string): SchemaRepresentation.Reference => ({ _tag: "Reference", $ref })
434 const filter: SchemaRepresentation.Filter = {
435 _tag: "Filter",
436 aborted: false,
437 representation: { id: "acme/schema/filter", payload: null, schemas: [reference("C")] },
438 annotations: {
439 toCode: () => ({ runtime: "Schema.makeFilter(() => true)" })
440 }
441 }
442 const output = SchemaRepresentation.toCodeDocument({
443 representations: [StringRepresentation],
444 references: {
445 A: StringRepresentation,
446 B: NumberRepresentation,
447 C: { _tag: "Boolean", checks: [] },
448 D: {
449 _tag: "Declaration",
450 typeParameters: [reference("A")],
451 representation: { id: "acme/schema/declaration", payload: null },
452 annotations: {
453 toCode: () => ({ runtime: "Schema.String", Type: "string" })
454 },
455 checks: [{ _tag: "FilterGroup", checks: [filter] }]
456 },
457 E: { _tag: "TemplateLiteral", parts: [reference("D")], checks: [] },
458 F: { _tag: "Union", types: [reference("E"), reference("A")], options: { mode: "anyOf" }, checks: [] },
459 G: {
460 _tag: "Arrays",
461 elements: [{ type: reference("F"), isOptional: false }],
462 rest: [reference("A")],
463 checks: []
464 },
465 H: {
466 _tag: "Objects",
467 propertySignatures: [{
468 name: "value",
469 type: reference("G"),
470 isOptional: false,
471 isMutable: false
472 }],
473 indexSignatures: [{ parameter: reference("A"), type: reference("B") }],
474 checks: []
475 },
476 I: { _tag: "Suspend", thunk: reference("H"), checks: [] }
477 }
478 })
479
480 assert.deepStrictEqual(
481 output.references.nonRecursives.map((entry) => entry.$ref),
482 ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
483 )
484 })
485
486 it("orders a shared dependency referenced from multiple representation positions once", () => {
487 const shared: SchemaRepresentation.Reference = { _tag: "Reference", $ref: "A" }
488 const output = SchemaRepresentation.toCodeDocument({
489 representations: [StringRepresentation],
490 references: {
491 A: StringRepresentation,
492 B: {
493 _tag: "Arrays",
494 elements: [{ type: shared, isOptional: false }],
495 rest: [shared],
496 checks: []
497 },
498 C: {
499 _tag: "Objects",
500 propertySignatures: [],
501 indexSignatures: [{ parameter: shared, type: shared }],
502 checks: []
503 }
504 }
505 })
506
507 assert.deepStrictEqual(
508 output.references.nonRecursives.map((entry) => entry.$ref),
509 ["A", "B", "C"]
510 )
511 })
512
513 it("generates a StructWithRest without fixed properties", () => {
514 const output = SchemaRepresentation.toCodeDocument({
515 representations: [{
516 _tag: "Objects",
517 propertySignatures: [],
518 indexSignatures: [
519 { parameter: StringRepresentation, type: NumberRepresentation },
520 { parameter: { _tag: "Symbol", checks: [] }, type: { _tag: "Boolean", checks: [] } }
521 ],
522 checks: []
523 }],
524 references: {}
525 })
526
527 assert.strictEqual(output.codes[0].Type, `{ readonly [x: string]: number, readonly [x: symbol]: boolean }`)
528 })
529
530 it("preserves explicit options on a single-literal Union", () => {
531 const output = SchemaRepresentation.toCodeDocument({
532 representations: [{
533 _tag: "Union",
534 types: [{ _tag: "Literal", literal: "a", checks: [] }],
535 options: { mode: "anyOf" },
536 checks: []
537 }],
538 references: {}
539 })
540
541 assert.deepStrictEqual(output.codes[0], {
542 runtime: `Schema.Union([Schema.Literal("a")], { mode: "anyOf" })`,
543 Type: `"a"`
544 })
545 })
546
547 it("preserves optional Union option properties", () => {
548 const output = SchemaRepresentation.toCodeDocument({
549 representations: [
550 {
551 _tag: "Union",
552 types: [StringRepresentation],
553 options: {},
554 checks: []
555 },
556 {
557 _tag: "Union",
558 types: [NumberRepresentation],
559 options: { mode: undefined },
560 checks: []
561 }
562 ],
563 references: {}
564 })
565
566 assert.deepStrictEqual(output.codes, [
567 { runtime: "Schema.Union([Schema.String], {})", Type: "string" },
568 { runtime: `Schema.Union([Schema.Number], { mode: undefined })`, Type: "number" }
569 ])
570 })
571
572 it("emits every member of a mutually recursive reference cycle", () => {
573 const output = SchemaRepresentation.toCodeDocument({
574 representations: [{ _tag: "Reference", $ref: "A" }],
575 references: {
576 A: { _tag: "Reference", $ref: "B" },
577 B: { _tag: "Reference", $ref: "A" }
578 }
579 })
580
581 assert.deepStrictEqual(output.references, {
582 nonRecursives: [],
583 recursives: {
584 A: { runtime: "Schema.suspend((): Schema.Codec<B> => B)", Type: "B" },
585 B: { runtime: "Schema.suspend((): Schema.Codec<A> => A)", Type: "A" }
586 }
587 })
588 })
589
590 it("emits a non-recursive definition that depends on a recursive definition", () => {
591 const output = SchemaRepresentation.toCodeDocument({
592 representations: [{ _tag: "Reference", $ref: "B" }],
593 references: {
594 A: { _tag: "Reference", $ref: "A" },
595 B: { _tag: "Reference", $ref: "A" }
596 }
597 })
598
599 assert.deepStrictEqual(output.references, {
600 nonRecursives: [{ $ref: "B", code: { runtime: "A", Type: "A" } }],
601 recursives: { A: { runtime: "Schema.suspend((): Schema.Codec<A> => A)", Type: "A" } }
602 })
603 })
604
605 it("reports missing references with their document path", () => {
606 const document: SchemaRepresentation.MultiDocument = {
607 representations: [{ _tag: "Reference", $ref: "Missing" }],
608 references: {}
609 }
610 expectError(
611 () => SchemaRepresentation.toCodeDocument(document),
612 `Invalid reference Missing\n at ["representations"][0]["$ref"]`
613 )
614 })
615
616 it("reports a missing reference from a definition", () => {
617 expectError(
618 () =>
619 SchemaRepresentation.toCodeDocument({
620 representations: [StringRepresentation],
621 references: { Value: { _tag: "Reference", $ref: "Missing" } }
622 }),
623 `Invalid reference Missing\n at ["references"]["Value"]["$ref"]`
624 )
625 })
626})