1import { assert } from "@effect/vitest"
2import { Brand, JsonSchema, Schema, SchemaRepresentation } from "effect"
3import { TestSchema } from "effect/testing"
4import { describe, it } from "vitest"
5import { assertTrue, deepStrictEqual, strictEqual, throws } from "../../utils/assert.ts"
6
7type Category = {
8 readonly name: string
9 readonly children: ReadonlyArray<Category>
10}
11
12const OuterCategory = Schema.Struct({
13 name: Schema.String,
14 children: Schema.Array(Schema.suspend((): Schema.Codec<Category> => OuterCategory))
15}).annotate({ identifier: "Category" })
16
17const InnerCategory = Schema.Struct({
18 name: Schema.String,
19 children: Schema.Array(
20 Schema.suspend((): Schema.Codec<Category> => InnerCategory.annotate({ identifier: "Category" }))
21 )
22})
23
24describe("toCodeDocument", () => {
25 type Expected = {
26 readonly codes: SchemaRepresentation.Code | ReadonlyArray<SchemaRepresentation.Code>
27 readonly references?: {
28 readonly nonRecursives?: ReadonlyArray<{
29 readonly $ref: string
30 readonly code: SchemaRepresentation.Code
31 }>
32 readonly recursives?: {
33 readonly [$ref: string]: SchemaRepresentation.Code
34 }
35 }
36 readonly artifacts?: ReadonlyArray<SchemaRepresentation.Artifact>
37 }
38
39 function assertSchema(input: {
40 readonly schema: Schema.Constraint
41 }, expected: Expected) {
42 const multiDocument = SchemaRepresentation.toRepresentations([input.schema.ast])
43 assertMultiDocument(multiDocument, expected)
44 }
45
46 function assertJsonSchema(input: {
47 readonly schema: JsonSchema.JsonSchema
48 }, expected: Expected) {
49 const schema = SchemaRepresentation.fromJsonSchemaDocument(
50 JsonSchema.fromSchemaDraft2020_12(input.schema),
51 {
52 onEnter: (js) =>
53 js.type === "object" && js.additionalProperties === undefined
54 ? { ...js, additionalProperties: false }
55 : js
56 }
57 )
58 assertMultiDocument(SchemaRepresentation.toRepresentations([schema.ast]), expected)
59 }
60
61 function assertMultiDocument(
62 multiDocument: SchemaRepresentation.MultiDocument,
63 expected: Expected
64 ) {
65 const codeDocument = SchemaRepresentation.toCodeDocument(multiDocument)
66 deepStrictEqual(
67 canonicalizeGeneratedCode(codeDocument),
68 canonicalizeGeneratedCode({
69 codes: Array.isArray(expected.codes) ? expected.codes : [expected.codes],
70 references: {
71 nonRecursives: expected.references?.nonRecursives ?? [],
72 recursives: expected.references?.recursives ?? {}
73 },
74 artifacts: expected.artifacts ?? []
75 })
76 )
77 }
78
79 function canonicalizeGeneratedCode(input: unknown): unknown {
80 if (typeof input === "string") {
81 return input
82 .replaceAll(/"expected": "(?:\\.|[^"\\])*"(?:, )?/g, "")
83 .replaceAll(", }", " }")
84 .replaceAll(".annotate({ })", "")
85 }
86 if (Array.isArray(input)) return input.map(canonicalizeGeneratedCode)
87 if (typeof input !== "object" || input === null) return input
88 return Object.fromEntries(
89 Object.entries(input).map(([key, value]) => [key, canonicalizeGeneratedCode(value)])
90 )
91 }
92
93 const makeCode = SchemaRepresentation.makeCode
94 const templateType = (...types: ReadonlyArray<string>) => `\`${types.map((type) => `\${${type}}`).join("")}\``
95
96 describe("Declaration", () => {
97 it("declaration without a toCode annotation", () => {
98 throws(
99 () => assertSchema({ schema: Schema.instanceOf(URL) }, { codes: makeCode("", "") }),
100 "Missing toCode callback\n at [\"representations\"][0][\"annotations\"][\"toCode\"]"
101 )
102 })
103
104 it("Error", () => {
105 assertSchema({ schema: Schema.ErrorInstance() }, {
106 codes: makeCode(`Schema.ErrorInstance()`, "globalThis.Error")
107 })
108 })
109
110 it("Error with stack", () => {
111 assertSchema({ schema: Schema.ErrorInstance({ includeStack: true }) }, {
112 codes: makeCode(`Schema.ErrorInstance({"includeStack":true})`, "globalThis.Error")
113 })
114 })
115
116 it("Error with excluded cause", () => {
117 assertSchema({ schema: Schema.ErrorInstance({ excludeCause: true }) }, {
118 codes: makeCode(`Schema.ErrorInstance({"excludeCause":true})`, "globalThis.Error")
119 })
120 })
121
122 it("RegExp", () => {
123 assertSchema({ schema: Schema.RegExp }, {
124 codes: makeCode(`Schema.RegExp`, "globalThis.RegExp")
125 })
126 })
127
128 it("URL", () => {
129 assertSchema({ schema: Schema.URL }, {
130 codes: makeCode(`Schema.URL`, "globalThis.URL")
131 })
132 })
133
134 it("Uint8Array", () => {
135 assertSchema({ schema: Schema.Uint8Array }, {
136 codes: makeCode(`Schema.Uint8Array`, "globalThis.Uint8Array")
137 })
138 })
139
140 it("URLSearchParams", () => {
141 assertSchema({ schema: Schema.URLSearchParams }, {
142 codes: makeCode(`Schema.URLSearchParams`, "globalThis.URLSearchParams")
143 })
144 })
145
146 it("File", () => {
147 assertSchema({ schema: Schema.File }, {
148 codes: makeCode(`Schema.File`, "globalThis.File")
149 })
150 })
151
152 it("FormData", () => {
153 assertSchema({ schema: Schema.FormData }, {
154 codes: makeCode(`Schema.FormData`, "globalThis.FormData")
155 })
156 })
157
158 it("URLSearchParams", () => {
159 assertSchema({ schema: Schema.URLSearchParams }, {
160 codes: makeCode(`Schema.URLSearchParams`, "globalThis.URLSearchParams")
161 })
162 })
163
164 describe("Date", () => {
165 it("Date", () => {
166 assertSchema({ schema: Schema.Date }, {
167 codes: makeCode(`Schema.Date`, "globalThis.Date")
168 })
169 })
170
171 it("Date & check", () => {
172 assertSchema(
173 { schema: Schema.Date.check(Schema.isGreaterThanDate(new Date(0))) },
174 {
175 codes: makeCode(`Schema.Date.check(Schema.isGreaterThanDate(new Date(0)))`, "globalThis.Date")
176 }
177 )
178 })
179 })
180
181 it("Option(String)", () => {
182 assertSchema(
183 { schema: Schema.Option(Schema.String) },
184 {
185 codes: makeCode("Schema.Option(Schema.String)", "Option.Option<string>"),
186 artifacts: [{
187 _tag: "Import",
188 importDeclaration: `import * as Option from "effect/Option"`
189 }]
190 }
191 )
192 })
193
194 it("Result(String, Number)", () => {
195 assertSchema(
196 { schema: Schema.Result(Schema.String, Schema.Number) },
197 {
198 codes: makeCode("Schema.Result(Schema.String, Schema.Number)", "Result.Result<string, number>"),
199 artifacts: [{
200 _tag: "Import",
201 importDeclaration: `import * as Result from "effect/Result"`
202 }]
203 }
204 )
205 })
206
207 it("Redacted options", () => {
208 assertSchema(
209 {
210 schema: Schema.Redacted(Schema.String, {
211 label: "password",
212 disallowJsonEncode: true
213 })
214 },
215 {
216 codes: makeCode(
217 `Schema.Redacted(Schema.String, {"label":"password","disallowJsonEncode":true})`,
218 "Redacted.Redacted<string>"
219 ),
220 artifacts: [{
221 _tag: "Import",
222 importDeclaration: `import * as Redacted from "effect/Redacted"`
223 }]
224 }
225 )
226 })
227
228 it("CauseReason(String, Number)", () => {
229 assertSchema({ schema: Schema.CauseReason(Schema.String, Schema.Number) }, {
230 codes: makeCode("Schema.CauseReason(Schema.String, Schema.Number)", "Cause.Failure<string, number>"),
231 artifacts: [{ _tag: "Import", importDeclaration: `import * as Cause from "effect/Cause"` }]
232 })
233 })
234
235 it("Cause(String, Number)", () => {
236 assertSchema({ schema: Schema.Cause(Schema.String, Schema.Number) }, {
237 codes: makeCode("Schema.Cause(Schema.String, Schema.Number)", "Cause.Cause<string, number>"),
238 artifacts: [{ _tag: "Import", importDeclaration: `import * as Cause from "effect/Cause"` }]
239 })
240 })
241
242 it("Exit(String, Number, String)", () => {
243 assertSchema({ schema: Schema.Exit(Schema.String, Schema.Number, Schema.Boolean) }, {
244 codes: makeCode(
245 "Schema.Exit(Schema.String, Schema.Number, Schema.Boolean)",
246 "Exit.Exit<string, number, boolean>"
247 ),
248 artifacts: [{ _tag: "Import", importDeclaration: `import * as Exit from "effect/Exit"` }]
249 })
250 })
251 })
252
253 it("Null", () => {
254 assertSchema({ schema: Schema.Null }, {
255 codes: makeCode("Schema.Null", "null")
256 })
257 assertSchema(
258 { schema: Schema.Null.annotate({ "description": "a" }) },
259 {
260 codes: makeCode(`Schema.Null.annotate({ "description": "a" })`, "null")
261 }
262 )
263 assertSchema({ schema: Schema.Null.annotate({}) }, {
264 codes: makeCode("Schema.Null", "null")
265 })
266 })
267
268 it("Undefined", () => {
269 assertSchema({ schema: Schema.Undefined }, {
270 codes: makeCode("Schema.Undefined", "undefined")
271 })
272 assertSchema(
273 { schema: Schema.Undefined.annotate({ "description": "a" }) },
274 {
275 codes: makeCode(`Schema.Undefined.annotate({ "description": "a" })`, "undefined")
276 }
277 )
278 })
279
280 it("Void", () => {
281 assertSchema({ schema: Schema.Void }, {
282 codes: makeCode("Schema.Void", "void")
283 })
284 assertSchema(
285 { schema: Schema.Void.annotate({ "description": "a" }) },
286 {
287 codes: makeCode(`Schema.Void.annotate({ "description": "a" })`, "void")
288 }
289 )
290 })
291
292 it("Never", () => {
293 assertSchema({ schema: Schema.Never }, {
294 codes: makeCode("Schema.Never", "never")
295 })
296 assertSchema(
297 { schema: Schema.Never.annotate({ "description": "a" }) },
298 {
299 codes: makeCode(`Schema.Never.annotate({ "description": "a" })`, "never")
300 }
301 )
302 })
303
304 it("Unknown", () => {
305 assertSchema({ schema: Schema.Unknown }, {
306 codes: makeCode("Schema.Unknown", "unknown")
307 })
308 assertSchema(
309 { schema: Schema.Unknown.annotate({ "description": "a" }) },
310 {
311 codes: makeCode(`Schema.Unknown.annotate({ "description": "a" })`, "unknown")
312 }
313 )
314 })
315
316 it("Any", () => {
317 assertSchema({ schema: Schema.Any }, {
318 codes: makeCode("Schema.Any", "any")
319 })
320 assertSchema(
321 { schema: Schema.Any.annotate({ "description": "a" }) },
322 {
323 codes: makeCode(`Schema.Any.annotate({ "description": "a" })`, "any")
324 }
325 )
326 })
327
328 describe("String", () => {
329 it("String", () => {
330 assertSchema({ schema: Schema.String }, {
331 codes: makeCode("Schema.String", "string")
332 })
333 })
334
335 it("String & identifier", () => {
336 assertSchema({ schema: Schema.String.annotate({ identifier: "ID" }) }, {
337 codes: makeCode("ID", "ID"),
338 references: {
339 nonRecursives: [
340 {
341 $ref: "ID",
342 code: makeCode(`Schema.String.annotate({ "identifier": "ID" })`, "string")
343 }
344 ]
345 }
346 })
347 })
348
349 it("String & annotations", () => {
350 assertSchema(
351 { schema: Schema.String.annotate({ "description": "a" }) },
352 {
353 codes: makeCode(`Schema.String.annotate({ "description": "a" })`, "string")
354 }
355 )
356 })
357
358 it("String & check", () => {
359 assertSchema(
360 { schema: Schema.String.check(Schema.isMinLength(1)) },
361 {
362 codes: makeCode(`Schema.String.check(Schema.isMinLength(1))`, "string")
363 }
364 )
365 })
366
367 it("String & annotations & check", () => {
368 assertSchema(
369 { schema: Schema.String.annotate({ "description": "a" }).check(Schema.isMinLength(1)) },
370 {
371 codes: makeCode(
372 `Schema.String.annotate({ "description": "a" }).check(Schema.isMinLength(1))`,
373 "string"
374 )
375 }
376 )
377 })
378
379 it("String & code point checks", () => {
380 for (
381 const [check, code] of [
382 [Schema.isMinCodePoints(2), "Schema.isMinCodePoints(2)"],
383 [Schema.isMaxCodePoints(3), "Schema.isMaxCodePoints(3)"],
384 [Schema.isBetweenCodePoints(2, 3), "Schema.isBetweenCodePoints(2, 3)"]
385 ] as const
386 ) {
387 assertSchema(
388 { schema: Schema.String.check(check) },
389 { codes: makeCode(`Schema.String.check(${code})`, "string") }
390 )
391 }
392 })
393
394 it("String & check + annotations", () => {
395 assertSchema(
396 { schema: Schema.String.check(Schema.isMinLength(1, { description: "a" })) },
397 {
398 codes: makeCode(`Schema.String.check(Schema.isMinLength(1).annotate({ "description": "a" }))`, "string")
399 }
400 )
401 })
402
403 it("String & check & annotations", () => {
404 assertSchema(
405 { schema: Schema.String.check(Schema.isMinLength(1)).annotate({ "description": "a" }) },
406 {
407 codes: makeCode(`Schema.String.check(Schema.isMinLength(1).annotate({ "description": "a" }))`, "string")
408 }
409 )
410 })
411
412 describe("checks", () => {
413 it("isStartingWith", () => {
414 assertSchema(
415 { schema: Schema.String.check(Schema.isStartingWith("a")) },
416 {
417 codes: makeCode(`Schema.String.check(Schema.isStartingWith("a"))`, "string")
418 }
419 )
420 })
421
422 it("isEndingWith", () => {
423 assertSchema(
424 { schema: Schema.String.check(Schema.isEndingWith("a")) },
425 {
426 codes: makeCode(`Schema.String.check(Schema.isEndingWith("a"))`, "string")
427 }
428 )
429 })
430
431 it("isIncluding", () => {
432 assertSchema(
433 { schema: Schema.String.check(Schema.isIncluding("a")) },
434 {
435 codes: makeCode(`Schema.String.check(Schema.isIncluding("a"))`, "string")
436 }
437 )
438 })
439
440 it("isGUID with annotations", () => {
441 assertSchema(
442 { schema: Schema.String.check(Schema.isGUID({ message: "message" })) },
443 {
444 codes: makeCode(`Schema.String.check(Schema.isGUID().annotate({ "message": "message" }))`, "string")
445 }
446 )
447 })
448 })
449 })
450
451 describe("Number", () => {
452 it("Number", () => {
453 assertSchema({ schema: Schema.Number }, {
454 codes: makeCode("Schema.Number", "number")
455 })
456 assertSchema(
457 { schema: Schema.Number.annotate({ "description": "a" }) },
458 {
459 codes: makeCode(`Schema.Number.annotate({ "description": "a" })`, "number")
460 }
461 )
462 })
463
464 it("Number & check", () => {
465 assertSchema(
466 { schema: Schema.Number.check(Schema.isGreaterThan(10)) },
467 {
468 codes: makeCode(`Schema.Number.check(Schema.isGreaterThan(10))`, "number")
469 }
470 )
471 })
472 })
473
474 it("Boolean", () => {
475 assertSchema({ schema: Schema.Boolean }, {
476 codes: makeCode("Schema.Boolean", "boolean")
477 })
478 assertSchema(
479 { schema: Schema.Boolean.annotate({ "description": "a" }) },
480 {
481 codes: makeCode(`Schema.Boolean.annotate({ "description": "a" })`, "boolean")
482 }
483 )
484 })
485
486 describe("BigInt", () => {
487 it("BigInt", () => {
488 assertSchema({ schema: Schema.BigInt }, {
489 codes: makeCode("Schema.BigInt", "bigint")
490 })
491 assertSchema(
492 { schema: Schema.BigInt.annotate({ "description": "a" }) },
493 {
494 codes: makeCode(`Schema.BigInt.annotate({ "description": "a" })`, "bigint")
495 }
496 )
497 })
498
499 it("BigInt & check", () => {
500 assertSchema(
501 { schema: Schema.BigInt.check(Schema.isGreaterThanBigInt(10n)) },
502 {
503 codes: makeCode(`Schema.BigInt.check(Schema.isGreaterThanBigInt(10n))`, "bigint")
504 }
505 )
506 })
507 })
508
509 it("Symbol", () => {
510 assertSchema({ schema: Schema.Symbol }, {
511 codes: makeCode("Schema.Symbol", "symbol")
512 })
513 assertSchema(
514 { schema: Schema.Symbol.annotate({ "description": "a" }) },
515 {
516 codes: makeCode(`Schema.Symbol.annotate({ "description": "a" })`, "symbol")
517 }
518 )
519 })
520
521 it("ObjectKeyword", () => {
522 assertSchema({ schema: Schema.ObjectKeyword }, {
523 codes: makeCode("Schema.ObjectKeyword", "object")
524 })
525 assertSchema(
526 { schema: Schema.ObjectKeyword.annotate({ "description": "a" }) },
527 {
528 codes: makeCode(`Schema.ObjectKeyword.annotate({ "description": "a" })`, "object")
529 }
530 )
531 })
532
533 describe("Literal", () => {
534 it("string literal", () => {
535 assertSchema({ schema: Schema.Literal("a") }, {
536 codes: makeCode(`Schema.Literal("a")`, `"a"`)
537 })
538 assertSchema(
539 { schema: Schema.Literal("a").annotate({ "description": "a" }) },
540 {
541 codes: makeCode(`Schema.Literal("a").annotate({ "description": "a" })`, `"a"`)
542 }
543 )
544 })
545
546 it("number literal", () => {
547 assertSchema({ schema: Schema.Literal(1) }, {
548 codes: makeCode(`Schema.Literal(1)`, "1")
549 })
550 assertSchema(
551 { schema: Schema.Literal(1).annotate({ "description": "a" }) },
552 {
553 codes: makeCode(`Schema.Literal(1).annotate({ "description": "a" })`, "1")
554 }
555 )
556 })
557
558 it("boolean literal", () => {
559 assertSchema({ schema: Schema.Literal(true) }, {
560 codes: makeCode(`Schema.Literal(true)`, "true")
561 })
562 assertSchema(
563 { schema: Schema.Literal(true).annotate({ "description": "a" }) },
564 {
565 codes: makeCode(`Schema.Literal(true).annotate({ "description": "a" })`, "true")
566 }
567 )
568 })
569
570 it("bigint literal", () => {
571 assertSchema({ schema: Schema.Literal(100n) }, {
572 codes: makeCode(`Schema.Literal(100n)`, "100n")
573 })
574 assertSchema(
575 { schema: Schema.Literal(100n).annotate({ "description": "a" }) },
576 {
577 codes: makeCode(`Schema.Literal(100n).annotate({ "description": "a" })`, "100n")
578 }
579 )
580 })
581 })
582
583 describe("UniqueSymbol", () => {
584 it("should create a Symbol artifact", () => {
585 assertSchema(
586 { schema: Schema.UniqueSymbol(Symbol("a")) },
587 {
588 codes: makeCode(`Schema.UniqueSymbol(_symbol)`, "typeof _symbol"),
589 artifacts: [{
590 _tag: "Symbol",
591 identifier: "_symbol",
592 code: makeCode(`Symbol("a")`, `typeof _symbol`)
593 }]
594 }
595 )
596 assertSchema(
597 { schema: Schema.UniqueSymbol(Symbol()) },
598 {
599 codes: makeCode(`Schema.UniqueSymbol(_symbol)`, "typeof _symbol"),
600 artifacts: [{
601 _tag: "Symbol",
602 identifier: "_symbol",
603 code: makeCode(`Symbol()`, `typeof _symbol`)
604 }]
605 }
606 )
607 })
608
609 it("should create a global Symbol artifact", () => {
610 assertSchema(
611 { schema: Schema.UniqueSymbol(Symbol.for("a")) },
612 {
613 codes: makeCode(`Schema.UniqueSymbol(_symbol)`, "typeof _symbol"),
614 artifacts: [{
615 _tag: "Symbol",
616 identifier: "_symbol",
617 code: makeCode(`Symbol.for("a")`, `typeof _symbol`)
618 }]
619 }
620 )
621 assertSchema(
622 { schema: Schema.UniqueSymbol(Symbol.for("a")).annotate({ "description": "a" }) },
623 {
624 codes: makeCode(
625 `Schema.UniqueSymbol(_symbol).annotate({ "description": "a" })`,
626 "typeof _symbol"
627 ),
628 artifacts: [{
629 _tag: "Symbol",
630 identifier: "_symbol",
631 code: makeCode(`Symbol.for("a")`, `typeof _symbol`)
632 }]
633 }
634 )
635 })
636 })
637
638 describe("Enum", () => {
639 it("string values", () => {
640 assertSchema(
641 {
642 schema: Schema.Enum({
643 A: "a",
644 B: "b"
645 })
646 },
647 {
648 codes: makeCode(`Schema.Enum(_Enum)`, `_Enum`),
649 artifacts: [{
650 _tag: "Enum",
651 identifier: "_Enum",
652 code: makeCode(`enum _Enum { "A" = "a", "B" = "b" }`, `typeof _Enum`)
653 }]
654 }
655 )
656 assertSchema(
657 {
658 schema: Schema.Enum({
659 A: "a",
660 B: "b"
661 }).annotate({ "description": "a" })
662 },
663 {
664 codes: makeCode(
665 `Schema.Enum(_Enum).annotate({ "description": "a" })`,
666 `_Enum`
667 ),
668 artifacts: [{
669 _tag: "Enum",
670 identifier: "_Enum",
671 code: makeCode(`enum _Enum { "A" = "a", "B" = "b" }`, `typeof _Enum`)
672 }]
673 }
674 )
675 })
676
677 it("number values", () => {
678 assertSchema(
679 {
680 schema: Schema.Enum({
681 One: 1,
682 Two: 2
683 })
684 },
685 {
686 codes: makeCode(`Schema.Enum(_Enum)`, `_Enum`),
687 artifacts: [{
688 _tag: "Enum",
689 identifier: "_Enum",
690 code: makeCode(`enum _Enum { "One" = 1, "Two" = 2 }`, `typeof _Enum`)
691 }]
692 }
693 )
694 assertSchema(
695 {
696 schema: Schema.Enum({
697 One: 1,
698 Two: 2
699 }).annotate({ "description": "a" })
700 },
701 {
702 codes: makeCode(
703 `Schema.Enum(_Enum).annotate({ "description": "a" })`,
704 `_Enum`
705 ),
706 artifacts: [{
707 _tag: "Enum",
708 identifier: "_Enum",
709 code: makeCode(`enum _Enum { "One" = 1, "Two" = 2 }`, `typeof _Enum`)
710 }]
711 }
712 )
713 })
714
715 it("mixed values", () => {
716 assertSchema(
717 {
718 schema: Schema.Enum({
719 A: "a",
720 One: 1
721 })
722 },
723 {
724 codes: makeCode(`Schema.Enum(_Enum)`, `_Enum`),
725 artifacts: [{
726 _tag: "Enum",
727 identifier: "_Enum",
728 code: makeCode(`enum _Enum { "A" = "a", "One" = 1 }`, `typeof _Enum`)
729 }]
730 }
731 )
732 assertSchema(
733 {
734 schema: Schema.Enum({
735 A: "a",
736 One: 1
737 }).annotate({ "description": "a" })
738 },
739 {
740 codes: makeCode(
741 `Schema.Enum(_Enum).annotate({ "description": "a" })`,
742 `_Enum`
743 ),
744 artifacts: [{
745 _tag: "Enum",
746 identifier: "_Enum",
747 code: makeCode(`enum _Enum { "A" = "a", "One" = 1 }`, `typeof _Enum`)
748 }]
749 }
750 )
751 })
752 })
753
754 describe("TemplateLiteral", () => {
755 it("empty template literal", () => {
756 assertSchema(
757 { schema: Schema.TemplateLiteral([]) },
758 {
759 codes: makeCode(`Schema.TemplateLiteral([])`, "``")
760 }
761 )
762 })
763
764 it("string literal", () => {
765 assertSchema(
766 { schema: Schema.TemplateLiteral([Schema.Literal("a")]) },
767 {
768 codes: makeCode(`Schema.TemplateLiteral([Schema.Literal("a")])`, templateType(`"a"`))
769 }
770 )
771 })
772
773 it("number literal", () => {
774 assertSchema(
775 { schema: Schema.TemplateLiteral([Schema.Literal(1)]) },
776 {
777 codes: makeCode(`Schema.TemplateLiteral([Schema.Literal(1)])`, templateType("1"))
778 }
779 )
780 })
781
782 it("bigint literal", () => {
783 assertSchema(
784 { schema: Schema.TemplateLiteral([Schema.Literal(1n)]) },
785 {
786 codes: makeCode(`Schema.TemplateLiteral([Schema.Literal(1n)])`, templateType("1n"))
787 }
788 )
789 })
790
791 it("multiple consecutive literals", () => {
792 assertSchema(
793 { schema: Schema.TemplateLiteral([Schema.Literal("a"), Schema.Literal("b"), Schema.Literal("c")]) },
794 {
795 codes: makeCode(
796 `Schema.TemplateLiteral([Schema.Literal("a"), Schema.Literal("b"), Schema.Literal("c")])`,
797 templateType(`"a"`, `"b"`, `"c"`)
798 )
799 }
800 )
801 })
802
803 it("special characters in literals", () => {
804 assertSchema(
805 { schema: Schema.TemplateLiteral([Schema.Literal("a b"), Schema.String]) },
806 {
807 codes: makeCode(
808 `Schema.TemplateLiteral([Schema.Literal("a b"), Schema.String])`,
809 templateType(`"a b"`, "string")
810 )
811 }
812 )
813 assertSchema(
814 { schema: Schema.TemplateLiteral([Schema.Literal("\n"), Schema.String]) },
815 {
816 codes: makeCode(
817 `Schema.TemplateLiteral([Schema.Literal("\\n"), Schema.String])`,
818 templateType(`"\\n"`, "string")
819 )
820 }
821 )
822
823 for (const literal of ["`", "${number}", "\\"]) {
824 const code = SchemaRepresentation.toCodeDocument(
825 SchemaRepresentation.toRepresentations([
826 Schema.TemplateLiteral([Schema.Literal(literal)]).ast
827 ])
828 ).codes[0]
829 strictEqual(code.Type, templateType(JSON.stringify(literal)))
830 }
831 })
832
833 it("only schemas", () => {
834 assertSchema(
835 { schema: Schema.TemplateLiteral([Schema.String]) },
836 {
837 codes: makeCode(`Schema.TemplateLiteral([Schema.String])`, "`${string}`")
838 }
839 )
840 assertSchema(
841 { schema: Schema.TemplateLiteral([Schema.Number]) },
842 {
843 codes: makeCode(`Schema.TemplateLiteral([Schema.Number])`, "`${number}`")
844 }
845 )
846 assertSchema(
847 { schema: Schema.TemplateLiteral([Schema.BigInt]) },
848 {
849 codes: makeCode(`Schema.TemplateLiteral([Schema.BigInt])`, "`${bigint}`")
850 }
851 )
852 assertSchema(
853 { schema: Schema.TemplateLiteral([Schema.String, Schema.Number]) },
854 {
855 codes: makeCode(
856 `Schema.TemplateLiteral([Schema.String, Schema.Number])`,
857 "`${string}${number}`"
858 )
859 }
860 )
861 })
862
863 it("schema & literal", () => {
864 assertSchema(
865 { schema: Schema.TemplateLiteral([Schema.String, Schema.Literal("a")]) },
866 {
867 codes: makeCode(
868 `Schema.TemplateLiteral([Schema.String, Schema.Literal("a")])`,
869 templateType("string", `"a"`)
870 )
871 }
872 )
873 assertSchema(
874 { schema: Schema.TemplateLiteral([Schema.Number, Schema.Literal("a")]) },
875 {
876 codes: makeCode(
877 `Schema.TemplateLiteral([Schema.Number, Schema.Literal("a")])`,
878 templateType("number", `"a"`)
879 )
880 }
881 )
882 assertSchema(
883 { schema: Schema.TemplateLiteral([Schema.BigInt, Schema.Literal("a")]) },
884 {
885 codes: makeCode(
886 `Schema.TemplateLiteral([Schema.BigInt, Schema.Literal("a")])`,
887 templateType("bigint", `"a"`)
888 )
889 }
890 )
891 })
892
893 it("literal & schema", () => {
894 assertSchema(
895 { schema: Schema.TemplateLiteral([Schema.Literal("a"), Schema.String]) },
896 {
897 codes: makeCode(
898 `Schema.TemplateLiteral([Schema.Literal("a"), Schema.String])`,
899 templateType(`"a"`, "string")
900 )
901 }
902 )
903 assertSchema(
904 { schema: Schema.TemplateLiteral([Schema.Literal("a"), Schema.Number]) },
905 {
906 codes: makeCode(
907 `Schema.TemplateLiteral([Schema.Literal("a"), Schema.Number])`,
908 templateType(`"a"`, "number")
909 )
910 }
911 )
912 assertSchema(
913 { schema: Schema.TemplateLiteral([Schema.Literal("a"), Schema.BigInt]) },
914 {
915 codes: makeCode(
916 `Schema.TemplateLiteral([Schema.Literal("a"), Schema.BigInt])`,
917 templateType(`"a"`, "bigint")
918 )
919 }
920 )
921 })
922
923 it("schema & literal & schema", () => {
924 assertSchema(
925 { schema: Schema.TemplateLiteral([Schema.String, Schema.Literal("-"), Schema.Number]) },
926 {
927 codes: makeCode(
928 `Schema.TemplateLiteral([Schema.String, Schema.Literal("-"), Schema.Number])`,
929 templateType("string", `"-"`, "number")
930 )
931 }
932 )
933 assertSchema(
934 {
935 schema: Schema.TemplateLiteral([Schema.String, Schema.Literal("-"), Schema.Number]).annotate({
936 "description": "ad"
937 })
938 },
939 {
940 codes: makeCode(
941 `Schema.TemplateLiteral([Schema.String, Schema.Literal("-"), Schema.Number]).annotate({ "description": "ad" })`,
942 templateType("string", `"-"`, "number")
943 )
944 }
945 )
946 })
947
948 it("TemplateLiteral as part", () => {
949 assertSchema(
950 {
951 schema: Schema.TemplateLiteral([
952 Schema.Literal("a"),
953 Schema.TemplateLiteral([Schema.String, Schema.Literals(["-", "+"]), Schema.Number])
954 ])
955 },
956 {
957 codes: makeCode(
958 `Schema.TemplateLiteral([Schema.Literal("a"), Schema.TemplateLiteral([Schema.String, Schema.Literals(["-", "+"]), Schema.Number])])`,
959 templateType(`"a"`, templateType("string", `"-" | "+"`, "number"))
960 )
961 }
962 )
963 })
964
965 it("Union as part", () => {
966 assertSchema(
967 {
968 schema: Schema.TemplateLiteral([Schema.Literal("a"), Schema.Union([Schema.String, Schema.Number])])
969 },
970 {
971 codes: makeCode(
972 `Schema.TemplateLiteral([Schema.Literal("a"), Schema.Union([Schema.String, Schema.Number])])`,
973 templateType(`"a"`, "string | number")
974 )
975 }
976 )
977 })
978
979 it("Literals as part", () => {
980 assertSchema(
981 {
982 schema: Schema.TemplateLiteral([Schema.Literals(["a", "b"]), Schema.String])
983 },
984 {
985 codes: makeCode(
986 `Schema.TemplateLiteral([Schema.Literals(["a", "b"]), Schema.String])`,
987 templateType(`"a" | "b"`, "string")
988 )
989 }
990 )
991 })
992
993 it("omits brands from parts", () => {
994 const schema = Schema.TemplateLiteral([
995 Schema.String.pipe(Schema.brand("StringPart")),
996 Schema.Union([
997 Schema.Number.pipe(Schema.brand("NumberPart")),
998 Schema.String.pipe(Schema.brand("OtherStringPart"))
999 ])
1000 ])
1001 const code = SchemaRepresentation.toCodeDocument(
1002 SchemaRepresentation.toRepresentations([schema.ast])
1003 ).codes[0]
1004
1005 strictEqual(
1006 code.runtime,
1007 `Schema.TemplateLiteral([Schema.String, Schema.Union([Schema.Number, Schema.String])])`
1008 )
1009 strictEqual(code.Type, templateType("string", "number | string"))
1010 })
1011
1012 it("uses references for branded parts", () => {
1013 const part = Schema.String.pipe(Schema.brand("Part")).annotate({ identifier: "Part" })
1014 const code = SchemaRepresentation.toCodeDocument(
1015 SchemaRepresentation.toRepresentations([Schema.TemplateLiteral([part]).ast])
1016 ).codes[0]
1017
1018 strictEqual(code.runtime, "Schema.TemplateLiteral([Part])")
1019 strictEqual(code.Type, templateType("Part"))
1020 })
1021
1022 it("multiple unions", () => {
1023 assertSchema(
1024 {
1025 schema: Schema.TemplateLiteral([
1026 Schema.Union([Schema.Literal("a"), Schema.Literal("b")]),
1027 Schema.String,
1028 Schema.Union([Schema.Number, Schema.BigInt])
1029 ])
1030 },
1031 {
1032 codes: makeCode(
1033 `Schema.TemplateLiteral([Schema.Literals(["a", "b"]), Schema.String, Schema.Union([Schema.Number, Schema.BigInt])])`,
1034 templateType(`"a" | "b"`, "string", "number | bigint")
1035 )
1036 }
1037 )
1038 })
1039
1040 it("does not expand combinations of unions", () => {
1041 const part = Schema.Literals(["a", "b"])
1042 const schema = Schema.TemplateLiteral(Array.from({ length: 20 }, () => part))
1043 const Type = SchemaRepresentation.toCodeDocument(
1044 SchemaRepresentation.toRepresentations([schema.ast])
1045 ).codes[0].Type
1046
1047 strictEqual(Type, templateType(...Array.from({ length: 20 }, () => `"a" | "b"`)))
1048 assertTrue(Type.length < 500)
1049 })
1050 })
1051
1052 describe("Tuple", () => {
1053 it("empty tuple", () => {
1054 assertSchema({ schema: Schema.Tuple([]) }, {
1055 codes: makeCode("Schema.Tuple([])", "readonly []")
1056 })
1057 assertSchema(
1058 { schema: Schema.Tuple([]).annotate({ "description": "a" }) },
1059 {
1060 codes: makeCode(`Schema.Tuple([]).annotate({ "description": "a" })`, "readonly []")
1061 }
1062 )
1063 })
1064
1065 it("required element", () => {
1066 assertSchema(
1067 { schema: Schema.Tuple([Schema.String]) },
1068 {
1069 codes: makeCode(`Schema.Tuple([Schema.String])`, "readonly [string]")
1070 }
1071 )
1072 assertSchema(
1073 { schema: Schema.Tuple([Schema.String]).annotate({ "description": "a" }) },
1074 {
1075 codes: makeCode(
1076 `Schema.Tuple([Schema.String]).annotate({ "description": "a" })`,
1077 "readonly [string]"
1078 )
1079 }
1080 )
1081 })
1082
1083 it("optional element", () => {
1084 assertSchema(
1085 { schema: Schema.Tuple([Schema.optionalKey(Schema.String)]) },
1086 {
1087 codes: makeCode(`Schema.Tuple([Schema.optionalKey(Schema.String)])`, `readonly [(string)?]`)
1088 }
1089 )
1090 assertSchema(
1091 { schema: Schema.Tuple([Schema.optionalKey(Schema.String)]).annotate({ "description": "a" }) },
1092 {
1093 codes: makeCode(
1094 `Schema.Tuple([Schema.optionalKey(Schema.String)]).annotate({ "description": "a" })`,
1095 `readonly [(string)?]`
1096 )
1097 }
1098 )
1099 })
1100
1101 it("optional union elements", () => {
1102 assertSchema(
1103 { schema: Schema.Tuple([Schema.optionalKey(Schema.Union([Schema.String, Schema.Number]))]) },
1104 {
1105 codes: makeCode(
1106 `Schema.Tuple([Schema.optionalKey(Schema.Union([Schema.String, Schema.Number]))])`,
1107 `readonly [(string | number)?]`
1108 )
1109 }
1110 )
1111 assertSchema(
1112 { schema: Schema.Tuple([Schema.optionalKey(Schema.Literals(["a", "b"]))]) },
1113 {
1114 codes: makeCode(
1115 `Schema.Tuple([Schema.optionalKey(Schema.Literals(["a", "b"]))])`,
1116 `readonly [("a" | "b")?]`
1117 )
1118 }
1119 )
1120 })
1121
1122 it("optional readonly tuple element", () => {
1123 assertSchema(
1124 { schema: Schema.Tuple([Schema.optionalKey(Schema.Tuple([Schema.String]))]) },
1125 {
1126 codes: makeCode(
1127 `Schema.Tuple([Schema.optionalKey(Schema.Tuple([Schema.String]))])`,
1128 `readonly [(readonly [string])?]`
1129 )
1130 }
1131 )
1132 })
1133
1134 it("optional union element imported from JSON Schema", () => {
1135 assertJsonSchema({
1136 schema: {
1137 type: "array",
1138 prefixItems: [{ anyOf: [{ type: "string" }, { type: "number" }] }],
1139 items: false
1140 }
1141 }, {
1142 codes: makeCode(
1143 `Schema.Tuple([Schema.optionalKey(Schema.Union([Schema.String, Schema.Number.check(Schema.isFinite())]))])`,
1144 `readonly [(string | number)?]`
1145 )
1146 })
1147 })
1148
1149 it("annotateKey", () => {
1150 assertSchema(
1151 { schema: Schema.Tuple([Schema.String.annotateKey({ "description": "a" })]) },
1152 {
1153 codes: makeCode(
1154 `Schema.Tuple([Schema.String.annotateKey({ "description": "a" })])`,
1155 "readonly [string]"
1156 )
1157 }
1158 )
1159 })
1160 })
1161
1162 it("Array", () => {
1163 assertSchema(
1164 { schema: Schema.Array(Schema.String) },
1165 {
1166 codes: makeCode("Schema.Array(Schema.String)", "ReadonlyArray<string>")
1167 }
1168 )
1169 assertSchema(
1170 { schema: Schema.Array(Schema.String).annotate({ "description": "a" }) },
1171 {
1172 codes: makeCode(
1173 `Schema.Array(Schema.String).annotate({ "description": "a" })`,
1174 "ReadonlyArray<string>"
1175 )
1176 }
1177 )
1178 })
1179
1180 it("TupleWithRest", () => {
1181 assertSchema(
1182 {
1183 schema: Schema.TupleWithRest(
1184 Schema.Tuple([Schema.optionalKey(Schema.Union([Schema.String, Schema.Number]))]),
1185 [Schema.Boolean]
1186 )
1187 },
1188 {
1189 codes: makeCode(
1190 `Schema.TupleWithRest(Schema.Tuple([Schema.optionalKey(Schema.Union([Schema.String, Schema.Number]))]), [Schema.Boolean])`,
1191 `readonly [(string | number)?, ...Array<boolean>]`
1192 )
1193 }
1194 )
1195 assertSchema(
1196 { schema: Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number]) },
1197 {
1198 codes: makeCode(
1199 `Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number])`,
1200 "readonly [string, ...Array<number>]"
1201 )
1202 }
1203 )
1204 assertSchema(
1205 {
1206 schema: Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number]).annotate({
1207 "description": "a"
1208 })
1209 },
1210 {
1211 codes: makeCode(
1212 `Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number]).annotate({ "description": "a" })`,
1213 "readonly [string, ...Array<number>]"
1214 )
1215 }
1216 )
1217 assertSchema(
1218 { schema: Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean]) },
1219 {
1220 codes: makeCode(
1221 `Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean])`,
1222 "readonly [string, ...Array<number>, boolean]"
1223 )
1224 }
1225 )
1226 })
1227
1228 describe("Struct", () => {
1229 it("preserves a required __proto__ property in generated code", async () => {
1230 const schema = Schema.Struct({ ["__proto__"]: Schema.String })
1231 assertSchema({ schema }, {
1232 codes: makeCode(
1233 `Schema.Struct({ ["__proto__"]: Schema.String })`,
1234 `{ readonly "__proto__": string }`
1235 )
1236 })
1237
1238 const document = SchemaRepresentation.toCodeDocument(SchemaRepresentation.toRepresentations([schema.ast]))
1239 const generated: typeof schema = new Function("Schema", `return ${document.codes[0].runtime}`)(Schema)
1240 assert.deepStrictEqual(Object.keys(generated.fields), ["__proto__"])
1241 const decoding = new TestSchema.Asserts(generated).decoding()
1242 await decoding.succeed({ ["__proto__"]: "value" })
1243 await decoding.fail({}, `Missing key\n at ["__proto__"]`)
1244 await decoding.fail({ ["__proto__"]: 123 }, `Expected string\n at ["__proto__"]`)
1245 })
1246
1247 it("preserves an optional __proto__ property in generated code", async () => {
1248 const schema = Schema.Struct({ ["__proto__"]: Schema.optionalKey(Schema.String) })
1249 assertSchema({ schema }, {
1250 codes: makeCode(
1251 `Schema.Struct({ ["__proto__"]: Schema.optionalKey(Schema.String) })`,
1252 `{ readonly "__proto__"?: string }`
1253 )
1254 })
1255
1256 const document = SchemaRepresentation.toCodeDocument(SchemaRepresentation.toRepresentations([schema.ast]))
1257 const generated: typeof schema = new Function("Schema", `return ${document.codes[0].runtime}`)(Schema)
1258 assert.deepStrictEqual(Object.keys(generated.fields), ["__proto__"])
1259 const decoding = new TestSchema.Asserts(generated).decoding()
1260 await decoding.succeed({})
1261 await decoding.succeed({ ["__proto__"]: "value" })
1262 await decoding.fail({ ["__proto__"]: 123 }, `Expected string\n at ["__proto__"]`)
1263 })
1264
1265 it("empty struct", () => {
1266 assertSchema({ schema: Schema.Struct({}) }, {
1267 codes: makeCode("Schema.Struct({ })", "{ }")
1268 })
1269 assertSchema(
1270 { schema: Schema.Struct({}).annotate({ "description": "a" }) },
1271 {
1272 codes: makeCode(`Schema.Struct({ }).annotate({ "description": "a" })`, "{ }")
1273 }
1274 )
1275 })
1276
1277 it("required properties", () => {
1278 assertSchema(
1279 {
1280 schema: Schema.Struct({
1281 a: Schema.String
1282 })
1283 },
1284 {
1285 codes: makeCode(
1286 `Schema.Struct({ "a": Schema.String })`,
1287 `{ readonly "a": string }`
1288 )
1289 }
1290 )
1291 assertSchema(
1292 {
1293 schema: Schema.Struct({
1294 a: Schema.String,
1295 b: Schema.Number
1296 })
1297 },
1298 {
1299 codes: makeCode(
1300 `Schema.Struct({ "a": Schema.String, "b": Schema.Number })`,
1301 `{ readonly "a": string, readonly "b": number }`
1302 )
1303 }
1304 )
1305 })
1306
1307 it("optional properties", () => {
1308 assertSchema(
1309 {
1310 schema: Schema.Struct({
1311 a: Schema.optionalKey(Schema.String)
1312 })
1313 },
1314 {
1315 codes: makeCode(
1316 `Schema.Struct({ "a": Schema.optionalKey(Schema.String) })`,
1317 `{ readonly "a"?: string }`
1318 )
1319 }
1320 )
1321 })
1322
1323 it("mutable properties", () => {
1324 assertSchema(
1325 {
1326 schema: Schema.Struct({
1327 a: Schema.mutableKey(Schema.String)
1328 })
1329 },
1330 {
1331 codes: makeCode(
1332 `Schema.Struct({ "a": Schema.mutableKey(Schema.String) })`,
1333 `{ "a": string }`
1334 )
1335 }
1336 )
1337 })
1338
1339 it("optional and mutable properties", () => {
1340 assertSchema(
1341 {
1342 schema: Schema.Struct({
1343 a: Schema.optionalKey(Schema.mutableKey(Schema.String))
1344 })
1345 },
1346 {
1347 codes: makeCode(
1348 `Schema.Struct({ "a": Schema.optionalKey(Schema.mutableKey(Schema.String)) })`,
1349 `{ "a"?: string }`
1350 )
1351 }
1352 )
1353 })
1354
1355 it("annotateKey", () => {
1356 assertSchema(
1357 {
1358 schema: Schema.Struct({
1359 a: Schema.String.annotateKey({ "description": "a" })
1360 })
1361 },
1362 {
1363 codes: makeCode(
1364 `Schema.Struct({ "a": Schema.String.annotateKey({ "description": "a" }) })`,
1365 `{ readonly "a": string }`
1366 )
1367 }
1368 )
1369 })
1370
1371 it("struct with symbol property key", () => {
1372 const sym = Symbol.for("a")
1373 assertSchema(
1374 {
1375 schema: Schema.Struct({
1376 [sym]: Schema.String
1377 })
1378 },
1379 {
1380 codes: makeCode(
1381 `Schema.Struct({ [_symbol]: Schema.String })`,
1382 `{ readonly [_symbol]: string }`
1383 ),
1384 artifacts: [{
1385 _tag: "Symbol",
1386 identifier: "_symbol",
1387 code: makeCode(`Symbol.for("a")`, `typeof _symbol`)
1388 }]
1389 }
1390 )
1391 })
1392 })
1393
1394 it("Record(String, Number)", () => {
1395 assertSchema(
1396 {
1397 schema: Schema.Record(Schema.String, Schema.Number)
1398 },
1399 {
1400 codes: makeCode("Schema.Record(Schema.String, Schema.Number)", "{ readonly [x: string]: number }")
1401 }
1402 )
1403 assertSchema(
1404 {
1405 schema: Schema.Record(Schema.String, Schema.Number).annotate({ "description": "a" })
1406 },
1407 {
1408 codes: makeCode(
1409 `Schema.Record(Schema.String, Schema.Number).annotate({ "description": "a" })`,
1410 "{ readonly [x: string]: number }"
1411 )
1412 }
1413 )
1414 })
1415
1416 it("StructWithRest", () => {
1417 assertSchema(
1418 {
1419 schema: Schema.StructWithRest(Schema.Struct({ a: Schema.Number }), [
1420 Schema.Record(Schema.String, Schema.Number)
1421 ])
1422 },
1423 {
1424 codes: makeCode(
1425 `Schema.StructWithRest(Schema.Struct({ "a": Schema.Number }), [Schema.Record(Schema.String, Schema.Number)])`,
1426 `{ readonly "a": number } & { readonly [x: string]: number }`
1427 )
1428 }
1429 )
1430 assertSchema(
1431 {
1432 schema: Schema.StructWithRest(Schema.Struct({ a: Schema.Number }), [
1433 Schema.Record(Schema.String, Schema.Number)
1434 ]).annotate({ description: "a" })
1435 },
1436 {
1437 codes: makeCode(
1438 `Schema.StructWithRest(Schema.Struct({ "a": Schema.Number }), [Schema.Record(Schema.String, Schema.Number)]).annotate({ "description": "a" })`,
1439 `{ readonly "a": number } & { readonly [x: string]: number }`
1440 )
1441 }
1442 )
1443 })
1444
1445 describe("Union", () => {
1446 it("union with anyOf mode (default)", () => {
1447 assertSchema(
1448 {
1449 schema: Schema.Union([Schema.String, Schema.Number])
1450 },
1451 {
1452 codes: makeCode("Schema.Union([Schema.String, Schema.Number])", "string | number")
1453 }
1454 )
1455 assertSchema(
1456 {
1457 schema: Schema.Union([Schema.String, Schema.Number]).annotate({ "description": "z" })
1458 },
1459 {
1460 codes: makeCode(
1461 `Schema.Union([Schema.String, Schema.Number]).annotate({ "description": "z" })`,
1462 "string | number"
1463 )
1464 }
1465 )
1466 })
1467
1468 it("union with oneOf mode", () => {
1469 assertSchema(
1470 {
1471 schema: Schema.Union([Schema.String, Schema.Number], { mode: "oneOf" })
1472 },
1473 {
1474 codes: makeCode(
1475 `Schema.Union([Schema.String, Schema.Number], { mode: "oneOf" })`,
1476 "string | number"
1477 )
1478 }
1479 )
1480 assertSchema(
1481 {
1482 schema: Schema.Union([Schema.String, Schema.Number], { mode: "oneOf" }).annotate({ "description": "aa" })
1483 },
1484 {
1485 codes: makeCode(
1486 `Schema.Union([Schema.String, Schema.Number], { mode: "oneOf" }).annotate({ "description": "aa" })`,
1487 "string | number"
1488 )
1489 }
1490 )
1491 })
1492
1493 it("union with multiple types", () => {
1494 assertSchema(
1495 {
1496 schema: Schema.Union([Schema.String, Schema.Number, Schema.Boolean])
1497 },
1498 {
1499 codes: makeCode(
1500 "Schema.Union([Schema.String, Schema.Number, Schema.Boolean])",
1501 "string | number | boolean"
1502 )
1503 }
1504 )
1505 assertSchema(
1506 {
1507 schema: Schema.Union([Schema.String, Schema.Number, Schema.Boolean]).annotate({ "description": "a" })
1508 },
1509 {
1510 codes: makeCode(
1511 `Schema.Union([Schema.String, Schema.Number, Schema.Boolean]).annotate({ "description": "a" })`,
1512 "string | number | boolean"
1513 )
1514 }
1515 )
1516 })
1517 })
1518
1519 describe("suspend", () => {
1520 it("implicit recursive reference", () => {
1521 assertMultiDocument({
1522 representations: [{ _tag: "Reference", $ref: "Category" }],
1523 references: {
1524 Category: {
1525 _tag: "Objects",
1526 propertySignatures: [{
1527 name: "children",
1528 type: {
1529 _tag: "Arrays",
1530 elements: [],
1531 rest: [{ _tag: "Reference", $ref: "Category" }],
1532 checks: []
1533 },
1534 isOptional: false,
1535 isMutable: false
1536 }],
1537 indexSignatures: [],
1538 checks: []
1539 }
1540 }
1541 }, {
1542 codes: makeCode("Category", "Category"),
1543 references: {
1544 recursives: {
1545 Category: makeCode(
1546 `Schema.Struct({ "children": Schema.Array(Schema.suspend((): Schema.Codec<Category> => Category)) })`,
1547 `{ readonly "children": ReadonlyArray<Category> }`
1548 )
1549 }
1550 }
1551 })
1552 })
1553
1554 it("supports __proto__ as a recursive reference", () => {
1555 const references = Object.fromEntries([["__proto__", {
1556 _tag: "Objects",
1557 propertySignatures: [{
1558 name: "next",
1559 type: { _tag: "Reference", $ref: "__proto__" },
1560 isOptional: true,
1561 isMutable: false
1562 }],
1563 indexSignatures: [],
1564 checks: []
1565 }]]) as SchemaRepresentation.References
1566
1567 assertMultiDocument({
1568 representations: [{ _tag: "Reference", $ref: "__proto__" }],
1569 references
1570 }, {
1571 codes: makeCode("__proto__", "__proto__"),
1572 references: {
1573 recursives: Object.fromEntries([[
1574 "__proto__",
1575 makeCode(
1576 `Schema.Struct({ "next": Schema.optionalKey(Schema.suspend((): Schema.Codec<__proto__> => __proto__)) })`,
1577 `{ readonly "next"?: __proto__ }`
1578 )
1579 ]])
1580 }
1581 })
1582 })
1583
1584 it("non-recursive", () => {
1585 assertSchema(
1586 {
1587 schema: Schema.suspend(() => Schema.String)
1588 },
1589 {
1590 codes: makeCode(`Schema.suspend((): Schema.Codec<string> => Schema.String)`, "string")
1591 }
1592 )
1593 })
1594
1595 it("no identifier annotation", () => {
1596 type A = {
1597 readonly a?: A
1598 }
1599 const A = Schema.Struct({
1600 a: Schema.optionalKey(Schema.suspend((): Schema.Codec<A> => A))
1601 })
1602
1603 assertSchema({ schema: A }, {
1604 codes: makeCode(`Objects_`, `Objects_`),
1605 references: {
1606 recursives: {
1607 Objects_: makeCode(
1608 `Schema.Struct({ "a": Schema.optionalKey(Schema.suspend((): Schema.Codec<Objects_> => Objects_)) })`,
1609 `{ readonly "a"?: Objects_ }`
1610 )
1611 }
1612 }
1613 })
1614 })
1615
1616 it("outer identifier annotation", () => {
1617 type A = {
1618 readonly a?: A
1619 }
1620 const A = Schema.Struct({
1621 a: Schema.optionalKey(Schema.suspend((): Schema.Codec<A> => A))
1622 }).annotate({ identifier: "A" }) // outer identifier annotation
1623
1624 assertSchema({ schema: A }, {
1625 codes: makeCode(`A`, `A`),
1626 references: {
1627 recursives: {
1628 A: makeCode(
1629 `Schema.Struct({ "a": Schema.optionalKey(Schema.suspend((): Schema.Codec<A> => A)) }).annotate({ "identifier": "A" })`,
1630 `{ readonly "a"?: A }`
1631 )
1632 }
1633 }
1634 })
1635 })
1636
1637 it("inner identifier annotation", () => {
1638 type A = {
1639 readonly a?: A
1640 }
1641 const A = Schema.Struct({
1642 a: Schema.optionalKey(Schema.suspend((): Schema.Codec<A> => A.annotate({ identifier: "A" })))
1643 })
1644
1645 assertSchema({ schema: A }, {
1646 codes: makeCode(
1647 `Schema.Struct({ "a": Schema.optionalKey(Suspend_) })`,
1648 `{ readonly "a"?: Suspend_ }`
1649 ),
1650 references: {
1651 recursives: {
1652 A: makeCode(
1653 `Schema.Struct({ "a": Schema.optionalKey(Schema.suspend((): Schema.Codec<Suspend_> => Suspend_)) }).annotate({ "identifier": "A" })`,
1654 `{ readonly "a"?: Suspend_ }`
1655 ),
1656 Suspend_: makeCode(
1657 `Schema.suspend((): Schema.Codec<A> => A)`,
1658 `A`
1659 )
1660 }
1661 }
1662 })
1663 })
1664
1665 it("suspend identifier annotation", () => {
1666 type A = {
1667 readonly a?: A
1668 }
1669 const A = Schema.Struct({
1670 a: Schema.optionalKey(Schema.suspend((): Schema.Codec<A> => A).annotate({ identifier: "A" }))
1671 })
1672
1673 assertSchema({ schema: A }, {
1674 codes: makeCode(`Objects_`, `Objects_`),
1675 references: {
1676 recursives: {
1677 A: makeCode(
1678 `Schema.suspend((): Schema.Codec<Objects_> => Objects_).annotate({ "identifier": "A" })`,
1679 `Objects_`
1680 ),
1681 Objects_: makeCode(
1682 `Schema.Struct({ "a": Schema.optionalKey(Schema.suspend((): Schema.Codec<A> => A)) })`,
1683 `{ readonly "a"?: A }`
1684 )
1685 }
1686 }
1687 })
1688 })
1689 })
1690
1691 describe("brand", () => {
1692 it("omits brands", () => {
1693 assertSchema(
1694 {
1695 schema: Schema.String.pipe(Schema.brand("a"), Schema.brand("b"))
1696 },
1697 {
1698 codes: makeCode(`Schema.String`, `string`)
1699 }
1700 )
1701 })
1702
1703 it("omits brands from unions", () => {
1704 assertSchema(
1705 {
1706 schema: Schema.Union([Schema.String, Schema.Number]).pipe(Schema.brand("a"))
1707 },
1708 {
1709 codes: makeCode(`Schema.Union([Schema.String, Schema.Number])`, `string | number`)
1710 }
1711 )
1712 })
1713
1714 it("preserves checks added by fromBrand", () => {
1715 type Int = number & Brand.Brand<"Int">
1716 const Int = Brand.check<Int>(Schema.isInt())
1717 type Positive = number & Brand.Brand<"Positive">
1718 const Positive = Brand.check<Positive>(Schema.isGreaterThan(0))
1719
1720 assertSchema(
1721 {
1722 schema: Schema.Number.pipe(
1723 Schema.fromBrand("Int", Int),
1724 Schema.fromBrand("Positive", Positive)
1725 )
1726 },
1727 {
1728 codes: makeCode(
1729 `Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThan(0).annotate({ "expected": "a value greater than 0" }))`,
1730 `number`
1731 )
1732 }
1733 )
1734 })
1735 })
1736
1737 describe("Date", () => {
1738 it("Date", () => {
1739 assertSchema({ schema: Schema.Date }, {
1740 codes: makeCode(`Schema.Date`, "globalThis.Date")
1741 })
1742 })
1743
1744 describe("checks", () => {
1745 it("isGreaterThanDate", () => {
1746 assertSchema(
1747 { schema: Schema.Date.check(Schema.isGreaterThanDate(new Date(0))) },
1748 {
1749 codes: makeCode(`Schema.Date.check(Schema.isGreaterThanDate(new Date(0)))`, "globalThis.Date")
1750 }
1751 )
1752 })
1753
1754 it("isGreaterThanOrEqualToDate", () => {
1755 assertSchema(
1756 { schema: Schema.Date.check(Schema.isGreaterThanOrEqualToDate(new Date(0))) },
1757 {
1758 codes: makeCode(`Schema.Date.check(Schema.isGreaterThanOrEqualToDate(new Date(0)))`, "globalThis.Date")
1759 }
1760 )
1761 })
1762
1763 it("isLessThanDate", () => {
1764 assertSchema(
1765 { schema: Schema.Date.check(Schema.isLessThanDate(new Date(0))) },
1766 {
1767 codes: makeCode(`Schema.Date.check(Schema.isLessThanDate(new Date(0)))`, "globalThis.Date")
1768 }
1769 )
1770 })
1771
1772 it("isLessThanOrEqualToDate", () => {
1773 assertSchema(
1774 { schema: Schema.Date.check(Schema.isLessThanOrEqualToDate(new Date(0))) },
1775 {
1776 codes: makeCode(`Schema.Date.check(Schema.isLessThanOrEqualToDate(new Date(0)))`, "globalThis.Date")
1777 }
1778 )
1779 })
1780
1781 it("isBetweenDate", () => {
1782 assertSchema(
1783 { schema: Schema.Date.check(Schema.isBetweenDate({ minimum: new Date(0), maximum: new Date(1) })) },
1784 {
1785 codes: makeCode(
1786 `Schema.Date.check(Schema.isBetweenDate({ minimum: new Date(0), maximum: new Date(1), exclusiveMinimum: undefined, exclusiveMaximum: undefined }))`,
1787 "globalThis.Date"
1788 )
1789 }
1790 )
1791 })
1792 })
1793 })
1794
1795 describe("ReadonlySet", () => {
1796 it("ReadonlySet(String)", () => {
1797 assertSchema({ schema: Schema.ReadonlySet(Schema.String) }, {
1798 codes: makeCode(
1799 `Schema.ReadonlySet(Schema.String)`,
1800 "globalThis.ReadonlySet<string>"
1801 )
1802 })
1803 })
1804
1805 describe("checks", () => {
1806 it("isMinSize", () => {
1807 assertSchema(
1808 { schema: Schema.ReadonlySet(Schema.String).check(Schema.isMinSize(2)) },
1809 {
1810 codes: makeCode(
1811 `Schema.ReadonlySet(Schema.String).check(Schema.isMinSize(2))`,
1812 "globalThis.ReadonlySet<string>"
1813 )
1814 }
1815 )
1816 })
1817
1818 it("isMaxSize", () => {
1819 assertSchema(
1820 { schema: Schema.ReadonlySet(Schema.String).check(Schema.isMaxSize(2)) },
1821 {
1822 codes: makeCode(
1823 `Schema.ReadonlySet(Schema.String).check(Schema.isMaxSize(2))`,
1824 "globalThis.ReadonlySet<string>"
1825 )
1826 }
1827 )
1828 })
1829 })
1830
1831 it("isBetweenSize", () => {
1832 assertSchema(
1833 { schema: Schema.ReadonlySet(Schema.String).check(Schema.isBetweenSize(2, 2)) },
1834 {
1835 codes: makeCode(
1836 `Schema.ReadonlySet(Schema.String).check(Schema.isBetweenSize(2, 2))`,
1837 "globalThis.ReadonlySet<string>"
1838 )
1839 }
1840 )
1841 })
1842 })
1843
1844 describe("HashMap", () => {
1845 it("HashMap(String, Number)", () => {
1846 assertSchema({ schema: Schema.HashMap(Schema.String, Schema.Number) }, {
1847 codes: makeCode(
1848 `Schema.HashMap(Schema.String, Schema.Number)`,
1849 "HashMap.HashMap<string, number>"
1850 ),
1851 artifacts: [{ _tag: "Import", importDeclaration: `import * as HashMap from "effect/HashMap"` }]
1852 })
1853 })
1854 })
1855
1856 describe("allOf", () => {
1857 it("should resolve references", () => {
1858 assertJsonSchema({
1859 schema: {
1860 allOf: [
1861 { $ref: "#/$defs/A" }
1862 ],
1863 $defs: {
1864 A: {
1865 type: "string"
1866 }
1867 }
1868 }
1869 }, {
1870 codes: makeCode(`A`, "A"),
1871 references: {
1872 nonRecursives: [{
1873 $ref: "A",
1874 code: makeCode(
1875 `Schema.String.annotate({ "identifier": "A" })`,
1876 "string"
1877 )
1878 }]
1879 }
1880 })
1881 })
1882
1883 it("should resolve references in definitions", () => {
1884 assertJsonSchema({
1885 schema: {
1886 $ref: "#/$defs/A",
1887 $defs: {
1888 A: {
1889 allOf: [
1890 { $ref: "#/$defs/B" },
1891 {
1892 type: "object",
1893 properties: {
1894 a: {
1895 type: "string"
1896 },
1897 b: {}
1898 },
1899 required: ["a"]
1900 }
1901 ]
1902 },
1903 B: {
1904 type: "object",
1905 properties: {
1906 b: {
1907 type: "number"
1908 },
1909 a: {}
1910 },
1911 required: ["b"]
1912 }
1913 }
1914 }
1915 }, {
1916 codes: makeCode(`A`, "A"),
1917 references: {
1918 nonRecursives: [
1919 {
1920 $ref: "A",
1921 code: makeCode(
1922 `Schema.Struct({ "b": Schema.Number.check(Schema.isFinite()), "a": Schema.String }).annotate({ "identifier": "A" })`,
1923 `{ readonly "b": number, readonly "a": string }`
1924 )
1925 }
1926 ]
1927 }
1928 })
1929 })
1930 })
1931})