@alt-stack/zod-openapi 1.1.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/dist/index.cjs +229 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -3
- package/dist/index.d.ts +34 -3
- package/dist/index.js +226 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/interface-generator.spec.ts +668 -0
- package/src/interface-generator.ts +290 -0
- package/src/registry.ts +32 -2
- package/src/to-typescript.spec.ts +60 -0
- package/src/to-typescript.ts +39 -3
- package/src/to-zod.spec.ts +10 -6
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { schemaToTypeString, generateInterface } from "./interface-generator";
|
|
4
|
+
import { openApiToZodTsCode } from "./to-typescript";
|
|
5
|
+
import {
|
|
6
|
+
registerZodSchemaToOpenApiSchema,
|
|
7
|
+
clearZodSchemaToOpenApiSchemaRegistry,
|
|
8
|
+
} from "./registry";
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
clearZodSchemaToOpenApiSchemaRegistry();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
clearZodSchemaToOpenApiSchemaRegistry();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("schemaToTypeString", () => {
|
|
19
|
+
describe("primitive types", () => {
|
|
20
|
+
it("should convert string type", () => {
|
|
21
|
+
expect(schemaToTypeString({ type: "string" })).toBe("string");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should convert number type", () => {
|
|
25
|
+
expect(schemaToTypeString({ type: "number" })).toBe("number");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should convert integer type", () => {
|
|
29
|
+
expect(schemaToTypeString({ type: "integer" })).toBe("number");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should convert boolean type", () => {
|
|
33
|
+
expect(schemaToTypeString({ type: "boolean" })).toBe("boolean");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should convert null type", () => {
|
|
37
|
+
expect(schemaToTypeString({ type: "null" })).toBe("null");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should return unknown for unknown schema", () => {
|
|
41
|
+
expect(schemaToTypeString({})).toBe("unknown");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should return unknown for null input", () => {
|
|
45
|
+
expect(schemaToTypeString(null as any)).toBe("unknown");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("string enums", () => {
|
|
50
|
+
it("should convert string enum", () => {
|
|
51
|
+
const result = schemaToTypeString({
|
|
52
|
+
type: "string",
|
|
53
|
+
enum: ["A", "B", "C"],
|
|
54
|
+
});
|
|
55
|
+
expect(result).toBe('"A" | "B" | "C"');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should convert single value enum", () => {
|
|
59
|
+
const result = schemaToTypeString({
|
|
60
|
+
type: "string",
|
|
61
|
+
enum: ["ONLY"],
|
|
62
|
+
});
|
|
63
|
+
expect(result).toBe('"ONLY"');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("registered schemas", () => {
|
|
68
|
+
it("should use output alias for registered string format", () => {
|
|
69
|
+
const uuidSchema = z.string().uuid();
|
|
70
|
+
registerZodSchemaToOpenApiSchema(uuidSchema, {
|
|
71
|
+
schemaExportedVariableName: "uuidSchema",
|
|
72
|
+
type: "string",
|
|
73
|
+
format: "uuid",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const result = schemaToTypeString({ type: "string", format: "uuid" });
|
|
77
|
+
expect(result).toBe("UuidSchemaOutput");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should use output alias for registered number type", () => {
|
|
81
|
+
const numberSchema = z.number();
|
|
82
|
+
registerZodSchemaToOpenApiSchema(numberSchema, {
|
|
83
|
+
schemaExportedVariableName: "numberSchema",
|
|
84
|
+
type: "number",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = schemaToTypeString({ type: "number" });
|
|
88
|
+
expect(result).toBe("NumberSchemaOutput");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("numeric enums", () => {
|
|
93
|
+
it("should convert number enum", () => {
|
|
94
|
+
const result = schemaToTypeString({
|
|
95
|
+
type: "number",
|
|
96
|
+
enum: [1, 2, 3],
|
|
97
|
+
});
|
|
98
|
+
expect(result).toBe("1 | 2 | 3");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should convert integer enum", () => {
|
|
102
|
+
const result = schemaToTypeString({
|
|
103
|
+
type: "integer",
|
|
104
|
+
enum: [100, 200],
|
|
105
|
+
});
|
|
106
|
+
expect(result).toBe("100 | 200");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("arrays", () => {
|
|
111
|
+
it("should convert array of strings", () => {
|
|
112
|
+
const result = schemaToTypeString({
|
|
113
|
+
type: "array",
|
|
114
|
+
items: { type: "string" },
|
|
115
|
+
});
|
|
116
|
+
expect(result).toBe("Array<string>");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should convert array of numbers", () => {
|
|
120
|
+
const result = schemaToTypeString({
|
|
121
|
+
type: "array",
|
|
122
|
+
items: { type: "number" },
|
|
123
|
+
});
|
|
124
|
+
expect(result).toBe("Array<number>");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should convert array of objects", () => {
|
|
128
|
+
const result = schemaToTypeString({
|
|
129
|
+
type: "array",
|
|
130
|
+
items: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: { id: { type: "string" } },
|
|
133
|
+
required: ["id"],
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
expect(result).toBe("Array<{ id: string }>");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should convert array without items", () => {
|
|
140
|
+
const result = schemaToTypeString({ type: "array" });
|
|
141
|
+
expect(result).toBe("unknown[]");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should convert nested arrays", () => {
|
|
145
|
+
const result = schemaToTypeString({
|
|
146
|
+
type: "array",
|
|
147
|
+
items: {
|
|
148
|
+
type: "array",
|
|
149
|
+
items: { type: "string" },
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
expect(result).toBe("Array<Array<string>>");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("objects", () => {
|
|
157
|
+
it("should convert object with required properties", () => {
|
|
158
|
+
const result = schemaToTypeString({
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
id: { type: "string" },
|
|
162
|
+
name: { type: "string" },
|
|
163
|
+
},
|
|
164
|
+
required: ["id", "name"],
|
|
165
|
+
});
|
|
166
|
+
expect(result).toBe("{ id: string; name: string }");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should convert object with optional properties", () => {
|
|
170
|
+
const result = schemaToTypeString({
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
id: { type: "string" },
|
|
174
|
+
name: { type: "string" },
|
|
175
|
+
},
|
|
176
|
+
required: ["id"],
|
|
177
|
+
});
|
|
178
|
+
expect(result).toBe("{ id: string; name?: string }");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should convert object without required array", () => {
|
|
182
|
+
const result = schemaToTypeString({
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
id: { type: "string" },
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
expect(result).toBe("{ id?: string }");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should convert empty object", () => {
|
|
192
|
+
const result = schemaToTypeString({ type: "object" });
|
|
193
|
+
expect(result).toBe("Record<string, unknown>");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should handle additionalProperties true", () => {
|
|
197
|
+
const result = schemaToTypeString({
|
|
198
|
+
type: "object",
|
|
199
|
+
properties: { id: { type: "string" } },
|
|
200
|
+
required: ["id"],
|
|
201
|
+
additionalProperties: true,
|
|
202
|
+
});
|
|
203
|
+
expect(result).toBe("{ id: string; [key: string]: unknown }");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should handle additionalProperties with type", () => {
|
|
207
|
+
const result = schemaToTypeString({
|
|
208
|
+
type: "object",
|
|
209
|
+
additionalProperties: { type: "number" },
|
|
210
|
+
});
|
|
211
|
+
expect(result).toBe("{ [key: string]: number }");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should quote property names with special characters", () => {
|
|
215
|
+
const result = schemaToTypeString({
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
"Content-Type": { type: "string" },
|
|
219
|
+
},
|
|
220
|
+
required: ["Content-Type"],
|
|
221
|
+
});
|
|
222
|
+
expect(result).toBe("{ 'Content-Type': string }");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("$ref", () => {
|
|
227
|
+
it("should convert $ref to type name", () => {
|
|
228
|
+
const result = schemaToTypeString({
|
|
229
|
+
$ref: "#/components/schemas/User",
|
|
230
|
+
});
|
|
231
|
+
expect(result).toBe("User");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should handle $ref with nullable", () => {
|
|
235
|
+
const result = schemaToTypeString({
|
|
236
|
+
$ref: "#/components/schemas/User",
|
|
237
|
+
nullable: true,
|
|
238
|
+
});
|
|
239
|
+
expect(result).toBe("(User | null)");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should decode URI-encoded $ref", () => {
|
|
243
|
+
const result = schemaToTypeString({
|
|
244
|
+
$ref: "#/components/schemas/My%20Type",
|
|
245
|
+
});
|
|
246
|
+
expect(result).toBe("My Type");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should return unknown for invalid $ref", () => {
|
|
250
|
+
const result = schemaToTypeString({
|
|
251
|
+
$ref: "invalid",
|
|
252
|
+
});
|
|
253
|
+
expect(result).toBe("unknown");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("nullable", () => {
|
|
258
|
+
it("should handle nullable string", () => {
|
|
259
|
+
const result = schemaToTypeString({
|
|
260
|
+
type: "string",
|
|
261
|
+
nullable: true,
|
|
262
|
+
});
|
|
263
|
+
expect(result).toBe("(string | null)");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should handle nullable number", () => {
|
|
267
|
+
const result = schemaToTypeString({
|
|
268
|
+
type: "number",
|
|
269
|
+
nullable: true,
|
|
270
|
+
});
|
|
271
|
+
expect(result).toBe("(number | null)");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should handle nullable array", () => {
|
|
275
|
+
const result = schemaToTypeString({
|
|
276
|
+
type: "array",
|
|
277
|
+
items: { type: "string" },
|
|
278
|
+
nullable: true,
|
|
279
|
+
});
|
|
280
|
+
expect(result).toBe("(Array<string> | null)");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should not add nullable when false", () => {
|
|
284
|
+
const result = schemaToTypeString({
|
|
285
|
+
type: "string",
|
|
286
|
+
nullable: false,
|
|
287
|
+
});
|
|
288
|
+
expect(result).toBe("string");
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("oneOf (union)", () => {
|
|
293
|
+
it("should convert oneOf to union type", () => {
|
|
294
|
+
const result = schemaToTypeString({
|
|
295
|
+
oneOf: [{ type: "string" }, { type: "number" }],
|
|
296
|
+
});
|
|
297
|
+
expect(result).toBe("(string | number)");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("should handle single oneOf", () => {
|
|
301
|
+
const result = schemaToTypeString({
|
|
302
|
+
oneOf: [{ type: "string" }],
|
|
303
|
+
});
|
|
304
|
+
expect(result).toBe("string");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("should handle oneOf with $ref", () => {
|
|
308
|
+
const result = schemaToTypeString({
|
|
309
|
+
oneOf: [
|
|
310
|
+
{ $ref: "#/components/schemas/User" },
|
|
311
|
+
{ $ref: "#/components/schemas/Admin" },
|
|
312
|
+
],
|
|
313
|
+
});
|
|
314
|
+
expect(result).toBe("(User | Admin)");
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe("anyOf (union)", () => {
|
|
319
|
+
it("should convert anyOf to union type", () => {
|
|
320
|
+
const result = schemaToTypeString({
|
|
321
|
+
anyOf: [{ type: "string" }, { type: "number" }],
|
|
322
|
+
});
|
|
323
|
+
expect(result).toBe("(string | number)");
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("allOf (intersection)", () => {
|
|
328
|
+
it("should convert allOf to intersection type", () => {
|
|
329
|
+
const result = schemaToTypeString({
|
|
330
|
+
allOf: [
|
|
331
|
+
{ type: "object", properties: { a: { type: "string" } }, required: ["a"] },
|
|
332
|
+
{ type: "object", properties: { b: { type: "number" } }, required: ["b"] },
|
|
333
|
+
],
|
|
334
|
+
});
|
|
335
|
+
expect(result).toBe("({ a: string } & { b: number })");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("should handle single allOf", () => {
|
|
339
|
+
const result = schemaToTypeString({
|
|
340
|
+
allOf: [{ type: "object", properties: { a: { type: "string" } }, required: ["a"] }],
|
|
341
|
+
});
|
|
342
|
+
expect(result).toBe("{ a: string }");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should handle allOf with $ref", () => {
|
|
346
|
+
const result = schemaToTypeString({
|
|
347
|
+
allOf: [
|
|
348
|
+
{ $ref: "#/components/schemas/Base" },
|
|
349
|
+
{ type: "object", properties: { extra: { type: "string" } }, required: ["extra"] },
|
|
350
|
+
],
|
|
351
|
+
});
|
|
352
|
+
expect(result).toBe("(Base & { extra: string })");
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("generateInterface", () => {
|
|
358
|
+
it("should generate interface for object schema", () => {
|
|
359
|
+
const result = generateInterface("User", {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
id: { type: "string" },
|
|
363
|
+
name: { type: "string" },
|
|
364
|
+
},
|
|
365
|
+
required: ["id", "name"],
|
|
366
|
+
});
|
|
367
|
+
expect(result).toBe(`export interface User {
|
|
368
|
+
id: string;
|
|
369
|
+
name: string;
|
|
370
|
+
}`);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should generate interface with optional properties", () => {
|
|
374
|
+
const result = generateInterface("User", {
|
|
375
|
+
type: "object",
|
|
376
|
+
properties: {
|
|
377
|
+
id: { type: "string" },
|
|
378
|
+
email: { type: "string" },
|
|
379
|
+
},
|
|
380
|
+
required: ["id"],
|
|
381
|
+
});
|
|
382
|
+
expect(result).toBe(`export interface User {
|
|
383
|
+
id: string;
|
|
384
|
+
email?: string;
|
|
385
|
+
}`);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("should generate type alias for non-object schema", () => {
|
|
389
|
+
const result = generateInterface("Status", {
|
|
390
|
+
type: "string",
|
|
391
|
+
enum: ["active", "inactive"],
|
|
392
|
+
});
|
|
393
|
+
expect(result).toBe('export type Status = "active" | "inactive";');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("should generate type alias for array schema", () => {
|
|
397
|
+
const result = generateInterface("UserIds", {
|
|
398
|
+
type: "array",
|
|
399
|
+
items: { type: "string" },
|
|
400
|
+
});
|
|
401
|
+
expect(result).toBe("export type UserIds = Array<string>;");
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe("openApiToZodTsCode - optimized .d.ts output", () => {
|
|
406
|
+
it("should generate _AssertEqual helper type", () => {
|
|
407
|
+
const spec = {
|
|
408
|
+
components: {
|
|
409
|
+
schemas: {
|
|
410
|
+
User: {
|
|
411
|
+
type: "object",
|
|
412
|
+
properties: { id: { type: "string" } },
|
|
413
|
+
required: ["id"],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const code = openApiToZodTsCode(spec);
|
|
420
|
+
expect(code).toContain("type _AssertEqual<T, U> = [T] extends [U] ? ([U] extends [T] ? true : never) : never;");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("should generate interface instead of z.infer", () => {
|
|
424
|
+
const spec = {
|
|
425
|
+
components: {
|
|
426
|
+
schemas: {
|
|
427
|
+
User: {
|
|
428
|
+
type: "object",
|
|
429
|
+
properties: { id: { type: "string" }, name: { type: "string" } },
|
|
430
|
+
required: ["id", "name"],
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const code = openApiToZodTsCode(spec);
|
|
437
|
+
|
|
438
|
+
// Should have interface declaration
|
|
439
|
+
expect(code).toContain("export interface User {");
|
|
440
|
+
expect(code).toContain("id: string;");
|
|
441
|
+
expect(code).toContain("name: string;");
|
|
442
|
+
|
|
443
|
+
// Should NOT have z.infer type alias
|
|
444
|
+
expect(code).not.toContain("export type User = z.infer<");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("should emit output alias once for registered schemas", () => {
|
|
448
|
+
const uuidSchema = z.string().uuid();
|
|
449
|
+
registerZodSchemaToOpenApiSchema(uuidSchema, {
|
|
450
|
+
schemaExportedVariableName: "uuidSchema",
|
|
451
|
+
type: "string",
|
|
452
|
+
format: "uuid",
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const spec = {
|
|
456
|
+
components: {
|
|
457
|
+
schemas: {
|
|
458
|
+
User: {
|
|
459
|
+
type: "object",
|
|
460
|
+
properties: {
|
|
461
|
+
id: { type: "string", format: "uuid" },
|
|
462
|
+
},
|
|
463
|
+
required: ["id"],
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const code = openApiToZodTsCode(spec, [
|
|
470
|
+
'import { uuidSchema } from "./custom-schemas";',
|
|
471
|
+
]);
|
|
472
|
+
|
|
473
|
+
expect(code).toContain(
|
|
474
|
+
"type UuidSchemaOutput = z.output<typeof uuidSchema>;",
|
|
475
|
+
);
|
|
476
|
+
expect(code).toContain("id: UuidSchemaOutput;");
|
|
477
|
+
expect(
|
|
478
|
+
code.match(/type UuidSchemaOutput = z\.output<typeof uuidSchema>;/g)?.length ?? 0,
|
|
479
|
+
).toBe(1);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("should generate ZodType<T> annotation", () => {
|
|
483
|
+
const spec = {
|
|
484
|
+
components: {
|
|
485
|
+
schemas: {
|
|
486
|
+
User: {
|
|
487
|
+
type: "object",
|
|
488
|
+
properties: { id: { type: "string" } },
|
|
489
|
+
required: ["id"],
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const code = openApiToZodTsCode(spec);
|
|
496
|
+
expect(code).toContain("export const UserSchema: z.ZodType<User> =");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("should generate type assertions", () => {
|
|
500
|
+
const spec = {
|
|
501
|
+
components: {
|
|
502
|
+
schemas: {
|
|
503
|
+
User: {
|
|
504
|
+
type: "object",
|
|
505
|
+
properties: { id: { type: "string" } },
|
|
506
|
+
required: ["id"],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const code = openApiToZodTsCode(spec);
|
|
513
|
+
expect(code).toContain("type _AssertUser = _AssertEqual<User, z.infer<typeof UserSchema>>;");
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("should handle all OpenAPI types in interfaces", () => {
|
|
517
|
+
const spec = {
|
|
518
|
+
components: {
|
|
519
|
+
schemas: {
|
|
520
|
+
ComplexType: {
|
|
521
|
+
type: "object",
|
|
522
|
+
properties: {
|
|
523
|
+
stringProp: { type: "string" },
|
|
524
|
+
numberProp: { type: "number" },
|
|
525
|
+
integerProp: { type: "integer" },
|
|
526
|
+
booleanProp: { type: "boolean" },
|
|
527
|
+
arrayProp: { type: "array", items: { type: "string" } },
|
|
528
|
+
enumProp: { type: "string", enum: ["A", "B"] },
|
|
529
|
+
nullableProp: { type: "string", nullable: true },
|
|
530
|
+
},
|
|
531
|
+
required: ["stringProp"],
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const code = openApiToZodTsCode(spec);
|
|
538
|
+
expect(code).toContain("export interface ComplexType {");
|
|
539
|
+
expect(code).toContain("stringProp: string;");
|
|
540
|
+
expect(code).toContain("numberProp?: number;");
|
|
541
|
+
expect(code).toContain("integerProp?: number;");
|
|
542
|
+
expect(code).toContain("booleanProp?: boolean;");
|
|
543
|
+
expect(code).toContain('arrayProp?: Array<string>;');
|
|
544
|
+
expect(code).toContain('enumProp?: "A" | "B";');
|
|
545
|
+
expect(code).toContain("nullableProp?: (string | null);");
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("should handle $ref in interfaces", () => {
|
|
549
|
+
const spec = {
|
|
550
|
+
components: {
|
|
551
|
+
schemas: {
|
|
552
|
+
Address: {
|
|
553
|
+
type: "object",
|
|
554
|
+
properties: { city: { type: "string" } },
|
|
555
|
+
required: ["city"],
|
|
556
|
+
},
|
|
557
|
+
User: {
|
|
558
|
+
type: "object",
|
|
559
|
+
properties: {
|
|
560
|
+
address: { $ref: "#/components/schemas/Address" },
|
|
561
|
+
},
|
|
562
|
+
required: ["address"],
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const code = openApiToZodTsCode(spec);
|
|
569
|
+
expect(code).toContain("address: Address;");
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe(".d.ts output verification", () => {
|
|
574
|
+
it("should generate output format that will produce optimized .d.ts", () => {
|
|
575
|
+
const spec = {
|
|
576
|
+
components: {
|
|
577
|
+
schemas: {
|
|
578
|
+
User: {
|
|
579
|
+
type: "object",
|
|
580
|
+
properties: {
|
|
581
|
+
id: { type: "string" },
|
|
582
|
+
name: { type: "string" },
|
|
583
|
+
age: { type: "integer" },
|
|
584
|
+
tags: { type: "array", items: { type: "string" } },
|
|
585
|
+
},
|
|
586
|
+
required: ["id", "name"],
|
|
587
|
+
},
|
|
588
|
+
Product: {
|
|
589
|
+
type: "object",
|
|
590
|
+
properties: {
|
|
591
|
+
sku: { type: "string" },
|
|
592
|
+
price: { type: "number" },
|
|
593
|
+
},
|
|
594
|
+
required: ["sku", "price"],
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const code = openApiToZodTsCode(spec);
|
|
601
|
+
|
|
602
|
+
// Verify the generated .ts code has the right format for optimized .d.ts output:
|
|
603
|
+
|
|
604
|
+
// 1. Should have interface declarations (will appear directly in .d.ts)
|
|
605
|
+
expect(code).toContain("export interface User {");
|
|
606
|
+
expect(code).toContain("export interface Product {");
|
|
607
|
+
|
|
608
|
+
// 2. Should have ZodType<T> annotations (will produce simple z.ZodType<T> in .d.ts)
|
|
609
|
+
expect(code).toContain("export const UserSchema: z.ZodType<User> =");
|
|
610
|
+
expect(code).toContain("export const ProductSchema: z.ZodType<Product> =");
|
|
611
|
+
|
|
612
|
+
// 3. Should NOT have z.infer type aliases (would require resolution in .d.ts)
|
|
613
|
+
expect(code).not.toContain("export type User = z.infer<");
|
|
614
|
+
expect(code).not.toContain("export type Product = z.infer<");
|
|
615
|
+
|
|
616
|
+
// 4. Should have type assertions
|
|
617
|
+
expect(code).toContain("type _AssertEqual<T, U>");
|
|
618
|
+
expect(code).toContain("type _AssertUser = _AssertEqual<User, z.infer<typeof UserSchema>>");
|
|
619
|
+
expect(code).toContain("type _AssertProduct = _AssertEqual<Product, z.infer<typeof ProductSchema>>");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("should produce valid TypeScript syntax for all schema types", () => {
|
|
623
|
+
const spec = {
|
|
624
|
+
components: {
|
|
625
|
+
schemas: {
|
|
626
|
+
// Test various complex types
|
|
627
|
+
StringEnum: {
|
|
628
|
+
type: "string",
|
|
629
|
+
enum: ["A", "B", "C"],
|
|
630
|
+
},
|
|
631
|
+
NullableType: {
|
|
632
|
+
type: "string",
|
|
633
|
+
nullable: true,
|
|
634
|
+
},
|
|
635
|
+
UnionType: {
|
|
636
|
+
oneOf: [{ type: "string" }, { type: "number" }],
|
|
637
|
+
},
|
|
638
|
+
ArrayType: {
|
|
639
|
+
type: "array",
|
|
640
|
+
items: { type: "string" },
|
|
641
|
+
},
|
|
642
|
+
NestedObject: {
|
|
643
|
+
type: "object",
|
|
644
|
+
properties: {
|
|
645
|
+
nested: {
|
|
646
|
+
type: "object",
|
|
647
|
+
properties: {
|
|
648
|
+
deep: { type: "string" },
|
|
649
|
+
},
|
|
650
|
+
required: ["deep"],
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
required: ["nested"],
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const code = openApiToZodTsCode(spec);
|
|
660
|
+
|
|
661
|
+
// All types should generate valid TypeScript
|
|
662
|
+
expect(code).toContain('export type StringEnum = "A" | "B" | "C"');
|
|
663
|
+
expect(code).toContain("export type NullableType = (string | null)");
|
|
664
|
+
expect(code).toContain("export type UnionType = (string | number)");
|
|
665
|
+
expect(code).toContain("export type ArrayType = Array<string>");
|
|
666
|
+
expect(code).toContain("export interface NestedObject {");
|
|
667
|
+
});
|
|
668
|
+
});
|