next-openapi-gen 0.7.7 → 0.7.9
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 +47 -0
- package/dist/lib/route-processor.js +26 -4
- package/dist/lib/schema-processor.js +309 -3
- package/dist/lib/utils.js +13 -9
- package/dist/lib/zod-converter.js +31 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -554,6 +554,53 @@ export async function GET() {
|
|
|
554
554
|
|
|
555
555
|
If no type/schema is provided for path parameters, a default schema will be generated.
|
|
556
556
|
|
|
557
|
+
### TypeScript Generics Support
|
|
558
|
+
|
|
559
|
+
The library supports TypeScript generic types and automatically resolves them during documentation generation:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// src/app/api/llms/route.ts
|
|
563
|
+
|
|
564
|
+
import { NextResponse } from "next/server";
|
|
565
|
+
|
|
566
|
+
// Define generic response wrapper
|
|
567
|
+
type MyApiSuccessResponseBody<T> = T & {
|
|
568
|
+
success: true;
|
|
569
|
+
httpCode: string;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// Define specific response data
|
|
573
|
+
type LLMSResponse = {
|
|
574
|
+
llms: Array<{
|
|
575
|
+
id: string;
|
|
576
|
+
name: string;
|
|
577
|
+
provider: string;
|
|
578
|
+
isDefault: boolean;
|
|
579
|
+
}>;
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Get list of available LLMs
|
|
584
|
+
* @description Get list of available LLMs with success wrapper
|
|
585
|
+
* @response 200:MyApiSuccessResponseBody<LLMSResponse>
|
|
586
|
+
* @openapi
|
|
587
|
+
*/
|
|
588
|
+
export async function GET() {
|
|
589
|
+
return NextResponse.json({
|
|
590
|
+
success: true,
|
|
591
|
+
httpCode: "200",
|
|
592
|
+
llms: [
|
|
593
|
+
{
|
|
594
|
+
id: "gpt-5",
|
|
595
|
+
name: "GPT-5",
|
|
596
|
+
provider: "OpenAI",
|
|
597
|
+
isDefault: true,
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
557
604
|
### Intelligent Examples
|
|
558
605
|
|
|
559
606
|
The library generates intelligent examples for parameters based on their name:
|
|
@@ -23,14 +23,15 @@ export class RouteProcessor {
|
|
|
23
23
|
// 1. Add success response
|
|
24
24
|
const successCode = dataTypes.successCode || this.getDefaultSuccessCode(method);
|
|
25
25
|
if (dataTypes.responseType) {
|
|
26
|
-
|
|
26
|
+
// Ensure the schema is defined in components/schemas
|
|
27
|
+
this.schemaProcessor.getSchemaContent({
|
|
27
28
|
responseType: dataTypes.responseType,
|
|
28
|
-
})
|
|
29
|
+
});
|
|
29
30
|
responses[successCode] = {
|
|
30
31
|
description: dataTypes.responseDescription || "Successful response",
|
|
31
32
|
content: {
|
|
32
33
|
"application/json": {
|
|
33
|
-
schema:
|
|
34
|
+
schema: { $ref: `#/components/schemas/${dataTypes.responseType}` },
|
|
34
35
|
},
|
|
35
36
|
},
|
|
36
37
|
};
|
|
@@ -243,7 +244,28 @@ export class RouteProcessor {
|
|
|
243
244
|
}
|
|
244
245
|
// Add request body
|
|
245
246
|
if (MUTATION_HTTP_METHODS.includes(method.toUpperCase())) {
|
|
246
|
-
|
|
247
|
+
if (dataTypes.bodyType) {
|
|
248
|
+
// Ensure the schema is defined in components/schemas
|
|
249
|
+
this.schemaProcessor.getSchemaContent({
|
|
250
|
+
bodyType: dataTypes.bodyType,
|
|
251
|
+
});
|
|
252
|
+
// Use reference to the schema
|
|
253
|
+
const contentType = this.schemaProcessor.detectContentType(dataTypes.bodyType || "", dataTypes.contentType);
|
|
254
|
+
definition.requestBody = {
|
|
255
|
+
content: {
|
|
256
|
+
[contentType]: {
|
|
257
|
+
schema: { $ref: `#/components/schemas/${dataTypes.bodyType}` },
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
if (bodyDescription) {
|
|
262
|
+
definition.requestBody.description = bodyDescription;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else if (body && Object.keys(body).length > 0) {
|
|
266
|
+
// Fallback to inline schema for backward compatibility
|
|
267
|
+
definition.requestBody = this.schemaProcessor.createRequestBodySchema(body, bodyDescription, dataTypes.contentType);
|
|
268
|
+
}
|
|
247
269
|
}
|
|
248
270
|
// Add responses
|
|
249
271
|
definition.responses = this.buildResponsesFromConfig(dataTypes, method);
|
|
@@ -28,20 +28,31 @@ export class SchemaProcessor {
|
|
|
28
28
|
* Get all defined schemas (for components.schemas section)
|
|
29
29
|
*/
|
|
30
30
|
getDefinedSchemas() {
|
|
31
|
+
// Filter out generic type parameters and invalid schema names
|
|
32
|
+
const filteredSchemas = {};
|
|
33
|
+
Object.entries(this.openapiDefinitions).forEach(([key, value]) => {
|
|
34
|
+
if (!this.isGenericTypeParameter(key) && !this.isInvalidSchemaName(key)) {
|
|
35
|
+
filteredSchemas[key] = value;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
31
38
|
// If using Zod, also include all processed Zod schemas
|
|
32
39
|
if (this.schemaType === "zod" && this.zodSchemaConverter) {
|
|
33
40
|
const zodSchemas = this.zodSchemaConverter.getProcessedSchemas();
|
|
34
41
|
return {
|
|
35
|
-
...
|
|
42
|
+
...filteredSchemas,
|
|
36
43
|
...zodSchemas,
|
|
37
44
|
};
|
|
38
45
|
}
|
|
39
|
-
return
|
|
46
|
+
return filteredSchemas;
|
|
40
47
|
}
|
|
41
48
|
findSchemaDefinition(schemaName, contentType) {
|
|
42
49
|
let schemaNode = null;
|
|
43
50
|
// Assign type that is actually processed
|
|
44
51
|
this.contentType = contentType;
|
|
52
|
+
// Check if the schemaName is a generic type (contains < and >)
|
|
53
|
+
if (schemaName.includes("<") && schemaName.includes(">")) {
|
|
54
|
+
return this.resolveGenericTypeFromString(schemaName);
|
|
55
|
+
}
|
|
45
56
|
// Check if we should use Zod schemas
|
|
46
57
|
if (this.schemaType === "zod") {
|
|
47
58
|
logger.debug(`Looking for Zod schema: ${schemaName}`);
|
|
@@ -95,7 +106,14 @@ export class SchemaProcessor {
|
|
|
95
106
|
TSTypeAliasDeclaration: (path) => {
|
|
96
107
|
if (t.isIdentifier(path.node.id, { name: schemaName })) {
|
|
97
108
|
const name = path.node.id.name;
|
|
98
|
-
|
|
109
|
+
// Store the full node for generic types, just the type annotation for regular types
|
|
110
|
+
if (path.node.typeParameters &&
|
|
111
|
+
path.node.typeParameters.params.length > 0) {
|
|
112
|
+
this.typeDefinitions[name] = path.node; // Store the full declaration for generic types
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.typeDefinitions[name] = path.node.typeAnnotation; // Store just the type annotation for regular types
|
|
116
|
+
}
|
|
99
117
|
}
|
|
100
118
|
},
|
|
101
119
|
TSInterfaceDeclaration: (path) => {
|
|
@@ -150,6 +168,13 @@ export class SchemaProcessor {
|
|
|
150
168
|
const typeNode = this.typeDefinitions[typeName.toString()];
|
|
151
169
|
if (!typeNode)
|
|
152
170
|
return {};
|
|
171
|
+
// Handle generic type alias declarations (full node)
|
|
172
|
+
if (t.isTSTypeAliasDeclaration(typeNode)) {
|
|
173
|
+
// This is a generic type, should be handled by the caller via resolveGenericType
|
|
174
|
+
// For non-generic access, just return the type annotation
|
|
175
|
+
const typeAnnotation = typeNode.typeAnnotation;
|
|
176
|
+
return this.resolveTSNodeType(typeAnnotation);
|
|
177
|
+
}
|
|
153
178
|
// Check if node is Zod
|
|
154
179
|
if (t.isCallExpression(typeNode) &&
|
|
155
180
|
t.isMemberExpression(typeNode.callee) &&
|
|
@@ -350,6 +375,16 @@ export class SchemaProcessor {
|
|
|
350
375
|
return this.resolveTSNodeType(node.typeParameters.params[0]);
|
|
351
376
|
}
|
|
352
377
|
}
|
|
378
|
+
// Handle custom generic types
|
|
379
|
+
if (node.typeParameters && node.typeParameters.params.length > 0) {
|
|
380
|
+
// Find the generic type definition first
|
|
381
|
+
this.findSchemaDefinition(typeName, this.contentType);
|
|
382
|
+
const genericTypeDefinition = this.typeDefinitions[typeName];
|
|
383
|
+
if (genericTypeDefinition) {
|
|
384
|
+
// Resolve the generic type by substituting type parameters
|
|
385
|
+
return this.resolveGenericType(genericTypeDefinition, node.typeParameters.params, typeName);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
353
388
|
// Check if it is a type that we are already processing
|
|
354
389
|
if (this.processingTypes.has(typeName)) {
|
|
355
390
|
return { $ref: `#/components/schemas/${typeName}` };
|
|
@@ -768,4 +803,275 @@ export class SchemaProcessor {
|
|
|
768
803
|
responses,
|
|
769
804
|
};
|
|
770
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* Parse and resolve a generic type from a string like "MyApiSuccessResponseBody<LLMSResponse>"
|
|
808
|
+
* @param genericTypeString - The generic type string to parse and resolve
|
|
809
|
+
* @returns The resolved OpenAPI schema
|
|
810
|
+
*/
|
|
811
|
+
resolveGenericTypeFromString(genericTypeString) {
|
|
812
|
+
// Parse the generic type string
|
|
813
|
+
const parsed = this.parseGenericTypeString(genericTypeString);
|
|
814
|
+
if (!parsed) {
|
|
815
|
+
return {};
|
|
816
|
+
}
|
|
817
|
+
const { baseTypeName, typeArguments } = parsed;
|
|
818
|
+
// Find the base generic type definition
|
|
819
|
+
this.scanSchemaDir(this.schemaDir, baseTypeName);
|
|
820
|
+
const genericTypeDefinition = this.typeDefinitions[baseTypeName];
|
|
821
|
+
if (!genericTypeDefinition) {
|
|
822
|
+
logger.debug(`Generic type definition not found for: ${baseTypeName}`);
|
|
823
|
+
return {};
|
|
824
|
+
}
|
|
825
|
+
// Also find all the type argument definitions
|
|
826
|
+
typeArguments.forEach((argTypeName) => {
|
|
827
|
+
// If it's a simple type reference (not another generic), find its definition
|
|
828
|
+
if (!argTypeName.includes("<") &&
|
|
829
|
+
!this.isGenericTypeParameter(argTypeName)) {
|
|
830
|
+
this.scanSchemaDir(this.schemaDir, argTypeName);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
// Create AST nodes for the type arguments by parsing them
|
|
834
|
+
const typeArgumentNodes = typeArguments.map((arg) => this.createTypeNodeFromString(arg));
|
|
835
|
+
// Resolve the generic type
|
|
836
|
+
const resolved = this.resolveGenericType(genericTypeDefinition, typeArgumentNodes, baseTypeName);
|
|
837
|
+
// Cache the resolved type for future reference
|
|
838
|
+
this.openapiDefinitions[genericTypeString] = resolved;
|
|
839
|
+
return resolved;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Check if a type name is likely a generic type parameter (e.g., T, U, K, V)
|
|
843
|
+
* @param {string} typeName - The type name to check
|
|
844
|
+
* @returns {boolean} - True if it's likely a generic type parameter
|
|
845
|
+
*/
|
|
846
|
+
isGenericTypeParameter(typeName) {
|
|
847
|
+
// Common generic type parameter patterns:
|
|
848
|
+
// - Single uppercase letters (T, U, K, V, etc.)
|
|
849
|
+
// - TKey, TValue, etc.
|
|
850
|
+
return /^[A-Z]$|^T[A-Z][a-zA-Z]*$/.test(typeName);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Check if a schema name is invalid (contains special characters, brackets, etc.)
|
|
854
|
+
* @param {string} schemaName - The schema name to check
|
|
855
|
+
* @returns {boolean} - True if the schema name is invalid
|
|
856
|
+
*/
|
|
857
|
+
isInvalidSchemaName(schemaName) {
|
|
858
|
+
// Schema names should not contain { } : ? spaces or other special characters
|
|
859
|
+
return /[{}\s:?]/.test(schemaName);
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Parse a generic type string into base type and arguments
|
|
863
|
+
* @param genericTypeString - The string like "MyApiSuccessResponseBody<LLMSResponse>"
|
|
864
|
+
* @returns Object with baseTypeName and typeArguments array
|
|
865
|
+
*/
|
|
866
|
+
parseGenericTypeString(genericTypeString) {
|
|
867
|
+
const match = genericTypeString.match(/^([^<]+)<(.+)>$/);
|
|
868
|
+
if (!match) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
const baseTypeName = match[1].trim();
|
|
872
|
+
const typeArgsString = match[2].trim();
|
|
873
|
+
// Split type arguments by comma, handling nested generics
|
|
874
|
+
const typeArguments = this.splitTypeArguments(typeArgsString);
|
|
875
|
+
return { baseTypeName, typeArguments };
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Split type arguments by comma, handling nested generics correctly
|
|
879
|
+
* @param typeArgsString - The string inside angle brackets
|
|
880
|
+
* @returns Array of individual type argument strings
|
|
881
|
+
*/
|
|
882
|
+
splitTypeArguments(typeArgsString) {
|
|
883
|
+
const args = [];
|
|
884
|
+
let currentArg = "";
|
|
885
|
+
let bracketDepth = 0;
|
|
886
|
+
for (let i = 0; i < typeArgsString.length; i++) {
|
|
887
|
+
const char = typeArgsString[i];
|
|
888
|
+
if (char === "<") {
|
|
889
|
+
bracketDepth++;
|
|
890
|
+
}
|
|
891
|
+
else if (char === ">") {
|
|
892
|
+
bracketDepth--;
|
|
893
|
+
}
|
|
894
|
+
else if (char === "," && bracketDepth === 0) {
|
|
895
|
+
args.push(currentArg.trim());
|
|
896
|
+
currentArg = "";
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
currentArg += char;
|
|
900
|
+
}
|
|
901
|
+
if (currentArg.trim()) {
|
|
902
|
+
args.push(currentArg.trim());
|
|
903
|
+
}
|
|
904
|
+
return args;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Create a TypeScript AST node from a type string
|
|
908
|
+
* @param typeString - The type string like "LLMSResponse"
|
|
909
|
+
* @returns A TypeScript AST node
|
|
910
|
+
*/
|
|
911
|
+
createTypeNodeFromString(typeString) {
|
|
912
|
+
// For simple type references, create a TSTypeReference node
|
|
913
|
+
if (!typeString.includes("<")) {
|
|
914
|
+
return {
|
|
915
|
+
type: "TSTypeReference",
|
|
916
|
+
typeName: {
|
|
917
|
+
type: "Identifier",
|
|
918
|
+
name: typeString,
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
// For nested generics, recursively parse
|
|
923
|
+
const parsed = this.parseGenericTypeString(typeString);
|
|
924
|
+
if (parsed) {
|
|
925
|
+
const typeParameterNodes = parsed.typeArguments.map((arg) => this.createTypeNodeFromString(arg));
|
|
926
|
+
return {
|
|
927
|
+
type: "TSTypeReference",
|
|
928
|
+
typeName: {
|
|
929
|
+
type: "Identifier",
|
|
930
|
+
name: parsed.baseTypeName,
|
|
931
|
+
},
|
|
932
|
+
typeParameters: {
|
|
933
|
+
type: "TSTypeParameterInstantiation",
|
|
934
|
+
params: typeParameterNodes,
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
// Fallback for unknown patterns
|
|
939
|
+
return {
|
|
940
|
+
type: "TSTypeReference",
|
|
941
|
+
typeName: {
|
|
942
|
+
type: "Identifier",
|
|
943
|
+
name: typeString,
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Resolve generic types by substituting type parameters with actual types
|
|
949
|
+
* @param genericTypeDefinition - The AST node of the generic type definition
|
|
950
|
+
* @param typeArguments - The type arguments passed to the generic type
|
|
951
|
+
* @param typeName - The name of the generic type
|
|
952
|
+
* @returns The resolved OpenAPI schema
|
|
953
|
+
*/
|
|
954
|
+
resolveGenericType(genericTypeDefinition, typeArguments, typeName) {
|
|
955
|
+
// Extract type parameters from the generic type definition
|
|
956
|
+
let typeParameters = [];
|
|
957
|
+
if (t.isTSTypeAliasDeclaration(genericTypeDefinition)) {
|
|
958
|
+
if (genericTypeDefinition.typeParameters &&
|
|
959
|
+
genericTypeDefinition.typeParameters.params) {
|
|
960
|
+
typeParameters = genericTypeDefinition.typeParameters.params.map((param) => {
|
|
961
|
+
if (t.isTSTypeParameter(param)) {
|
|
962
|
+
return param.name;
|
|
963
|
+
}
|
|
964
|
+
return t.isIdentifier(param)
|
|
965
|
+
? param.name
|
|
966
|
+
: param.name?.name || param;
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
// Create a mapping from type parameters to actual types
|
|
970
|
+
const typeParameterMap = {};
|
|
971
|
+
typeParameters.forEach((param, index) => {
|
|
972
|
+
if (index < typeArguments.length) {
|
|
973
|
+
typeParameterMap[param] = typeArguments[index];
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
// Resolve the type annotation with substituted type parameters
|
|
977
|
+
return this.resolveTypeWithSubstitution(genericTypeDefinition.typeAnnotation, typeParameterMap);
|
|
978
|
+
}
|
|
979
|
+
// If we can't process the generic type, return empty object
|
|
980
|
+
return {};
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Resolve a type node with type parameter substitution
|
|
984
|
+
* @param node - The AST node to resolve
|
|
985
|
+
* @param typeParameterMap - Mapping from type parameter names to actual types
|
|
986
|
+
* @returns The resolved OpenAPI schema
|
|
987
|
+
*/
|
|
988
|
+
resolveTypeWithSubstitution(node, typeParameterMap) {
|
|
989
|
+
if (!node)
|
|
990
|
+
return { type: "object" };
|
|
991
|
+
// If this is a type parameter reference, substitute it
|
|
992
|
+
if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
|
|
993
|
+
const paramName = node.typeName.name;
|
|
994
|
+
if (typeParameterMap[paramName]) {
|
|
995
|
+
// The mapped value is an AST node, resolve it
|
|
996
|
+
const mappedNode = typeParameterMap[paramName];
|
|
997
|
+
if (t.isTSTypeReference(mappedNode) &&
|
|
998
|
+
t.isIdentifier(mappedNode.typeName)) {
|
|
999
|
+
// If it's a reference to another type, get the resolved schema from openapiDefinitions
|
|
1000
|
+
const referencedTypeName = mappedNode.typeName.name;
|
|
1001
|
+
if (this.openapiDefinitions[referencedTypeName]) {
|
|
1002
|
+
return this.openapiDefinitions[referencedTypeName];
|
|
1003
|
+
}
|
|
1004
|
+
// If not in openapiDefinitions, try to resolve it
|
|
1005
|
+
this.findSchemaDefinition(referencedTypeName, this.contentType);
|
|
1006
|
+
return this.openapiDefinitions[referencedTypeName] || {};
|
|
1007
|
+
}
|
|
1008
|
+
return this.resolveTSNodeType(typeParameterMap[paramName]);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
// Handle intersection types (e.g., T & { success: true })
|
|
1012
|
+
if (t.isTSIntersectionType(node)) {
|
|
1013
|
+
const allProperties = {};
|
|
1014
|
+
const requiredProperties = [];
|
|
1015
|
+
node.types.forEach((typeNode, index) => {
|
|
1016
|
+
let resolvedType;
|
|
1017
|
+
// Check if this is a type parameter reference
|
|
1018
|
+
if (t.isTSTypeReference(typeNode) &&
|
|
1019
|
+
t.isIdentifier(typeNode.typeName)) {
|
|
1020
|
+
const paramName = typeNode.typeName.name;
|
|
1021
|
+
if (typeParameterMap[paramName]) {
|
|
1022
|
+
const mappedNode = typeParameterMap[paramName];
|
|
1023
|
+
if (t.isTSTypeReference(mappedNode) &&
|
|
1024
|
+
t.isIdentifier(mappedNode.typeName)) {
|
|
1025
|
+
// If it's a reference to another type, get the resolved schema
|
|
1026
|
+
const referencedTypeName = mappedNode.typeName.name;
|
|
1027
|
+
if (this.openapiDefinitions[referencedTypeName]) {
|
|
1028
|
+
resolvedType = this.openapiDefinitions[referencedTypeName];
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
// If not in openapiDefinitions, try to resolve it
|
|
1032
|
+
this.findSchemaDefinition(referencedTypeName, this.contentType);
|
|
1033
|
+
resolvedType =
|
|
1034
|
+
this.openapiDefinitions[referencedTypeName] || {};
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
resolvedType = this.resolveTSNodeType(mappedNode);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
resolvedType = this.resolveTSNodeType(typeNode);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
resolvedType = this.resolveTypeWithSubstitution(typeNode, typeParameterMap);
|
|
1047
|
+
}
|
|
1048
|
+
if (resolvedType.type === "object" && resolvedType.properties) {
|
|
1049
|
+
Object.entries(resolvedType.properties).forEach(([key, value]) => {
|
|
1050
|
+
allProperties[key] = value;
|
|
1051
|
+
if (value.required) {
|
|
1052
|
+
requiredProperties.push(key);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
return {
|
|
1058
|
+
type: "object",
|
|
1059
|
+
properties: allProperties,
|
|
1060
|
+
required: requiredProperties.length > 0 ? requiredProperties : undefined,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
// For other types, use the standard resolution but with parameter substitution
|
|
1064
|
+
if (t.isTSTypeLiteral(node)) {
|
|
1065
|
+
const properties = {};
|
|
1066
|
+
node.members.forEach((member) => {
|
|
1067
|
+
if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
|
|
1068
|
+
const propName = member.key.name;
|
|
1069
|
+
properties[propName] = this.resolveTypeWithSubstitution(member.typeAnnotation?.typeAnnotation, typeParameterMap);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
return { type: "object", properties };
|
|
1073
|
+
}
|
|
1074
|
+
// Fallback to standard type resolution
|
|
1075
|
+
return this.resolveTSNodeType(node);
|
|
1076
|
+
}
|
|
771
1077
|
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -48,7 +48,11 @@ export function extractJSDocComments(path) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
if (!summary) {
|
|
51
|
-
|
|
51
|
+
const firstLine = commentValue.split("\n")[0];
|
|
52
|
+
// Don't use tags as summary - only use actual descriptions
|
|
53
|
+
if (!firstLine.trim().startsWith("@")) {
|
|
54
|
+
summary = firstLine;
|
|
55
|
+
}
|
|
52
56
|
}
|
|
53
57
|
if (commentValue.includes("@auth")) {
|
|
54
58
|
const regex = /@auth\s*(.*)/;
|
|
@@ -117,15 +121,12 @@ export function extractJSDocComments(path) {
|
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
if (commentValue.includes("@response")) {
|
|
120
|
-
|
|
124
|
+
// Updated regex to support generic types
|
|
125
|
+
const responseMatch = commentValue.match(/@response\s+(?:(\d+):)?([^@\n\r]+)(?:\s+(.*))?/);
|
|
121
126
|
if (responseMatch) {
|
|
122
|
-
const [, code, type
|
|
127
|
+
const [, code, type] = responseMatch;
|
|
123
128
|
successCode = code || "";
|
|
124
|
-
responseType = type;
|
|
125
|
-
// Set responseDescription only if not already set by @responseDescription
|
|
126
|
-
if (description?.trim() && !responseDescription) {
|
|
127
|
-
responseDescription = description.trim();
|
|
128
|
-
}
|
|
129
|
+
responseType = type?.trim();
|
|
129
130
|
}
|
|
130
131
|
else {
|
|
131
132
|
responseType = extractTypeFromComment(commentValue, "@response");
|
|
@@ -153,7 +154,10 @@ export function extractJSDocComments(path) {
|
|
|
153
154
|
};
|
|
154
155
|
}
|
|
155
156
|
export function extractTypeFromComment(commentValue, tag) {
|
|
156
|
-
|
|
157
|
+
// Updated regex to support generic types with angle brackets
|
|
158
|
+
return (commentValue
|
|
159
|
+
.match(new RegExp(`${tag}\\s*\\s*([\\w<>,\\s]+)`))?.[1]
|
|
160
|
+
?.trim() || "");
|
|
157
161
|
}
|
|
158
162
|
export function cleanComment(commentValue) {
|
|
159
163
|
return commentValue.replace(/\*\s*/g, "").trim();
|
|
@@ -855,22 +855,6 @@ export class ZodSchemaConverter {
|
|
|
855
855
|
!t.isIdentifier(node.callee.property)) {
|
|
856
856
|
return { type: "string" };
|
|
857
857
|
}
|
|
858
|
-
if (t.isMemberExpression(node.callee) &&
|
|
859
|
-
t.isIdentifier(node.callee.property)) {
|
|
860
|
-
const zodType = node.callee.property.name;
|
|
861
|
-
// Custom() support for FormData
|
|
862
|
-
if (zodType === "custom" && node.arguments.length > 0) {
|
|
863
|
-
// Check if it is FormData
|
|
864
|
-
if (t.isArrowFunctionExpression(node.arguments[0])) {
|
|
865
|
-
// Assume custom FormData validation
|
|
866
|
-
return {
|
|
867
|
-
type: "object",
|
|
868
|
-
additionalProperties: true,
|
|
869
|
-
description: "Form data object",
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
858
|
const zodType = node.callee.property.name;
|
|
875
859
|
let schema = {};
|
|
876
860
|
// Basic type mapping
|
|
@@ -990,6 +974,37 @@ export class ZodSchemaConverter {
|
|
|
990
974
|
schema = { type: "object" };
|
|
991
975
|
}
|
|
992
976
|
break;
|
|
977
|
+
case "custom":
|
|
978
|
+
// Check if it has TypeScript generic type parameters (z.custom<File>())
|
|
979
|
+
if (node.typeParameters && node.typeParameters.params.length > 0) {
|
|
980
|
+
const typeParam = node.typeParameters.params[0];
|
|
981
|
+
// Check if the generic type is File
|
|
982
|
+
if (t.isTSTypeReference(typeParam) &&
|
|
983
|
+
t.isIdentifier(typeParam.typeName) &&
|
|
984
|
+
typeParam.typeName.name === "File") {
|
|
985
|
+
schema = {
|
|
986
|
+
type: "string",
|
|
987
|
+
format: "binary",
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
// Other generic types default to string
|
|
992
|
+
schema = { type: "string" };
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
else if (node.arguments.length > 0 &&
|
|
996
|
+
t.isArrowFunctionExpression(node.arguments[0])) {
|
|
997
|
+
// Legacy support: FormData validation
|
|
998
|
+
schema = {
|
|
999
|
+
type: "object",
|
|
1000
|
+
additionalProperties: true,
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
else {
|
|
1004
|
+
// Default case for z.custom() without specific type detection
|
|
1005
|
+
schema = { type: "string" };
|
|
1006
|
+
}
|
|
1007
|
+
break;
|
|
993
1008
|
default:
|
|
994
1009
|
schema = { type: "string" };
|
|
995
1010
|
break;
|
package/package.json
CHANGED