1import { assert, describe, it } from "@effect/vitest"
2import { JsonSchema, Schema, SchemaRepresentation, SchemaTransformation } from "effect"
3import { throws } from "../../utils/assert.ts"
4
5// oxlint-disable-next-line @typescript-eslint/no-require-imports
6const AjvDraft07 = require("ajv")
7
8function expectError(thunk: () => void, expected: string | Error): void {
9 if (typeof expected === "string") {
10 throws(thunk, expected)
11 } else {
12 throws(thunk, (error: unknown) => {
13 assert.strictEqual(error, expected)
14 return undefined
15 })
16 }
17}
18
19const StringRepresentation: SchemaRepresentation.Representation = {
20 _tag: "String",
21 checks: []
22}
23
24const stringIdentityTransformation = SchemaTransformation.transform({
25 decode: (value: string) => value,
26 encode: (value: string) => value
27})
28
29describe("SchemaRepresentation.toJsonSchemaMultiDocument", () => {
30 describe("definition canonicalization", () => {
31 it("deduplicates equivalent fallback definitions", () => {
32 const Content = Schema.Struct({ text: Schema.String }).annotate({ identifier: "Tool.Content" })
33 const first = Schema.toCodecJson(Schema.fromJsonString(Content))
34 const second = Schema.toCodecJson(Schema.fromJsonString(Content))
35 const document = SchemaRepresentation.toRepresentations([first.ast, second.ast])
36
37 assert.deepStrictEqual(SchemaRepresentation.toJsonSchemaMultiDocument(document), {
38 dialect: "draft-2020-12",
39 schemas: [
40 { $ref: "#/$defs/Tool.ContentEncoded" },
41 { $ref: "#/$defs/Tool.ContentEncoded" }
42 ],
43 definitions: {
44 "Tool.ContentEncoded": {
45 type: "string",
46 contentMediaType: "application/json"
47 }
48 }
49 })
50 })
51
52 it("does not deduplicate different fallback definitions", () => {
53 const Content = Schema.String.annotate({ identifier: "Fallback" })
54 const string = Schema.toCodecJson(
55 Schema.String.pipe(Schema.decodeTo(Content, stringIdentityTransformation))
56 )
57 const boolean = Schema.toCodecJson(
58 Schema.Boolean.pipe(
59 Schema.decodeTo(
60 Content,
61 SchemaTransformation.transform({
62 decode: (value) => String(value),
63 encode: (value) => value === "true"
64 })
65 )
66 )
67 )
68 const output = SchemaRepresentation.toJsonSchemaMultiDocument(
69 SchemaRepresentation.toRepresentations([string.ast, boolean.ast])
70 )
71
72 assert.deepStrictEqual(output.schemas, [
73 { $ref: "#/$defs/FallbackEncoded" },
74 { $ref: "#/$defs/FallbackEncoded_1" }
75 ])
76 assert.deepStrictEqual(output.definitions, {
77 FallbackEncoded: { type: "string" },
78 FallbackEncoded_1: { type: "boolean" }
79 })
80 })
81
82 it("does not deduplicate explicit identifiers", () => {
83 const ExplicitA = Schema.String.annotate({ identifier: "ExplicitA" })
84 const ExplicitB = Schema.String.annotate({ identifier: "ExplicitB" })
85 const output = SchemaRepresentation.toJsonSchemaMultiDocument(
86 SchemaRepresentation.toRepresentations([ExplicitA.ast, ExplicitB.ast])
87 )
88
89 assert.deepStrictEqual(output.schemas, [
90 { $ref: "#/$defs/ExplicitA" },
91 { $ref: "#/$defs/ExplicitB" }
92 ])
93 assert.deepStrictEqual(output.definitions, {
94 ExplicitA: { type: "string" },
95 ExplicitB: { type: "string" }
96 })
97 })
98
99 it("deduplicates fallback definitions after linking their dependencies", () => {
100 const Child = Schema.String.annotate({ identifier: "Child" })
101 const Parent = Schema.Struct({ child: Child }).annotate({ identifier: "Parent" })
102 const make = () => {
103 const encoded = Schema.Struct({
104 child: Schema.fromJsonString(Child)
105 })
106 return Schema.toCodecJson(
107 encoded.pipe(
108 Schema.decodeTo(
109 Parent,
110 SchemaTransformation.transform({
111 decode: ({ child }) => ({ child }),
112 encode: ({ child }) => ({ child })
113 })
114 )
115 )
116 )
117 }
118 const first = make()
119 const second = make()
120 const output = SchemaRepresentation.toJsonSchemaMultiDocument(
121 SchemaRepresentation.toRepresentations([first.ast, second.ast])
122 )
123
124 assert.deepStrictEqual(output.schemas, [
125 { $ref: "#/$defs/ParentEncoded" },
126 { $ref: "#/$defs/ParentEncoded" }
127 ])
128 assert.deepStrictEqual(output.definitions, {
129 ChildEncoded: {
130 type: "string",
131 contentMediaType: "application/json"
132 },
133 ParentEncoded: {
134 type: "object",
135 properties: {
136 child: { $ref: "#/$defs/ChildEncoded" }
137 },
138 required: ["child"],
139 additionalProperties: true
140 }
141 })
142 })
143
144 it("escapes canonical fallback references", () => {
145 const Content = Schema.String.annotate({ identifier: "Child/~" })
146 const first = Schema.toCodecJson(Schema.fromJsonString(Content))
147 const second = Schema.toCodecJson(Schema.fromJsonString(Content))
148 const output = SchemaRepresentation.toJsonSchemaMultiDocument(
149 SchemaRepresentation.toRepresentations([first.ast, second.ast])
150 )
151
152 assert.deepStrictEqual(output.schemas, [
153 { $ref: "#/$defs/Child~1~0Encoded" },
154 { $ref: "#/$defs/Child~1~0Encoded" }
155 ])
156 assert.deepStrictEqual(output.definitions, {
157 "Child/~Encoded": {
158 type: "string",
159 contentMediaType: "application/json"
160 }
161 })
162 })
163
164 for (
165 const [identifier, token] of [
166 ["Rate", "Rate"],
167 ["id~a/b", "id~0a~1b"],
168 ["Rate%", "Rate%25"],
169 ["Rate%2F", "Rate%252F"],
170 ["Rate%25", "Rate%2525"]
171 ]
172 ) {
173 it(`rewrites callback references to the canonical ${identifier} definition`, () => {
174 const Content = Schema.String.annotate({ identifier })
175 const make = () =>
176 Schema.toCodecJson(
177 Schema.String.check(Schema.isMinLength(1)).pipe(
178 Schema.decodeTo(Content, stringIdentityTransformation)
179 )
180 )
181 const first = make()
182 const second = make()
183 const callback = Schema.Unknown.check(Schema.makeFilter(() => true, {
184 toJsonSchema: () => ({ $ref: `#/$defs/${token}Encoded_1` })
185 }))
186 const document = SchemaRepresentation.toRepresentations([first.ast, second.ast, callback.ast])
187 assert.deepStrictEqual(Object.keys(document.references), [`${identifier}Encoded`, `${identifier}Encoded_1`])
188 const output = SchemaRepresentation.toJsonSchemaMultiDocument(document)
189
190 assert.deepStrictEqual(Object.keys(output.definitions), [`${identifier}Encoded`])
191 assert.deepStrictEqual(output.schemas[2], { $ref: `#/$defs/${token}Encoded` })
192 for (const restored of SchemaRepresentation.fromJsonSchemaMultiDocument(output)) {
193 assert.strictEqual(Schema.is(restored)("value"), true)
194 assert.strictEqual(Schema.is(restored)(1), false)
195 }
196 })
197 }
198
199 it("rewrites callback references with encoded leading separators", () => {
200 const Content = Schema.String.annotate({ identifier: "Rate%" })
201 const make = () =>
202 Schema.toCodecJson(
203 Schema.String.check(Schema.isMinLength(1)).pipe(
204 Schema.decodeTo(Content, stringIdentityTransformation)
205 )
206 )
207 const callback = Schema.Unknown.check(Schema.makeFilter(() => true, {
208 toJsonSchema: () => ({ $ref: "#%2F$defs%2FRate%25Encoded_1" })
209 }))
210 const output = SchemaRepresentation.toJsonSchemaMultiDocument(
211 SchemaRepresentation.toRepresentations([make().ast, make().ast, callback.ast])
212 )
213
214 assert.deepStrictEqual(Object.keys(output.definitions), ["Rate%Encoded"])
215 assert.deepStrictEqual(output.schemas[2], { $ref: "#/$defs/Rate%25Encoded" })
216 assert.doesNotThrow(() => SchemaRepresentation.fromJsonSchemaMultiDocument(output))
217 })
218
219 for (
220 const $ref of [
221 "#/$defs/Rate%Encoded_1",
222 "#%2F$defs%2FRate%Encoded_1",
223 "#/$defs/Rate#Encoded_1"
224 ]
225 ) {
226 it(`rejects malformed callback reference ${JSON.stringify($ref)}`, () => {
227 const callback = Schema.Unknown.check(Schema.makeFilter(() => true, {
228 toJsonSchema: () => ({ $ref })
229 }))
230 expectError(
231 () =>
232 SchemaRepresentation.toJsonSchemaMultiDocument(
233 SchemaRepresentation.toRepresentations([callback.ast])
234 ),
235 `Invalid JSON Pointer URI fragment ${JSON.stringify($ref)}`
236 )
237 })
238 }
239
240 for (const separator of ["/", "%2F", "%2f", "~1", "%7E1"]) {
241 it(`distinguishes pointer separators from escaped slashes in ${separator} callback references`, () => {
242 const Content = Schema.String.annotate({ identifier: "Box/properties/value" })
243 const make = () =>
244 Schema.toCodecJson(
245 Schema.String.check(Schema.isMinLength(1)).pipe(
246 Schema.decodeTo(Content, stringIdentityTransformation)
247 )
248 )
249 const Box = Schema.Struct({ valueEncoded_1: Schema.Boolean }).annotate({ identifier: "Box" })
250 const $ref = `#/$defs/Box${separator}properties${separator}valueEncoded_1`
251 const callback = Schema.Unknown.check(Schema.makeFilter(() => true, {
252 toJsonSchema: () => ({ $ref })
253 }))
254 const output = SchemaRepresentation.toJsonSchemaMultiDocument(
255 SchemaRepresentation.toRepresentations([make().ast, make().ast, Box.ast, callback.ast])
256 )
257 // Normalize URI fragments before validation: Ajv alone treats %2F as token contents.
258 const document = JsonSchema.toDocumentDraft07({
259 dialect: output.dialect,
260 schema: output.schemas[3],
261 definitions: output.definitions
262 })
263 const validate = new AjvDraft07.default({ strict: false }).compile({
264 ...document.schema,
265 definitions: document.definitions
266 })
267 const isName = separator === "~1" || separator === "%7E1"
268 assert.strictEqual(validate(true), !isName)
269 assert.strictEqual(validate("value"), isName)
270 assert.strictEqual(output.schemas[3].$ref, isName ? "#/$defs/Box~1properties~1valueEncoded" : $ref)
271 })
272 }
273
274 it("preserves an encoded pointer suffix when rewriting a percent-named alias", () => {
275 const representation = SchemaRepresentation.toRepresentation(
276 Schema.Struct({ "value%2F": Schema.Boolean }).ast
277 ).representation
278 if (representation._tag !== "Objects") throw new Error("Expected an object representation")
279 const definition: SchemaRepresentation.Representation = {
280 ...representation,
281 annotations: { "~identifier": "Box%" }
282 }
283 const $ref = "#/$defs/Box%25_1%2Fproperties%2Fvalue%252F"
284 const reference = Object.freeze({ $ref, default: Object.freeze({ $ref }) })
285 const output = SchemaRepresentation.toJsonSchemaMultiDocument({
286 representations: [{
287 _tag: "Unknown",
288 checks: [{
289 _tag: "Filter",
290 aborted: false,
291 annotations: { toJsonSchema: () => reference }
292 }]
293 }],
294 references: { "Box%": definition, "Box%_1": definition }
295 })
296
297 assert.strictEqual(output.schemas[0].$ref, "#/$defs/Box%25%2Fproperties%2Fvalue%252F")
298 assert.deepStrictEqual(output.schemas[0].default, { $ref })
299 assert.strictEqual(reference.$ref, $ref)
300 const document = JsonSchema.toDocumentDraft07({
301 dialect: output.dialect,
302 schema: output.schemas[0],
303 definitions: output.definitions
304 })
305 const validate = new AjvDraft07.default({ strict: false }).compile({
306 ...document.schema,
307 definitions: document.definitions
308 })
309 assert.strictEqual(validate(true), true)
310 assert.strictEqual(validate("value"), false)
311 })
312
313 // JSON Schema callbacks are user-provided and may have effects, so invocation count is observable.
314 it("invokes each fallback callback once before deduplicating equivalent output", () => {
315 let visits = 0
316 const Content = Schema.String.annotate({ identifier: "Callback" })
317 const make = (reverse: boolean) => {
318 const encoded = Schema.String.check(
319 Schema.makeFilter<string>(() => true, {
320 toJsonSchema: () => {
321 visits++
322 return reverse ? { maxLength: 10, minLength: 1 } : { minLength: 1, maxLength: 10 }
323 }
324 })
325 )
326 return Schema.toCodecJson(
327 encoded.pipe(Schema.decodeTo(Content, stringIdentityTransformation))
328 )
329 }
330 const first = make(false)
331 const second = make(true)
332 const output = SchemaRepresentation.toJsonSchemaMultiDocument(
333 SchemaRepresentation.toRepresentations([first.ast, second.ast])
334 )
335
336 assert.strictEqual(visits, 2)
337 assert.deepStrictEqual(output.schemas, [
338 { $ref: "#/$defs/CallbackEncoded" },
339 { $ref: "#/$defs/CallbackEncoded" }
340 ])
341 assert.deepStrictEqual(output.definitions, {
342 CallbackEncoded: {
343 type: "string",
344 minLength: 1,
345 maxLength: 10
346 }
347 })
348 })
349
350 it("reuses a compiled definition when extracting an index signature pattern", () => {
351 let visits = 0
352 const Key = Schema.String.check(
353 Schema.makeFilter<string>(() => true, {
354 identifier: "Key",
355 toJsonSchema: () => {
356 visits++
357 return { pattern: "^key$" }
358 }
359 })
360 )
361 const Root = Schema.Record(Key, Schema.String)
362 const output = SchemaRepresentation.toJsonSchemaMultiDocument(
363 SchemaRepresentation.toRepresentations([Root.ast])
364 )
365
366 assert.strictEqual(visits, 1)
367 assert.deepStrictEqual(output.schemas, [{
368 type: "object",
369 patternProperties: {
370 "^key$": { type: "string" }
371 },
372 additionalProperties: true
373 }])
374 })
375
376 it("deduplicates fallback definitions that share a recursive dependency", () => {
377 interface OptionalNode {
378 readonly next?: OptionalNode | undefined
379 }
380 const Encoded: Schema.Codec<OptionalNode> = Schema.suspend(() =>
381 Schema.Struct({
382 next: Schema.optional(Encoded)
383 })
384 )
385 const Content = Encoded.annotate({ identifier: "Node" })
386 const make = () =>
387 Schema.toCodecJson(
388 Encoded.pipe(
389 Schema.decodeTo(
390 Content,
391 SchemaTransformation.transform({
392 decode: (node) => node,
393 encode: (node) => node
394 })
395 )
396 )
397 )
398 const first = make()
399 const second = make()
400 const output = SchemaRepresentation.toJsonSchemaMultiDocument(
401 SchemaRepresentation.toRepresentations([first.ast, second.ast])
402 )
403
404 assert.deepStrictEqual(output.schemas, [
405 { $ref: "#/$defs/NodeEncoded" },
406 { $ref: "#/$defs/NodeEncoded" }
407 ])
408 assert.deepStrictEqual(output.definitions, {
409 Objects_: {
410 type: "object",
411 properties: {
412 next: {
413 anyOf: [
414 { $ref: "#/$defs/Objects_" },
415 { type: "null" }
416 ]
417 }
418 },
419 additionalProperties: true
420 },
421 NodeEncoded: { $ref: "#/$defs/Objects_" }
422 })
423 })
424
425 it("does not deduplicate fallback definitions with distinct recursive dependencies", () => {
426 interface RequiredNode {
427 readonly next: RequiredNode
428 }
429 const Content: Schema.Codec<RequiredNode> = Schema.Struct({
430 next: Schema.suspend(() => Content)
431 }).annotate({ identifier: "Node" })
432 const make = () => {
433 const Encoded: Schema.Codec<RequiredNode> = Schema.Struct({
434 next: Schema.suspend(() => Encoded)
435 })
436 return Schema.toCodecJson(
437 Encoded.pipe(
438 Schema.decodeTo(
439 Content,
440 SchemaTransformation.transform({
441 decode: (node) => node,
442 encode: (node) => node
443 })
444 )
445 )
446 )
447 }
448 const first = make()
449 const second = make()
450 const output = SchemaRepresentation.toJsonSchemaMultiDocument(
451 SchemaRepresentation.toRepresentations([first.ast, second.ast])
452 )
453
454 assert.deepStrictEqual(output.schemas, [
455 { $ref: "#/$defs/NodeEncoded" },
456 { $ref: "#/$defs/NodeEncoded_1" }
457 ])
458 assert.deepStrictEqual(output.definitions, {
459 Suspend_: {
460 type: "object",
461 properties: {
462 next: { $ref: "#/$defs/Suspend_" }
463 },
464 required: ["next"],
465 additionalProperties: true
466 },
467 NodeEncoded: {
468 type: "object",
469 properties: {
470 next: { $ref: "#/$defs/Suspend_" }
471 },
472 required: ["next"],
473 additionalProperties: true
474 },
475 Suspend_1: {
476 type: "object",
477 properties: {
478 next: { $ref: "#/$defs/Suspend_1" }
479 },
480 required: ["next"],
481 additionalProperties: true
482 },
483 NodeEncoded_1: {
484 type: "object",
485 properties: {
486 next: { $ref: "#/$defs/Suspend_1" }
487 },
488 required: ["next"],
489 additionalProperties: true
490 }
491 })
492 })
493 })
494
495 describe("multi-root compilation", () => {
496 it("preserves root order and shared named definitions", () => {
497 const A = Schema.String.annotate({ identifier: "A", description: "a" })
498 const B = Schema.String.annotate({ identifier: "B", description: "b" })
499 const C = Schema.Tuple([A, B])
500 const multiDocument = SchemaRepresentation.toRepresentations([A.ast, B.ast, C.ast])
501 const jsonMultiDocument = SchemaRepresentation.toJsonSchemaMultiDocument(multiDocument)
502 assert.deepStrictEqual(jsonMultiDocument, {
503 dialect: "draft-2020-12",
504 schemas: [
505 { "$ref": "#/$defs/A" },
506 { "$ref": "#/$defs/B" },
507 {
508 "type": "array",
509 "prefixItems": [
510 { "$ref": "#/$defs/A" },
511 { "$ref": "#/$defs/B" }
512 ],
513 "minItems": 2,
514 "maxItems": 2
515 }
516 ],
517 definitions: {
518 A: {
519 "type": "string",
520 "description": "a"
521 },
522 B: {
523 "type": "string",
524 "description": "b"
525 }
526 }
527 })
528 })
529 it("emits standard annotations and oneOf unions", () => {
530 const output = SchemaRepresentation.toJsonSchemaMultiDocument({
531 representations: [
532 {
533 _tag: "String",
534 annotations: {
535 format: "email",
536 contentEncoding: "base64",
537 contentMediaType: "application/json",
538 contentSchema: { type: "string" }
539 },
540 checks: []
541 },
542 {
543 _tag: "Union",
544 types: [StringRepresentation, { _tag: "Boolean", checks: [] }],
545 options: { mode: "oneOf" },
546 checks: []
547 }
548 ],
549 references: {}
550 })
551
552 assert.deepStrictEqual(output.schemas, [
553 {
554 type: "string",
555 format: "email",
556 contentEncoding: "base64",
557 contentMediaType: "application/json",
558 contentSchema: { type: "string" }
559 },
560 { oneOf: [{ type: "string" }, { type: "boolean" }] }
561 ])
562 })
563
564 it("resolves oneOf exactness through recursive references", () => {
565 const approximate: SchemaRepresentation.Representation = {
566 _tag: "String",
567 checks: [{
568 _tag: "Filter",
569 aborted: false,
570 annotations: { toJsonSchema: () => [{ minLength: 1 }, true] }
571 }]
572 }
573 const output = SchemaRepresentation.toJsonSchemaMultiDocument({
574 representations: [
575 { _tag: "Reference", $ref: "Exact" },
576 { _tag: "Reference", $ref: "Approximate" }
577 ],
578 references: {
579 Exact: {
580 _tag: "Union",
581 types: [{ _tag: "Reference", $ref: "Exact" }, { _tag: "Boolean", checks: [] }],
582 options: { mode: "oneOf" },
583 checks: []
584 },
585 Approximate: {
586 _tag: "Union",
587 types: [{ _tag: "Reference", $ref: "Approximate" }, approximate],
588 options: { mode: "oneOf" },
589 checks: []
590 }
591 }
592 })
593
594 assert.deepStrictEqual(output, {
595 dialect: "draft-2020-12",
596 schemas: [
597 { $ref: "#/$defs/Exact" },
598 { $ref: "#/$defs/Approximate" }
599 ],
600 definitions: {
601 Exact: {
602 oneOf: [{ $ref: "#/$defs/Exact" }, { type: "boolean" }]
603 },
604 Approximate: {
605 anyOf: [{ $ref: "#/$defs/Approximate" }, { type: "string", minLength: 1 }]
606 }
607 }
608 })
609 })
610
611 it("uses group overrides without visiting children and otherwise falls back to allOf", () => {
612 let visits = 0
613 const child: SchemaRepresentation.Filter = {
614 _tag: "Filter",
615 aborted: false,
616 annotations: {
617 toJsonSchema: () => {
618 visits++
619 return { minLength: 1 }
620 }
621 }
622 }
623 const override: SchemaRepresentation.FilterGroup = {
624 _tag: "FilterGroup",
625 checks: [child],
626 annotations: {
627 description: "override",
628 toJsonSchema: () => ({ format: "custom" })
629 }
630 }
631 const fallback: SchemaRepresentation.FilterGroup = {
632 _tag: "FilterGroup",
633 checks: [child, { _tag: "Filter", aborted: false }],
634 annotations: { description: "fallback" }
635 }
636 const document: SchemaRepresentation.MultiDocument = {
637 representations: [
638 { _tag: "String", checks: [override] },
639 { _tag: "String", checks: [fallback] }
640 ],
641 references: {}
642 }
643
644 const output = SchemaRepresentation.toJsonSchemaMultiDocument(document)
645 assert.strictEqual(visits, 1)
646 assert.deepStrictEqual(output.schemas, [
647 {
648 type: "string",
649 allOf: [{ format: "custom", description: "override" }]
650 },
651 {
652 type: "string",
653 allOf: [{ allOf: [{ minLength: 1 }], description: "fallback" }]
654 }
655 ])
656 })
657
658 it("preserves unknown references returned by callbacks", () => {
659 const reference = { $ref: "#/$defs/Unknown" }
660 const output = SchemaRepresentation.toJsonSchemaMultiDocument({
661 representations: [{
662 _tag: "String",
663 checks: [{
664 _tag: "Filter",
665 aborted: false,
666 annotations: { toJsonSchema: () => reference }
667 }]
668 }],
669 references: {}
670 })
671
672 assert.deepStrictEqual(output.schemas, [{
673 type: "string",
674 allOf: [reference]
675 }])
676 })
677
678 it("rewrites callback references without mutating callback output", () => {
679 const defaultValue = Object.freeze({ $ref: "#/$defs//properties/value" })
680 const reference = Object.freeze({ $ref: "#/$defs//properties/value", default: defaultValue })
681 const definition = (): SchemaRepresentation.Representation => ({
682 _tag: "String",
683 annotations: { "~identifier": "Value" },
684 checks: []
685 })
686 const output = SchemaRepresentation.toJsonSchemaMultiDocument({
687 representations: [{
688 _tag: "String",
689 annotations: { default: -0 },
690 checks: [{
691 _tag: "Filter",
692 aborted: false,
693 annotations: { toJsonSchema: () => reference }
694 }]
695 }],
696 references: { A: definition(), "": definition() }
697 })
698
699 assert.deepStrictEqual(reference, { $ref: "#/$defs//properties/value", default: defaultValue })
700 assert.strictEqual(Object.is(output.schemas[0].default, -0), true)
701 assert.deepStrictEqual(output, {
702 dialect: "draft-2020-12",
703 schemas: [{
704 type: "string",
705 default: -0,
706 allOf: [{ $ref: "#/$defs/A/properties/value", default: { $ref: "#/$defs//properties/value" } }]
707 }],
708 definitions: { A: { type: "string" } }
709 })
710 })
711
712 it("preserves unknown references in annotation values", () => {
713 const reference = { $ref: "#/$defs/Unknown" }
714 const output = SchemaRepresentation.toJsonSchemaMultiDocument({
715 representations: [{
716 _tag: "String",
717 annotations: { default: reference },
718 checks: []
719 }],
720 references: {}
721 })
722
723 assert.deepStrictEqual(output.schemas, [{
724 type: "string",
725 default: reference
726 }])
727 })
728
729 it("resolves referenced index-signature parameters and stops cycles", () => {
730 const record = (
731 parameter: SchemaRepresentation.Representation
732 ): SchemaRepresentation.Representation => ({
733 _tag: "Objects",
734 propertySignatures: [],
735 indexSignatures: [{ parameter, type: StringRepresentation }],
736 checks: []
737 })
738 const pattern = SchemaRepresentation.toRepresentation(
739 Schema.String.check(Schema.isPattern(/^a/u)).ast
740 ).representation
741 const output = SchemaRepresentation.toJsonSchemaMultiDocument({
742 representations: [
743 record({ _tag: "Reference", $ref: "Pattern" }),
744 record({ _tag: "Reference", $ref: "Cycle" })
745 ],
746 references: {
747 Pattern: pattern,
748 Cycle: { _tag: "Reference", $ref: "Cycle" }
749 }
750 })
751
752 assert.deepStrictEqual(output.schemas, [
753 {
754 type: "object",
755 patternProperties: { "^a": { type: "string" } },
756 additionalProperties: true
757 },
758 {
759 type: "object",
760 additionalProperties: true
761 }
762 ])
763
764 expectError(
765 () =>
766 SchemaRepresentation.toJsonSchemaDocument({
767 representation: record({ _tag: "Reference", $ref: "Missing" }),
768 references: {}
769 }),
770 `Invalid reference Missing\n at ["representation"]["indexSignatures"][0]["parameter"]["$ref"]`
771 )
772 })
773 })
774})