zod-codegen 1.6.3 → 1.7.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/.github/workflows/ci.yml +50 -48
- package/.github/workflows/release.yml +13 -3
- package/.husky/commit-msg +1 -1
- package/.husky/pre-commit +1 -1
- package/.lintstagedrc.json +5 -1
- package/.nvmrc +1 -1
- package/.prettierrc.json +12 -5
- package/CHANGELOG.md +11 -0
- package/CONTRIBUTING.md +12 -12
- package/EXAMPLES.md +135 -57
- package/PERFORMANCE.md +4 -4
- package/README.md +87 -64
- package/SECURITY.md +1 -1
- package/dist/src/cli.js +11 -18
- package/dist/src/generator.d.ts +2 -2
- package/dist/src/generator.d.ts.map +1 -1
- package/dist/src/generator.js +5 -3
- package/dist/src/interfaces/code-generator.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.d.ts +3 -1
- package/dist/src/services/code-generator.service.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.js +236 -219
- package/dist/src/services/file-reader.service.d.ts.map +1 -1
- package/dist/src/services/file-reader.service.js +1 -1
- package/dist/src/services/file-writer.service.d.ts.map +1 -1
- package/dist/src/services/file-writer.service.js +2 -2
- package/dist/src/services/import-builder.service.d.ts.map +1 -1
- package/dist/src/services/import-builder.service.js +3 -3
- package/dist/src/services/type-builder.service.d.ts.map +1 -1
- package/dist/src/types/generator-options.d.ts.map +1 -1
- package/dist/src/types/openapi.d.ts.map +1 -1
- package/dist/src/types/openapi.js +20 -20
- package/dist/src/utils/error-handler.d.ts.map +1 -1
- package/dist/src/utils/naming-convention.d.ts.map +1 -1
- package/dist/src/utils/naming-convention.js +6 -3
- package/dist/src/utils/signal-handler.d.ts.map +1 -1
- package/dist/tests/integration/cli-comprehensive.test.d.ts +2 -0
- package/dist/tests/integration/cli-comprehensive.test.d.ts.map +1 -0
- package/dist/tests/integration/cli-comprehensive.test.js +110 -0
- package/dist/tests/integration/cli.test.d.ts +2 -0
- package/dist/tests/integration/cli.test.d.ts.map +1 -0
- package/dist/tests/integration/cli.test.js +25 -0
- package/dist/tests/integration/error-scenarios.test.d.ts +2 -0
- package/dist/tests/integration/error-scenarios.test.d.ts.map +1 -0
- package/dist/tests/integration/error-scenarios.test.js +169 -0
- package/dist/tests/integration/snapshots.test.d.ts +2 -0
- package/dist/tests/integration/snapshots.test.d.ts.map +1 -0
- package/dist/tests/integration/snapshots.test.js +100 -0
- package/dist/tests/unit/code-generator-edge-cases.test.d.ts +2 -0
- package/dist/tests/unit/code-generator-edge-cases.test.d.ts.map +1 -0
- package/dist/tests/unit/code-generator-edge-cases.test.js +506 -0
- package/dist/tests/unit/code-generator.test.d.ts +2 -0
- package/dist/tests/unit/code-generator.test.d.ts.map +1 -0
- package/dist/tests/unit/code-generator.test.js +1364 -0
- package/dist/tests/unit/file-reader.test.d.ts +2 -0
- package/dist/tests/unit/file-reader.test.d.ts.map +1 -0
- package/dist/tests/unit/file-reader.test.js +125 -0
- package/dist/tests/unit/generator.test.d.ts +2 -0
- package/dist/tests/unit/generator.test.d.ts.map +1 -0
- package/dist/tests/unit/generator.test.js +119 -0
- package/dist/tests/unit/naming-convention.test.d.ts +2 -0
- package/dist/tests/unit/naming-convention.test.d.ts.map +1 -0
- package/dist/tests/unit/naming-convention.test.js +256 -0
- package/dist/tests/unit/reporter.test.d.ts +2 -0
- package/dist/tests/unit/reporter.test.d.ts.map +1 -0
- package/dist/tests/unit/reporter.test.js +44 -0
- package/dist/tests/unit/type-builder.test.d.ts +2 -0
- package/dist/tests/unit/type-builder.test.d.ts.map +1 -0
- package/dist/tests/unit/type-builder.test.js +108 -0
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/vitest.config.js +10 -20
- package/eslint.config.mjs +38 -28
- package/examples/.gitkeep +1 -1
- package/examples/README.md +4 -2
- package/examples/petstore/README.md +18 -17
- package/examples/petstore/{type.ts → api.ts} +158 -74
- package/examples/petstore/authenticated-usage.ts +6 -4
- package/examples/petstore/basic-usage.ts +4 -3
- package/examples/petstore/error-handling-usage.ts +84 -0
- package/examples/petstore/retry-handler-usage.ts +11 -18
- package/examples/petstore/server-variables-usage.ts +10 -10
- package/examples/pokeapi/README.md +8 -8
- package/examples/pokeapi/api.ts +218 -0
- package/examples/pokeapi/basic-usage.ts +3 -2
- package/examples/pokeapi/custom-client.ts +5 -4
- package/package.json +17 -21
- package/src/cli.ts +20 -25
- package/src/generator.ts +13 -11
- package/src/interfaces/code-generator.ts +1 -1
- package/src/services/code-generator.service.ts +799 -1120
- package/src/services/file-reader.service.ts +6 -5
- package/src/services/file-writer.service.ts +7 -7
- package/src/services/import-builder.service.ts +9 -13
- package/src/services/type-builder.service.ts +8 -19
- package/src/types/generator-options.ts +1 -1
- package/src/types/openapi.ts +22 -22
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/naming-convention.ts +13 -10
- package/src/utils/reporter.ts +2 -2
- package/src/utils/signal-handler.ts +7 -8
- package/tests/integration/cli-comprehensive.test.ts +38 -32
- package/tests/integration/cli.test.ts +5 -5
- package/tests/integration/error-scenarios.test.ts +20 -26
- package/tests/integration/snapshots.test.ts +19 -23
- package/tests/unit/code-generator-edge-cases.test.ts +133 -133
- package/tests/unit/code-generator.test.ts +431 -330
- package/tests/unit/file-reader.test.ts +14 -14
- package/tests/unit/generator.test.ts +30 -18
- package/tests/unit/naming-convention.test.ts +27 -27
- package/tests/unit/type-builder.test.ts +2 -2
- package/tsconfig.json +5 -3
- package/vitest.config.ts +11 -21
- package/dist/scripts/update-manifest.d.ts +0 -14
- package/dist/scripts/update-manifest.d.ts.map +0 -1
- package/dist/scripts/update-manifest.js +0 -33
- package/dist/src/assets/manifest.json +0 -5
- package/examples/pokeapi/type.ts +0 -109
- package/generated/type.ts +0 -371
- package/scripts/update-manifest.ts +0 -49
- package/src/assets/manifest.json +0 -5
|
@@ -0,0 +1,1364 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { TypeScriptCodeGeneratorService } from '../../src/services/code-generator.service';
|
|
3
|
+
describe('TypeScriptCodeGeneratorService', () => {
|
|
4
|
+
let generator;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
generator = new TypeScriptCodeGeneratorService();
|
|
7
|
+
});
|
|
8
|
+
describe('naming conventions', () => {
|
|
9
|
+
it('should apply camelCase naming convention', () => {
|
|
10
|
+
const spec = {
|
|
11
|
+
openapi: '3.0.0',
|
|
12
|
+
info: {
|
|
13
|
+
title: 'Test API',
|
|
14
|
+
version: '1.0.0'
|
|
15
|
+
},
|
|
16
|
+
paths: {
|
|
17
|
+
'/users': {
|
|
18
|
+
get: {
|
|
19
|
+
operationId: 'get_user_by_id',
|
|
20
|
+
responses: {
|
|
21
|
+
'200': {
|
|
22
|
+
description: 'Success',
|
|
23
|
+
content: {
|
|
24
|
+
'application/json': {
|
|
25
|
+
schema: { type: 'string' }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const generatorWithConvention = new TypeScriptCodeGeneratorService({
|
|
35
|
+
namingConvention: 'camelCase'
|
|
36
|
+
});
|
|
37
|
+
const code = generatorWithConvention.generate(spec);
|
|
38
|
+
expect(code).toContain('async getUserById');
|
|
39
|
+
expect(code).not.toContain('async get_user_by_id');
|
|
40
|
+
});
|
|
41
|
+
it('should apply PascalCase naming convention', () => {
|
|
42
|
+
const spec = {
|
|
43
|
+
openapi: '3.0.0',
|
|
44
|
+
info: {
|
|
45
|
+
title: 'Test API',
|
|
46
|
+
version: '1.0.0'
|
|
47
|
+
},
|
|
48
|
+
paths: {
|
|
49
|
+
'/users': {
|
|
50
|
+
get: {
|
|
51
|
+
operationId: 'get_user_by_id',
|
|
52
|
+
responses: {
|
|
53
|
+
'200': {
|
|
54
|
+
description: 'Success',
|
|
55
|
+
content: {
|
|
56
|
+
'application/json': {
|
|
57
|
+
schema: { type: 'string' }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const generatorWithConvention = new TypeScriptCodeGeneratorService({
|
|
67
|
+
namingConvention: 'PascalCase'
|
|
68
|
+
});
|
|
69
|
+
const code = generatorWithConvention.generate(spec);
|
|
70
|
+
expect(code).toContain('async GetUserById');
|
|
71
|
+
expect(code).not.toContain('async get_user_by_id');
|
|
72
|
+
});
|
|
73
|
+
it('should apply snake_case naming convention', () => {
|
|
74
|
+
const spec = {
|
|
75
|
+
openapi: '3.0.0',
|
|
76
|
+
info: {
|
|
77
|
+
title: 'Test API',
|
|
78
|
+
version: '1.0.0'
|
|
79
|
+
},
|
|
80
|
+
paths: {
|
|
81
|
+
'/users': {
|
|
82
|
+
get: {
|
|
83
|
+
operationId: 'getUserById',
|
|
84
|
+
responses: {
|
|
85
|
+
'200': {
|
|
86
|
+
description: 'Success',
|
|
87
|
+
content: {
|
|
88
|
+
'application/json': {
|
|
89
|
+
schema: { type: 'string' }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const generatorWithConvention = new TypeScriptCodeGeneratorService({
|
|
99
|
+
namingConvention: 'snake_case'
|
|
100
|
+
});
|
|
101
|
+
const code = generatorWithConvention.generate(spec);
|
|
102
|
+
expect(code).toContain('async get_user_by_id');
|
|
103
|
+
expect(code).not.toContain('async getUserById');
|
|
104
|
+
});
|
|
105
|
+
it('should use custom transformer when provided', () => {
|
|
106
|
+
const spec = {
|
|
107
|
+
openapi: '3.0.0',
|
|
108
|
+
info: {
|
|
109
|
+
title: 'Test API',
|
|
110
|
+
version: '1.0.0'
|
|
111
|
+
},
|
|
112
|
+
paths: {
|
|
113
|
+
'/users/{id}': {
|
|
114
|
+
get: {
|
|
115
|
+
operationId: 'getUserById',
|
|
116
|
+
tags: ['users'],
|
|
117
|
+
summary: 'Get user',
|
|
118
|
+
responses: {
|
|
119
|
+
'200': {
|
|
120
|
+
description: 'Success',
|
|
121
|
+
content: {
|
|
122
|
+
'application/json': {
|
|
123
|
+
schema: { type: 'string' }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const customTransformer = (details) => {
|
|
133
|
+
return `${details.method.toUpperCase()}_${details.tags?.[0] || 'default'}_${details.operationId}`;
|
|
134
|
+
};
|
|
135
|
+
const generatorWithTransformer = new TypeScriptCodeGeneratorService({
|
|
136
|
+
operationNameTransformer: customTransformer
|
|
137
|
+
});
|
|
138
|
+
const code = generatorWithTransformer.generate(spec);
|
|
139
|
+
expect(code).toContain('async GET_users_getUserById');
|
|
140
|
+
});
|
|
141
|
+
it('should prioritize custom transformer over naming convention', () => {
|
|
142
|
+
const spec = {
|
|
143
|
+
openapi: '3.0.0',
|
|
144
|
+
info: {
|
|
145
|
+
title: 'Test API',
|
|
146
|
+
version: '1.0.0'
|
|
147
|
+
},
|
|
148
|
+
paths: {
|
|
149
|
+
'/users': {
|
|
150
|
+
get: {
|
|
151
|
+
operationId: 'get_user_by_id',
|
|
152
|
+
responses: {
|
|
153
|
+
'200': {
|
|
154
|
+
description: 'Success',
|
|
155
|
+
content: {
|
|
156
|
+
'application/json': {
|
|
157
|
+
schema: { type: 'string' }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const customTransformer = () => 'customName';
|
|
167
|
+
const generatorWithBoth = new TypeScriptCodeGeneratorService({
|
|
168
|
+
namingConvention: 'PascalCase',
|
|
169
|
+
operationNameTransformer: customTransformer
|
|
170
|
+
});
|
|
171
|
+
const code = generatorWithBoth.generate(spec);
|
|
172
|
+
expect(code).toContain('async customName');
|
|
173
|
+
expect(code).not.toContain('GetUserById');
|
|
174
|
+
expect(code).not.toContain('get_user_by_id');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe('generate', () => {
|
|
178
|
+
it('should generate code for a minimal OpenAPI spec', () => {
|
|
179
|
+
const spec = {
|
|
180
|
+
openapi: '3.0.0',
|
|
181
|
+
info: {
|
|
182
|
+
title: 'Test API',
|
|
183
|
+
version: '1.0.0'
|
|
184
|
+
},
|
|
185
|
+
paths: {}
|
|
186
|
+
};
|
|
187
|
+
const code = generator.generate(spec);
|
|
188
|
+
expect(code).toBeTruthy();
|
|
189
|
+
expect(typeof code).toBe('string');
|
|
190
|
+
expect(code).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
|
|
191
|
+
expect(code).toContain('export default class');
|
|
192
|
+
});
|
|
193
|
+
it('should generate schemas for component schemas', () => {
|
|
194
|
+
const spec = {
|
|
195
|
+
openapi: '3.0.0',
|
|
196
|
+
info: {
|
|
197
|
+
title: 'Test API',
|
|
198
|
+
version: '1.0.0'
|
|
199
|
+
},
|
|
200
|
+
paths: {},
|
|
201
|
+
components: {
|
|
202
|
+
schemas: {
|
|
203
|
+
User: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
id: { type: 'integer' },
|
|
207
|
+
name: { type: 'string' }
|
|
208
|
+
},
|
|
209
|
+
required: ['id', 'name']
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const code = generator.generate(spec);
|
|
215
|
+
expect(code).toContain('export const User');
|
|
216
|
+
expect(code).toContain('z.object');
|
|
217
|
+
expect(code).toContain('z.number().int()');
|
|
218
|
+
expect(code).toContain('z.string()');
|
|
219
|
+
});
|
|
220
|
+
it('should generate client methods for paths', () => {
|
|
221
|
+
const spec = {
|
|
222
|
+
openapi: '3.0.0',
|
|
223
|
+
info: {
|
|
224
|
+
title: 'Test API',
|
|
225
|
+
version: '1.0.0'
|
|
226
|
+
},
|
|
227
|
+
paths: {
|
|
228
|
+
'/users': {
|
|
229
|
+
get: {
|
|
230
|
+
operationId: 'getUsers',
|
|
231
|
+
responses: {
|
|
232
|
+
'200': {
|
|
233
|
+
description: 'Success',
|
|
234
|
+
content: {
|
|
235
|
+
'application/json': {
|
|
236
|
+
schema: {
|
|
237
|
+
type: 'array',
|
|
238
|
+
items: { type: 'string' }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const code = generator.generate(spec);
|
|
249
|
+
expect(code).toContain('async getUsers');
|
|
250
|
+
expect(code).toContain('makeRequest');
|
|
251
|
+
});
|
|
252
|
+
it('should generate getBaseRequestOptions method', () => {
|
|
253
|
+
const spec = {
|
|
254
|
+
openapi: '3.0.0',
|
|
255
|
+
info: {
|
|
256
|
+
title: 'Test API',
|
|
257
|
+
version: '1.0.0'
|
|
258
|
+
},
|
|
259
|
+
paths: {}
|
|
260
|
+
};
|
|
261
|
+
const code = generator.generate(spec);
|
|
262
|
+
expect(code).toContain('protected getBaseRequestOptions');
|
|
263
|
+
expect(code).toContain('Partial<Omit<RequestInit');
|
|
264
|
+
});
|
|
265
|
+
it('should handle servers configuration', () => {
|
|
266
|
+
const spec = {
|
|
267
|
+
openapi: '3.0.0',
|
|
268
|
+
info: {
|
|
269
|
+
title: 'Test API',
|
|
270
|
+
version: '1.0.0'
|
|
271
|
+
},
|
|
272
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
273
|
+
paths: {}
|
|
274
|
+
};
|
|
275
|
+
const code = generator.generate(spec);
|
|
276
|
+
expect(code).toContain('defaultBaseUrl');
|
|
277
|
+
expect(code).toContain('https://api.example.com');
|
|
278
|
+
});
|
|
279
|
+
it('should handle complex schemas with references', () => {
|
|
280
|
+
const spec = {
|
|
281
|
+
openapi: '3.0.0',
|
|
282
|
+
info: {
|
|
283
|
+
title: 'Test API',
|
|
284
|
+
version: '1.0.0'
|
|
285
|
+
},
|
|
286
|
+
paths: {},
|
|
287
|
+
components: {
|
|
288
|
+
schemas: {
|
|
289
|
+
User: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: {
|
|
292
|
+
id: { type: 'integer' },
|
|
293
|
+
profile: { $ref: '#/components/schemas/Profile' }
|
|
294
|
+
},
|
|
295
|
+
required: ['id']
|
|
296
|
+
},
|
|
297
|
+
Profile: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
properties: {
|
|
300
|
+
name: { type: 'string' },
|
|
301
|
+
email: { type: 'string', format: 'email' }
|
|
302
|
+
},
|
|
303
|
+
required: ['name', 'email']
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const code = generator.generate(spec);
|
|
309
|
+
expect(code).toContain('export const User');
|
|
310
|
+
expect(code).toContain('export const Profile');
|
|
311
|
+
// Profile should be defined before User (topological sort)
|
|
312
|
+
const profileIndex = code.indexOf('Profile');
|
|
313
|
+
const userIndex = code.indexOf('User');
|
|
314
|
+
expect(profileIndex).toBeLessThan(userIndex);
|
|
315
|
+
});
|
|
316
|
+
it('should handle enum types', () => {
|
|
317
|
+
const spec = {
|
|
318
|
+
openapi: '3.0.0',
|
|
319
|
+
info: {
|
|
320
|
+
title: 'Test API',
|
|
321
|
+
version: '1.0.0'
|
|
322
|
+
},
|
|
323
|
+
paths: {},
|
|
324
|
+
components: {
|
|
325
|
+
schemas: {
|
|
326
|
+
Status: {
|
|
327
|
+
type: 'string',
|
|
328
|
+
enum: ['active', 'inactive', 'pending']
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const code = generator.generate(spec);
|
|
334
|
+
expect(code).toContain('z.enum');
|
|
335
|
+
expect(code).toContain('active');
|
|
336
|
+
expect(code).toContain('inactive');
|
|
337
|
+
expect(code).toContain('pending');
|
|
338
|
+
});
|
|
339
|
+
it('should handle numeric enum types with z.union and z.literal', () => {
|
|
340
|
+
const spec = {
|
|
341
|
+
openapi: '3.0.0',
|
|
342
|
+
info: {
|
|
343
|
+
title: 'Test API',
|
|
344
|
+
version: '1.0.0'
|
|
345
|
+
},
|
|
346
|
+
paths: {},
|
|
347
|
+
components: {
|
|
348
|
+
schemas: {
|
|
349
|
+
Status: {
|
|
350
|
+
type: 'integer',
|
|
351
|
+
enum: [-99, 0, 1, 2]
|
|
352
|
+
},
|
|
353
|
+
ExecutionMode: {
|
|
354
|
+
type: 'integer',
|
|
355
|
+
enum: [1, 2]
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const code = generator.generate(spec);
|
|
361
|
+
// Numeric enums should use z.union([z.literal(...), ...])
|
|
362
|
+
expect(code).toContain('z.union');
|
|
363
|
+
expect(code).toContain('z.literal');
|
|
364
|
+
expect(code).toContain('-99');
|
|
365
|
+
expect(code).toContain('0');
|
|
366
|
+
expect(code).toContain('1');
|
|
367
|
+
expect(code).toContain('2');
|
|
368
|
+
// Should not use z.enum for numeric enums
|
|
369
|
+
expect(code).not.toContain('Status: z.enum');
|
|
370
|
+
expect(code).not.toContain('ExecutionMode: z.enum');
|
|
371
|
+
});
|
|
372
|
+
it('should merge baseOptions with request-specific options in makeRequest', () => {
|
|
373
|
+
const spec = {
|
|
374
|
+
openapi: '3.0.0',
|
|
375
|
+
info: {
|
|
376
|
+
title: 'Test API',
|
|
377
|
+
version: '1.0.0'
|
|
378
|
+
},
|
|
379
|
+
paths: {
|
|
380
|
+
'/test': {
|
|
381
|
+
get: {
|
|
382
|
+
operationId: 'testEndpoint',
|
|
383
|
+
responses: {
|
|
384
|
+
'200': {
|
|
385
|
+
description: 'Success',
|
|
386
|
+
content: {
|
|
387
|
+
'application/json': {
|
|
388
|
+
schema: { type: 'string' }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
const code = generator.generate(spec);
|
|
398
|
+
// Should call getBaseRequestOptions()
|
|
399
|
+
expect(code).toContain('getBaseRequestOptions()');
|
|
400
|
+
// Should merge headers: baseHeaders + Content-Type + request headers
|
|
401
|
+
expect(code).toContain('Object.assign');
|
|
402
|
+
expect(code).toContain('baseHeaders');
|
|
403
|
+
expect(code).toContain('Content-Type');
|
|
404
|
+
// Should merge all options: baseOptions + {method, headers, body}
|
|
405
|
+
expect(code).toMatch(/Object\.assign\s*\(\s*\{\s*\}\s*,\s*baseOptions/);
|
|
406
|
+
expect(code).toContain('method');
|
|
407
|
+
expect(code).toContain('headers');
|
|
408
|
+
expect(code).toContain('body');
|
|
409
|
+
});
|
|
410
|
+
it('should handle array types', () => {
|
|
411
|
+
const spec = {
|
|
412
|
+
openapi: '3.0.0',
|
|
413
|
+
info: {
|
|
414
|
+
title: 'Test API',
|
|
415
|
+
version: '1.0.0'
|
|
416
|
+
},
|
|
417
|
+
paths: {},
|
|
418
|
+
components: {
|
|
419
|
+
schemas: {
|
|
420
|
+
Tags: {
|
|
421
|
+
type: 'array',
|
|
422
|
+
items: { type: 'string' }
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
const code = generator.generate(spec);
|
|
428
|
+
expect(code).toContain('z.array');
|
|
429
|
+
expect(code).toContain('z.string()');
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
describe('ResponseValidationError', () => {
|
|
433
|
+
it('should generate ResponseValidationError class', () => {
|
|
434
|
+
const spec = {
|
|
435
|
+
openapi: '3.0.0',
|
|
436
|
+
info: {
|
|
437
|
+
title: 'Test API',
|
|
438
|
+
version: '1.0.0'
|
|
439
|
+
},
|
|
440
|
+
paths: {}
|
|
441
|
+
};
|
|
442
|
+
const code = generator.generate(spec);
|
|
443
|
+
expect(code).toContain('class ResponseValidationError<T> extends Error');
|
|
444
|
+
expect(code).toContain('readonly response: Response');
|
|
445
|
+
expect(code).toContain('readonly error: z.ZodError<T>');
|
|
446
|
+
expect(code).toContain("this.name = 'ResponseValidationError' as const");
|
|
447
|
+
expect(code).toContain('get data(): T');
|
|
448
|
+
});
|
|
449
|
+
it('should use safeParse with ResponseValidationError for methods with response schemas', () => {
|
|
450
|
+
const spec = {
|
|
451
|
+
openapi: '3.0.0',
|
|
452
|
+
info: {
|
|
453
|
+
title: 'Test API',
|
|
454
|
+
version: '1.0.0'
|
|
455
|
+
},
|
|
456
|
+
paths: {
|
|
457
|
+
'/pets': {
|
|
458
|
+
post: {
|
|
459
|
+
operationId: 'createPet',
|
|
460
|
+
requestBody: {
|
|
461
|
+
content: {
|
|
462
|
+
'application/json': {
|
|
463
|
+
schema: { $ref: '#/components/schemas/Pet' }
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
responses: {
|
|
468
|
+
'200': {
|
|
469
|
+
description: 'Success',
|
|
470
|
+
content: {
|
|
471
|
+
'application/json': {
|
|
472
|
+
schema: { $ref: '#/components/schemas/Pet' }
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
components: {
|
|
481
|
+
schemas: {
|
|
482
|
+
Pet: {
|
|
483
|
+
type: 'object',
|
|
484
|
+
properties: {
|
|
485
|
+
id: { type: 'integer' },
|
|
486
|
+
name: { type: 'string' }
|
|
487
|
+
},
|
|
488
|
+
required: ['name']
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
const code = generator.generate(spec);
|
|
494
|
+
expect(code).toContain('Pet.safeParse(response)');
|
|
495
|
+
expect(code).toContain('parsedPet.success');
|
|
496
|
+
expect(code).toContain('new ResponseValidationError<Pet>');
|
|
497
|
+
expect(code).toContain('parsedPet.data');
|
|
498
|
+
expect(code).not.toContain('Pet.parse(');
|
|
499
|
+
});
|
|
500
|
+
it('should not use safeParse for methods without response schemas', () => {
|
|
501
|
+
const spec = {
|
|
502
|
+
openapi: '3.0.0',
|
|
503
|
+
info: {
|
|
504
|
+
title: 'Test API',
|
|
505
|
+
version: '1.0.0'
|
|
506
|
+
},
|
|
507
|
+
paths: {
|
|
508
|
+
'/pets/{id}': {
|
|
509
|
+
delete: {
|
|
510
|
+
operationId: 'deletePet',
|
|
511
|
+
parameters: [
|
|
512
|
+
{
|
|
513
|
+
name: 'id',
|
|
514
|
+
in: 'path',
|
|
515
|
+
required: true,
|
|
516
|
+
schema: { type: 'integer' }
|
|
517
|
+
}
|
|
518
|
+
],
|
|
519
|
+
responses: {
|
|
520
|
+
'204': {
|
|
521
|
+
description: 'Deleted'
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
const code = generator.generate(spec);
|
|
529
|
+
expect(code).toContain('async deletePet');
|
|
530
|
+
expect(code).toContain('return await this.makeRequest');
|
|
531
|
+
expect(code).not.toContain('safeParse');
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
describe('buildSchema', () => {
|
|
535
|
+
it('should build schema for string type', () => {
|
|
536
|
+
const schema = { type: 'string' };
|
|
537
|
+
const result = generator.buildSchema(schema, true);
|
|
538
|
+
expect(result).toBeDefined();
|
|
539
|
+
});
|
|
540
|
+
it('should build schema for number type', () => {
|
|
541
|
+
const schema = { type: 'number' };
|
|
542
|
+
const result = generator.buildSchema(schema, true);
|
|
543
|
+
expect(result).toBeDefined();
|
|
544
|
+
});
|
|
545
|
+
it('should build schema for boolean type', () => {
|
|
546
|
+
const schema = { type: 'boolean' };
|
|
547
|
+
const result = generator.buildSchema(schema, true);
|
|
548
|
+
expect(result).toBeDefined();
|
|
549
|
+
});
|
|
550
|
+
it('should handle optional fields', () => {
|
|
551
|
+
const schema = { type: 'string' };
|
|
552
|
+
const result = generator.buildSchema(schema, false);
|
|
553
|
+
expect(result).toBeDefined();
|
|
554
|
+
});
|
|
555
|
+
it('should handle string formats', () => {
|
|
556
|
+
const schema = { type: 'string', format: 'email' };
|
|
557
|
+
const result = generator.buildSchema(schema, true);
|
|
558
|
+
expect(result).toBeDefined();
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
describe('circular dependencies', () => {
|
|
562
|
+
it('should use z.lazy() for direct self-referencing schemas', () => {
|
|
563
|
+
const spec = {
|
|
564
|
+
openapi: '3.0.0',
|
|
565
|
+
info: {
|
|
566
|
+
title: 'Test API',
|
|
567
|
+
version: '1.0.0'
|
|
568
|
+
},
|
|
569
|
+
paths: {},
|
|
570
|
+
components: {
|
|
571
|
+
schemas: {
|
|
572
|
+
TreeNode: {
|
|
573
|
+
type: 'object',
|
|
574
|
+
properties: {
|
|
575
|
+
value: { type: 'string' },
|
|
576
|
+
children: {
|
|
577
|
+
type: 'array',
|
|
578
|
+
items: { $ref: '#/components/schemas/TreeNode' }
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
required: ['value']
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
const code = generator.generate(spec);
|
|
587
|
+
expect(code).toContain('z.lazy(() => TreeNode)');
|
|
588
|
+
});
|
|
589
|
+
it('should use z.lazy() for indirect circular dependencies (A -> B -> A)', () => {
|
|
590
|
+
const spec = {
|
|
591
|
+
openapi: '3.0.0',
|
|
592
|
+
info: {
|
|
593
|
+
title: 'Test API',
|
|
594
|
+
version: '1.0.0'
|
|
595
|
+
},
|
|
596
|
+
paths: {},
|
|
597
|
+
components: {
|
|
598
|
+
schemas: {
|
|
599
|
+
Person: {
|
|
600
|
+
type: 'object',
|
|
601
|
+
properties: {
|
|
602
|
+
name: { type: 'string' },
|
|
603
|
+
bestFriend: { $ref: '#/components/schemas/Friend' }
|
|
604
|
+
},
|
|
605
|
+
required: ['name']
|
|
606
|
+
},
|
|
607
|
+
Friend: {
|
|
608
|
+
type: 'object',
|
|
609
|
+
properties: {
|
|
610
|
+
nickname: { type: 'string' },
|
|
611
|
+
person: { $ref: '#/components/schemas/Person' }
|
|
612
|
+
},
|
|
613
|
+
required: ['nickname']
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
const code = generator.generate(spec);
|
|
619
|
+
// Both references should use z.lazy()
|
|
620
|
+
expect(code).toContain('z.lazy(() => Friend)');
|
|
621
|
+
expect(code).toContain('z.lazy(() => Person)');
|
|
622
|
+
});
|
|
623
|
+
it('should not use z.lazy() for non-circular references', () => {
|
|
624
|
+
const spec = {
|
|
625
|
+
openapi: '3.0.0',
|
|
626
|
+
info: {
|
|
627
|
+
title: 'Test API',
|
|
628
|
+
version: '1.0.0'
|
|
629
|
+
},
|
|
630
|
+
paths: {},
|
|
631
|
+
components: {
|
|
632
|
+
schemas: {
|
|
633
|
+
User: {
|
|
634
|
+
type: 'object',
|
|
635
|
+
properties: {
|
|
636
|
+
id: { type: 'integer' },
|
|
637
|
+
profile: { $ref: '#/components/schemas/Profile' }
|
|
638
|
+
},
|
|
639
|
+
required: ['id']
|
|
640
|
+
},
|
|
641
|
+
Profile: {
|
|
642
|
+
type: 'object',
|
|
643
|
+
properties: {
|
|
644
|
+
name: { type: 'string' }
|
|
645
|
+
},
|
|
646
|
+
required: ['name']
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
const code = generator.generate(spec);
|
|
652
|
+
// Profile should be referenced directly, not with z.lazy()
|
|
653
|
+
expect(code).not.toContain('z.lazy(() => Profile)');
|
|
654
|
+
expect(code).toContain('profile: Profile.optional()');
|
|
655
|
+
});
|
|
656
|
+
it('should handle complex circular chains (A -> B -> C -> A)', () => {
|
|
657
|
+
const spec = {
|
|
658
|
+
openapi: '3.0.0',
|
|
659
|
+
info: {
|
|
660
|
+
title: 'Test API',
|
|
661
|
+
version: '1.0.0'
|
|
662
|
+
},
|
|
663
|
+
paths: {},
|
|
664
|
+
components: {
|
|
665
|
+
schemas: {
|
|
666
|
+
Alpha: {
|
|
667
|
+
type: 'object',
|
|
668
|
+
properties: {
|
|
669
|
+
beta: { $ref: '#/components/schemas/Beta' }
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
Beta: {
|
|
673
|
+
type: 'object',
|
|
674
|
+
properties: {
|
|
675
|
+
gamma: { $ref: '#/components/schemas/Gamma' }
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
Gamma: {
|
|
679
|
+
type: 'object',
|
|
680
|
+
properties: {
|
|
681
|
+
alpha: { $ref: '#/components/schemas/Alpha' }
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
const code = generator.generate(spec);
|
|
688
|
+
// All references in the cycle should use z.lazy()
|
|
689
|
+
expect(code).toContain('z.lazy(() => Beta)');
|
|
690
|
+
expect(code).toContain('z.lazy(() => Gamma)');
|
|
691
|
+
expect(code).toContain('z.lazy(() => Alpha)');
|
|
692
|
+
});
|
|
693
|
+
it('should handle self-referencing schemas in arrays', () => {
|
|
694
|
+
const spec = {
|
|
695
|
+
openapi: '3.0.0',
|
|
696
|
+
info: {
|
|
697
|
+
title: 'Test API',
|
|
698
|
+
version: '1.0.0'
|
|
699
|
+
},
|
|
700
|
+
paths: {},
|
|
701
|
+
components: {
|
|
702
|
+
schemas: {
|
|
703
|
+
Category: {
|
|
704
|
+
type: 'object',
|
|
705
|
+
properties: {
|
|
706
|
+
name: { type: 'string' },
|
|
707
|
+
subcategories: {
|
|
708
|
+
type: 'array',
|
|
709
|
+
items: { $ref: '#/components/schemas/Category' }
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
required: ['name']
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
const code = generator.generate(spec);
|
|
718
|
+
expect(code).toContain('z.array(z.lazy(() => Category))');
|
|
719
|
+
});
|
|
720
|
+
it('should handle self-referencing schemas in anyOf', () => {
|
|
721
|
+
const spec = {
|
|
722
|
+
openapi: '3.0.0',
|
|
723
|
+
info: {
|
|
724
|
+
title: 'Test API',
|
|
725
|
+
version: '1.0.0'
|
|
726
|
+
},
|
|
727
|
+
paths: {},
|
|
728
|
+
components: {
|
|
729
|
+
schemas: {
|
|
730
|
+
Expression: {
|
|
731
|
+
type: 'object',
|
|
732
|
+
properties: {
|
|
733
|
+
value: {
|
|
734
|
+
anyOf: [{ type: 'string' }, { $ref: '#/components/schemas/Expression' }]
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
const code = generator.generate(spec);
|
|
742
|
+
expect(code).toContain('z.lazy(() => Expression)');
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
describe('logical operators edge cases', () => {
|
|
746
|
+
it('should handle anyOf with basic type schemas', () => {
|
|
747
|
+
const spec = {
|
|
748
|
+
openapi: '3.0.0',
|
|
749
|
+
info: {
|
|
750
|
+
title: 'Test API',
|
|
751
|
+
version: '1.0.0'
|
|
752
|
+
},
|
|
753
|
+
paths: {},
|
|
754
|
+
components: {
|
|
755
|
+
schemas: {
|
|
756
|
+
StringOrNumber: {
|
|
757
|
+
anyOf: [{ type: 'string' }, { type: 'number' }]
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
const code = generator.generate(spec);
|
|
763
|
+
expect(code).toContain('z.union');
|
|
764
|
+
expect(code).toContain('z.string()');
|
|
765
|
+
expect(code).toContain('z.number()');
|
|
766
|
+
});
|
|
767
|
+
it('should handle oneOf with object schemas', () => {
|
|
768
|
+
const spec = {
|
|
769
|
+
openapi: '3.0.0',
|
|
770
|
+
info: {
|
|
771
|
+
title: 'Test API',
|
|
772
|
+
version: '1.0.0'
|
|
773
|
+
},
|
|
774
|
+
paths: {},
|
|
775
|
+
components: {
|
|
776
|
+
schemas: {
|
|
777
|
+
Variant: {
|
|
778
|
+
oneOf: [
|
|
779
|
+
{ type: 'object', properties: { name: { type: 'string' } } },
|
|
780
|
+
{ type: 'object', properties: { id: { type: 'number' } } }
|
|
781
|
+
]
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
const code = generator.generate(spec);
|
|
787
|
+
expect(code).toContain('z.union');
|
|
788
|
+
});
|
|
789
|
+
it('should handle allOf with multiple schemas', () => {
|
|
790
|
+
const spec = {
|
|
791
|
+
openapi: '3.0.0',
|
|
792
|
+
info: {
|
|
793
|
+
title: 'Test API',
|
|
794
|
+
version: '1.0.0'
|
|
795
|
+
},
|
|
796
|
+
paths: {},
|
|
797
|
+
components: {
|
|
798
|
+
schemas: {
|
|
799
|
+
Combined: {
|
|
800
|
+
allOf: [
|
|
801
|
+
{ type: 'object', properties: { id: { type: 'number' } } },
|
|
802
|
+
{ type: 'object', properties: { name: { type: 'string' } } }
|
|
803
|
+
]
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
const code = generator.generate(spec);
|
|
809
|
+
expect(code).toContain('z.intersection');
|
|
810
|
+
});
|
|
811
|
+
it('should handle object type with empty properties in logical operators', () => {
|
|
812
|
+
const spec = {
|
|
813
|
+
openapi: '3.0.0',
|
|
814
|
+
info: {
|
|
815
|
+
title: 'Test API',
|
|
816
|
+
version: '1.0.0'
|
|
817
|
+
},
|
|
818
|
+
paths: {},
|
|
819
|
+
components: {
|
|
820
|
+
schemas: {
|
|
821
|
+
EmptyObject: {
|
|
822
|
+
anyOf: [{ type: 'object', properties: {} }]
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
const code = generator.generate(spec);
|
|
828
|
+
// Empty object should fallback to record type
|
|
829
|
+
expect(code).toContain('z.record');
|
|
830
|
+
});
|
|
831
|
+
it('should handle array type without items in logical operators', () => {
|
|
832
|
+
const spec = {
|
|
833
|
+
openapi: '3.0.0',
|
|
834
|
+
info: {
|
|
835
|
+
title: 'Test API',
|
|
836
|
+
version: '1.0.0'
|
|
837
|
+
},
|
|
838
|
+
paths: {},
|
|
839
|
+
components: {
|
|
840
|
+
schemas: {
|
|
841
|
+
GenericArray: {
|
|
842
|
+
anyOf: [{ type: 'array' }]
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
const code = generator.generate(spec);
|
|
848
|
+
// Array without items should use z.array() with unknown
|
|
849
|
+
expect(code).toContain('z.array');
|
|
850
|
+
expect(code).toContain('z.unknown()');
|
|
851
|
+
});
|
|
852
|
+
it('should handle unknown type in logical operators', () => {
|
|
853
|
+
const spec = {
|
|
854
|
+
openapi: '3.0.0',
|
|
855
|
+
info: {
|
|
856
|
+
title: 'Test API',
|
|
857
|
+
version: '1.0.0'
|
|
858
|
+
},
|
|
859
|
+
paths: {},
|
|
860
|
+
components: {
|
|
861
|
+
schemas: {
|
|
862
|
+
UnknownType: {
|
|
863
|
+
anyOf: [{ type: 'unknown' }]
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
const code = generator.generate(spec);
|
|
869
|
+
expect(code).toContain('z.unknown()');
|
|
870
|
+
});
|
|
871
|
+
it('should handle non-object schema in logical operators', () => {
|
|
872
|
+
const spec = {
|
|
873
|
+
openapi: '3.0.0',
|
|
874
|
+
info: {
|
|
875
|
+
title: 'Test API',
|
|
876
|
+
version: '1.0.0'
|
|
877
|
+
},
|
|
878
|
+
paths: {},
|
|
879
|
+
components: {
|
|
880
|
+
schemas: {
|
|
881
|
+
InvalidSchema: {
|
|
882
|
+
anyOf: [null]
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
const code = generator.generate(spec);
|
|
888
|
+
// Should fallback to unknown for invalid schemas
|
|
889
|
+
expect(code).toContain('z.unknown()');
|
|
890
|
+
});
|
|
891
|
+
it('should handle integer type in logical operators', () => {
|
|
892
|
+
const spec = {
|
|
893
|
+
openapi: '3.0.0',
|
|
894
|
+
info: {
|
|
895
|
+
title: 'Test API',
|
|
896
|
+
version: '1.0.0'
|
|
897
|
+
},
|
|
898
|
+
paths: {},
|
|
899
|
+
components: {
|
|
900
|
+
schemas: {
|
|
901
|
+
IntOrString: {
|
|
902
|
+
anyOf: [{ type: 'integer' }, { type: 'string' }]
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
const code = generator.generate(spec);
|
|
908
|
+
expect(code).toContain('z.number().int()');
|
|
909
|
+
expect(code).toContain('z.string()');
|
|
910
|
+
});
|
|
911
|
+
it('should handle boolean type in logical operators', () => {
|
|
912
|
+
const spec = {
|
|
913
|
+
openapi: '3.0.0',
|
|
914
|
+
info: {
|
|
915
|
+
title: 'Test API',
|
|
916
|
+
version: '1.0.0'
|
|
917
|
+
},
|
|
918
|
+
paths: {},
|
|
919
|
+
components: {
|
|
920
|
+
schemas: {
|
|
921
|
+
BoolOrString: {
|
|
922
|
+
anyOf: [{ type: 'boolean' }, { type: 'string' }]
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
const code = generator.generate(spec);
|
|
928
|
+
expect(code).toContain('z.boolean()');
|
|
929
|
+
expect(code).toContain('z.string()');
|
|
930
|
+
});
|
|
931
|
+
it('should handle object type with properties in logical operators', () => {
|
|
932
|
+
const spec = {
|
|
933
|
+
openapi: '3.0.0',
|
|
934
|
+
info: {
|
|
935
|
+
title: 'Test API',
|
|
936
|
+
version: '1.0.0'
|
|
937
|
+
},
|
|
938
|
+
paths: {},
|
|
939
|
+
components: {
|
|
940
|
+
schemas: {
|
|
941
|
+
ObjectVariant: {
|
|
942
|
+
anyOf: [
|
|
943
|
+
{
|
|
944
|
+
type: 'object',
|
|
945
|
+
properties: {
|
|
946
|
+
name: { type: 'string' },
|
|
947
|
+
age: { type: 'number' }
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
]
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
const code = generator.generate(spec);
|
|
956
|
+
expect(code).toContain('z.union');
|
|
957
|
+
expect(code).toContain('name');
|
|
958
|
+
expect(code).toContain('age');
|
|
959
|
+
});
|
|
960
|
+
it('should handle array type with items in logical operators', () => {
|
|
961
|
+
const spec = {
|
|
962
|
+
openapi: '3.0.0',
|
|
963
|
+
info: {
|
|
964
|
+
title: 'Test API',
|
|
965
|
+
version: '1.0.0'
|
|
966
|
+
},
|
|
967
|
+
paths: {},
|
|
968
|
+
components: {
|
|
969
|
+
schemas: {
|
|
970
|
+
ArrayVariant: {
|
|
971
|
+
anyOf: [
|
|
972
|
+
{
|
|
973
|
+
type: 'array',
|
|
974
|
+
items: { type: 'string' }
|
|
975
|
+
}
|
|
976
|
+
]
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
const code = generator.generate(spec);
|
|
982
|
+
expect(code).toContain('z.union');
|
|
983
|
+
expect(code).toContain('z.array');
|
|
984
|
+
expect(code).toContain('z.string()');
|
|
985
|
+
});
|
|
986
|
+
it('should handle schema without type property in logical operators', () => {
|
|
987
|
+
const spec = {
|
|
988
|
+
openapi: '3.0.0',
|
|
989
|
+
info: {
|
|
990
|
+
title: 'Test API',
|
|
991
|
+
version: '1.0.0'
|
|
992
|
+
},
|
|
993
|
+
paths: {},
|
|
994
|
+
components: {
|
|
995
|
+
schemas: {
|
|
996
|
+
InvalidSchema: {
|
|
997
|
+
anyOf: [{}]
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const code = generator.generate(spec);
|
|
1003
|
+
// Should fallback to unknown for schemas without type
|
|
1004
|
+
expect(code).toContain('z.unknown()');
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
describe('server variables', () => {
|
|
1008
|
+
it('should generate server configuration with variables', () => {
|
|
1009
|
+
const spec = {
|
|
1010
|
+
openapi: '3.0.0',
|
|
1011
|
+
info: {
|
|
1012
|
+
title: 'Test API',
|
|
1013
|
+
version: '1.0.0'
|
|
1014
|
+
},
|
|
1015
|
+
servers: [
|
|
1016
|
+
{
|
|
1017
|
+
url: 'https://{environment}.example.com:{port}/v{version}',
|
|
1018
|
+
variables: {
|
|
1019
|
+
environment: {
|
|
1020
|
+
default: 'api',
|
|
1021
|
+
enum: ['api', 'api.staging', 'api.prod']
|
|
1022
|
+
},
|
|
1023
|
+
port: {
|
|
1024
|
+
default: '443'
|
|
1025
|
+
},
|
|
1026
|
+
version: {
|
|
1027
|
+
default: '1'
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
],
|
|
1032
|
+
paths: {}
|
|
1033
|
+
};
|
|
1034
|
+
const code = generator.generate(spec);
|
|
1035
|
+
expect(code).toContain('serverConfigurations');
|
|
1036
|
+
expect(code).toContain('serverVariables');
|
|
1037
|
+
expect(code).toContain('resolveServerUrl');
|
|
1038
|
+
expect(code).toContain('environment');
|
|
1039
|
+
expect(code).toContain('port');
|
|
1040
|
+
expect(code).toContain('version');
|
|
1041
|
+
});
|
|
1042
|
+
it('should handle server variables with enum values', () => {
|
|
1043
|
+
const spec = {
|
|
1044
|
+
openapi: '3.0.0',
|
|
1045
|
+
info: {
|
|
1046
|
+
title: 'Test API',
|
|
1047
|
+
version: '1.0.0'
|
|
1048
|
+
},
|
|
1049
|
+
servers: [
|
|
1050
|
+
{
|
|
1051
|
+
url: 'https://{env}.example.com',
|
|
1052
|
+
variables: {
|
|
1053
|
+
env: {
|
|
1054
|
+
default: 'prod',
|
|
1055
|
+
enum: ['dev', 'staging', 'prod'],
|
|
1056
|
+
description: 'Environment'
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
],
|
|
1061
|
+
paths: {}
|
|
1062
|
+
};
|
|
1063
|
+
const code = generator.generate(spec);
|
|
1064
|
+
expect(code).toContain('enum');
|
|
1065
|
+
expect(code).toContain('dev');
|
|
1066
|
+
expect(code).toContain('staging');
|
|
1067
|
+
expect(code).toContain('prod');
|
|
1068
|
+
});
|
|
1069
|
+
it('should handle multiple servers with different variables', () => {
|
|
1070
|
+
const spec = {
|
|
1071
|
+
openapi: '3.0.0',
|
|
1072
|
+
info: {
|
|
1073
|
+
title: 'Test API',
|
|
1074
|
+
version: '1.0.0'
|
|
1075
|
+
},
|
|
1076
|
+
servers: [
|
|
1077
|
+
{
|
|
1078
|
+
url: 'https://api.example.com'
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
url: 'https://{env}.example.com',
|
|
1082
|
+
variables: {
|
|
1083
|
+
env: {
|
|
1084
|
+
default: 'staging'
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
],
|
|
1089
|
+
paths: {}
|
|
1090
|
+
};
|
|
1091
|
+
const code = generator.generate(spec);
|
|
1092
|
+
expect(code).toContain('serverConfigurations');
|
|
1093
|
+
// Should have both servers
|
|
1094
|
+
expect(code).toContain('https://api.example.com');
|
|
1095
|
+
expect(code).toContain('https://{env}.example.com');
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
describe('explicit types', () => {
|
|
1099
|
+
it('should generate explicit interface for object schema', () => {
|
|
1100
|
+
const spec = {
|
|
1101
|
+
openapi: '3.0.0',
|
|
1102
|
+
info: {
|
|
1103
|
+
title: 'Test API',
|
|
1104
|
+
version: '1.0.0'
|
|
1105
|
+
},
|
|
1106
|
+
paths: {},
|
|
1107
|
+
components: {
|
|
1108
|
+
schemas: {
|
|
1109
|
+
Order: {
|
|
1110
|
+
type: 'object',
|
|
1111
|
+
properties: {
|
|
1112
|
+
id: { type: 'integer' },
|
|
1113
|
+
name: { type: 'string' }
|
|
1114
|
+
},
|
|
1115
|
+
required: ['id', 'name']
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
const code = generator.generate(spec);
|
|
1121
|
+
// Should generate explicit interface
|
|
1122
|
+
expect(code).toContain('export interface Order');
|
|
1123
|
+
expect(code).toContain('id: number');
|
|
1124
|
+
expect(code).toContain('name: string');
|
|
1125
|
+
// Should add type annotation to schema
|
|
1126
|
+
expect(code).toContain('export const Order: z.ZodType<Order>');
|
|
1127
|
+
// Should NOT generate z.infer type export
|
|
1128
|
+
expect(code).not.toContain('z.infer<typeof Order>');
|
|
1129
|
+
});
|
|
1130
|
+
it('should generate type alias for enum schema', () => {
|
|
1131
|
+
const spec = {
|
|
1132
|
+
openapi: '3.0.0',
|
|
1133
|
+
info: {
|
|
1134
|
+
title: 'Test API',
|
|
1135
|
+
version: '1.0.0'
|
|
1136
|
+
},
|
|
1137
|
+
paths: {},
|
|
1138
|
+
components: {
|
|
1139
|
+
schemas: {
|
|
1140
|
+
Status: {
|
|
1141
|
+
type: 'string',
|
|
1142
|
+
enum: ['active', 'inactive', 'pending']
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
const code = generator.generate(spec);
|
|
1148
|
+
// Should generate type alias (not interface) for enum
|
|
1149
|
+
expect(code).toContain('export type Status');
|
|
1150
|
+
expect(code).toContain("'active'");
|
|
1151
|
+
expect(code).toContain("'inactive'");
|
|
1152
|
+
expect(code).toContain("'pending'");
|
|
1153
|
+
// Should add type annotation to schema
|
|
1154
|
+
expect(code).toContain('export const Status: z.ZodType<Status>');
|
|
1155
|
+
});
|
|
1156
|
+
it('should generate type alias for array schema', () => {
|
|
1157
|
+
const spec = {
|
|
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
|
+
const code = generator.generate(spec);
|
|
1174
|
+
// Should generate type alias for array
|
|
1175
|
+
expect(code).toContain('export type Tags = string[]');
|
|
1176
|
+
// Should add type annotation to schema
|
|
1177
|
+
expect(code).toContain('export const Tags: z.ZodType<Tags>');
|
|
1178
|
+
});
|
|
1179
|
+
it('should handle nested objects with references', () => {
|
|
1180
|
+
const spec = {
|
|
1181
|
+
openapi: '3.0.0',
|
|
1182
|
+
info: {
|
|
1183
|
+
title: 'Test API',
|
|
1184
|
+
version: '1.0.0'
|
|
1185
|
+
},
|
|
1186
|
+
paths: {},
|
|
1187
|
+
components: {
|
|
1188
|
+
schemas: {
|
|
1189
|
+
User: {
|
|
1190
|
+
type: 'object',
|
|
1191
|
+
properties: {
|
|
1192
|
+
id: { type: 'integer' },
|
|
1193
|
+
profile: { $ref: '#/components/schemas/Profile' }
|
|
1194
|
+
},
|
|
1195
|
+
required: ['id']
|
|
1196
|
+
},
|
|
1197
|
+
Profile: {
|
|
1198
|
+
type: 'object',
|
|
1199
|
+
properties: {
|
|
1200
|
+
name: { type: 'string' }
|
|
1201
|
+
},
|
|
1202
|
+
required: ['name']
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
const code = generator.generate(spec);
|
|
1208
|
+
// Should generate interfaces for both
|
|
1209
|
+
expect(code).toContain('export interface User');
|
|
1210
|
+
expect(code).toContain('export interface Profile');
|
|
1211
|
+
// User should reference Profile type
|
|
1212
|
+
expect(code).toContain('profile?: Profile');
|
|
1213
|
+
// Both should have type annotations
|
|
1214
|
+
expect(code).toContain('export const User: z.ZodType<User>');
|
|
1215
|
+
expect(code).toContain('export const Profile: z.ZodType<Profile>');
|
|
1216
|
+
});
|
|
1217
|
+
it('should handle union types (anyOf)', () => {
|
|
1218
|
+
const spec = {
|
|
1219
|
+
openapi: '3.0.0',
|
|
1220
|
+
info: {
|
|
1221
|
+
title: 'Test API',
|
|
1222
|
+
version: '1.0.0'
|
|
1223
|
+
},
|
|
1224
|
+
paths: {},
|
|
1225
|
+
components: {
|
|
1226
|
+
schemas: {
|
|
1227
|
+
StringOrNumber: {
|
|
1228
|
+
anyOf: [{ type: 'string' }, { type: 'number' }]
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
const code = generator.generate(spec);
|
|
1234
|
+
// Should generate type alias for union
|
|
1235
|
+
expect(code).toContain('export type StringOrNumber = string | number');
|
|
1236
|
+
// Should add type annotation to schema
|
|
1237
|
+
expect(code).toContain('export const StringOrNumber: z.ZodType<StringOrNumber>');
|
|
1238
|
+
});
|
|
1239
|
+
it('should handle intersection types (allOf)', () => {
|
|
1240
|
+
const spec = {
|
|
1241
|
+
openapi: '3.0.0',
|
|
1242
|
+
info: {
|
|
1243
|
+
title: 'Test API',
|
|
1244
|
+
version: '1.0.0'
|
|
1245
|
+
},
|
|
1246
|
+
paths: {},
|
|
1247
|
+
components: {
|
|
1248
|
+
schemas: {
|
|
1249
|
+
Base: {
|
|
1250
|
+
type: 'object',
|
|
1251
|
+
properties: {
|
|
1252
|
+
id: { type: 'integer' }
|
|
1253
|
+
},
|
|
1254
|
+
required: ['id']
|
|
1255
|
+
},
|
|
1256
|
+
Extended: {
|
|
1257
|
+
allOf: [
|
|
1258
|
+
{ $ref: '#/components/schemas/Base' },
|
|
1259
|
+
{
|
|
1260
|
+
type: 'object',
|
|
1261
|
+
properties: {
|
|
1262
|
+
name: { type: 'string' }
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
]
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
const code = generator.generate(spec);
|
|
1271
|
+
// Should generate interface for Base
|
|
1272
|
+
expect(code).toContain('export interface Base');
|
|
1273
|
+
// Should generate type alias for Extended (intersection)
|
|
1274
|
+
expect(code).toContain('export type Extended = Base &');
|
|
1275
|
+
// Should add type annotations to schemas
|
|
1276
|
+
expect(code).toContain('export const Base: z.ZodType<Base>');
|
|
1277
|
+
expect(code).toContain('export const Extended: z.ZodType<Extended>');
|
|
1278
|
+
});
|
|
1279
|
+
it('should handle optional properties', () => {
|
|
1280
|
+
const spec = {
|
|
1281
|
+
openapi: '3.0.0',
|
|
1282
|
+
info: {
|
|
1283
|
+
title: 'Test API',
|
|
1284
|
+
version: '1.0.0'
|
|
1285
|
+
},
|
|
1286
|
+
paths: {},
|
|
1287
|
+
components: {
|
|
1288
|
+
schemas: {
|
|
1289
|
+
User: {
|
|
1290
|
+
type: 'object',
|
|
1291
|
+
properties: {
|
|
1292
|
+
id: { type: 'integer' },
|
|
1293
|
+
email: { type: 'string' }
|
|
1294
|
+
},
|
|
1295
|
+
required: ['id']
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
const code = generator.generate(spec);
|
|
1301
|
+
// id should be required (no ?)
|
|
1302
|
+
expect(code).toContain('id: number');
|
|
1303
|
+
// email should be optional (with ?)
|
|
1304
|
+
expect(code).toContain('email?: string');
|
|
1305
|
+
});
|
|
1306
|
+
it('should handle circular dependencies', () => {
|
|
1307
|
+
const spec = {
|
|
1308
|
+
openapi: '3.0.0',
|
|
1309
|
+
info: {
|
|
1310
|
+
title: 'Test API',
|
|
1311
|
+
version: '1.0.0'
|
|
1312
|
+
},
|
|
1313
|
+
paths: {},
|
|
1314
|
+
components: {
|
|
1315
|
+
schemas: {
|
|
1316
|
+
Node: {
|
|
1317
|
+
type: 'object',
|
|
1318
|
+
properties: {
|
|
1319
|
+
id: { type: 'integer' },
|
|
1320
|
+
children: {
|
|
1321
|
+
type: 'array',
|
|
1322
|
+
items: { $ref: '#/components/schemas/Node' }
|
|
1323
|
+
}
|
|
1324
|
+
},
|
|
1325
|
+
required: ['id']
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
const code = generator.generate(spec);
|
|
1331
|
+
// Should generate interface with self-reference
|
|
1332
|
+
expect(code).toContain('export interface Node');
|
|
1333
|
+
expect(code).toContain('children?: Node[]');
|
|
1334
|
+
// Should add type annotation to schema
|
|
1335
|
+
expect(code).toContain('export const Node: z.ZodType<Node>');
|
|
1336
|
+
// Zod schema should use z.lazy for circular reference
|
|
1337
|
+
expect(code).toContain('z.lazy');
|
|
1338
|
+
});
|
|
1339
|
+
it('should handle numeric enum types', () => {
|
|
1340
|
+
const spec = {
|
|
1341
|
+
openapi: '3.0.0',
|
|
1342
|
+
info: {
|
|
1343
|
+
title: 'Test API',
|
|
1344
|
+
version: '1.0.0'
|
|
1345
|
+
},
|
|
1346
|
+
paths: {},
|
|
1347
|
+
components: {
|
|
1348
|
+
schemas: {
|
|
1349
|
+
Priority: {
|
|
1350
|
+
type: 'integer',
|
|
1351
|
+
enum: [0, 1, 2]
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
const code = generator.generate(spec);
|
|
1357
|
+
// Should generate type alias for numeric enum
|
|
1358
|
+
expect(code).toContain('export type Priority');
|
|
1359
|
+
expect(code).toMatch(/0\s*\|\s*1\s*\|\s*2/);
|
|
1360
|
+
// Should add type annotation to schema
|
|
1361
|
+
expect(code).toContain('export const Priority: z.ZodType<Priority>');
|
|
1362
|
+
});
|
|
1363
|
+
});
|
|
1364
|
+
});
|