zod-codegen 1.0.2 → 1.1.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/CHANGELOG.md +13 -0
- package/CONTRIBUTING.md +1 -1
- package/EXAMPLES.md +704 -0
- package/PERFORMANCE.md +59 -0
- package/README.md +270 -58
- package/dist/src/cli.js +25 -5
- package/dist/src/services/code-generator.service.js +211 -26
- package/dist/src/types/openapi.js +1 -1
- package/dist/tests/unit/code-generator.test.js +219 -0
- package/dist/tests/unit/file-reader.test.js +110 -0
- package/dist/tests/unit/generator.test.js +77 -7
- package/eslint.config.mjs +1 -0
- package/examples/.gitkeep +3 -0
- package/examples/README.md +74 -0
- package/examples/petstore/README.md +121 -0
- package/examples/petstore/authenticated-usage.ts +60 -0
- package/examples/petstore/basic-usage.ts +51 -0
- package/examples/petstore/server-variables-usage.ts +63 -0
- package/examples/petstore/type.ts +217 -0
- package/examples/pokeapi/README.md +105 -0
- package/examples/pokeapi/basic-usage.ts +57 -0
- package/examples/pokeapi/custom-client.ts +56 -0
- package/examples/pokeapi/type.ts +109 -0
- package/package.json +4 -2
- package/samples/pokeapi-openapi.json +212 -0
- package/samples/server-variables-example.yaml +49 -0
- package/src/cli.ts +30 -5
- package/src/services/code-generator.service.ts +641 -57
- package/src/types/openapi.ts +1 -1
- package/tests/unit/code-generator.test.ts +243 -0
- package/tests/unit/file-reader.test.ts +134 -0
- package/tests/unit/generator.test.ts +99 -7
- package/tsconfig.examples.json +17 -0
- package/tsconfig.json +1 -1
|
@@ -40,14 +40,14 @@ export class TypeScriptCodeGeneratorService {
|
|
|
40
40
|
buildAST(openapi) {
|
|
41
41
|
const imports = this.importBuilder.buildImports();
|
|
42
42
|
const schemas = this.buildSchemas(openapi);
|
|
43
|
+
const serverConfig = this.buildServerConfiguration(openapi);
|
|
43
44
|
const clientClass = this.buildClientClass(openapi, schemas);
|
|
44
|
-
const baseUrlConstant = this.buildBaseUrlConstant(openapi);
|
|
45
45
|
return [
|
|
46
46
|
this.createComment('Imports'),
|
|
47
47
|
...imports,
|
|
48
48
|
this.createComment('Components schemas'),
|
|
49
49
|
...Object.values(schemas),
|
|
50
|
-
...
|
|
50
|
+
...serverConfig,
|
|
51
51
|
this.createComment('Client class'),
|
|
52
52
|
clientClass,
|
|
53
53
|
];
|
|
@@ -73,24 +73,54 @@ export class TypeScriptCodeGeneratorService {
|
|
|
73
73
|
const methods = this.buildClientMethods(openapi, schemas);
|
|
74
74
|
return ts.factory.createClassDeclaration([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(clientName), undefined, undefined, [
|
|
75
75
|
this.typeBuilder.createProperty('#baseUrl', 'string', true),
|
|
76
|
-
this.buildConstructor(),
|
|
76
|
+
this.buildConstructor(openapi),
|
|
77
|
+
this.buildGetBaseRequestOptionsMethod(),
|
|
77
78
|
this.buildHttpRequestMethod(),
|
|
78
79
|
...methods,
|
|
79
80
|
]);
|
|
80
81
|
}
|
|
81
|
-
buildConstructor() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
buildConstructor(openapi) {
|
|
83
|
+
const hasServers = openapi.servers && openapi.servers.length > 0;
|
|
84
|
+
if (hasServers) {
|
|
85
|
+
// Options-based constructor
|
|
86
|
+
return ts.factory.createConstructorDeclaration(undefined, [
|
|
87
|
+
ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createIdentifier('options'), undefined, ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('ClientOptions'), undefined), undefined),
|
|
88
|
+
], ts.factory.createBlock([
|
|
89
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
90
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('resolvedUrl'), undefined, undefined, ts.factory.createConditionalExpression(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('baseUrl')), ts.factory.createToken(ts.SyntaxKind.ExclamationEqualsEqualsToken), ts.factory.createNull()), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('baseUrl')), ts.factory.createToken(ts.SyntaxKind.ColonToken), ts.factory.createCallExpression(ts.factory.createIdentifier('resolveServerUrl'), undefined, [
|
|
91
|
+
ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('serverIndex')),
|
|
92
|
+
ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('serverVariables')),
|
|
93
|
+
]))),
|
|
94
|
+
], ts.NodeFlags.Const)),
|
|
95
|
+
ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createPrivateIdentifier('#baseUrl')), ts.factory.createToken(ts.SyntaxKind.EqualsToken), ts.factory.createIdentifier('resolvedUrl'))),
|
|
96
|
+
], true));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Fallback: simple baseUrl parameter
|
|
100
|
+
return ts.factory.createConstructorDeclaration(undefined, [
|
|
101
|
+
this.typeBuilder.createParameter('baseUrl', 'string', ts.factory.createStringLiteral('/', true)),
|
|
102
|
+
this.typeBuilder.createParameter('_', 'unknown', undefined, true),
|
|
103
|
+
], ts.factory.createBlock([
|
|
104
|
+
ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createPrivateIdentifier('#baseUrl')), ts.factory.createToken(ts.SyntaxKind.EqualsToken), ts.factory.createIdentifier('baseUrl'))),
|
|
105
|
+
], true));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
buildGetBaseRequestOptionsMethod() {
|
|
109
|
+
return ts.factory.createMethodDeclaration([ts.factory.createToken(ts.SyntaxKind.ProtectedKeyword)], undefined, ts.factory.createIdentifier('getBaseRequestOptions'), undefined, undefined, [], ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Partial'), [
|
|
110
|
+
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Omit'), [
|
|
111
|
+
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('RequestInit'), undefined),
|
|
112
|
+
ts.factory.createUnionTypeNode([
|
|
113
|
+
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral('method', true)),
|
|
114
|
+
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral('body', true)),
|
|
115
|
+
]),
|
|
116
|
+
]),
|
|
117
|
+
]), ts.factory.createBlock([ts.factory.createReturnStatement(ts.factory.createObjectLiteralExpression([], false))], true));
|
|
88
118
|
}
|
|
89
119
|
buildHttpRequestMethod() {
|
|
90
120
|
return ts.factory.createMethodDeclaration([ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)], undefined, ts.factory.createPrivateIdentifier('#makeRequest'), undefined, [this.typeBuilder.createGenericType('T')], [
|
|
91
121
|
this.typeBuilder.createParameter('method', 'string'),
|
|
92
122
|
this.typeBuilder.createParameter('path', 'string'),
|
|
93
|
-
this.typeBuilder.createParameter('options', '{params?: Record<string, string | number | boolean>; data?: unknown; contentType?: string}', ts.factory.createObjectLiteralExpression([], false), false),
|
|
123
|
+
this.typeBuilder.createParameter('options', '{params?: Record<string, string | number | boolean>; data?: unknown; contentType?: string; headers?: Record<string, string>}', ts.factory.createObjectLiteralExpression([], false), false),
|
|
94
124
|
], ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Promise'), [
|
|
95
125
|
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('T'), undefined),
|
|
96
126
|
]), ts.factory.createBlock([
|
|
@@ -132,11 +162,27 @@ export class TypeScriptCodeGeneratorService {
|
|
|
132
162
|
ts.factory.createIdentifier('baseUrl'),
|
|
133
163
|
]), ts.factory.createIdentifier('toString')), undefined, []))),
|
|
134
164
|
], ts.NodeFlags.Const)),
|
|
135
|
-
//
|
|
165
|
+
// Get base request options (headers, signal, credentials, etc.)
|
|
136
166
|
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
137
|
-
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('
|
|
138
|
-
|
|
139
|
-
|
|
167
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('baseOptions'), undefined, undefined, ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createIdentifier('getBaseRequestOptions')), undefined, [])),
|
|
168
|
+
], ts.NodeFlags.Const)),
|
|
169
|
+
// Build Content-Type header
|
|
170
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
171
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('contentType'), undefined, undefined, ts.factory.createConditionalExpression(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('contentType')), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createStringLiteral('application/x-www-form-urlencoded', true)), undefined, ts.factory.createStringLiteral('application/x-www-form-urlencoded', true), undefined, ts.factory.createStringLiteral('application/json', true))),
|
|
172
|
+
], ts.NodeFlags.Const)),
|
|
173
|
+
// Merge headers: base headers, Content-Type, and request-specific headers
|
|
174
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
175
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('baseHeaders'), undefined, undefined, ts.factory.createConditionalExpression(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('baseOptions'), ts.factory.createIdentifier('headers')), ts.factory.createToken(ts.SyntaxKind.ExclamationEqualsEqualsToken), ts.factory.createIdentifier('undefined')), undefined, ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('baseOptions'), ts.factory.createIdentifier('headers')), undefined, ts.factory.createObjectLiteralExpression([], false))),
|
|
176
|
+
], ts.NodeFlags.Const)),
|
|
177
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
178
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('headers'), undefined, undefined, ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Object'), ts.factory.createIdentifier('assign')), undefined, [
|
|
179
|
+
ts.factory.createObjectLiteralExpression([], false),
|
|
180
|
+
ts.factory.createIdentifier('baseHeaders'),
|
|
181
|
+
ts.factory.createObjectLiteralExpression([
|
|
182
|
+
ts.factory.createPropertyAssignment(ts.factory.createStringLiteral('Content-Type', true), ts.factory.createIdentifier('contentType')),
|
|
183
|
+
], false),
|
|
184
|
+
ts.factory.createConditionalExpression(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('headers')), ts.factory.createToken(ts.SyntaxKind.ExclamationEqualsEqualsToken), ts.factory.createIdentifier('undefined')), undefined, ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('headers')), undefined, ts.factory.createObjectLiteralExpression([], false)),
|
|
185
|
+
])),
|
|
140
186
|
], ts.NodeFlags.Const)),
|
|
141
187
|
// Build body with form-urlencoded support
|
|
142
188
|
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
@@ -168,15 +214,19 @@ export class TypeScriptCodeGeneratorService {
|
|
|
168
214
|
ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('data')),
|
|
169
215
|
])), undefined, ts.factory.createNull())),
|
|
170
216
|
], ts.NodeFlags.Const)),
|
|
171
|
-
// Make fetch request
|
|
217
|
+
// Make fetch request: merge base options with method, headers, and body
|
|
172
218
|
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
173
219
|
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('response'), undefined, undefined, ts.factory.createAwaitExpression(ts.factory.createCallExpression(ts.factory.createIdentifier('fetch'), undefined, [
|
|
174
220
|
ts.factory.createIdentifier('url'),
|
|
175
|
-
ts.factory.
|
|
176
|
-
ts.factory.
|
|
177
|
-
ts.factory.
|
|
178
|
-
ts.factory.
|
|
179
|
-
|
|
221
|
+
ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Object'), ts.factory.createIdentifier('assign')), undefined, [
|
|
222
|
+
ts.factory.createObjectLiteralExpression([], false),
|
|
223
|
+
ts.factory.createIdentifier('baseOptions'),
|
|
224
|
+
ts.factory.createObjectLiteralExpression([
|
|
225
|
+
ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier('method'), undefined),
|
|
226
|
+
ts.factory.createPropertyAssignment(ts.factory.createIdentifier('headers'), ts.factory.createIdentifier('headers')),
|
|
227
|
+
ts.factory.createPropertyAssignment(ts.factory.createIdentifier('body'), ts.factory.createIdentifier('body')),
|
|
228
|
+
], false),
|
|
229
|
+
]),
|
|
180
230
|
]))),
|
|
181
231
|
], ts.NodeFlags.Const)),
|
|
182
232
|
// Check response status
|
|
@@ -462,16 +512,151 @@ export class TypeScriptCodeGeneratorService {
|
|
|
462
512
|
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(typeName), undefined),
|
|
463
513
|
]);
|
|
464
514
|
}
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
if (!
|
|
515
|
+
buildServerConfiguration(openapi) {
|
|
516
|
+
const servers = openapi.servers;
|
|
517
|
+
if (!servers || servers.length === 0) {
|
|
468
518
|
return [];
|
|
469
519
|
}
|
|
470
|
-
|
|
520
|
+
const statements = [];
|
|
521
|
+
// Build server configuration array
|
|
522
|
+
const serverConfigElements = servers.map((server) => {
|
|
523
|
+
const properties = [
|
|
524
|
+
ts.factory.createPropertyAssignment('url', ts.factory.createStringLiteral(server.url, true)),
|
|
525
|
+
];
|
|
526
|
+
if (server.description) {
|
|
527
|
+
properties.push(ts.factory.createPropertyAssignment('description', ts.factory.createStringLiteral(server.description, true)));
|
|
528
|
+
}
|
|
529
|
+
if (server.variables && Object.keys(server.variables).length > 0) {
|
|
530
|
+
const variableProperties = Object.entries(server.variables).map(([varName, varDef]) => {
|
|
531
|
+
const varProps = [
|
|
532
|
+
ts.factory.createPropertyAssignment('default', ts.factory.createStringLiteral(varDef.default, true)),
|
|
533
|
+
];
|
|
534
|
+
if (varDef.enum && varDef.enum.length > 0) {
|
|
535
|
+
varProps.push(ts.factory.createPropertyAssignment('enum', ts.factory.createArrayLiteralExpression(varDef.enum.map((val) => ts.factory.createStringLiteral(val, true)), false)));
|
|
536
|
+
}
|
|
537
|
+
if (varDef.description) {
|
|
538
|
+
varProps.push(ts.factory.createPropertyAssignment('description', ts.factory.createStringLiteral(varDef.description, true)));
|
|
539
|
+
}
|
|
540
|
+
return ts.factory.createPropertyAssignment(varName, ts.factory.createObjectLiteralExpression(varProps, true));
|
|
541
|
+
});
|
|
542
|
+
properties.push(ts.factory.createPropertyAssignment('variables', ts.factory.createObjectLiteralExpression(variableProperties, true)));
|
|
543
|
+
}
|
|
544
|
+
return ts.factory.createObjectLiteralExpression(properties, true);
|
|
545
|
+
});
|
|
546
|
+
// Export server configuration
|
|
547
|
+
statements.push(ts.factory.createVariableStatement([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
|
|
548
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('serverConfigurations'), undefined, undefined, ts.factory.createArrayLiteralExpression(serverConfigElements, false)),
|
|
549
|
+
], ts.NodeFlags.Const)));
|
|
550
|
+
// Export default base URL (first server with default variables)
|
|
551
|
+
const firstServer = servers[0];
|
|
552
|
+
const defaultBaseUrl = firstServer ? this.resolveServerUrl(firstServer, {}) : '/';
|
|
553
|
+
statements.push(ts.factory.createVariableStatement([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
|
|
554
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('defaultBaseUrl'), undefined, undefined, ts.factory.createStringLiteral(defaultBaseUrl, true)),
|
|
555
|
+
], ts.NodeFlags.Const)));
|
|
556
|
+
// Build ClientOptions type
|
|
557
|
+
const optionProperties = [
|
|
558
|
+
ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier('baseUrl'), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)),
|
|
559
|
+
ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier('serverIndex'), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
|
|
560
|
+
ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier('serverVariables'), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Record'), [
|
|
561
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
|
562
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
|
563
|
+
])),
|
|
564
|
+
];
|
|
565
|
+
statements.push(ts.factory.createTypeAliasDeclaration([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier('ClientOptions'), undefined, ts.factory.createTypeLiteralNode(optionProperties)));
|
|
566
|
+
// Build resolveServerUrl helper function
|
|
567
|
+
statements.push(this.buildResolveServerUrlFunction(servers));
|
|
568
|
+
return statements;
|
|
569
|
+
}
|
|
570
|
+
resolveServerUrl(server, variables) {
|
|
571
|
+
let url = server.url;
|
|
572
|
+
if (server.variables) {
|
|
573
|
+
for (const [varName, varDef] of Object.entries(server.variables)) {
|
|
574
|
+
const value = variables[varName] ?? varDef.default;
|
|
575
|
+
url = url.replace(`{${varName}}`, value);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return url;
|
|
579
|
+
}
|
|
580
|
+
buildResolveServerUrlFunction(servers) {
|
|
581
|
+
// Build server configs array inline
|
|
582
|
+
const serverConfigElements = servers.map((server) => {
|
|
583
|
+
const properties = [
|
|
584
|
+
ts.factory.createPropertyAssignment('url', ts.factory.createStringLiteral(server.url, true)),
|
|
585
|
+
];
|
|
586
|
+
if (server.variables && Object.keys(server.variables).length > 0) {
|
|
587
|
+
const variableProperties = Object.entries(server.variables).map(([varName, varDef]) => {
|
|
588
|
+
const varProps = [
|
|
589
|
+
ts.factory.createPropertyAssignment('default', ts.factory.createStringLiteral(varDef.default, true)),
|
|
590
|
+
];
|
|
591
|
+
if (varDef.enum && varDef.enum.length > 0) {
|
|
592
|
+
varProps.push(ts.factory.createPropertyAssignment('enum', ts.factory.createArrayLiteralExpression(varDef.enum.map((val) => ts.factory.createStringLiteral(val, true)), false)));
|
|
593
|
+
}
|
|
594
|
+
return ts.factory.createPropertyAssignment(varName, ts.factory.createObjectLiteralExpression(varProps, true));
|
|
595
|
+
});
|
|
596
|
+
properties.push(ts.factory.createPropertyAssignment('variables', ts.factory.createObjectLiteralExpression(variableProperties, true)));
|
|
597
|
+
}
|
|
598
|
+
return ts.factory.createObjectLiteralExpression(properties, true);
|
|
599
|
+
});
|
|
600
|
+
// Build function body - simplified version
|
|
601
|
+
const idx = ts.factory.createIdentifier('idx');
|
|
602
|
+
const configs = ts.factory.createIdentifier('configs');
|
|
603
|
+
const config = ts.factory.createIdentifier('config');
|
|
604
|
+
const url = ts.factory.createIdentifier('url');
|
|
605
|
+
const key = ts.factory.createIdentifier('key');
|
|
606
|
+
const value = ts.factory.createIdentifier('value');
|
|
607
|
+
const bodyStatements = [
|
|
608
|
+
// const configs = [...]
|
|
471
609
|
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
472
|
-
ts.factory.createVariableDeclaration(
|
|
610
|
+
ts.factory.createVariableDeclaration(configs, undefined, undefined, ts.factory.createArrayLiteralExpression(serverConfigElements, false)),
|
|
473
611
|
], ts.NodeFlags.Const)),
|
|
612
|
+
// const idx = serverIndex ?? 0
|
|
613
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
614
|
+
ts.factory.createVariableDeclaration(idx, undefined, undefined, ts.factory.createBinaryExpression(ts.factory.createIdentifier('serverIndex'), ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken), ts.factory.createNumericLiteral('0'))),
|
|
615
|
+
], ts.NodeFlags.Const)),
|
|
616
|
+
// if (idx < configs.length) { ... }
|
|
617
|
+
ts.factory.createIfStatement(ts.factory.createBinaryExpression(idx, ts.factory.createToken(ts.SyntaxKind.LessThanToken), ts.factory.createPropertyAccessExpression(configs, ts.factory.createIdentifier('length'))), ts.factory.createBlock([
|
|
618
|
+
// const config = configs[idx]
|
|
619
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
620
|
+
ts.factory.createVariableDeclaration(config, undefined, undefined, ts.factory.createElementAccessExpression(configs, idx)),
|
|
621
|
+
], ts.NodeFlags.Const)),
|
|
622
|
+
// let url = config.url
|
|
623
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
624
|
+
ts.factory.createVariableDeclaration(url, undefined, undefined, ts.factory.createPropertyAccessExpression(config, ts.factory.createIdentifier('url'))),
|
|
625
|
+
], ts.NodeFlags.Let)),
|
|
626
|
+
// if (config.variables && serverVariables) { ... }
|
|
627
|
+
ts.factory.createIfStatement(ts.factory.createLogicalAnd(ts.factory.createPropertyAccessExpression(config, ts.factory.createIdentifier('variables')), ts.factory.createIdentifier('serverVariables')), ts.factory.createBlock([
|
|
628
|
+
// for (const [key, value] of Object.entries(serverVariables)) { url = url.replace(...) }
|
|
629
|
+
ts.factory.createForOfStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
630
|
+
ts.factory.createVariableDeclaration(ts.factory.createArrayBindingPattern([
|
|
631
|
+
ts.factory.createBindingElement(undefined, undefined, key),
|
|
632
|
+
ts.factory.createBindingElement(undefined, undefined, value),
|
|
633
|
+
]), undefined, undefined),
|
|
634
|
+
], ts.NodeFlags.Const), ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Object'), ts.factory.createIdentifier('entries')), undefined, [ts.factory.createIdentifier('serverVariables')]), ts.factory.createBlock([
|
|
635
|
+
ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(url, ts.factory.createToken(ts.SyntaxKind.EqualsToken), ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(url, ts.factory.createIdentifier('replace')), undefined, [
|
|
636
|
+
ts.factory.createNewExpression(ts.factory.createIdentifier('RegExp'), undefined, [
|
|
637
|
+
ts.factory.createBinaryExpression(ts.factory.createBinaryExpression(ts.factory.createStringLiteral('\\{'), ts.factory.createToken(ts.SyntaxKind.PlusToken), key), ts.factory.createToken(ts.SyntaxKind.PlusToken), ts.factory.createStringLiteral('\\}')),
|
|
638
|
+
ts.factory.createStringLiteral('g'),
|
|
639
|
+
]),
|
|
640
|
+
value,
|
|
641
|
+
]))),
|
|
642
|
+
], true)),
|
|
643
|
+
], true)),
|
|
644
|
+
// return url
|
|
645
|
+
ts.factory.createReturnStatement(url),
|
|
646
|
+
], true)),
|
|
647
|
+
// return default (first server with defaults)
|
|
648
|
+
ts.factory.createReturnStatement(ts.factory.createStringLiteral(servers[0] ? this.resolveServerUrl(servers[0], {}) : '/', true)),
|
|
474
649
|
];
|
|
650
|
+
return ts.factory.createFunctionDeclaration(undefined, undefined, ts.factory.createIdentifier('resolveServerUrl'), undefined, [
|
|
651
|
+
ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createIdentifier('serverIndex'), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createUnionTypeNode([
|
|
652
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
|
|
653
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
|
|
654
|
+
]), undefined),
|
|
655
|
+
ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createIdentifier('serverVariables'), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Record'), [
|
|
656
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
|
657
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
|
658
|
+
]), ts.factory.createObjectLiteralExpression([], false)),
|
|
659
|
+
], ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), ts.factory.createBlock(bodyStatements, true));
|
|
475
660
|
}
|
|
476
661
|
generateClientName(title) {
|
|
477
662
|
return title
|
|
@@ -54,7 +54,7 @@ const ServerVariable = z.object({
|
|
|
54
54
|
enum: z.array(z.string()).optional(),
|
|
55
55
|
});
|
|
56
56
|
const Server = z.object({
|
|
57
|
-
url: z.
|
|
57
|
+
url: z.string(), // Allow templated URLs with {variables}
|
|
58
58
|
description: z.string().optional(),
|
|
59
59
|
variables: z.record(z.string(), ServerVariable).optional(),
|
|
60
60
|
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { TypeScriptCodeGeneratorService } from '../../src/services/code-generator.service.js';
|
|
3
|
+
describe('TypeScriptCodeGeneratorService', () => {
|
|
4
|
+
let generator;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
generator = new TypeScriptCodeGeneratorService();
|
|
7
|
+
});
|
|
8
|
+
describe('generate', () => {
|
|
9
|
+
it('should generate code for a minimal OpenAPI spec', () => {
|
|
10
|
+
const spec = {
|
|
11
|
+
openapi: '3.0.0',
|
|
12
|
+
info: {
|
|
13
|
+
title: 'Test API',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
},
|
|
16
|
+
paths: {},
|
|
17
|
+
};
|
|
18
|
+
const code = generator.generate(spec);
|
|
19
|
+
expect(code).toBeTruthy();
|
|
20
|
+
expect(typeof code).toBe('string');
|
|
21
|
+
expect(code).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
|
|
22
|
+
expect(code).toContain('export class');
|
|
23
|
+
});
|
|
24
|
+
it('should generate schemas for component schemas', () => {
|
|
25
|
+
const spec = {
|
|
26
|
+
openapi: '3.0.0',
|
|
27
|
+
info: {
|
|
28
|
+
title: 'Test API',
|
|
29
|
+
version: '1.0.0',
|
|
30
|
+
},
|
|
31
|
+
paths: {},
|
|
32
|
+
components: {
|
|
33
|
+
schemas: {
|
|
34
|
+
User: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
id: { type: 'integer' },
|
|
38
|
+
name: { type: 'string' },
|
|
39
|
+
},
|
|
40
|
+
required: ['id', 'name'],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const code = generator.generate(spec);
|
|
46
|
+
expect(code).toContain('export const User');
|
|
47
|
+
expect(code).toContain('z.object');
|
|
48
|
+
expect(code).toContain('z.number().int()');
|
|
49
|
+
expect(code).toContain('z.string()');
|
|
50
|
+
});
|
|
51
|
+
it('should generate client methods for paths', () => {
|
|
52
|
+
const spec = {
|
|
53
|
+
openapi: '3.0.0',
|
|
54
|
+
info: {
|
|
55
|
+
title: 'Test API',
|
|
56
|
+
version: '1.0.0',
|
|
57
|
+
},
|
|
58
|
+
paths: {
|
|
59
|
+
'/users': {
|
|
60
|
+
get: {
|
|
61
|
+
operationId: 'getUsers',
|
|
62
|
+
responses: {
|
|
63
|
+
'200': {
|
|
64
|
+
description: 'Success',
|
|
65
|
+
content: {
|
|
66
|
+
'application/json': {
|
|
67
|
+
schema: {
|
|
68
|
+
type: 'array',
|
|
69
|
+
items: { type: 'string' },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const code = generator.generate(spec);
|
|
80
|
+
expect(code).toContain('async getUsers');
|
|
81
|
+
expect(code).toContain('#makeRequest');
|
|
82
|
+
});
|
|
83
|
+
it('should generate getBaseRequestOptions method', () => {
|
|
84
|
+
const spec = {
|
|
85
|
+
openapi: '3.0.0',
|
|
86
|
+
info: {
|
|
87
|
+
title: 'Test API',
|
|
88
|
+
version: '1.0.0',
|
|
89
|
+
},
|
|
90
|
+
paths: {},
|
|
91
|
+
};
|
|
92
|
+
const code = generator.generate(spec);
|
|
93
|
+
expect(code).toContain('protected getBaseRequestOptions');
|
|
94
|
+
expect(code).toContain('Partial<Omit<RequestInit');
|
|
95
|
+
});
|
|
96
|
+
it('should handle servers configuration', () => {
|
|
97
|
+
const spec = {
|
|
98
|
+
openapi: '3.0.0',
|
|
99
|
+
info: {
|
|
100
|
+
title: 'Test API',
|
|
101
|
+
version: '1.0.0',
|
|
102
|
+
},
|
|
103
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
104
|
+
paths: {},
|
|
105
|
+
};
|
|
106
|
+
const code = generator.generate(spec);
|
|
107
|
+
expect(code).toContain('defaultBaseUrl');
|
|
108
|
+
expect(code).toContain('https://api.example.com');
|
|
109
|
+
});
|
|
110
|
+
it('should handle complex schemas with references', () => {
|
|
111
|
+
const spec = {
|
|
112
|
+
openapi: '3.0.0',
|
|
113
|
+
info: {
|
|
114
|
+
title: 'Test API',
|
|
115
|
+
version: '1.0.0',
|
|
116
|
+
},
|
|
117
|
+
paths: {},
|
|
118
|
+
components: {
|
|
119
|
+
schemas: {
|
|
120
|
+
User: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
id: { type: 'integer' },
|
|
124
|
+
profile: { $ref: '#/components/schemas/Profile' },
|
|
125
|
+
},
|
|
126
|
+
required: ['id'],
|
|
127
|
+
},
|
|
128
|
+
Profile: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
name: { type: 'string' },
|
|
132
|
+
email: { type: 'string', format: 'email' },
|
|
133
|
+
},
|
|
134
|
+
required: ['name', 'email'],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const code = generator.generate(spec);
|
|
140
|
+
expect(code).toContain('export const User');
|
|
141
|
+
expect(code).toContain('export const Profile');
|
|
142
|
+
// Profile should be defined before User (topological sort)
|
|
143
|
+
const profileIndex = code.indexOf('Profile');
|
|
144
|
+
const userIndex = code.indexOf('User');
|
|
145
|
+
expect(profileIndex).toBeLessThan(userIndex);
|
|
146
|
+
});
|
|
147
|
+
it('should handle enum types', () => {
|
|
148
|
+
const spec = {
|
|
149
|
+
openapi: '3.0.0',
|
|
150
|
+
info: {
|
|
151
|
+
title: 'Test API',
|
|
152
|
+
version: '1.0.0',
|
|
153
|
+
},
|
|
154
|
+
paths: {},
|
|
155
|
+
components: {
|
|
156
|
+
schemas: {
|
|
157
|
+
Status: {
|
|
158
|
+
type: 'string',
|
|
159
|
+
enum: ['active', 'inactive', 'pending'],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
const code = generator.generate(spec);
|
|
165
|
+
expect(code).toContain('z.enum');
|
|
166
|
+
expect(code).toContain('active');
|
|
167
|
+
expect(code).toContain('inactive');
|
|
168
|
+
expect(code).toContain('pending');
|
|
169
|
+
});
|
|
170
|
+
it('should handle array types', () => {
|
|
171
|
+
const spec = {
|
|
172
|
+
openapi: '3.0.0',
|
|
173
|
+
info: {
|
|
174
|
+
title: 'Test API',
|
|
175
|
+
version: '1.0.0',
|
|
176
|
+
},
|
|
177
|
+
paths: {},
|
|
178
|
+
components: {
|
|
179
|
+
schemas: {
|
|
180
|
+
Tags: {
|
|
181
|
+
type: 'array',
|
|
182
|
+
items: { type: 'string' },
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
const code = generator.generate(spec);
|
|
188
|
+
expect(code).toContain('z.array');
|
|
189
|
+
expect(code).toContain('z.string()');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('buildSchema', () => {
|
|
193
|
+
it('should build schema for string type', () => {
|
|
194
|
+
const schema = { type: 'string' };
|
|
195
|
+
const result = generator.buildSchema(schema, true);
|
|
196
|
+
expect(result).toBeDefined();
|
|
197
|
+
});
|
|
198
|
+
it('should build schema for number type', () => {
|
|
199
|
+
const schema = { type: 'number' };
|
|
200
|
+
const result = generator.buildSchema(schema, true);
|
|
201
|
+
expect(result).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
it('should build schema for boolean type', () => {
|
|
204
|
+
const schema = { type: 'boolean' };
|
|
205
|
+
const result = generator.buildSchema(schema, true);
|
|
206
|
+
expect(result).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
it('should handle optional fields', () => {
|
|
209
|
+
const schema = { type: 'string' };
|
|
210
|
+
const result = generator.buildSchema(schema, false);
|
|
211
|
+
expect(result).toBeDefined();
|
|
212
|
+
});
|
|
213
|
+
it('should handle string formats', () => {
|
|
214
|
+
const schema = { type: 'string', format: 'email' };
|
|
215
|
+
const result = generator.buildSchema(schema, true);
|
|
216
|
+
expect(result).toBeDefined();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { SyncFileReaderService, OpenApiFileParserService } from '../../src/services/file-reader.service.js';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
describe('SyncFileReaderService', () => {
|
|
8
|
+
let reader;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
reader = new SyncFileReaderService();
|
|
11
|
+
});
|
|
12
|
+
describe('readFile', () => {
|
|
13
|
+
it('should read local JSON files', async () => {
|
|
14
|
+
const content = await reader.readFile(join(__dirname, '../../samples/openapi.json'));
|
|
15
|
+
expect(content).toBeTruthy();
|
|
16
|
+
expect(typeof content).toBe('string');
|
|
17
|
+
});
|
|
18
|
+
it('should read local YAML files', async () => {
|
|
19
|
+
const content = await reader.readFile(join(__dirname, '../../samples/swagger-petstore.yaml'));
|
|
20
|
+
expect(content).toBeTruthy();
|
|
21
|
+
expect(typeof content).toBe('string');
|
|
22
|
+
expect(content).toContain('openapi:');
|
|
23
|
+
});
|
|
24
|
+
it('should handle non-existent files', async () => {
|
|
25
|
+
await expect(reader.readFile('./non-existent-file.json')).rejects.toThrow();
|
|
26
|
+
});
|
|
27
|
+
it('should handle URLs', async () => {
|
|
28
|
+
// Mock fetch for URL test
|
|
29
|
+
const originalFetch = global.fetch;
|
|
30
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
31
|
+
ok: true,
|
|
32
|
+
text: async () => '{"openapi": "3.0.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": {}}',
|
|
33
|
+
});
|
|
34
|
+
const content = await reader.readFile('https://example.com/openapi.json');
|
|
35
|
+
expect(content).toBeTruthy();
|
|
36
|
+
global.fetch = originalFetch;
|
|
37
|
+
});
|
|
38
|
+
it('should handle URL fetch errors with non-ok responses', async () => {
|
|
39
|
+
const originalFetch = global.fetch;
|
|
40
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
41
|
+
ok: false,
|
|
42
|
+
status: 404,
|
|
43
|
+
statusText: 'Not Found',
|
|
44
|
+
});
|
|
45
|
+
global.fetch = mockFetch;
|
|
46
|
+
await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow();
|
|
47
|
+
// Verify fetch was called
|
|
48
|
+
expect(mockFetch).toHaveBeenCalledWith('https://example.com/not-found.json');
|
|
49
|
+
global.fetch = originalFetch;
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('OpenApiFileParserService', () => {
|
|
54
|
+
let parser;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
parser = new OpenApiFileParserService();
|
|
57
|
+
});
|
|
58
|
+
describe('parse', () => {
|
|
59
|
+
it('should parse valid JSON OpenAPI spec', () => {
|
|
60
|
+
const jsonSpec = JSON.stringify({
|
|
61
|
+
openapi: '3.0.0',
|
|
62
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
63
|
+
paths: {},
|
|
64
|
+
});
|
|
65
|
+
const result = parser.parse(jsonSpec);
|
|
66
|
+
expect(result).toBeDefined();
|
|
67
|
+
expect(result.openapi).toBe('3.0.0');
|
|
68
|
+
expect(result.info.title).toBe('Test API');
|
|
69
|
+
});
|
|
70
|
+
it('should parse valid YAML OpenAPI spec', () => {
|
|
71
|
+
const yamlSpec = `
|
|
72
|
+
openapi: 3.0.0
|
|
73
|
+
info:
|
|
74
|
+
title: Test API
|
|
75
|
+
version: 1.0.0
|
|
76
|
+
paths: {}
|
|
77
|
+
`;
|
|
78
|
+
const result = parser.parse(yamlSpec);
|
|
79
|
+
expect(result).toBeDefined();
|
|
80
|
+
expect(result.openapi).toBe('3.0.0');
|
|
81
|
+
expect(result.info.title).toBe('Test API');
|
|
82
|
+
});
|
|
83
|
+
it('should parse already parsed objects', () => {
|
|
84
|
+
const spec = {
|
|
85
|
+
openapi: '3.0.0',
|
|
86
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
87
|
+
paths: {},
|
|
88
|
+
};
|
|
89
|
+
const result = parser.parse(spec);
|
|
90
|
+
expect(result).toBeDefined();
|
|
91
|
+
expect(result.openapi).toBe('3.0.0');
|
|
92
|
+
});
|
|
93
|
+
it('should validate OpenAPI structure', () => {
|
|
94
|
+
const invalidSpec = {
|
|
95
|
+
openapi: '2.0.0', // Wrong version format
|
|
96
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
97
|
+
paths: {},
|
|
98
|
+
};
|
|
99
|
+
expect(() => parser.parse(invalidSpec)).toThrow();
|
|
100
|
+
});
|
|
101
|
+
it('should handle missing required fields', () => {
|
|
102
|
+
const invalidSpec = {
|
|
103
|
+
openapi: '3.0.0',
|
|
104
|
+
// Missing info
|
|
105
|
+
paths: {},
|
|
106
|
+
};
|
|
107
|
+
expect(() => parser.parse(invalidSpec)).toThrow();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|