next-openapi-gen 0.3.2 → 0.4.1
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/README.md +331 -151
- package/dist/components/scalar.js +16 -16
- package/dist/lib/openapi-generator.js +42 -3
- package/dist/lib/route-processor.js +23 -3
- package/dist/lib/schema-processor.js +88 -17
- package/dist/lib/utils.js +14 -14
- package/dist/lib/zod-converter.js +916 -0
- package/dist/openapi-template.js +1 -0
- package/package.json +8 -5
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { parse } from "@babel/parser";
|
|
4
|
+
import traverse from "@babel/traverse";
|
|
5
|
+
import * as t from "@babel/types";
|
|
6
|
+
/**
|
|
7
|
+
* Class for converting Zod schemas to OpenAPI specifications
|
|
8
|
+
*/
|
|
9
|
+
export class ZodSchemaConverter {
|
|
10
|
+
schemaDir;
|
|
11
|
+
zodSchemas = {};
|
|
12
|
+
processedFiles = {};
|
|
13
|
+
processingSchemas = new Set();
|
|
14
|
+
processedModules = new Set();
|
|
15
|
+
constructor(schemaDir) {
|
|
16
|
+
this.schemaDir = path.resolve(schemaDir);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Find a Zod schema by name and convert it to OpenAPI spec
|
|
20
|
+
*/
|
|
21
|
+
convertZodSchemaToOpenApi(schemaName) {
|
|
22
|
+
console.log(`Looking for Zod schema: ${schemaName}`);
|
|
23
|
+
// Check for circular references
|
|
24
|
+
if (this.processingSchemas.has(schemaName)) {
|
|
25
|
+
return { $ref: `#/components/schemas/${schemaName}` };
|
|
26
|
+
}
|
|
27
|
+
// Add to processing set
|
|
28
|
+
this.processingSchemas.add(schemaName);
|
|
29
|
+
try {
|
|
30
|
+
// Return cached schema if it exists
|
|
31
|
+
if (this.zodSchemas[schemaName]) {
|
|
32
|
+
return this.zodSchemas[schemaName];
|
|
33
|
+
}
|
|
34
|
+
// Find all route files and process them first
|
|
35
|
+
const routeFiles = this.findRouteFiles();
|
|
36
|
+
for (const routeFile of routeFiles) {
|
|
37
|
+
this.processFileForZodSchema(routeFile, schemaName);
|
|
38
|
+
if (this.zodSchemas[schemaName]) {
|
|
39
|
+
console.log(`Found Zod schema '${schemaName}' in route file: ${routeFile}`);
|
|
40
|
+
return this.zodSchemas[schemaName];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Scan schema directory
|
|
44
|
+
this.scanDirectoryForZodSchema(this.schemaDir, schemaName);
|
|
45
|
+
// Return the schema if found, or null if not
|
|
46
|
+
if (this.zodSchemas[schemaName]) {
|
|
47
|
+
console.log(`Found and processed Zod schema: ${schemaName}`);
|
|
48
|
+
return this.zodSchemas[schemaName];
|
|
49
|
+
}
|
|
50
|
+
console.log(`Could not find Zod schema: ${schemaName}`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
// Remove from processing set
|
|
55
|
+
this.processingSchemas.delete(schemaName);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Find all route files in the project
|
|
60
|
+
*/
|
|
61
|
+
findRouteFiles() {
|
|
62
|
+
const routeFiles = [];
|
|
63
|
+
// Look for route files in common Next.js API directories
|
|
64
|
+
const possibleApiDirs = [
|
|
65
|
+
path.join(process.cwd(), "src", "app", "api"),
|
|
66
|
+
path.join(process.cwd(), "src", "pages", "api"),
|
|
67
|
+
path.join(process.cwd(), "app", "api"),
|
|
68
|
+
path.join(process.cwd(), "pages", "api"),
|
|
69
|
+
];
|
|
70
|
+
for (const dir of possibleApiDirs) {
|
|
71
|
+
if (fs.existsSync(dir)) {
|
|
72
|
+
this.findRouteFilesInDir(dir, routeFiles);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return routeFiles;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Recursively find route files in a directory
|
|
79
|
+
*/
|
|
80
|
+
findRouteFilesInDir(dir, routeFiles) {
|
|
81
|
+
try {
|
|
82
|
+
const files = fs.readdirSync(dir);
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const filePath = path.join(dir, file);
|
|
85
|
+
const stats = fs.statSync(filePath);
|
|
86
|
+
if (stats.isDirectory()) {
|
|
87
|
+
this.findRouteFilesInDir(filePath, routeFiles);
|
|
88
|
+
}
|
|
89
|
+
else if (file === "route.ts" ||
|
|
90
|
+
file === "route.tsx" ||
|
|
91
|
+
(file.endsWith(".ts") && file.includes("api"))) {
|
|
92
|
+
routeFiles.push(filePath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.error(`Error scanning directory ${dir} for route files:`, error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Recursively scan directory for Zod schemas
|
|
102
|
+
*/
|
|
103
|
+
scanDirectoryForZodSchema(dir, schemaName) {
|
|
104
|
+
try {
|
|
105
|
+
const files = fs.readdirSync(dir);
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const filePath = path.join(dir, file);
|
|
108
|
+
const stats = fs.statSync(filePath);
|
|
109
|
+
if (stats.isDirectory()) {
|
|
110
|
+
this.scanDirectoryForZodSchema(filePath, schemaName);
|
|
111
|
+
}
|
|
112
|
+
else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
|
|
113
|
+
this.processFileForZodSchema(filePath, schemaName);
|
|
114
|
+
// Stop searching if we found the schema
|
|
115
|
+
if (this.zodSchemas[schemaName]) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
console.error(`Error scanning directory ${dir}:`, error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Process a file to find Zod schema definitions
|
|
127
|
+
*/
|
|
128
|
+
processFileForZodSchema(filePath, schemaName) {
|
|
129
|
+
// Skip if already processed
|
|
130
|
+
if (this.processedFiles[filePath])
|
|
131
|
+
return;
|
|
132
|
+
this.processedFiles[filePath] = true;
|
|
133
|
+
try {
|
|
134
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
135
|
+
// Parse the file
|
|
136
|
+
const ast = parse(content, {
|
|
137
|
+
sourceType: "module",
|
|
138
|
+
plugins: ["typescript", "decorators-legacy"],
|
|
139
|
+
});
|
|
140
|
+
// Create a map to store imported modules
|
|
141
|
+
const importedModules = {};
|
|
142
|
+
// Look for all exported Zod schemas
|
|
143
|
+
traverse.default(ast, {
|
|
144
|
+
// Track imports for resolving local and imported schemas
|
|
145
|
+
ImportDeclaration: (path) => {
|
|
146
|
+
// Keep track of imports to resolve external schemas
|
|
147
|
+
const source = path.node.source.value;
|
|
148
|
+
// Process each import specifier
|
|
149
|
+
path.node.specifiers.forEach((specifier) => {
|
|
150
|
+
if (t.isImportSpecifier(specifier) ||
|
|
151
|
+
t.isImportDefaultSpecifier(specifier)) {
|
|
152
|
+
const importedName = specifier.local.name;
|
|
153
|
+
importedModules[importedName] = source;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
// For export const SchemaName = z.object({...})
|
|
158
|
+
ExportNamedDeclaration: (path) => {
|
|
159
|
+
if (t.isVariableDeclaration(path.node.declaration)) {
|
|
160
|
+
path.node.declaration.declarations.forEach((declaration) => {
|
|
161
|
+
if (t.isIdentifier(declaration.id) &&
|
|
162
|
+
declaration.id.name === schemaName &&
|
|
163
|
+
declaration.init) {
|
|
164
|
+
const schema = this.processZodNode(declaration.init);
|
|
165
|
+
if (schema) {
|
|
166
|
+
this.zodSchemas[schemaName] = schema;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else if (t.isTSTypeAliasDeclaration(path.node.declaration)) {
|
|
172
|
+
// Handle export type aliases with z schema definitions
|
|
173
|
+
if (t.isIdentifier(path.node.declaration.id) &&
|
|
174
|
+
path.node.declaration.id.name === schemaName) {
|
|
175
|
+
const typeAnnotation = path.node.declaration.typeAnnotation;
|
|
176
|
+
// Check if this is a reference to a z schema (e.g., export type UserSchema = z.infer<typeof UserSchema>)
|
|
177
|
+
if (t.isTSTypeReference(typeAnnotation) &&
|
|
178
|
+
t.isIdentifier(typeAnnotation.typeName) &&
|
|
179
|
+
typeAnnotation.typeName.name === "z.infer") {
|
|
180
|
+
// Extract the schema name from z.infer<typeof SchemaName>
|
|
181
|
+
if (typeAnnotation.typeParameters &&
|
|
182
|
+
typeAnnotation.typeParameters.params.length > 0 &&
|
|
183
|
+
t.isTSTypeReference(typeAnnotation.typeParameters.params[0]) &&
|
|
184
|
+
t.isTSTypeQuery(typeAnnotation.typeParameters.params[0].typeName) &&
|
|
185
|
+
t.isIdentifier(
|
|
186
|
+
// @ts-ignore
|
|
187
|
+
typeAnnotation.typeParameters.params[0].typeName.exprName)) {
|
|
188
|
+
const referencedSchema =
|
|
189
|
+
// @ts-ignore
|
|
190
|
+
typeAnnotation.typeParameters.params[0].typeName.exprName
|
|
191
|
+
.name;
|
|
192
|
+
// Look for the referenced schema in the same file
|
|
193
|
+
if (!this.zodSchemas[referencedSchema]) {
|
|
194
|
+
this.processFileForZodSchema(filePath, referencedSchema);
|
|
195
|
+
}
|
|
196
|
+
// Use the referenced schema for this type alias
|
|
197
|
+
if (this.zodSchemas[referencedSchema]) {
|
|
198
|
+
this.zodSchemas[schemaName] =
|
|
199
|
+
this.zodSchemas[referencedSchema];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
// For const SchemaName = z.object({...})
|
|
207
|
+
VariableDeclarator: (path) => {
|
|
208
|
+
if (t.isIdentifier(path.node.id) &&
|
|
209
|
+
path.node.id.name === schemaName &&
|
|
210
|
+
path.node.init) {
|
|
211
|
+
const schema = this.processZodNode(path.node.init);
|
|
212
|
+
if (schema) {
|
|
213
|
+
this.zodSchemas[schemaName] = schema;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
// For type aliases that reference Zod schemas
|
|
218
|
+
TSTypeAliasDeclaration: (path) => {
|
|
219
|
+
if (t.isIdentifier(path.node.id) &&
|
|
220
|
+
path.node.id.name === schemaName) {
|
|
221
|
+
// Try to find if this is a z.infer<typeof SchemaName> pattern
|
|
222
|
+
if (t.isTSTypeReference(path.node.typeAnnotation) &&
|
|
223
|
+
t.isIdentifier(path.node.typeAnnotation.typeName) &&
|
|
224
|
+
path.node.typeAnnotation.typeName.name === "infer" &&
|
|
225
|
+
path.node.typeAnnotation.typeParameters &&
|
|
226
|
+
path.node.typeAnnotation.typeParameters.params.length > 0) {
|
|
227
|
+
const param = path.node.typeAnnotation.typeParameters.params[0];
|
|
228
|
+
if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
|
|
229
|
+
const referencedSchemaName = param.exprName.name;
|
|
230
|
+
// Find the referenced schema
|
|
231
|
+
this.processFileForZodSchema(filePath, referencedSchemaName);
|
|
232
|
+
if (this.zodSchemas[referencedSchemaName]) {
|
|
233
|
+
this.zodSchemas[schemaName] =
|
|
234
|
+
this.zodSchemas[referencedSchemaName];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
console.error(`Error processing file ${filePath} for schema ${schemaName}: ${error}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Process a Zod node and convert it to OpenAPI schema
|
|
248
|
+
*/
|
|
249
|
+
processZodNode(node) {
|
|
250
|
+
// Handle z.object({...})
|
|
251
|
+
if (t.isCallExpression(node) &&
|
|
252
|
+
t.isMemberExpression(node.callee) &&
|
|
253
|
+
t.isIdentifier(node.callee.object) &&
|
|
254
|
+
node.callee.object.name === "z" &&
|
|
255
|
+
t.isIdentifier(node.callee.property)) {
|
|
256
|
+
const methodName = node.callee.property.name;
|
|
257
|
+
if (methodName === "object" && node.arguments.length > 0) {
|
|
258
|
+
return this.processZodObject(node);
|
|
259
|
+
}
|
|
260
|
+
else if (methodName === "union" && node.arguments.length > 0) {
|
|
261
|
+
return this.processZodUnion(node);
|
|
262
|
+
}
|
|
263
|
+
else if (methodName === "intersection" && node.arguments.length > 0) {
|
|
264
|
+
return this.processZodIntersection(node);
|
|
265
|
+
}
|
|
266
|
+
else if (methodName === "tuple" && node.arguments.length > 0) {
|
|
267
|
+
return this.processZodTuple(node);
|
|
268
|
+
}
|
|
269
|
+
else if (methodName === "discriminatedUnion" &&
|
|
270
|
+
node.arguments.length > 1) {
|
|
271
|
+
return this.processZodDiscriminatedUnion(node);
|
|
272
|
+
}
|
|
273
|
+
else if (methodName === "literal" && node.arguments.length > 0) {
|
|
274
|
+
return this.processZodLiteral(node);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
return this.processZodPrimitive(node);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Handle chained methods, e.g., z.string().email().min(5)
|
|
281
|
+
if (t.isCallExpression(node) &&
|
|
282
|
+
t.isMemberExpression(node.callee) &&
|
|
283
|
+
t.isCallExpression(node.callee.object)) {
|
|
284
|
+
return this.processZodChain(node);
|
|
285
|
+
}
|
|
286
|
+
// Handle schema references like z.lazy(() => AnotherSchema)
|
|
287
|
+
if (t.isCallExpression(node) &&
|
|
288
|
+
t.isMemberExpression(node.callee) &&
|
|
289
|
+
t.isIdentifier(node.callee.object) &&
|
|
290
|
+
node.callee.object.name === "z" &&
|
|
291
|
+
t.isIdentifier(node.callee.property) &&
|
|
292
|
+
node.callee.property.name === "lazy" &&
|
|
293
|
+
node.arguments.length > 0) {
|
|
294
|
+
return this.processZodLazy(node);
|
|
295
|
+
}
|
|
296
|
+
console.warn("Unknown Zod schema node:", node);
|
|
297
|
+
return { type: "object" };
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Process a Zod lazy schema: z.lazy(() => Schema)
|
|
301
|
+
*/
|
|
302
|
+
processZodLazy(node) {
|
|
303
|
+
// Get the function in z.lazy(() => Schema)
|
|
304
|
+
if (node.arguments.length > 0 &&
|
|
305
|
+
t.isArrowFunctionExpression(node.arguments[0]) &&
|
|
306
|
+
node.arguments[0].body) {
|
|
307
|
+
const returnExpr = node.arguments[0].body;
|
|
308
|
+
// If the function returns an identifier, it's likely a reference to another schema
|
|
309
|
+
if (t.isIdentifier(returnExpr)) {
|
|
310
|
+
const schemaName = returnExpr.name;
|
|
311
|
+
// Create a reference to the schema
|
|
312
|
+
return { $ref: `#/components/schemas/${schemaName}` };
|
|
313
|
+
}
|
|
314
|
+
// If the function returns a complex expression, try to process it
|
|
315
|
+
return this.processZodNode(returnExpr);
|
|
316
|
+
}
|
|
317
|
+
return { type: "object" };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Process a Zod literal schema: z.literal("value")
|
|
321
|
+
*/
|
|
322
|
+
processZodLiteral(node) {
|
|
323
|
+
if (node.arguments.length === 0) {
|
|
324
|
+
return { type: "string" };
|
|
325
|
+
}
|
|
326
|
+
const arg = node.arguments[0];
|
|
327
|
+
if (t.isStringLiteral(arg)) {
|
|
328
|
+
return {
|
|
329
|
+
type: "string",
|
|
330
|
+
enum: [arg.value],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
else if (t.isNumericLiteral(arg)) {
|
|
334
|
+
return {
|
|
335
|
+
type: "number",
|
|
336
|
+
enum: [arg.value],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
else if (t.isBooleanLiteral(arg)) {
|
|
340
|
+
return {
|
|
341
|
+
type: "boolean",
|
|
342
|
+
enum: [arg.value],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
return { type: "string" };
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Process a Zod discriminated union: z.discriminatedUnion("type", [schema1, schema2])
|
|
349
|
+
*/
|
|
350
|
+
processZodDiscriminatedUnion(node) {
|
|
351
|
+
if (node.arguments.length < 2) {
|
|
352
|
+
return { type: "object" };
|
|
353
|
+
}
|
|
354
|
+
// Get the discriminator field name
|
|
355
|
+
let discriminator = "";
|
|
356
|
+
if (t.isStringLiteral(node.arguments[0])) {
|
|
357
|
+
discriminator = node.arguments[0].value;
|
|
358
|
+
}
|
|
359
|
+
// Get the schemas array
|
|
360
|
+
const schemasArray = node.arguments[1];
|
|
361
|
+
if (!t.isArrayExpression(schemasArray)) {
|
|
362
|
+
return { type: "object" };
|
|
363
|
+
}
|
|
364
|
+
const schemas = schemasArray.elements
|
|
365
|
+
.map((element) => this.processZodNode(element))
|
|
366
|
+
.filter((schema) => schema !== null);
|
|
367
|
+
if (schemas.length === 0) {
|
|
368
|
+
return { type: "object" };
|
|
369
|
+
}
|
|
370
|
+
// Create a discriminated mapping for oneOf
|
|
371
|
+
return {
|
|
372
|
+
type: "object",
|
|
373
|
+
discriminator: discriminator
|
|
374
|
+
? {
|
|
375
|
+
propertyName: discriminator,
|
|
376
|
+
}
|
|
377
|
+
: undefined,
|
|
378
|
+
oneOf: schemas,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Process a Zod tuple schema: z.tuple([z.string(), z.number()])
|
|
383
|
+
*/
|
|
384
|
+
processZodTuple(node) {
|
|
385
|
+
if (node.arguments.length === 0 ||
|
|
386
|
+
!t.isArrayExpression(node.arguments[0])) {
|
|
387
|
+
return { type: "array", items: { type: "string" } };
|
|
388
|
+
}
|
|
389
|
+
const tupleItems = node.arguments[0].elements.map((element) => this.processZodNode(element));
|
|
390
|
+
// In OpenAPI, we can represent this as an array with prefixItems (OpenAPI 3.1+)
|
|
391
|
+
// For OpenAPI 3.0.x, we'll use items with type: array
|
|
392
|
+
return {
|
|
393
|
+
type: "array",
|
|
394
|
+
items: tupleItems.length > 0 ? tupleItems[0] : { type: "string" },
|
|
395
|
+
// For OpenAPI 3.1+: prefixItems: tupleItems
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Process a Zod intersection schema: z.intersection(schema1, schema2)
|
|
400
|
+
*/
|
|
401
|
+
processZodIntersection(node) {
|
|
402
|
+
if (node.arguments.length < 2) {
|
|
403
|
+
return { type: "object" };
|
|
404
|
+
}
|
|
405
|
+
const schema1 = this.processZodNode(node.arguments[0]);
|
|
406
|
+
const schema2 = this.processZodNode(node.arguments[1]);
|
|
407
|
+
// In OpenAPI, we can use allOf to represent intersection
|
|
408
|
+
return {
|
|
409
|
+
allOf: [schema1, schema2],
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Process a Zod union schema: z.union([schema1, schema2])
|
|
414
|
+
*/
|
|
415
|
+
processZodUnion(node) {
|
|
416
|
+
if (node.arguments.length === 0 ||
|
|
417
|
+
!t.isArrayExpression(node.arguments[0])) {
|
|
418
|
+
return { type: "object" };
|
|
419
|
+
}
|
|
420
|
+
const unionItems = node.arguments[0].elements.map((element) => this.processZodNode(element));
|
|
421
|
+
// Check for common pattern: z.union([z.string(), z.null()]) which should be nullable string
|
|
422
|
+
if (unionItems.length === 2) {
|
|
423
|
+
const isNullable = unionItems.some((item) => item.type === "null" ||
|
|
424
|
+
(item.enum && item.enum.length === 1 && item.enum[0] === null));
|
|
425
|
+
if (isNullable) {
|
|
426
|
+
const nonNullItem = unionItems.find((item) => item.type !== "null" &&
|
|
427
|
+
!(item.enum && item.enum.length === 1 && item.enum[0] === null));
|
|
428
|
+
if (nonNullItem) {
|
|
429
|
+
return {
|
|
430
|
+
...nonNullItem,
|
|
431
|
+
nullable: true,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Check if all union items are of the same type with different enum values
|
|
437
|
+
// This is common for string literals like: z.union([z.literal("a"), z.literal("b")])
|
|
438
|
+
const allSameType = unionItems.length > 0 &&
|
|
439
|
+
unionItems.every((item) => item.type === unionItems[0].type && item.enum);
|
|
440
|
+
if (allSameType) {
|
|
441
|
+
// Combine all enum values
|
|
442
|
+
const combinedEnums = unionItems.flatMap((item) => item.enum || []);
|
|
443
|
+
return {
|
|
444
|
+
type: unionItems[0].type,
|
|
445
|
+
enum: combinedEnums,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
// Otherwise, use oneOf for general unions
|
|
449
|
+
return {
|
|
450
|
+
oneOf: unionItems,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Process a Zod object schema: z.object({...})
|
|
455
|
+
*/
|
|
456
|
+
processZodObject(node) {
|
|
457
|
+
if (node.arguments.length === 0 ||
|
|
458
|
+
!t.isObjectExpression(node.arguments[0])) {
|
|
459
|
+
return { type: "object" };
|
|
460
|
+
}
|
|
461
|
+
const objectExpression = node.arguments[0];
|
|
462
|
+
const properties = {};
|
|
463
|
+
const required = [];
|
|
464
|
+
objectExpression.properties.forEach((prop) => {
|
|
465
|
+
if (t.isObjectProperty(prop)) {
|
|
466
|
+
let propName;
|
|
467
|
+
// Handle both identifier and string literal keys
|
|
468
|
+
if (t.isIdentifier(prop.key)) {
|
|
469
|
+
propName = prop.key.name;
|
|
470
|
+
}
|
|
471
|
+
else if (t.isStringLiteral(prop.key)) {
|
|
472
|
+
propName = prop.key.value;
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
return; // Skip if key is not identifier or string literal
|
|
476
|
+
}
|
|
477
|
+
// Process property value (a Zod schema)
|
|
478
|
+
const propSchema = this.processZodNode(prop.value);
|
|
479
|
+
if (propSchema) {
|
|
480
|
+
properties[propName] = propSchema;
|
|
481
|
+
// If the property is not marked as optional, add it to required list
|
|
482
|
+
// @ts-ignore
|
|
483
|
+
if (!this.isOptional(prop.value)) {
|
|
484
|
+
required.push(propName);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
const schema = {
|
|
490
|
+
type: "object",
|
|
491
|
+
properties,
|
|
492
|
+
};
|
|
493
|
+
if (required.length > 0) {
|
|
494
|
+
// @ts-ignore
|
|
495
|
+
schema.required = required;
|
|
496
|
+
}
|
|
497
|
+
return schema;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Process a Zod primitive schema: z.string(), z.number(), etc.
|
|
501
|
+
*/
|
|
502
|
+
processZodPrimitive(node) {
|
|
503
|
+
if (!t.isMemberExpression(node.callee) ||
|
|
504
|
+
!t.isIdentifier(node.callee.property)) {
|
|
505
|
+
return { type: "string" };
|
|
506
|
+
}
|
|
507
|
+
const zodType = node.callee.property.name;
|
|
508
|
+
let schema = {};
|
|
509
|
+
// Basic type mapping
|
|
510
|
+
switch (zodType) {
|
|
511
|
+
case "string":
|
|
512
|
+
schema = { type: "string" };
|
|
513
|
+
break;
|
|
514
|
+
case "number":
|
|
515
|
+
schema = { type: "number" };
|
|
516
|
+
break;
|
|
517
|
+
case "boolean":
|
|
518
|
+
schema = { type: "boolean" };
|
|
519
|
+
break;
|
|
520
|
+
case "date":
|
|
521
|
+
schema = { type: "string", format: "date-time" };
|
|
522
|
+
break;
|
|
523
|
+
case "bigint":
|
|
524
|
+
schema = { type: "integer", format: "int64" };
|
|
525
|
+
break;
|
|
526
|
+
case "any":
|
|
527
|
+
case "unknown":
|
|
528
|
+
schema = {}; // Empty schema matches anything
|
|
529
|
+
break;
|
|
530
|
+
case "null":
|
|
531
|
+
case "undefined":
|
|
532
|
+
schema = { type: "null" };
|
|
533
|
+
break;
|
|
534
|
+
case "array":
|
|
535
|
+
let itemsType = { type: "string" };
|
|
536
|
+
if (node.arguments.length > 0) {
|
|
537
|
+
itemsType = this.processZodNode(node.arguments[0]);
|
|
538
|
+
}
|
|
539
|
+
schema = { type: "array", items: itemsType };
|
|
540
|
+
break;
|
|
541
|
+
case "enum":
|
|
542
|
+
if (node.arguments.length > 0 &&
|
|
543
|
+
t.isArrayExpression(node.arguments[0])) {
|
|
544
|
+
const enumValues = node.arguments[0].elements
|
|
545
|
+
.filter((el) => t.isStringLiteral(el) || t.isNumericLiteral(el))
|
|
546
|
+
// @ts-ignore
|
|
547
|
+
.map((el) => el.value);
|
|
548
|
+
const firstValue = enumValues[0];
|
|
549
|
+
const valueType = typeof firstValue;
|
|
550
|
+
schema = {
|
|
551
|
+
type: valueType === "number" ? "number" : "string",
|
|
552
|
+
enum: enumValues,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
else if (node.arguments.length > 0 &&
|
|
556
|
+
t.isObjectExpression(node.arguments[0])) {
|
|
557
|
+
// Handle z.enum({ KEY1: "value1", KEY2: "value2" })
|
|
558
|
+
const enumValues = [];
|
|
559
|
+
node.arguments[0].properties.forEach((prop) => {
|
|
560
|
+
if (t.isObjectProperty(prop) && t.isStringLiteral(prop.value)) {
|
|
561
|
+
enumValues.push(prop.value.value);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
if (enumValues.length > 0) {
|
|
565
|
+
schema = {
|
|
566
|
+
type: "string",
|
|
567
|
+
enum: enumValues,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
schema = { type: "string" };
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
schema = { type: "string" };
|
|
576
|
+
}
|
|
577
|
+
break;
|
|
578
|
+
case "record":
|
|
579
|
+
let valueType = { type: "string" };
|
|
580
|
+
if (node.arguments.length > 0) {
|
|
581
|
+
valueType = this.processZodNode(node.arguments[0]);
|
|
582
|
+
}
|
|
583
|
+
schema = {
|
|
584
|
+
type: "object",
|
|
585
|
+
additionalProperties: valueType,
|
|
586
|
+
};
|
|
587
|
+
break;
|
|
588
|
+
case "map":
|
|
589
|
+
schema = {
|
|
590
|
+
type: "object",
|
|
591
|
+
additionalProperties: true,
|
|
592
|
+
};
|
|
593
|
+
break;
|
|
594
|
+
case "set":
|
|
595
|
+
let setItemType = { type: "string" };
|
|
596
|
+
if (node.arguments.length > 0) {
|
|
597
|
+
setItemType = this.processZodNode(node.arguments[0]);
|
|
598
|
+
}
|
|
599
|
+
schema = {
|
|
600
|
+
type: "array",
|
|
601
|
+
items: setItemType,
|
|
602
|
+
uniqueItems: true,
|
|
603
|
+
};
|
|
604
|
+
break;
|
|
605
|
+
case "object":
|
|
606
|
+
if (node.arguments.length > 0) {
|
|
607
|
+
schema = this.processZodObject(node);
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
schema = { type: "object" };
|
|
611
|
+
}
|
|
612
|
+
break;
|
|
613
|
+
default:
|
|
614
|
+
schema = { type: "string" };
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
// Extract description if it exists from direct method calls
|
|
618
|
+
const description = this.extractDescriptionFromArguments(node);
|
|
619
|
+
if (description) {
|
|
620
|
+
schema.description = description;
|
|
621
|
+
}
|
|
622
|
+
return schema;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Extract description from method arguments if it's a .describe() call
|
|
626
|
+
*/
|
|
627
|
+
extractDescriptionFromArguments(node) {
|
|
628
|
+
if (t.isMemberExpression(node.callee) &&
|
|
629
|
+
t.isIdentifier(node.callee.property) &&
|
|
630
|
+
node.callee.property.name === "describe" &&
|
|
631
|
+
node.arguments.length > 0 &&
|
|
632
|
+
t.isStringLiteral(node.arguments[0])) {
|
|
633
|
+
return node.arguments[0].value;
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Process a Zod chained method call: z.string().email().min(5)
|
|
639
|
+
*/
|
|
640
|
+
processZodChain(node) {
|
|
641
|
+
if (!t.isMemberExpression(node.callee) ||
|
|
642
|
+
!t.isIdentifier(node.callee.property)) {
|
|
643
|
+
return { type: "object" };
|
|
644
|
+
}
|
|
645
|
+
const methodName = node.callee.property.name;
|
|
646
|
+
// Process the parent chain first
|
|
647
|
+
let schema = this.processZodNode(node.callee.object);
|
|
648
|
+
// Apply the current method
|
|
649
|
+
switch (methodName) {
|
|
650
|
+
case "optional":
|
|
651
|
+
schema.nullable = true;
|
|
652
|
+
break;
|
|
653
|
+
case "nullable":
|
|
654
|
+
schema.nullable = true;
|
|
655
|
+
break;
|
|
656
|
+
case "nullish": // Handles both null and undefined
|
|
657
|
+
schema.nullable = true;
|
|
658
|
+
break;
|
|
659
|
+
case "describe":
|
|
660
|
+
if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
|
|
661
|
+
schema.description = node.arguments[0].value;
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
case "min":
|
|
665
|
+
if (node.arguments.length > 0 &&
|
|
666
|
+
t.isNumericLiteral(node.arguments[0])) {
|
|
667
|
+
if (schema.type === "string") {
|
|
668
|
+
schema.minLength = node.arguments[0].value;
|
|
669
|
+
}
|
|
670
|
+
else if (schema.type === "number" || schema.type === "integer") {
|
|
671
|
+
schema.minimum = node.arguments[0].value;
|
|
672
|
+
}
|
|
673
|
+
else if (schema.type === "array") {
|
|
674
|
+
schema.minItems = node.arguments[0].value;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
break;
|
|
678
|
+
case "max":
|
|
679
|
+
if (node.arguments.length > 0 &&
|
|
680
|
+
t.isNumericLiteral(node.arguments[0])) {
|
|
681
|
+
if (schema.type === "string") {
|
|
682
|
+
schema.maxLength = node.arguments[0].value;
|
|
683
|
+
}
|
|
684
|
+
else if (schema.type === "number" || schema.type === "integer") {
|
|
685
|
+
schema.maximum = node.arguments[0].value;
|
|
686
|
+
}
|
|
687
|
+
else if (schema.type === "array") {
|
|
688
|
+
schema.maxItems = node.arguments[0].value;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
break;
|
|
692
|
+
case "length":
|
|
693
|
+
if (node.arguments.length > 0 &&
|
|
694
|
+
t.isNumericLiteral(node.arguments[0])) {
|
|
695
|
+
if (schema.type === "string") {
|
|
696
|
+
schema.minLength = node.arguments[0].value;
|
|
697
|
+
schema.maxLength = node.arguments[0].value;
|
|
698
|
+
}
|
|
699
|
+
else if (schema.type === "array") {
|
|
700
|
+
schema.minItems = node.arguments[0].value;
|
|
701
|
+
schema.maxItems = node.arguments[0].value;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
break;
|
|
705
|
+
case "email":
|
|
706
|
+
schema.format = "email";
|
|
707
|
+
break;
|
|
708
|
+
case "url":
|
|
709
|
+
schema.format = "uri";
|
|
710
|
+
break;
|
|
711
|
+
case "uri":
|
|
712
|
+
schema.format = "uri";
|
|
713
|
+
break;
|
|
714
|
+
case "uuid":
|
|
715
|
+
schema.format = "uuid";
|
|
716
|
+
break;
|
|
717
|
+
case "cuid":
|
|
718
|
+
schema.format = "cuid";
|
|
719
|
+
break;
|
|
720
|
+
case "regex":
|
|
721
|
+
if (node.arguments.length > 0 && t.isRegExpLiteral(node.arguments[0])) {
|
|
722
|
+
schema.pattern = node.arguments[0].pattern;
|
|
723
|
+
}
|
|
724
|
+
break;
|
|
725
|
+
case "startsWith":
|
|
726
|
+
if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
|
|
727
|
+
schema.pattern = `^${this.escapeRegExp(node.arguments[0].value)}`;
|
|
728
|
+
}
|
|
729
|
+
break;
|
|
730
|
+
case "endsWith":
|
|
731
|
+
if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
|
|
732
|
+
schema.pattern = `${this.escapeRegExp(node.arguments[0].value)}`;
|
|
733
|
+
}
|
|
734
|
+
case "includes":
|
|
735
|
+
if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
|
|
736
|
+
schema.pattern = this.escapeRegExp(node.arguments[0].value);
|
|
737
|
+
}
|
|
738
|
+
break;
|
|
739
|
+
case "int":
|
|
740
|
+
schema.type = "integer";
|
|
741
|
+
break;
|
|
742
|
+
case "positive":
|
|
743
|
+
schema.minimum = 0;
|
|
744
|
+
schema.exclusiveMinimum = true;
|
|
745
|
+
break;
|
|
746
|
+
case "nonnegative":
|
|
747
|
+
schema.minimum = 0;
|
|
748
|
+
break;
|
|
749
|
+
case "negative":
|
|
750
|
+
schema.maximum = 0;
|
|
751
|
+
schema.exclusiveMaximum = true;
|
|
752
|
+
break;
|
|
753
|
+
case "nonpositive":
|
|
754
|
+
schema.maximum = 0;
|
|
755
|
+
break;
|
|
756
|
+
case "finite":
|
|
757
|
+
// Can't express directly in OpenAPI
|
|
758
|
+
break;
|
|
759
|
+
case "safe":
|
|
760
|
+
// Number is within the IEEE-754 "safe integer" range
|
|
761
|
+
schema.minimum = -9007199254740991; // -(2^53 - 1)
|
|
762
|
+
schema.maximum = 9007199254740991; // 2^53 - 1
|
|
763
|
+
break;
|
|
764
|
+
case "default":
|
|
765
|
+
if (node.arguments.length > 0) {
|
|
766
|
+
if (t.isStringLiteral(node.arguments[0])) {
|
|
767
|
+
schema.default = node.arguments[0].value;
|
|
768
|
+
}
|
|
769
|
+
else if (t.isNumericLiteral(node.arguments[0])) {
|
|
770
|
+
schema.default = node.arguments[0].value;
|
|
771
|
+
}
|
|
772
|
+
else if (t.isBooleanLiteral(node.arguments[0])) {
|
|
773
|
+
schema.default = node.arguments[0].value;
|
|
774
|
+
}
|
|
775
|
+
else if (t.isNullLiteral(node.arguments[0])) {
|
|
776
|
+
schema.default = null;
|
|
777
|
+
}
|
|
778
|
+
else if (t.isObjectExpression(node.arguments[0])) {
|
|
779
|
+
// Try to create a default object, but this might not be complete
|
|
780
|
+
const defaultObj = {};
|
|
781
|
+
node.arguments[0].properties.forEach((prop) => {
|
|
782
|
+
if (t.isObjectProperty(prop) &&
|
|
783
|
+
(t.isIdentifier(prop.key) || t.isStringLiteral(prop.key)) &&
|
|
784
|
+
(t.isStringLiteral(prop.value) ||
|
|
785
|
+
t.isNumericLiteral(prop.value) ||
|
|
786
|
+
t.isBooleanLiteral(prop.value))) {
|
|
787
|
+
const key = t.isIdentifier(prop.key)
|
|
788
|
+
? prop.key.name
|
|
789
|
+
: prop.key.value;
|
|
790
|
+
const value = t.isStringLiteral(prop.value)
|
|
791
|
+
? prop.value.value
|
|
792
|
+
: t.isNumericLiteral(prop.value)
|
|
793
|
+
? prop.value.value
|
|
794
|
+
: t.isBooleanLiteral(prop.value)
|
|
795
|
+
? prop.value.value
|
|
796
|
+
: null;
|
|
797
|
+
if (key !== null && value !== null) {
|
|
798
|
+
defaultObj[key] = value;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
schema.default = defaultObj;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
break;
|
|
806
|
+
case "extend":
|
|
807
|
+
if (node.arguments.length > 0 &&
|
|
808
|
+
t.isObjectExpression(node.arguments[0])) {
|
|
809
|
+
const extendedProps = this.processZodObject({
|
|
810
|
+
type: "CallExpression",
|
|
811
|
+
callee: {
|
|
812
|
+
type: "MemberExpression",
|
|
813
|
+
object: { type: "Identifier", name: "z" },
|
|
814
|
+
property: { type: "Identifier", name: "object" },
|
|
815
|
+
computed: false,
|
|
816
|
+
optional: false,
|
|
817
|
+
},
|
|
818
|
+
arguments: [node.arguments[0]],
|
|
819
|
+
});
|
|
820
|
+
if (extendedProps && extendedProps.properties) {
|
|
821
|
+
schema.properties = {
|
|
822
|
+
...schema.properties,
|
|
823
|
+
...extendedProps.properties,
|
|
824
|
+
};
|
|
825
|
+
if (extendedProps.required) {
|
|
826
|
+
schema.required = [
|
|
827
|
+
...(schema.required || []),
|
|
828
|
+
...extendedProps.required,
|
|
829
|
+
];
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
break;
|
|
834
|
+
case "refine":
|
|
835
|
+
case "superRefine":
|
|
836
|
+
// These are custom validators that cannot be easily represented in OpenAPI
|
|
837
|
+
// We preserve the schema as is
|
|
838
|
+
break;
|
|
839
|
+
case "transform":
|
|
840
|
+
// Transform doesn't change the schema validation, only the output format
|
|
841
|
+
break;
|
|
842
|
+
case "or":
|
|
843
|
+
if (node.arguments.length > 0) {
|
|
844
|
+
const alternativeSchema = this.processZodNode(node.arguments[0]);
|
|
845
|
+
if (alternativeSchema) {
|
|
846
|
+
schema = {
|
|
847
|
+
oneOf: [schema, alternativeSchema],
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
break;
|
|
852
|
+
case "and":
|
|
853
|
+
if (node.arguments.length > 0) {
|
|
854
|
+
const additionalSchema = this.processZodNode(node.arguments[0]);
|
|
855
|
+
if (additionalSchema) {
|
|
856
|
+
schema = {
|
|
857
|
+
allOf: [schema, additionalSchema],
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
return schema;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Helper to escape special regex characters for pattern creation
|
|
867
|
+
*/
|
|
868
|
+
escapeRegExp(string) {
|
|
869
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Check if a Zod schema is optional
|
|
873
|
+
*/
|
|
874
|
+
isOptional(node) {
|
|
875
|
+
// Direct .optional() call
|
|
876
|
+
if (t.isCallExpression(node) &&
|
|
877
|
+
t.isMemberExpression(node.callee) &&
|
|
878
|
+
t.isIdentifier(node.callee.property) &&
|
|
879
|
+
node.callee.property.name === "optional") {
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
// Check for chained calls that end with .optional()
|
|
883
|
+
if (t.isCallExpression(node) &&
|
|
884
|
+
t.isMemberExpression(node.callee) &&
|
|
885
|
+
t.isCallExpression(node.callee.object)) {
|
|
886
|
+
return this.hasOptionalMethod(node);
|
|
887
|
+
}
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Check if a node has .optional() in its method chain
|
|
892
|
+
*/
|
|
893
|
+
hasOptionalMethod(node) {
|
|
894
|
+
if (!t.isCallExpression(node)) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
if (t.isMemberExpression(node.callee) &&
|
|
898
|
+
t.isIdentifier(node.callee.property) &&
|
|
899
|
+
(node.callee.property.name === "optional" ||
|
|
900
|
+
node.callee.property.name === "nullable" ||
|
|
901
|
+
node.callee.property.name === "nullish")) {
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
if (t.isMemberExpression(node.callee) &&
|
|
905
|
+
t.isCallExpression(node.callee.object)) {
|
|
906
|
+
return this.hasOptionalMethod(node.callee.object);
|
|
907
|
+
}
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Get all processed Zod schemas
|
|
912
|
+
*/
|
|
913
|
+
getProcessedSchemas() {
|
|
914
|
+
return this.zodSchemas;
|
|
915
|
+
}
|
|
916
|
+
}
|