zod-codegen 1.6.2 → 1.6.3
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 +4 -0
- package/dist/src/services/code-generator.service.d.ts +21 -0
- package/dist/src/services/code-generator.service.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.js +156 -4
- package/generated/type.ts +63 -18
- package/package.json +1 -1
- package/src/services/code-generator.service.ts +222 -11
- package/tests/unit/code-generator.test.ts +305 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## <small>1.6.3 (2026-01-22)</small>
|
|
2
|
+
|
|
3
|
+
- fix: do not depend on inference anymore and build strict types all ar… (#60) ([b0b663e](https://github.com/julienandreu/zod-codegen/commit/b0b663e)), closes [#60](https://github.com/julienandreu/zod-codegen/issues/60)
|
|
4
|
+
|
|
1
5
|
## <small>1.6.2 (2026-01-19)</small>
|
|
2
6
|
|
|
3
7
|
- fix: improve URL construction and filter undefined query params (#58) ([b9c3d32](https://github.com/julienandreu/zod-codegen/commit/b9c3d32)), closes [#58](https://github.com/julienandreu/zod-codegen/issues/58)
|
|
@@ -17,6 +17,27 @@ export declare class TypeScriptCodeGeneratorService implements CodeGenerator, Sc
|
|
|
17
17
|
private buildAST;
|
|
18
18
|
private buildSchemas;
|
|
19
19
|
private buildSchemaTypeAliases;
|
|
20
|
+
/**
|
|
21
|
+
* Builds explicit TypeScript type declarations for all schemas.
|
|
22
|
+
* Returns interface declarations for object types and type aliases for other types.
|
|
23
|
+
*/
|
|
24
|
+
private buildExplicitTypeDeclarations;
|
|
25
|
+
/**
|
|
26
|
+
* Converts an OpenAPI schema to a TypeScript type node.
|
|
27
|
+
*/
|
|
28
|
+
private buildTypeNode;
|
|
29
|
+
/**
|
|
30
|
+
* Builds the base type node without nullable handling.
|
|
31
|
+
*/
|
|
32
|
+
private buildBaseTypeNode;
|
|
33
|
+
/**
|
|
34
|
+
* Builds a TypeScript type literal for an object schema.
|
|
35
|
+
*/
|
|
36
|
+
private buildObjectTypeLiteral;
|
|
37
|
+
/**
|
|
38
|
+
* Builds a TypeScript interface declaration for an object schema.
|
|
39
|
+
*/
|
|
40
|
+
private buildInterfaceDeclaration;
|
|
20
41
|
private buildClientClass;
|
|
21
42
|
private buildConstructor;
|
|
22
43
|
private buildGetBaseRequestOptionsMethod;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"code-generator.service.d.ts","sourceRoot":"","sources":["../../../src/services/code-generator.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,KAAK,EAAC,aAAa,EAAE,aAAa,EAAC,MAAM,8BAA8B,CAAC;AAC/E,OAAO,KAAK,EAAmB,eAAe,EAAgB,MAAM,kBAAkB,CAAC;AACvF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,4BAA4B,CAAC;AAWjE,qBAAa,8BAA+B,YAAW,aAAa,EAAE,aAAa;IACjF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsC;IAClE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwC;IACtE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwD;IAChF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA+B;IAChE,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAuC;IAGhF,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,iBAAiB,CAAuB;gBAEpC,OAAO,GAAE,gBAAqB;IAK1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGpB;IAEH,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM;IAMvC,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,UAAO,GAAG,EAAE,CAAC,cAAc,GAAG,EAAE,CAAC,UAAU;IA2BhF,OAAO,CAAC,QAAQ;
|
|
1
|
+
{"version":3,"file":"code-generator.service.d.ts","sourceRoot":"","sources":["../../../src/services/code-generator.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,KAAK,EAAC,aAAa,EAAE,aAAa,EAAC,MAAM,8BAA8B,CAAC;AAC/E,OAAO,KAAK,EAAmB,eAAe,EAAgB,MAAM,kBAAkB,CAAC;AACvF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,4BAA4B,CAAC;AAWjE,qBAAa,8BAA+B,YAAW,aAAa,EAAE,aAAa;IACjF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsC;IAClE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwC;IACtE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwD;IAChF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA+B;IAChE,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAuC;IAGhF,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,iBAAiB,CAAuB;gBAEpC,OAAO,GAAE,gBAAqB;IAK1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGpB;IAEH,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM;IAMvC,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,UAAO,GAAG,EAAE,CAAC,cAAc,GAAG,EAAE,CAAC,UAAU;IA2BhF,OAAO,CAAC,QAAQ;IAuBhB,OAAO,CAAC,YAAY;IAmDpB,OAAO,CAAC,sBAAsB;IAK9B;;;OAGG;IACH,OAAO,CAAC,6BAA6B;IAkDrC;;OAEG;IACH,OAAO,CAAC,aAAa;IA2BrB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA2EzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAgB9B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAyBjC,OAAO,CAAC,gBAAgB;IAuBxB,OAAO,CAAC,gBAAgB;IAmGxB,OAAO,CAAC,gCAAgC;IAwBxC,OAAO,CAAC,yBAAyB;IAuBjC,OAAO,CAAC,sBAAsB;IA8pB9B,OAAO,CAAC,kBAAkB;IAgE1B;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAoC9B,OAAO,CAAC,mBAAmB;IAgH3B,OAAO,CAAC,mBAAmB;IA6D3B,OAAO,CAAC,qBAAqB;IAwF7B,OAAO,CAAC,gBAAgB;IA0CxB,OAAO,CAAC,iBAAiB;IAmCzB,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,eAAe;IAuDvB,OAAO,CAAC,wBAAwB;IA4IhC,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,6BAA6B;IAkPrC,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAuFzB,OAAO,CAAC,WAAW;IA0CnB,OAAO,CAAC,aAAa;IAmjBrB,OAAO,CAAC,iBAAiB;IAuCzB,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,oBAAoB;IAyE5B,OAAO,CAAC,8BAA8B;IActC,OAAO,CAAC,wBAAwB;IAyBhC,OAAO,CAAC,yBAAyB;IA8BjC,OAAO,CAAC,wBAAwB;IAYhC,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,kBAAkB;IA8B1B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAmB3B;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;IAiFlC,OAAO,CAAC,eAAe;CAuCxB"}
|
|
@@ -53,9 +53,12 @@ export class TypeScriptCodeGeneratorService {
|
|
|
53
53
|
const schemaTypeAliases = this.buildSchemaTypeAliases(schemas);
|
|
54
54
|
const serverConfig = this.buildServerConfiguration(openapi);
|
|
55
55
|
const clientClass = this.buildClientClass(openapi, schemas);
|
|
56
|
+
const explicitTypeDeclarations = this.buildExplicitTypeDeclarations(openapi);
|
|
56
57
|
return [
|
|
57
58
|
this.createComment('Imports'),
|
|
58
59
|
...imports,
|
|
60
|
+
this.createComment('Explicit type declarations'),
|
|
61
|
+
...explicitTypeDeclarations,
|
|
59
62
|
this.createComment('Components schemas'),
|
|
60
63
|
...Object.values(schemas),
|
|
61
64
|
...schemaTypeAliases,
|
|
@@ -79,8 +82,11 @@ export class TypeScriptCodeGeneratorService {
|
|
|
79
82
|
const schemaExpression = this.buildSchema(schema);
|
|
80
83
|
// Clear context
|
|
81
84
|
this.currentSchemaName = null;
|
|
85
|
+
const sanitizedName = this.typeBuilder.sanitizeIdentifier(name);
|
|
86
|
+
// Add type annotation: z.ZodType<Name>
|
|
87
|
+
const typeAnnotation = ts.factory.createTypeReferenceNode(ts.factory.createQualifiedName(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('ZodType')), [ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(sanitizedName), undefined)]);
|
|
82
88
|
const variableStatement = ts.factory.createVariableStatement([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
|
|
83
|
-
ts.factory.createVariableDeclaration(ts.factory.createIdentifier(
|
|
89
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier(sanitizedName), undefined, typeAnnotation, schemaExpression),
|
|
84
90
|
], ts.NodeFlags.Const));
|
|
85
91
|
return {
|
|
86
92
|
...schemaRegistered,
|
|
@@ -88,11 +94,157 @@ export class TypeScriptCodeGeneratorService {
|
|
|
88
94
|
};
|
|
89
95
|
}, {});
|
|
90
96
|
}
|
|
91
|
-
buildSchemaTypeAliases(
|
|
92
|
-
|
|
97
|
+
buildSchemaTypeAliases(_schemas) {
|
|
98
|
+
// Explicit type declarations are used instead of z.infer type exports
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Builds explicit TypeScript type declarations for all schemas.
|
|
103
|
+
* Returns interface declarations for object types and type aliases for other types.
|
|
104
|
+
*/
|
|
105
|
+
buildExplicitTypeDeclarations(openapi) {
|
|
106
|
+
const schemasEntries = Object.entries(openapi.components?.schemas ?? {});
|
|
107
|
+
const schemasMap = Object.fromEntries(schemasEntries);
|
|
108
|
+
const sortedSchemaNames = this.topologicalSort(schemasMap);
|
|
109
|
+
const statements = [];
|
|
110
|
+
for (const name of sortedSchemaNames) {
|
|
111
|
+
const schema = openapi.components?.schemas?.[name];
|
|
112
|
+
if (!schema)
|
|
113
|
+
continue;
|
|
93
114
|
const sanitizedName = this.typeBuilder.sanitizeIdentifier(name);
|
|
94
|
-
|
|
115
|
+
const safeSchema = SchemaProperties.safeParse(schema);
|
|
116
|
+
if (!safeSchema.success) {
|
|
117
|
+
// Unknown schema type, create a type alias to unknown
|
|
118
|
+
statements.push(ts.factory.createTypeAliasDeclaration([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(sanitizedName), undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const schemaData = safeSchema.data;
|
|
122
|
+
const typeNode = this.buildTypeNode(schemaData);
|
|
123
|
+
// For object types with properties, create an interface
|
|
124
|
+
if (schemaData['type'] === 'object' && schemaData['properties']) {
|
|
125
|
+
statements.push(this.buildInterfaceDeclaration(sanitizedName, schemaData));
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// For all other types (enums, arrays, unions, etc.), create a type alias
|
|
129
|
+
statements.push(ts.factory.createTypeAliasDeclaration([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(sanitizedName), undefined, typeNode));
|
|
130
|
+
}
|
|
131
|
+
return statements;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Converts an OpenAPI schema to a TypeScript type node.
|
|
135
|
+
*/
|
|
136
|
+
buildTypeNode(schema) {
|
|
137
|
+
const safeSchema = SchemaProperties.safeParse(schema);
|
|
138
|
+
if (!safeSchema.success) {
|
|
139
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
140
|
+
}
|
|
141
|
+
const prop = safeSchema.data;
|
|
142
|
+
// Handle $ref
|
|
143
|
+
if (this.isReference(prop)) {
|
|
144
|
+
const { $ref = '' } = Reference.parse(prop);
|
|
145
|
+
const refName = $ref.split('/').pop() ?? 'never';
|
|
146
|
+
const sanitizedRefName = this.typeBuilder.sanitizeIdentifier(refName);
|
|
147
|
+
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(sanitizedRefName), undefined);
|
|
148
|
+
}
|
|
149
|
+
// Handle nullable
|
|
150
|
+
const isNullable = prop['nullable'] === true;
|
|
151
|
+
const baseTypeNode = this.buildBaseTypeNode(prop);
|
|
152
|
+
if (isNullable) {
|
|
153
|
+
return ts.factory.createUnionTypeNode([baseTypeNode, ts.factory.createLiteralTypeNode(ts.factory.createNull())]);
|
|
154
|
+
}
|
|
155
|
+
return baseTypeNode;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Builds the base type node without nullable handling.
|
|
159
|
+
*/
|
|
160
|
+
buildBaseTypeNode(prop) {
|
|
161
|
+
// Handle anyOf/oneOf (union types)
|
|
162
|
+
if (prop['anyOf'] && Array.isArray(prop['anyOf']) && prop['anyOf'].length > 0) {
|
|
163
|
+
const types = prop['anyOf'].map((s) => this.buildTypeNode(s));
|
|
164
|
+
return ts.factory.createUnionTypeNode(types);
|
|
165
|
+
}
|
|
166
|
+
if (prop['oneOf'] && Array.isArray(prop['oneOf']) && prop['oneOf'].length > 0) {
|
|
167
|
+
const types = prop['oneOf'].map((s) => this.buildTypeNode(s));
|
|
168
|
+
return ts.factory.createUnionTypeNode(types);
|
|
169
|
+
}
|
|
170
|
+
// Handle allOf (intersection types)
|
|
171
|
+
if (prop['allOf'] && Array.isArray(prop['allOf']) && prop['allOf'].length > 0) {
|
|
172
|
+
const types = prop['allOf'].map((s) => this.buildTypeNode(s));
|
|
173
|
+
return ts.factory.createIntersectionTypeNode(types);
|
|
174
|
+
}
|
|
175
|
+
// Handle enum
|
|
176
|
+
if (prop['enum'] && Array.isArray(prop['enum']) && prop['enum'].length > 0) {
|
|
177
|
+
const literalTypes = prop['enum'].map((val) => {
|
|
178
|
+
if (typeof val === 'string') {
|
|
179
|
+
return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(val, true));
|
|
180
|
+
}
|
|
181
|
+
else if (typeof val === 'number') {
|
|
182
|
+
if (val < 0) {
|
|
183
|
+
return ts.factory.createLiteralTypeNode(ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, ts.factory.createNumericLiteral(String(Math.abs(val)))));
|
|
184
|
+
}
|
|
185
|
+
return ts.factory.createLiteralTypeNode(ts.factory.createNumericLiteral(String(val)));
|
|
186
|
+
}
|
|
187
|
+
else if (typeof val === 'boolean') {
|
|
188
|
+
return ts.factory.createLiteralTypeNode(val ? ts.factory.createTrue() : ts.factory.createFalse());
|
|
189
|
+
}
|
|
190
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
191
|
+
});
|
|
192
|
+
return ts.factory.createUnionTypeNode(literalTypes);
|
|
193
|
+
}
|
|
194
|
+
// Handle type-specific schemas
|
|
195
|
+
switch (prop['type']) {
|
|
196
|
+
case 'string':
|
|
197
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
198
|
+
case 'number':
|
|
199
|
+
case 'integer':
|
|
200
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
|
|
201
|
+
case 'boolean':
|
|
202
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
|
203
|
+
case 'array': {
|
|
204
|
+
const itemsType = prop['items']
|
|
205
|
+
? this.buildTypeNode(prop['items'])
|
|
206
|
+
: ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
207
|
+
return ts.factory.createArrayTypeNode(itemsType);
|
|
208
|
+
}
|
|
209
|
+
case 'object': {
|
|
210
|
+
const properties = (prop['properties'] ?? {});
|
|
211
|
+
const requiredProps = (prop['required'] ?? []);
|
|
212
|
+
if (Object.keys(properties).length > 0) {
|
|
213
|
+
return this.buildObjectTypeLiteral(properties, requiredProps);
|
|
214
|
+
}
|
|
215
|
+
// Empty object or additionalProperties - use Record<string, unknown>
|
|
216
|
+
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Record'), [
|
|
217
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
|
218
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
|
|
219
|
+
]);
|
|
220
|
+
}
|
|
221
|
+
default:
|
|
222
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Builds a TypeScript type literal for an object schema.
|
|
227
|
+
*/
|
|
228
|
+
buildObjectTypeLiteral(properties, requiredProps) {
|
|
229
|
+
const members = Object.entries(properties).map(([name, propSchema]) => {
|
|
230
|
+
const isRequired = requiredProps.includes(name);
|
|
231
|
+
const typeNode = this.buildTypeNode(propSchema);
|
|
232
|
+
return ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier(name), isRequired ? undefined : ts.factory.createToken(ts.SyntaxKind.QuestionToken), typeNode);
|
|
233
|
+
});
|
|
234
|
+
return ts.factory.createTypeLiteralNode(members);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Builds a TypeScript interface declaration for an object schema.
|
|
238
|
+
*/
|
|
239
|
+
buildInterfaceDeclaration(name, schema) {
|
|
240
|
+
const properties = (schema['properties'] ?? {});
|
|
241
|
+
const requiredProps = (schema['required'] ?? []);
|
|
242
|
+
const members = Object.entries(properties).map(([propName, propSchema]) => {
|
|
243
|
+
const isRequired = requiredProps.includes(propName);
|
|
244
|
+
const typeNode = this.buildTypeNode(propSchema);
|
|
245
|
+
return ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier(propName), isRequired ? undefined : ts.factory.createToken(ts.SyntaxKind.QuestionToken), typeNode);
|
|
95
246
|
});
|
|
247
|
+
return ts.factory.createInterfaceDeclaration([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(name), undefined, undefined, members);
|
|
96
248
|
}
|
|
97
249
|
buildClientClass(openapi, schemas) {
|
|
98
250
|
const clientName = this.generateClientName(openapi.info.title);
|
package/generated/type.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
2
|
-
// Built with zod-codegen@1.6.
|
|
3
|
-
// Latest edit:
|
|
2
|
+
// Built with zod-codegen@1.6.3
|
|
3
|
+
// Latest edit: Thu, 22 Jan 2026 16:08:32 GMT
|
|
4
4
|
// Source file: ./samples/swagger-petstore.yaml
|
|
5
5
|
/* eslint-disable */
|
|
6
6
|
// @ts-nocheck
|
|
@@ -10,9 +10,62 @@
|
|
|
10
10
|
;
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
|
|
13
|
+
// Explicit type declarations
|
|
14
|
+
;
|
|
15
|
+
export interface Order {
|
|
16
|
+
id?: number;
|
|
17
|
+
petId?: number;
|
|
18
|
+
quantity?: number;
|
|
19
|
+
shipDate?: string;
|
|
20
|
+
status?: 'placed' | 'approved' | 'delivered';
|
|
21
|
+
complete?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface Address {
|
|
24
|
+
street?: string;
|
|
25
|
+
city?: string;
|
|
26
|
+
state?: string;
|
|
27
|
+
zip?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Customer {
|
|
30
|
+
id?: number;
|
|
31
|
+
username?: string;
|
|
32
|
+
address?: Address[];
|
|
33
|
+
}
|
|
34
|
+
export interface Category {
|
|
35
|
+
id?: number;
|
|
36
|
+
name?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface User {
|
|
39
|
+
id?: number;
|
|
40
|
+
username?: string;
|
|
41
|
+
firstName?: string;
|
|
42
|
+
lastName?: string;
|
|
43
|
+
email?: string;
|
|
44
|
+
password?: string;
|
|
45
|
+
phone?: string;
|
|
46
|
+
userStatus?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface Tag {
|
|
49
|
+
id?: number;
|
|
50
|
+
name?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface Pet {
|
|
53
|
+
id?: number;
|
|
54
|
+
name: string;
|
|
55
|
+
category?: Category;
|
|
56
|
+
photoUrls: string[];
|
|
57
|
+
tags?: Tag[];
|
|
58
|
+
status?: 'available' | 'pending' | 'sold';
|
|
59
|
+
}
|
|
60
|
+
export interface ApiResponse {
|
|
61
|
+
code?: number;
|
|
62
|
+
type?: string;
|
|
63
|
+
message?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
13
66
|
// Components schemas
|
|
14
67
|
;
|
|
15
|
-
export const Order = z.object({
|
|
68
|
+
export const Order: z.ZodType<Order> = z.object({
|
|
16
69
|
id: z.number().int().optional(),
|
|
17
70
|
petId: z.number().int().optional(),
|
|
18
71
|
quantity: z.number().int().optional(),
|
|
@@ -22,22 +75,22 @@ export const Order = z.object({
|
|
|
22
75
|
status: z.enum(['placed', 'approved', 'delivered']).optional(),
|
|
23
76
|
complete: z.boolean().optional()
|
|
24
77
|
});
|
|
25
|
-
export const Address = z.object({
|
|
78
|
+
export const Address: z.ZodType<Address> = z.object({
|
|
26
79
|
street: z.string().optional(),
|
|
27
80
|
city: z.string().optional(),
|
|
28
81
|
state: z.string().optional(),
|
|
29
82
|
zip: z.string().optional()
|
|
30
83
|
});
|
|
31
|
-
export const Customer = z.object({
|
|
84
|
+
export const Customer: z.ZodType<Customer> = z.object({
|
|
32
85
|
id: z.number().int().optional(),
|
|
33
86
|
username: z.string().optional(),
|
|
34
87
|
address: z.array(Address).optional()
|
|
35
88
|
});
|
|
36
|
-
export const Category = z.object({
|
|
89
|
+
export const Category: z.ZodType<Category> = z.object({
|
|
37
90
|
id: z.number().int().optional(),
|
|
38
91
|
name: z.string().optional()
|
|
39
92
|
});
|
|
40
|
-
export const User = z.object({
|
|
93
|
+
export const User: z.ZodType<User> = z.object({
|
|
41
94
|
id: z.number().int().optional(),
|
|
42
95
|
username: z.string().optional(),
|
|
43
96
|
firstName: z.string().optional(),
|
|
@@ -47,11 +100,11 @@ export const User = z.object({
|
|
|
47
100
|
phone: z.string().optional(),
|
|
48
101
|
userStatus: z.number().int().optional()
|
|
49
102
|
});
|
|
50
|
-
export const Tag = z.object({
|
|
103
|
+
export const Tag: z.ZodType<Tag> = z.object({
|
|
51
104
|
id: z.number().int().optional(),
|
|
52
105
|
name: z.string().optional()
|
|
53
106
|
});
|
|
54
|
-
export const Pet = z.object({
|
|
107
|
+
export const Pet: z.ZodType<Pet> = z.object({
|
|
55
108
|
id: z.number().int().optional(),
|
|
56
109
|
name: z.string(),
|
|
57
110
|
category: Category.optional(),
|
|
@@ -59,19 +112,11 @@ export const Pet = z.object({
|
|
|
59
112
|
tags: z.array(Tag).optional(),
|
|
60
113
|
status: z.enum(['available', 'pending', 'sold']).optional()
|
|
61
114
|
});
|
|
62
|
-
export const ApiResponse = z.object({
|
|
115
|
+
export const ApiResponse: z.ZodType<ApiResponse> = z.object({
|
|
63
116
|
code: z.number().int().optional(),
|
|
64
117
|
type: z.string().optional(),
|
|
65
118
|
message: z.string().optional()
|
|
66
119
|
});
|
|
67
|
-
export type Order = z.infer<typeof Order>;
|
|
68
|
-
export type Address = z.infer<typeof Address>;
|
|
69
|
-
export type Customer = z.infer<typeof Customer>;
|
|
70
|
-
export type Category = z.infer<typeof Category>;
|
|
71
|
-
export type User = z.infer<typeof User>;
|
|
72
|
-
export type Tag = z.infer<typeof Tag>;
|
|
73
|
-
export type Pet = z.infer<typeof Pet>;
|
|
74
|
-
export type ApiResponse = z.infer<typeof ApiResponse>;
|
|
75
120
|
export const serverConfigurations = [{
|
|
76
121
|
url: 'https://petstore3.swagger.io/api/v3'
|
|
77
122
|
}];
|
package/package.json
CHANGED
|
@@ -75,9 +75,13 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
75
75
|
const serverConfig = this.buildServerConfiguration(openapi);
|
|
76
76
|
const clientClass = this.buildClientClass(openapi, schemas);
|
|
77
77
|
|
|
78
|
+
const explicitTypeDeclarations = this.buildExplicitTypeDeclarations(openapi);
|
|
79
|
+
|
|
78
80
|
return [
|
|
79
81
|
this.createComment('Imports'),
|
|
80
82
|
...imports,
|
|
83
|
+
this.createComment('Explicit type declarations'),
|
|
84
|
+
...explicitTypeDeclarations,
|
|
81
85
|
this.createComment('Components schemas'),
|
|
82
86
|
...Object.values(schemas),
|
|
83
87
|
...schemaTypeAliases,
|
|
@@ -108,14 +112,22 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
108
112
|
// Clear context
|
|
109
113
|
this.currentSchemaName = null;
|
|
110
114
|
|
|
115
|
+
const sanitizedName = this.typeBuilder.sanitizeIdentifier(name);
|
|
116
|
+
|
|
117
|
+
// Add type annotation: z.ZodType<Name>
|
|
118
|
+
const typeAnnotation = ts.factory.createTypeReferenceNode(
|
|
119
|
+
ts.factory.createQualifiedName(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('ZodType')),
|
|
120
|
+
[ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(sanitizedName), undefined)],
|
|
121
|
+
);
|
|
122
|
+
|
|
111
123
|
const variableStatement = ts.factory.createVariableStatement(
|
|
112
124
|
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
|
113
125
|
ts.factory.createVariableDeclarationList(
|
|
114
126
|
[
|
|
115
127
|
ts.factory.createVariableDeclaration(
|
|
116
|
-
ts.factory.createIdentifier(
|
|
117
|
-
undefined,
|
|
128
|
+
ts.factory.createIdentifier(sanitizedName),
|
|
118
129
|
undefined,
|
|
130
|
+
typeAnnotation,
|
|
119
131
|
schemaExpression,
|
|
120
132
|
),
|
|
121
133
|
],
|
|
@@ -130,19 +142,218 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
130
142
|
}, {});
|
|
131
143
|
}
|
|
132
144
|
|
|
133
|
-
private buildSchemaTypeAliases(
|
|
134
|
-
|
|
145
|
+
private buildSchemaTypeAliases(_schemas: Record<string, ts.VariableStatement>): ts.TypeAliasDeclaration[] {
|
|
146
|
+
// Explicit type declarations are used instead of z.infer type exports
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Builds explicit TypeScript type declarations for all schemas.
|
|
152
|
+
* Returns interface declarations for object types and type aliases for other types.
|
|
153
|
+
*/
|
|
154
|
+
private buildExplicitTypeDeclarations(openapi: OpenApiSpecType): ts.Statement[] {
|
|
155
|
+
const schemasEntries = Object.entries(openapi.components?.schemas ?? {});
|
|
156
|
+
const schemasMap = Object.fromEntries(schemasEntries);
|
|
157
|
+
const sortedSchemaNames = this.topologicalSort(schemasMap);
|
|
158
|
+
|
|
159
|
+
const statements: ts.Statement[] = [];
|
|
160
|
+
|
|
161
|
+
for (const name of sortedSchemaNames) {
|
|
162
|
+
const schema = openapi.components?.schemas?.[name];
|
|
163
|
+
if (!schema) continue;
|
|
164
|
+
|
|
135
165
|
const sanitizedName = this.typeBuilder.sanitizeIdentifier(name);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
ts.factory.
|
|
142
|
-
|
|
166
|
+
const safeSchema = SchemaProperties.safeParse(schema);
|
|
167
|
+
|
|
168
|
+
if (!safeSchema.success) {
|
|
169
|
+
// Unknown schema type, create a type alias to unknown
|
|
170
|
+
statements.push(
|
|
171
|
+
ts.factory.createTypeAliasDeclaration(
|
|
172
|
+
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
|
173
|
+
ts.factory.createIdentifier(sanitizedName),
|
|
174
|
+
undefined,
|
|
175
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const schemaData = safeSchema.data;
|
|
182
|
+
const typeNode = this.buildTypeNode(schemaData);
|
|
183
|
+
|
|
184
|
+
// For object types with properties, create an interface
|
|
185
|
+
if (schemaData['type'] === 'object' && schemaData['properties']) {
|
|
186
|
+
statements.push(this.buildInterfaceDeclaration(sanitizedName, schemaData));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// For all other types (enums, arrays, unions, etc.), create a type alias
|
|
191
|
+
statements.push(
|
|
192
|
+
ts.factory.createTypeAliasDeclaration(
|
|
193
|
+
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
|
194
|
+
ts.factory.createIdentifier(sanitizedName),
|
|
195
|
+
undefined,
|
|
196
|
+
typeNode,
|
|
143
197
|
),
|
|
144
198
|
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return statements;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Converts an OpenAPI schema to a TypeScript type node.
|
|
206
|
+
*/
|
|
207
|
+
private buildTypeNode(schema: unknown): ts.TypeNode {
|
|
208
|
+
const safeSchema = SchemaProperties.safeParse(schema);
|
|
209
|
+
if (!safeSchema.success) {
|
|
210
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const prop = safeSchema.data;
|
|
214
|
+
|
|
215
|
+
// Handle $ref
|
|
216
|
+
if (this.isReference(prop)) {
|
|
217
|
+
const {$ref = ''} = Reference.parse(prop);
|
|
218
|
+
const refName = $ref.split('/').pop() ?? 'never';
|
|
219
|
+
const sanitizedRefName = this.typeBuilder.sanitizeIdentifier(refName);
|
|
220
|
+
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(sanitizedRefName), undefined);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle nullable
|
|
224
|
+
const isNullable = prop['nullable'] === true;
|
|
225
|
+
const baseTypeNode = this.buildBaseTypeNode(prop);
|
|
226
|
+
|
|
227
|
+
if (isNullable) {
|
|
228
|
+
return ts.factory.createUnionTypeNode([baseTypeNode, ts.factory.createLiteralTypeNode(ts.factory.createNull())]);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return baseTypeNode;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Builds the base type node without nullable handling.
|
|
236
|
+
*/
|
|
237
|
+
private buildBaseTypeNode(prop: Record<string, unknown>): ts.TypeNode {
|
|
238
|
+
// Handle anyOf/oneOf (union types)
|
|
239
|
+
if (prop['anyOf'] && Array.isArray(prop['anyOf']) && prop['anyOf'].length > 0) {
|
|
240
|
+
const types = prop['anyOf'].map((s: unknown) => this.buildTypeNode(s));
|
|
241
|
+
return ts.factory.createUnionTypeNode(types);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (prop['oneOf'] && Array.isArray(prop['oneOf']) && prop['oneOf'].length > 0) {
|
|
245
|
+
const types = prop['oneOf'].map((s: unknown) => this.buildTypeNode(s));
|
|
246
|
+
return ts.factory.createUnionTypeNode(types);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Handle allOf (intersection types)
|
|
250
|
+
if (prop['allOf'] && Array.isArray(prop['allOf']) && prop['allOf'].length > 0) {
|
|
251
|
+
const types = prop['allOf'].map((s: unknown) => this.buildTypeNode(s));
|
|
252
|
+
return ts.factory.createIntersectionTypeNode(types);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Handle enum
|
|
256
|
+
if (prop['enum'] && Array.isArray(prop['enum']) && prop['enum'].length > 0) {
|
|
257
|
+
const literalTypes = prop['enum'].map((val: unknown) => {
|
|
258
|
+
if (typeof val === 'string') {
|
|
259
|
+
return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(val, true));
|
|
260
|
+
} else if (typeof val === 'number') {
|
|
261
|
+
if (val < 0) {
|
|
262
|
+
return ts.factory.createLiteralTypeNode(
|
|
263
|
+
ts.factory.createPrefixUnaryExpression(
|
|
264
|
+
ts.SyntaxKind.MinusToken,
|
|
265
|
+
ts.factory.createNumericLiteral(String(Math.abs(val))),
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return ts.factory.createLiteralTypeNode(ts.factory.createNumericLiteral(String(val)));
|
|
270
|
+
} else if (typeof val === 'boolean') {
|
|
271
|
+
return ts.factory.createLiteralTypeNode(val ? ts.factory.createTrue() : ts.factory.createFalse());
|
|
272
|
+
}
|
|
273
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
274
|
+
});
|
|
275
|
+
return ts.factory.createUnionTypeNode(literalTypes);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Handle type-specific schemas
|
|
279
|
+
switch (prop['type']) {
|
|
280
|
+
case 'string':
|
|
281
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
282
|
+
case 'number':
|
|
283
|
+
case 'integer':
|
|
284
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
|
|
285
|
+
case 'boolean':
|
|
286
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
|
287
|
+
case 'array': {
|
|
288
|
+
const itemsType = prop['items']
|
|
289
|
+
? this.buildTypeNode(prop['items'])
|
|
290
|
+
: ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
291
|
+
return ts.factory.createArrayTypeNode(itemsType);
|
|
292
|
+
}
|
|
293
|
+
case 'object': {
|
|
294
|
+
const properties = (prop['properties'] ?? {}) as Record<string, unknown>;
|
|
295
|
+
const requiredProps = (prop['required'] ?? []) as string[];
|
|
296
|
+
|
|
297
|
+
if (Object.keys(properties).length > 0) {
|
|
298
|
+
return this.buildObjectTypeLiteral(properties, requiredProps);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Empty object or additionalProperties - use Record<string, unknown>
|
|
302
|
+
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Record'), [
|
|
303
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
|
304
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
|
|
305
|
+
]);
|
|
306
|
+
}
|
|
307
|
+
default:
|
|
308
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Builds a TypeScript type literal for an object schema.
|
|
314
|
+
*/
|
|
315
|
+
private buildObjectTypeLiteral(properties: Record<string, unknown>, requiredProps: string[]): ts.TypeLiteralNode {
|
|
316
|
+
const members = Object.entries(properties).map(([name, propSchema]) => {
|
|
317
|
+
const isRequired = requiredProps.includes(name);
|
|
318
|
+
const typeNode = this.buildTypeNode(propSchema);
|
|
319
|
+
|
|
320
|
+
return ts.factory.createPropertySignature(
|
|
321
|
+
undefined,
|
|
322
|
+
ts.factory.createIdentifier(name),
|
|
323
|
+
isRequired ? undefined : ts.factory.createToken(ts.SyntaxKind.QuestionToken),
|
|
324
|
+
typeNode,
|
|
325
|
+
);
|
|
145
326
|
});
|
|
327
|
+
|
|
328
|
+
return ts.factory.createTypeLiteralNode(members);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Builds a TypeScript interface declaration for an object schema.
|
|
333
|
+
*/
|
|
334
|
+
private buildInterfaceDeclaration(name: string, schema: Record<string, unknown>): ts.InterfaceDeclaration {
|
|
335
|
+
const properties = (schema['properties'] ?? {}) as Record<string, unknown>;
|
|
336
|
+
const requiredProps = (schema['required'] ?? []) as string[];
|
|
337
|
+
|
|
338
|
+
const members = Object.entries(properties).map(([propName, propSchema]) => {
|
|
339
|
+
const isRequired = requiredProps.includes(propName);
|
|
340
|
+
const typeNode = this.buildTypeNode(propSchema);
|
|
341
|
+
|
|
342
|
+
return ts.factory.createPropertySignature(
|
|
343
|
+
undefined,
|
|
344
|
+
ts.factory.createIdentifier(propName),
|
|
345
|
+
isRequired ? undefined : ts.factory.createToken(ts.SyntaxKind.QuestionToken),
|
|
346
|
+
typeNode,
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return ts.factory.createInterfaceDeclaration(
|
|
351
|
+
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
|
352
|
+
ts.factory.createIdentifier(name),
|
|
353
|
+
undefined,
|
|
354
|
+
undefined,
|
|
355
|
+
members,
|
|
356
|
+
);
|
|
146
357
|
}
|
|
147
358
|
|
|
148
359
|
private buildClientClass(
|
|
@@ -1085,4 +1085,309 @@ describe('TypeScriptCodeGeneratorService', () => {
|
|
|
1085
1085
|
expect(code).toContain('https://{env}.example.com');
|
|
1086
1086
|
});
|
|
1087
1087
|
});
|
|
1088
|
+
|
|
1089
|
+
describe('explicit types', () => {
|
|
1090
|
+
it('should generate explicit interface for object schema', () => {
|
|
1091
|
+
const spec: OpenApiSpecType = {
|
|
1092
|
+
openapi: '3.0.0',
|
|
1093
|
+
info: {
|
|
1094
|
+
title: 'Test API',
|
|
1095
|
+
version: '1.0.0',
|
|
1096
|
+
},
|
|
1097
|
+
paths: {},
|
|
1098
|
+
components: {
|
|
1099
|
+
schemas: {
|
|
1100
|
+
Order: {
|
|
1101
|
+
type: 'object',
|
|
1102
|
+
properties: {
|
|
1103
|
+
id: {type: 'integer'},
|
|
1104
|
+
name: {type: 'string'},
|
|
1105
|
+
},
|
|
1106
|
+
required: ['id', 'name'],
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1109
|
+
},
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
const code = generator.generate(spec);
|
|
1113
|
+
|
|
1114
|
+
// Should generate explicit interface
|
|
1115
|
+
expect(code).toContain('export interface Order');
|
|
1116
|
+
expect(code).toContain('id: number');
|
|
1117
|
+
expect(code).toContain('name: string');
|
|
1118
|
+
|
|
1119
|
+
// Should add type annotation to schema
|
|
1120
|
+
expect(code).toContain('export const Order: z.ZodType<Order>');
|
|
1121
|
+
|
|
1122
|
+
// Should NOT generate z.infer type export
|
|
1123
|
+
expect(code).not.toContain('z.infer<typeof Order>');
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
it('should generate type alias for enum schema', () => {
|
|
1127
|
+
const spec: OpenApiSpecType = {
|
|
1128
|
+
openapi: '3.0.0',
|
|
1129
|
+
info: {
|
|
1130
|
+
title: 'Test API',
|
|
1131
|
+
version: '1.0.0',
|
|
1132
|
+
},
|
|
1133
|
+
paths: {},
|
|
1134
|
+
components: {
|
|
1135
|
+
schemas: {
|
|
1136
|
+
Status: {
|
|
1137
|
+
type: 'string',
|
|
1138
|
+
enum: ['active', 'inactive', 'pending'],
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
},
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
const code = generator.generate(spec);
|
|
1145
|
+
|
|
1146
|
+
// Should generate type alias (not interface) for enum
|
|
1147
|
+
expect(code).toContain('export type Status');
|
|
1148
|
+
expect(code).toContain("'active'");
|
|
1149
|
+
expect(code).toContain("'inactive'");
|
|
1150
|
+
expect(code).toContain("'pending'");
|
|
1151
|
+
|
|
1152
|
+
// Should add type annotation to schema
|
|
1153
|
+
expect(code).toContain('export const Status: z.ZodType<Status>');
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it('should generate type alias for array schema', () => {
|
|
1157
|
+
const spec: OpenApiSpecType = {
|
|
1158
|
+
openapi: '3.0.0',
|
|
1159
|
+
info: {
|
|
1160
|
+
title: 'Test API',
|
|
1161
|
+
version: '1.0.0',
|
|
1162
|
+
},
|
|
1163
|
+
paths: {},
|
|
1164
|
+
components: {
|
|
1165
|
+
schemas: {
|
|
1166
|
+
Tags: {
|
|
1167
|
+
type: 'array',
|
|
1168
|
+
items: {type: 'string'},
|
|
1169
|
+
},
|
|
1170
|
+
},
|
|
1171
|
+
},
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
const code = generator.generate(spec);
|
|
1175
|
+
|
|
1176
|
+
// Should generate type alias for array
|
|
1177
|
+
expect(code).toContain('export type Tags = string[]');
|
|
1178
|
+
|
|
1179
|
+
// Should add type annotation to schema
|
|
1180
|
+
expect(code).toContain('export const Tags: z.ZodType<Tags>');
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
it('should handle nested objects with references', () => {
|
|
1184
|
+
const spec: OpenApiSpecType = {
|
|
1185
|
+
openapi: '3.0.0',
|
|
1186
|
+
info: {
|
|
1187
|
+
title: 'Test API',
|
|
1188
|
+
version: '1.0.0',
|
|
1189
|
+
},
|
|
1190
|
+
paths: {},
|
|
1191
|
+
components: {
|
|
1192
|
+
schemas: {
|
|
1193
|
+
User: {
|
|
1194
|
+
type: 'object',
|
|
1195
|
+
properties: {
|
|
1196
|
+
id: {type: 'integer'},
|
|
1197
|
+
profile: {$ref: '#/components/schemas/Profile'},
|
|
1198
|
+
},
|
|
1199
|
+
required: ['id'],
|
|
1200
|
+
},
|
|
1201
|
+
Profile: {
|
|
1202
|
+
type: 'object',
|
|
1203
|
+
properties: {
|
|
1204
|
+
name: {type: 'string'},
|
|
1205
|
+
},
|
|
1206
|
+
required: ['name'],
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1209
|
+
},
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
const code = generator.generate(spec);
|
|
1213
|
+
|
|
1214
|
+
// Should generate interfaces for both
|
|
1215
|
+
expect(code).toContain('export interface User');
|
|
1216
|
+
expect(code).toContain('export interface Profile');
|
|
1217
|
+
|
|
1218
|
+
// User should reference Profile type
|
|
1219
|
+
expect(code).toContain('profile?: Profile');
|
|
1220
|
+
|
|
1221
|
+
// Both should have type annotations
|
|
1222
|
+
expect(code).toContain('export const User: z.ZodType<User>');
|
|
1223
|
+
expect(code).toContain('export const Profile: z.ZodType<Profile>');
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
it('should handle union types (anyOf)', () => {
|
|
1227
|
+
const spec: OpenApiSpecType = {
|
|
1228
|
+
openapi: '3.0.0',
|
|
1229
|
+
info: {
|
|
1230
|
+
title: 'Test API',
|
|
1231
|
+
version: '1.0.0',
|
|
1232
|
+
},
|
|
1233
|
+
paths: {},
|
|
1234
|
+
components: {
|
|
1235
|
+
schemas: {
|
|
1236
|
+
StringOrNumber: {
|
|
1237
|
+
anyOf: [{type: 'string'}, {type: 'number'}],
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
},
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
const code = generator.generate(spec);
|
|
1244
|
+
|
|
1245
|
+
// Should generate type alias for union
|
|
1246
|
+
expect(code).toContain('export type StringOrNumber = string | number');
|
|
1247
|
+
|
|
1248
|
+
// Should add type annotation to schema
|
|
1249
|
+
expect(code).toContain('export const StringOrNumber: z.ZodType<StringOrNumber>');
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it('should handle intersection types (allOf)', () => {
|
|
1253
|
+
const spec: OpenApiSpecType = {
|
|
1254
|
+
openapi: '3.0.0',
|
|
1255
|
+
info: {
|
|
1256
|
+
title: 'Test API',
|
|
1257
|
+
version: '1.0.0',
|
|
1258
|
+
},
|
|
1259
|
+
paths: {},
|
|
1260
|
+
components: {
|
|
1261
|
+
schemas: {
|
|
1262
|
+
Base: {
|
|
1263
|
+
type: 'object',
|
|
1264
|
+
properties: {
|
|
1265
|
+
id: {type: 'integer'},
|
|
1266
|
+
},
|
|
1267
|
+
required: ['id'],
|
|
1268
|
+
},
|
|
1269
|
+
Extended: {
|
|
1270
|
+
allOf: [
|
|
1271
|
+
{$ref: '#/components/schemas/Base'},
|
|
1272
|
+
{
|
|
1273
|
+
type: 'object',
|
|
1274
|
+
properties: {
|
|
1275
|
+
name: {type: 'string'},
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
],
|
|
1279
|
+
},
|
|
1280
|
+
},
|
|
1281
|
+
},
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
const code = generator.generate(spec);
|
|
1285
|
+
|
|
1286
|
+
// Should generate interface for Base
|
|
1287
|
+
expect(code).toContain('export interface Base');
|
|
1288
|
+
|
|
1289
|
+
// Should generate type alias for Extended (intersection)
|
|
1290
|
+
expect(code).toContain('export type Extended = Base &');
|
|
1291
|
+
|
|
1292
|
+
// Should add type annotations to schemas
|
|
1293
|
+
expect(code).toContain('export const Base: z.ZodType<Base>');
|
|
1294
|
+
expect(code).toContain('export const Extended: z.ZodType<Extended>');
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
it('should handle optional properties', () => {
|
|
1298
|
+
const spec: OpenApiSpecType = {
|
|
1299
|
+
openapi: '3.0.0',
|
|
1300
|
+
info: {
|
|
1301
|
+
title: 'Test API',
|
|
1302
|
+
version: '1.0.0',
|
|
1303
|
+
},
|
|
1304
|
+
paths: {},
|
|
1305
|
+
components: {
|
|
1306
|
+
schemas: {
|
|
1307
|
+
User: {
|
|
1308
|
+
type: 'object',
|
|
1309
|
+
properties: {
|
|
1310
|
+
id: {type: 'integer'},
|
|
1311
|
+
email: {type: 'string'},
|
|
1312
|
+
},
|
|
1313
|
+
required: ['id'],
|
|
1314
|
+
},
|
|
1315
|
+
},
|
|
1316
|
+
},
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
const code = generator.generate(spec);
|
|
1320
|
+
|
|
1321
|
+
// id should be required (no ?)
|
|
1322
|
+
expect(code).toContain('id: number');
|
|
1323
|
+
// email should be optional (with ?)
|
|
1324
|
+
expect(code).toContain('email?: string');
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
it('should handle circular dependencies', () => {
|
|
1328
|
+
const spec: OpenApiSpecType = {
|
|
1329
|
+
openapi: '3.0.0',
|
|
1330
|
+
info: {
|
|
1331
|
+
title: 'Test API',
|
|
1332
|
+
version: '1.0.0',
|
|
1333
|
+
},
|
|
1334
|
+
paths: {},
|
|
1335
|
+
components: {
|
|
1336
|
+
schemas: {
|
|
1337
|
+
Node: {
|
|
1338
|
+
type: 'object',
|
|
1339
|
+
properties: {
|
|
1340
|
+
id: {type: 'integer'},
|
|
1341
|
+
children: {
|
|
1342
|
+
type: 'array',
|
|
1343
|
+
items: {$ref: '#/components/schemas/Node'},
|
|
1344
|
+
},
|
|
1345
|
+
},
|
|
1346
|
+
required: ['id'],
|
|
1347
|
+
},
|
|
1348
|
+
},
|
|
1349
|
+
},
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
const code = generator.generate(spec);
|
|
1353
|
+
|
|
1354
|
+
// Should generate interface with self-reference
|
|
1355
|
+
expect(code).toContain('export interface Node');
|
|
1356
|
+
expect(code).toContain('children?: Node[]');
|
|
1357
|
+
|
|
1358
|
+
// Should add type annotation to schema
|
|
1359
|
+
expect(code).toContain('export const Node: z.ZodType<Node>');
|
|
1360
|
+
|
|
1361
|
+
// Zod schema should use z.lazy for circular reference
|
|
1362
|
+
expect(code).toContain('z.lazy');
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
it('should handle numeric enum types', () => {
|
|
1366
|
+
const spec: OpenApiSpecType = {
|
|
1367
|
+
openapi: '3.0.0',
|
|
1368
|
+
info: {
|
|
1369
|
+
title: 'Test API',
|
|
1370
|
+
version: '1.0.0',
|
|
1371
|
+
},
|
|
1372
|
+
paths: {},
|
|
1373
|
+
components: {
|
|
1374
|
+
schemas: {
|
|
1375
|
+
Priority: {
|
|
1376
|
+
type: 'integer',
|
|
1377
|
+
enum: [0, 1, 2],
|
|
1378
|
+
},
|
|
1379
|
+
},
|
|
1380
|
+
},
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
const code = generator.generate(spec);
|
|
1384
|
+
|
|
1385
|
+
// Should generate type alias for numeric enum
|
|
1386
|
+
expect(code).toContain('export type Priority');
|
|
1387
|
+
expect(code).toMatch(/0\s*\|\s*1\s*\|\s*2/);
|
|
1388
|
+
|
|
1389
|
+
// Should add type annotation to schema
|
|
1390
|
+
expect(code).toContain('export const Priority: z.ZodType<Priority>');
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1088
1393
|
});
|