1import * as JsonSchemaGenerator from "@effect/openapi-generator/JsonSchemaGenerator"
2import { describe, expect, it } from "@effect/vitest"
3
4describe("JsonSchemaGenerator", () => {
5 it("schema & no definitions", () => {
6 const generator = JsonSchemaGenerator.make()
7 generator.addSchema("A", { type: "string" })
8 const definitions = {}
9 const result = generator.generate("openapi-3.1", definitions, false)
10 expect(result).toBe(`// schemas
11export type A = string
12export const A = Schema.String
13`)
14 })
15
16 it("schema & definitions", () => {
17 const generator = JsonSchemaGenerator.make()
18 generator.addSchema("A", { $ref: "#/components/schemas/B" })
19 const definitions = {
20 B: { type: "string" }
21 }
22 const result = generator.generate("openapi-3.1", definitions, false)
23 expect(result).toBe(`// non-recursive definitions
24export type B = string
25export const B = Schema.String.annotate({ "identifier": "B" })
26// schemas
27export type A = B
28export const A = B
29`)
30 })
31
32 it("onEnter strips specified keys", () => {
33 const generator = JsonSchemaGenerator.make()
34 generator.addSchema("A", { type: "string", description: "desc", examples: ["ex"] })
35 const definitions = {}
36 const result = generator.generate("openapi-3.1", definitions, false, {
37 onEnter: (js) => {
38 const out = { ...js }
39 delete out.examples
40 return out
41 }
42 })
43 expect(result).toBe(`// schemas
44export type A = string
45export const A = Schema.String.annotate({ "description": "desc" })
46`)
47 })
48
49 it("default preserves all annotations", () => {
50 const generator = JsonSchemaGenerator.make()
51 generator.addSchema("A", { type: "string", description: "desc", examples: ["ex"] })
52 const definitions = {}
53 const result = generator.generate("openapi-3.1", definitions, false)
54 expect(result).toBe(`// schemas
55export type A = string
56export const A = Schema.String.annotate({ "description": "desc", "examples": ["ex"] })
57`)
58 })
59
60 const definitions = {
61 A: {
62 type: "object",
63 properties: { a: { type: "string" } },
64 required: ["a"]
65 },
66 B: {
67 type: "object",
68 properties: { b: { type: "string" } },
69 required: ["b"]
70 }
71 } as const
72
73 const openA =
74 `Schema.StructWithRest(Schema.Struct({ "a": Schema.String }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))])`
75 const openB =
76 `Schema.StructWithRest(Schema.Struct({ "b": Schema.String }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))])`
77
78 for (
79 const [keyword, expected] of [
80 ["oneOf", `export const Choice = Schema.Union([${openA}, ${openB}], { mode: "oneOf" })`],
81 ["anyOf", `export const Choice = Schema.Union([${openA}, ${openB}])`],
82 [
83 "allOf",
84 `export const Choice = Schema.StructWithRest(Schema.Struct({ "a": Schema.String, "b": Schema.String }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))])`
85 ]
86 ] as const
87 ) {
88 it(`preserves omitted additionalProperties with ${keyword}`, () => {
89 const generator = JsonSchemaGenerator.make()
90 generator.addSchema("Choice", {
91 type: "object",
92 [keyword]: [
93 { $ref: "#/components/schemas/A" },
94 { $ref: "#/components/schemas/B" }
95 ]
96 })
97
98 const result = generator.generate("openapi-3.1", definitions, false)
99
100 expect(result).toContain(expected)
101 })
102 }
103
104 it("honors explicit additionalProperties false with oneOf", () => {
105 const generator = JsonSchemaGenerator.make()
106 generator.addSchema("Choice", {
107 type: "object",
108 additionalProperties: false,
109 oneOf: [
110 { $ref: "#/components/schemas/A" },
111 { $ref: "#/components/schemas/B" }
112 ]
113 })
114
115 const result = generator.generate("openapi-3.1", definitions, false)
116
117 // The closed outer object forbids the properties required by both branches.
118 expect(result).toContain("export const Choice = Schema.Never")
119 })
120
121 it("generateHttpApi emits explicit type and const declarations", () => {
122 const generator = JsonSchemaGenerator.make()
123 generator.addSchema("A", { type: "string" })
124 generator.addSchema("B", {
125 type: "object",
126 properties: {
127 id: {
128 type: "string"
129 }
130 },
131 required: ["id"],
132 additionalProperties: false
133 })
134
135 const result = generator.generateHttpApi("openapi-3.1", {})
136
137 expect(result).toContain(`export type A = string
138export const A = Schema.String`)
139 expect(result).toContain(`export type B = { readonly "id": string }
140export const B = Schema.Struct({ "id": Schema.String })`)
141 expect(result).not.toContain("Schema.Class<")
142 expect(result).not.toContain("Schema.Opaque<")
143 })
144
145 it("recursive schema", () => {
146 const generator = JsonSchemaGenerator.make()
147 generator.addSchema("A", { $ref: "#/components/schemas/B" })
148 const definitions = {
149 B: {
150 "type": "object",
151 "properties": {
152 "name": {
153 "type": "string"
154 },
155 "children": {
156 "type": "array",
157 "items": {
158 "$ref": "#/components/schemas/B"
159 }
160 }
161 },
162 "required": [
163 "name",
164 "children"
165 ],
166 "additionalProperties": false
167 }
168 }
169 const result = generator.generate("openapi-3.1", definitions, false)
170 expect(result).toBe(`// recursive definitions
171export type B = { readonly "name": string, readonly "children": ReadonlyArray<B> }
172export const B = Schema.Struct({ "name": Schema.String, "children": Schema.Array(Schema.suspend((): Schema.Codec<B> => B)) }).annotate({ "identifier": "B" })
173// schemas
174export type A = B
175export const A = B
176`)
177 })
178
179 it("hoists recursive definitions referenced by earlier recursive definitions", () => {
180 const generator = JsonSchemaGenerator.make()
181 generator.addSchema("A", { $ref: "#/components/schemas/ResourcesNetworkCard" })
182 const definitions = {
183 ResourcesNetworkCard: {
184 type: "object",
185 properties: {
186 sriov: {
187 $ref: "#/components/schemas/ResourcesNetworkCardSRIOV"
188 }
189 },
190 additionalProperties: false
191 },
192 ResourcesNetworkCardSRIOV: {
193 type: "object",
194 properties: {
195 vfs: {
196 type: "array",
197 items: {
198 $ref: "#/components/schemas/ResourcesNetworkCard"
199 }
200 }
201 },
202 additionalProperties: false
203 }
204 }
205
206 const result = generator.generate("openapi-3.1", definitions, false)
207 const recursiveDeclaration =
208 "export const ResourcesNetworkCard = Schema.suspend((): Schema.Codec<ResourcesNetworkCard> => __recursive_ResourcesNetworkCard)"
209
210 expect(result).toContain(recursiveDeclaration)
211 expect(result).toContain("const __recursive_ResourcesNetworkCard =")
212 expect(result.indexOf(recursiveDeclaration)).toBeLessThan(
213 result.indexOf("export const ResourcesNetworkCardSRIOV =")
214 )
215 expect(result.indexOf("export const ResourcesNetworkCardSRIOV =")).toBeLessThan(
216 result.indexOf("const __recursive_ResourcesNetworkCard =")
217 )
218 })
219
220 it("renders recursive definitions before non-recursive references for runtime generation", () => {
221 const generator = JsonSchemaGenerator.make()
222 generator.addSchema("A", { $ref: "#/components/schemas/ErrorResponse" })
223 const definitions = {
224 InnerErrors: {
225 type: "object",
226 properties: {
227 field: {
228 type: "string"
229 }
230 },
231 required: ["field"],
232 additionalProperties: false
233 },
234 ErrorDetails: {
235 oneOf: [
236 {
237 type: "object",
238 additionalProperties: {
239 $ref: "#/components/schemas/ErrorDetails"
240 }
241 },
242 {
243 $ref: "#/components/schemas/InnerErrors"
244 }
245 ]
246 },
247 ErrorResponse: {
248 type: "object",
249 properties: {
250 errors: {
251 $ref: "#/components/schemas/ErrorDetails"
252 }
253 },
254 additionalProperties: false
255 }
256 }
257
258 const runtimeResult = generator.generate("openapi-3.1", definitions, false)
259 const recursiveDeclaration =
260 "export const ErrorDetails = Schema.suspend((): Schema.Codec<ErrorDetails> => __recursive_ErrorDetails)"
261
262 expect(runtimeResult).toContain(recursiveDeclaration)
263 expect(runtimeResult).toContain("const __recursive_ErrorDetails =")
264 expect(runtimeResult.indexOf(recursiveDeclaration)).toBeLessThan(
265 runtimeResult.indexOf("export const ErrorResponse =")
266 )
267
268 const httpApiResult = generator.generateHttpApi("openapi-3.1", definitions)
269 expect(httpApiResult).toContain(recursiveDeclaration)
270 expect(httpApiResult).toContain("const __recursive_ErrorDetails =")
271 expect(httpApiResult.indexOf(recursiveDeclaration)).toBeLessThan(
272 httpApiResult.indexOf("export const ErrorResponse =")
273 )
274 })
275})