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.
@@ -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
+ }