zod-codegen 1.0.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.
Files changed (76) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +93 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +70 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +87 -0
  4. package/.github/dependabot.yml +76 -0
  5. package/.github/workflows/ci.yml +143 -0
  6. package/.github/workflows/release.yml +65 -0
  7. package/.husky/commit-msg +2 -0
  8. package/.husky/pre-commit +5 -0
  9. package/.lintstagedrc.json +4 -0
  10. package/.nvmrc +1 -0
  11. package/.prettierrc.json +7 -0
  12. package/.releaserc.json +159 -0
  13. package/CHANGELOG.md +24 -0
  14. package/CONTRIBUTING.md +274 -0
  15. package/LICENCE +201 -0
  16. package/README.md +263 -0
  17. package/SECURITY.md +108 -0
  18. package/codecov.yml +29 -0
  19. package/commitlint.config.mjs +28 -0
  20. package/dist/scripts/update-manifest.js +31 -0
  21. package/dist/src/assets/manifest.json +5 -0
  22. package/dist/src/cli.js +60 -0
  23. package/dist/src/generator.js +55 -0
  24. package/dist/src/http/fetch-client.js +141 -0
  25. package/dist/src/interfaces/code-generator.js +1 -0
  26. package/dist/src/interfaces/file-reader.js +1 -0
  27. package/dist/src/polyfills/fetch.js +18 -0
  28. package/dist/src/services/code-generator.service.js +419 -0
  29. package/dist/src/services/file-reader.service.js +25 -0
  30. package/dist/src/services/file-writer.service.js +32 -0
  31. package/dist/src/services/import-builder.service.js +45 -0
  32. package/dist/src/services/type-builder.service.js +42 -0
  33. package/dist/src/types/http.js +10 -0
  34. package/dist/src/types/openapi.js +173 -0
  35. package/dist/src/utils/error-handler.js +11 -0
  36. package/dist/src/utils/execution-time.js +3 -0
  37. package/dist/src/utils/manifest.js +9 -0
  38. package/dist/src/utils/reporter.js +15 -0
  39. package/dist/src/utils/signal-handler.js +12 -0
  40. package/dist/src/utils/tty.js +3 -0
  41. package/dist/tests/integration/cli.test.js +25 -0
  42. package/dist/tests/unit/generator.test.js +29 -0
  43. package/dist/vitest.config.js +38 -0
  44. package/eslint.config.mjs +33 -0
  45. package/package.json +102 -0
  46. package/samples/openapi.json +1 -0
  47. package/samples/saris-openapi.json +7122 -0
  48. package/samples/swagger-petstore.yaml +802 -0
  49. package/samples/swagger-saris.yaml +3585 -0
  50. package/samples/test-logical.yaml +50 -0
  51. package/scripts/update-manifest.js +31 -0
  52. package/scripts/update-manifest.ts +47 -0
  53. package/src/assets/manifest.json +5 -0
  54. package/src/cli.ts +68 -0
  55. package/src/generator.ts +61 -0
  56. package/src/http/fetch-client.ts +181 -0
  57. package/src/interfaces/code-generator.ts +25 -0
  58. package/src/interfaces/file-reader.ts +15 -0
  59. package/src/polyfills/fetch.ts +26 -0
  60. package/src/services/code-generator.service.ts +775 -0
  61. package/src/services/file-reader.service.ts +29 -0
  62. package/src/services/file-writer.service.ts +36 -0
  63. package/src/services/import-builder.service.ts +64 -0
  64. package/src/services/type-builder.service.ts +77 -0
  65. package/src/types/http.ts +35 -0
  66. package/src/types/openapi.ts +202 -0
  67. package/src/utils/error-handler.ts +13 -0
  68. package/src/utils/execution-time.ts +3 -0
  69. package/src/utils/manifest.ts +17 -0
  70. package/src/utils/reporter.ts +16 -0
  71. package/src/utils/signal-handler.ts +14 -0
  72. package/src/utils/tty.ts +3 -0
  73. package/tests/integration/cli.test.ts +29 -0
  74. package/tests/unit/generator.test.ts +36 -0
  75. package/tsconfig.json +44 -0
  76. package/vitest.config.ts +39 -0
@@ -0,0 +1,775 @@
1
+ import jp from 'jsonpath';
2
+ import * as ts from 'typescript';
3
+ import {z} from 'zod';
4
+ import type {CodeGenerator, SchemaBuilder} from '../interfaces/code-generator.js';
5
+ import type {MethodSchemaType, OpenApiSpecType, ReferenceType} from '../types/openapi.js';
6
+ import {MethodSchema, Reference, SchemaProperties} from '../types/openapi.js';
7
+ import {TypeScriptImportBuilderService} from './import-builder.service.js';
8
+ import {TypeScriptTypeBuilderService} from './type-builder.service.js';
9
+
10
+ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuilder {
11
+ private readonly typeBuilder = new TypeScriptTypeBuilderService();
12
+ private readonly importBuilder = new TypeScriptImportBuilderService();
13
+ private readonly printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
14
+
15
+ private readonly ZodAST = z.object({
16
+ type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'unknown', 'record']),
17
+ args: z.array(z.unknown()).optional(),
18
+ });
19
+
20
+ generate(spec: OpenApiSpecType): string {
21
+ const file = ts.createSourceFile('generated.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
22
+ const nodes = this.buildAST(spec);
23
+ return this.printer.printList(ts.ListFormat.MultiLine, ts.factory.createNodeArray(nodes), file);
24
+ }
25
+
26
+ buildSchema(schema: unknown, required = true): ts.CallExpression | ts.Identifier {
27
+ const safeCategorySchema = SchemaProperties.safeParse(schema);
28
+ if (safeCategorySchema.success) {
29
+ const safeCategory = safeCategorySchema.data;
30
+
31
+ if (safeCategory.anyOf && Array.isArray(safeCategory.anyOf) && safeCategory.anyOf.length > 0) {
32
+ return this.handleLogicalOperator('anyOf', safeCategory.anyOf, required);
33
+ }
34
+
35
+ if (safeCategory.oneOf && Array.isArray(safeCategory.oneOf) && safeCategory.oneOf.length > 0) {
36
+ return this.handleLogicalOperator('oneOf', safeCategory.oneOf, required);
37
+ }
38
+
39
+ if (safeCategory.allOf && Array.isArray(safeCategory.allOf) && safeCategory.allOf.length > 0) {
40
+ return this.handleLogicalOperator('allOf', safeCategory.allOf, required);
41
+ }
42
+
43
+ if (safeCategory.not) {
44
+ return this.handleLogicalOperator('not', [safeCategory.not], required);
45
+ }
46
+
47
+ return this.buildProperty(safeCategory, required);
48
+ }
49
+
50
+ throw safeCategorySchema.error;
51
+ }
52
+
53
+ private buildAST(openapi: OpenApiSpecType): ts.Statement[] {
54
+ const imports = this.importBuilder.buildImports();
55
+ const schemas = this.buildSchemas(openapi);
56
+ const clientClass = this.buildClientClass(openapi, schemas);
57
+ const baseUrlConstant = this.buildBaseUrlConstant(openapi);
58
+
59
+ return [
60
+ this.createComment('Imports'),
61
+ ...imports,
62
+ this.createComment('Components schemas'),
63
+ ...Object.values(schemas),
64
+ ...baseUrlConstant,
65
+ this.createComment('Client class'),
66
+ clientClass,
67
+ ];
68
+ }
69
+
70
+ private buildSchemas(openapi: OpenApiSpecType): Record<string, ts.VariableStatement> {
71
+ const schemasEntries = Object.entries(openapi.components?.schemas ?? {});
72
+ const sortedSchemaNames = this.topologicalSort(Object.fromEntries(schemasEntries));
73
+
74
+ return sortedSchemaNames.reduce<Record<string, ts.VariableStatement>>((schemaRegistered, name) => {
75
+ const schema = openapi.components?.schemas?.[name];
76
+ if (!schema) return schemaRegistered;
77
+
78
+ const variableStatement = ts.factory.createVariableStatement(
79
+ [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
80
+ ts.factory.createVariableDeclarationList(
81
+ [
82
+ ts.factory.createVariableDeclaration(
83
+ ts.factory.createIdentifier(this.typeBuilder.sanitizeIdentifier(name)),
84
+ undefined,
85
+ undefined,
86
+ this.buildSchema(schema),
87
+ ),
88
+ ],
89
+ ts.NodeFlags.Const,
90
+ ),
91
+ );
92
+
93
+ return {
94
+ ...schemaRegistered,
95
+ [name]: variableStatement,
96
+ };
97
+ }, {});
98
+ }
99
+
100
+ private buildClientClass(
101
+ openapi: OpenApiSpecType,
102
+ schemas: Record<string, ts.VariableStatement>,
103
+ ): ts.ClassDeclaration {
104
+ const clientName = this.generateClientName(openapi.info.title);
105
+ const methods = this.buildClientMethods(openapi, schemas);
106
+
107
+ return ts.factory.createClassDeclaration(
108
+ [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
109
+ ts.factory.createIdentifier(clientName),
110
+ undefined,
111
+ undefined,
112
+ [
113
+ this.typeBuilder.createProperty('#baseUrl', 'string', true),
114
+ this.buildConstructor(),
115
+ this.buildHttpRequestMethod(),
116
+ ...methods,
117
+ ],
118
+ );
119
+ }
120
+
121
+ private buildConstructor(): ts.ConstructorDeclaration {
122
+ return ts.factory.createConstructorDeclaration(
123
+ undefined,
124
+ [
125
+ this.typeBuilder.createParameter('baseUrl', 'string', ts.factory.createIdentifier('defaultBaseUrl')),
126
+ this.typeBuilder.createParameter('_', 'unknown', undefined, true),
127
+ ],
128
+ ts.factory.createBlock(
129
+ [
130
+ ts.factory.createExpressionStatement(
131
+ ts.factory.createBinaryExpression(
132
+ ts.factory.createPropertyAccessExpression(
133
+ ts.factory.createThis(),
134
+ ts.factory.createPrivateIdentifier('#baseUrl'),
135
+ ),
136
+ ts.factory.createToken(ts.SyntaxKind.EqualsToken),
137
+ ts.factory.createIdentifier('baseUrl'),
138
+ ),
139
+ ),
140
+ ],
141
+ true,
142
+ ),
143
+ );
144
+ }
145
+
146
+ private buildHttpRequestMethod(): ts.MethodDeclaration {
147
+ return ts.factory.createMethodDeclaration(
148
+ [ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)],
149
+ undefined,
150
+ ts.factory.createPrivateIdentifier('#makeRequest'),
151
+ undefined,
152
+ [this.typeBuilder.createGenericType('T')],
153
+ [
154
+ this.typeBuilder.createParameter('method', 'string'),
155
+ this.typeBuilder.createParameter('path', 'string'),
156
+ this.typeBuilder.createParameter(
157
+ 'options',
158
+ 'unknown',
159
+ ts.factory.createObjectLiteralExpression([], false),
160
+ true,
161
+ ),
162
+ ],
163
+ ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Promise'), [
164
+ ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('T'), undefined),
165
+ ]),
166
+ ts.factory.createBlock(
167
+ [
168
+ ts.factory.createVariableStatement(
169
+ undefined,
170
+ ts.factory.createVariableDeclarationList(
171
+ [
172
+ ts.factory.createVariableDeclaration(
173
+ ts.factory.createIdentifier('url'),
174
+ undefined,
175
+ undefined,
176
+ ts.factory.createTemplateExpression(ts.factory.createTemplateHead('', ''), [
177
+ ts.factory.createTemplateSpan(
178
+ ts.factory.createPropertyAccessExpression(
179
+ ts.factory.createThis(),
180
+ ts.factory.createPrivateIdentifier('#baseUrl'),
181
+ ),
182
+ ts.factory.createTemplateMiddle('', ''),
183
+ ),
184
+ ts.factory.createTemplateSpan(
185
+ ts.factory.createIdentifier('path'),
186
+ ts.factory.createTemplateTail('', ''),
187
+ ),
188
+ ]),
189
+ ),
190
+ ],
191
+ ts.NodeFlags.Const,
192
+ ),
193
+ ),
194
+ ts.factory.createVariableStatement(
195
+ undefined,
196
+ ts.factory.createVariableDeclarationList(
197
+ [
198
+ ts.factory.createVariableDeclaration(
199
+ ts.factory.createIdentifier('response'),
200
+ undefined,
201
+ undefined,
202
+ ts.factory.createAwaitExpression(
203
+ ts.factory.createCallExpression(ts.factory.createIdentifier('fetch'), undefined, [
204
+ ts.factory.createIdentifier('url'),
205
+ ts.factory.createObjectLiteralExpression(
206
+ [
207
+ ts.factory.createShorthandPropertyAssignment(
208
+ ts.factory.createIdentifier('method'),
209
+ undefined,
210
+ ),
211
+ ts.factory.createPropertyAssignment(
212
+ ts.factory.createIdentifier('headers'),
213
+ ts.factory.createObjectLiteralExpression(
214
+ [
215
+ ts.factory.createPropertyAssignment(
216
+ ts.factory.createStringLiteral('Content-Type', true),
217
+ ts.factory.createStringLiteral('application/json', true),
218
+ ),
219
+ ],
220
+ true,
221
+ ),
222
+ ),
223
+ ],
224
+ true,
225
+ ),
226
+ ]),
227
+ ),
228
+ ),
229
+ ],
230
+ ts.NodeFlags.Const,
231
+ ),
232
+ ),
233
+ ts.factory.createIfStatement(
234
+ ts.factory.createPrefixUnaryExpression(
235
+ ts.SyntaxKind.ExclamationToken,
236
+ ts.factory.createPropertyAccessExpression(
237
+ ts.factory.createIdentifier('response'),
238
+ ts.factory.createIdentifier('ok'),
239
+ ),
240
+ ),
241
+ ts.factory.createThrowStatement(
242
+ ts.factory.createNewExpression(ts.factory.createIdentifier('Error'), undefined, [
243
+ ts.factory.createTemplateExpression(ts.factory.createTemplateHead('HTTP ', 'HTTP '), [
244
+ ts.factory.createTemplateSpan(
245
+ ts.factory.createPropertyAccessExpression(
246
+ ts.factory.createIdentifier('response'),
247
+ ts.factory.createIdentifier('status'),
248
+ ),
249
+ ts.factory.createTemplateMiddle(': ', ': '),
250
+ ),
251
+ ts.factory.createTemplateSpan(
252
+ ts.factory.createPropertyAccessExpression(
253
+ ts.factory.createIdentifier('response'),
254
+ ts.factory.createIdentifier('statusText'),
255
+ ),
256
+ ts.factory.createTemplateTail('', ''),
257
+ ),
258
+ ]),
259
+ ]),
260
+ ),
261
+ undefined,
262
+ ),
263
+ ts.factory.createReturnStatement(
264
+ ts.factory.createAwaitExpression(
265
+ ts.factory.createCallExpression(
266
+ ts.factory.createPropertyAccessExpression(
267
+ ts.factory.createIdentifier('response'),
268
+ ts.factory.createIdentifier('json'),
269
+ ),
270
+ undefined,
271
+ [],
272
+ ),
273
+ ),
274
+ ),
275
+ ],
276
+ true,
277
+ ),
278
+ );
279
+ }
280
+
281
+ private buildClientMethods(
282
+ openapi: OpenApiSpecType,
283
+ schemas: Record<string, ts.VariableStatement>,
284
+ ): ts.MethodDeclaration[] {
285
+ return Object.entries(openapi.paths).reduce<ts.MethodDeclaration[]>((endpoints, [path, pathItem]) => {
286
+ const methods = Object.entries(pathItem)
287
+ .filter(([method]) => ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method))
288
+ .map(([method, methodSchema]) => {
289
+ const safeMethodSchema = MethodSchema.parse(methodSchema);
290
+
291
+ if (!safeMethodSchema.operationId) {
292
+ return null;
293
+ }
294
+
295
+ return this.buildEndpointMethod(method, path, safeMethodSchema, schemas);
296
+ })
297
+ .filter((method): method is ts.MethodDeclaration => method !== null);
298
+
299
+ return [...endpoints, ...methods];
300
+ }, []);
301
+ }
302
+
303
+ private buildEndpointMethod(
304
+ method: string,
305
+ path: string,
306
+ schema: MethodSchemaType,
307
+ schemas: Record<string, ts.VariableStatement>,
308
+ ): ts.MethodDeclaration {
309
+ const parameters = this.buildMethodParameters(schema, schemas);
310
+ const responseType = this.getResponseType(schema, schemas);
311
+
312
+ return ts.factory.createMethodDeclaration(
313
+ [ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)],
314
+ undefined,
315
+ ts.factory.createIdentifier(String(schema.operationId)),
316
+ undefined,
317
+ undefined,
318
+ parameters,
319
+ responseType,
320
+ ts.factory.createBlock(
321
+ [
322
+ ts.factory.createReturnStatement(
323
+ ts.factory.createAwaitExpression(
324
+ ts.factory.createCallExpression(
325
+ ts.factory.createPropertyAccessExpression(
326
+ ts.factory.createThis(),
327
+ ts.factory.createPrivateIdentifier('#makeRequest'),
328
+ ),
329
+ undefined,
330
+ [
331
+ ts.factory.createStringLiteral(method.toUpperCase(), true),
332
+ ts.factory.createStringLiteral(path, true),
333
+ ],
334
+ ),
335
+ ),
336
+ ),
337
+ ],
338
+ true,
339
+ ),
340
+ );
341
+ }
342
+
343
+ private buildMethodParameters(
344
+ schema: MethodSchemaType,
345
+ schemas: Record<string, ts.VariableStatement>,
346
+ ): ts.ParameterDeclaration[] {
347
+ void schemas; // Mark as intentionally unused
348
+ const parameters: ts.ParameterDeclaration[] = [];
349
+
350
+ if (schema.parameters) {
351
+ schema.parameters.forEach((param) => {
352
+ if (param.in === 'path' && param.required) {
353
+ parameters.push(
354
+ this.typeBuilder.createParameter(
355
+ this.typeBuilder.sanitizeIdentifier(param.name),
356
+ 'string',
357
+ undefined,
358
+ false,
359
+ ),
360
+ );
361
+ }
362
+ });
363
+ }
364
+
365
+ parameters.push(this.typeBuilder.createParameter('_', 'unknown', undefined, true));
366
+
367
+ return parameters;
368
+ }
369
+
370
+ private getResponseType(
371
+ schema: MethodSchemaType,
372
+ schemas: Record<string, ts.VariableStatement>,
373
+ ): ts.TypeNode | undefined {
374
+ void schemas; // Mark as intentionally unused
375
+ const response200 = schema.responses?.['200'];
376
+ if (!response200?.content?.['application/json']?.schema) {
377
+ return undefined;
378
+ }
379
+
380
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Promise'), [
381
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
382
+ ]);
383
+ }
384
+
385
+ private buildBaseUrlConstant(openapi: OpenApiSpecType): ts.Statement[] {
386
+ const baseUrl = openapi.servers?.[0]?.url;
387
+ if (!baseUrl) {
388
+ return [];
389
+ }
390
+
391
+ return [
392
+ ts.factory.createVariableStatement(
393
+ undefined,
394
+ ts.factory.createVariableDeclarationList(
395
+ [
396
+ ts.factory.createVariableDeclaration(
397
+ ts.factory.createIdentifier('defaultBaseUrl'),
398
+ undefined,
399
+ undefined,
400
+ ts.factory.createStringLiteral(baseUrl, true),
401
+ ),
402
+ ],
403
+ ts.NodeFlags.Const,
404
+ ),
405
+ ),
406
+ ];
407
+ }
408
+
409
+ private generateClientName(title: string): string {
410
+ return title
411
+ .split(/[^a-zA-Z0-9]/g)
412
+ .map((word) => this.typeBuilder.toPascalCase(word))
413
+ .join('');
414
+ }
415
+
416
+ private createComment(text: string): ts.Statement {
417
+ const commentNode = ts.factory.createIdentifier('\n');
418
+ ts.addSyntheticTrailingComment(commentNode, ts.SyntaxKind.SingleLineCommentTrivia, ` ${text}`, true);
419
+ return ts.factory.createExpressionStatement(commentNode);
420
+ }
421
+
422
+ private buildZodAST(input: (string | z.infer<typeof this.ZodAST>)[]): ts.CallExpression {
423
+ const [initial, ...rest] = input;
424
+
425
+ const safeInitial = this.ZodAST.safeParse(initial);
426
+
427
+ const initialExpression = !safeInitial.success
428
+ ? ts.factory.createCallExpression(
429
+ ts.factory.createPropertyAccessExpression(
430
+ ts.factory.createIdentifier('z'),
431
+ ts.factory.createIdentifier(this.ZodAST.shape.type.parse(initial)),
432
+ ),
433
+ undefined,
434
+ [],
435
+ )
436
+ : ts.factory.createCallExpression(
437
+ ts.factory.createPropertyAccessExpression(
438
+ ts.factory.createIdentifier('z'),
439
+ ts.factory.createIdentifier(safeInitial.data.type),
440
+ ),
441
+ undefined,
442
+ (safeInitial.data.args ?? []) as ts.Expression[],
443
+ );
444
+
445
+ return rest.reduce((expression, exp: unknown) => {
446
+ const safeExp = this.ZodAST.safeParse(exp);
447
+ return !safeExp.success
448
+ ? ts.factory.createCallExpression(
449
+ ts.factory.createPropertyAccessExpression(
450
+ expression,
451
+ ts.factory.createIdentifier(typeof exp === 'string' ? exp : String(exp)),
452
+ ),
453
+ undefined,
454
+ [],
455
+ )
456
+ : ts.factory.createCallExpression(
457
+ ts.factory.createPropertyAccessExpression(expression, ts.factory.createIdentifier(safeExp.data.type)),
458
+ undefined,
459
+ (safeExp.data.args ?? []) as ts.Expression[],
460
+ );
461
+ }, initialExpression);
462
+ }
463
+
464
+ private buildProperty(property: unknown, required = false): ts.CallExpression | ts.Identifier {
465
+ const safeProperty = SchemaProperties.safeParse(property);
466
+
467
+ if (!safeProperty.success) {
468
+ return this.buildZodAST(['unknown']);
469
+ }
470
+
471
+ const prop = safeProperty.data;
472
+
473
+ if (this.isReference(prop)) {
474
+ return this.buildFromReference(prop);
475
+ }
476
+
477
+ const methodsToApply: string[] = [];
478
+
479
+ if (prop.anyOf && Array.isArray(prop.anyOf) && prop.anyOf.length > 0) {
480
+ return this.handleLogicalOperator('anyOf', prop.anyOf, required);
481
+ }
482
+
483
+ if (prop.oneOf && Array.isArray(prop.oneOf) && prop.oneOf.length > 0) {
484
+ return this.handleLogicalOperator('oneOf', prop.oneOf, required);
485
+ }
486
+
487
+ if (prop.allOf && Array.isArray(prop.allOf) && prop.allOf.length > 0) {
488
+ return this.handleLogicalOperator('allOf', prop.allOf, required);
489
+ }
490
+
491
+ if (prop.not) {
492
+ return this.handleLogicalOperator('not', [prop.not], required);
493
+ }
494
+
495
+ switch (prop.type) {
496
+ case 'array':
497
+ return this.buildZodAST([
498
+ {
499
+ type: 'array',
500
+ args: prop.items ? [this.buildProperty(prop.items, true)] : [],
501
+ },
502
+ ...(!required ? ['optional'] : []),
503
+ ]);
504
+ case 'object': {
505
+ const {properties = {}, required: propRequired = []} = prop as {
506
+ properties?: Record<string, unknown>;
507
+ required?: string[];
508
+ };
509
+
510
+ const propertiesEntries = Object.entries(properties);
511
+
512
+ if (propertiesEntries.length > 0) {
513
+ return this.buildZodAST([
514
+ {
515
+ type: 'object',
516
+ args: [
517
+ ts.factory.createObjectLiteralExpression(
518
+ propertiesEntries.map(([name, propValue]): ts.ObjectLiteralElementLike => {
519
+ return ts.factory.createPropertyAssignment(
520
+ ts.factory.createIdentifier(name),
521
+ this.buildProperty(propValue, propRequired.includes(name)),
522
+ );
523
+ }),
524
+ true,
525
+ ),
526
+ ],
527
+ },
528
+ ...(!required ? ['optional'] : []),
529
+ ]);
530
+ }
531
+
532
+ return this.buildZodAST([
533
+ {
534
+ type: 'record',
535
+ args: [this.buildZodAST(['string']), this.buildZodAST(['unknown'])],
536
+ },
537
+ ]);
538
+ }
539
+ case 'integer':
540
+ methodsToApply.push('int');
541
+ return this.buildZodAST(['number', ...methodsToApply, ...(!required ? ['optional'] : [])]);
542
+ case 'number':
543
+ return this.buildZodAST(['number', ...(!required ? ['optional'] : [])]);
544
+ case 'string':
545
+ return this.buildZodAST(['string', ...(!required ? ['optional'] : [])]);
546
+ case 'boolean':
547
+ return this.buildZodAST(['boolean', ...(!required ? ['optional'] : [])]);
548
+ case 'unknown':
549
+ default:
550
+ return this.buildZodAST(['unknown', ...(!required ? ['optional'] : [])]);
551
+ }
552
+ }
553
+
554
+ private handleLogicalOperator(
555
+ operator: 'anyOf' | 'oneOf' | 'allOf' | 'not',
556
+ schemas: unknown[],
557
+ required: boolean,
558
+ ): ts.CallExpression {
559
+ const logicalExpression = this.buildLogicalOperator(operator, schemas);
560
+ return required
561
+ ? logicalExpression
562
+ : ts.factory.createCallExpression(
563
+ ts.factory.createPropertyAccessExpression(logicalExpression, ts.factory.createIdentifier('optional')),
564
+ undefined,
565
+ [],
566
+ );
567
+ }
568
+
569
+ private buildLogicalOperator(operator: 'anyOf' | 'oneOf' | 'allOf' | 'not', schemas: unknown[]): ts.CallExpression {
570
+ switch (operator) {
571
+ case 'anyOf':
572
+ case 'oneOf': {
573
+ const unionSchemas = schemas.map((schema) => this.buildSchemaFromLogicalOperator(schema));
574
+ return ts.factory.createCallExpression(
575
+ ts.factory.createPropertyAccessExpression(
576
+ ts.factory.createIdentifier('z'),
577
+ ts.factory.createIdentifier('union'),
578
+ ),
579
+ undefined,
580
+ [ts.factory.createArrayLiteralExpression(unionSchemas, false)],
581
+ );
582
+ }
583
+ case 'allOf': {
584
+ if (schemas.length === 0) {
585
+ throw new Error('allOf requires at least one schema');
586
+ }
587
+
588
+ const firstSchema = this.buildSchemaFromLogicalOperator(schemas[0]);
589
+ return schemas.slice(1).reduce<ts.Expression>((acc, schema) => {
590
+ const schemaExpression = this.buildSchemaFromLogicalOperator(schema);
591
+ return ts.factory.createCallExpression(
592
+ ts.factory.createPropertyAccessExpression(
593
+ ts.factory.createIdentifier('z'),
594
+ ts.factory.createIdentifier('intersection'),
595
+ ),
596
+ undefined,
597
+ [acc, schemaExpression],
598
+ );
599
+ }, firstSchema) as ts.CallExpression;
600
+ }
601
+ case 'not': {
602
+ const notSchema = this.buildSchemaFromLogicalOperator(schemas[0]);
603
+ return ts.factory.createCallExpression(
604
+ ts.factory.createPropertyAccessExpression(
605
+ ts.factory.createPropertyAccessExpression(
606
+ ts.factory.createIdentifier('z'),
607
+ ts.factory.createIdentifier('any'),
608
+ ),
609
+ ts.factory.createIdentifier('refine'),
610
+ ),
611
+ undefined,
612
+ [
613
+ ts.factory.createArrowFunction(
614
+ undefined,
615
+ undefined,
616
+ [ts.factory.createParameterDeclaration(undefined, undefined, 'val', undefined, undefined, undefined)],
617
+ undefined,
618
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
619
+ ts.factory.createPrefixUnaryExpression(
620
+ ts.SyntaxKind.ExclamationToken,
621
+ ts.factory.createCallExpression(
622
+ ts.factory.createPropertyAccessExpression(notSchema, ts.factory.createIdentifier('safeParse')),
623
+ undefined,
624
+ [ts.factory.createIdentifier('val')],
625
+ ),
626
+ ),
627
+ ),
628
+ ts.factory.createObjectLiteralExpression([
629
+ ts.factory.createPropertyAssignment(
630
+ ts.factory.createIdentifier('message'),
631
+ ts.factory.createStringLiteral('Value must not match the excluded schema'),
632
+ ),
633
+ ]),
634
+ ],
635
+ );
636
+ }
637
+ default:
638
+ throw new Error(`Unsupported logical operator: ${String(operator)}`);
639
+ }
640
+ }
641
+
642
+ private buildSchemaFromLogicalOperator(schema: unknown): ts.CallExpression | ts.Identifier {
643
+ if (this.isReference(schema)) {
644
+ return this.buildFromReference(schema);
645
+ }
646
+
647
+ const safeSchema = SchemaProperties.safeParse(schema);
648
+ if (safeSchema.success) {
649
+ return this.buildProperty(safeSchema.data, true);
650
+ }
651
+
652
+ return this.buildBasicTypeFromSchema(schema);
653
+ }
654
+
655
+ private buildBasicTypeFromSchema(schema: unknown): ts.CallExpression | ts.Identifier {
656
+ if (typeof schema === 'object' && schema !== null && 'type' in schema) {
657
+ const schemaObj = schema as {type: string; properties?: Record<string, unknown>; items?: unknown};
658
+
659
+ switch (schemaObj.type) {
660
+ case 'string':
661
+ return this.buildZodAST(['string']);
662
+ case 'number':
663
+ return this.buildZodAST(['number']);
664
+ case 'integer':
665
+ return this.buildZodAST(['number', 'int']);
666
+ case 'boolean':
667
+ return this.buildZodAST(['boolean']);
668
+ case 'object':
669
+ return this.buildObjectTypeFromSchema(schemaObj);
670
+ case 'array':
671
+ return this.buildArrayTypeFromSchema(schemaObj);
672
+ default:
673
+ return this.buildZodAST(['unknown']);
674
+ }
675
+ }
676
+
677
+ return this.buildZodAST(['unknown']);
678
+ }
679
+
680
+ private buildObjectTypeFromSchema(schemaObj: {properties?: Record<string, unknown>}): ts.CallExpression {
681
+ const properties = Object.entries(schemaObj.properties ?? {});
682
+
683
+ if (properties.length > 0) {
684
+ return this.buildZodAST([
685
+ {
686
+ type: 'object',
687
+ args: [
688
+ ts.factory.createObjectLiteralExpression(
689
+ properties.map(([name, property]): ts.ObjectLiteralElementLike => {
690
+ return ts.factory.createPropertyAssignment(
691
+ ts.factory.createIdentifier(name),
692
+ this.buildSchemaFromLogicalOperator(property),
693
+ );
694
+ }),
695
+ true,
696
+ ),
697
+ ],
698
+ },
699
+ ]);
700
+ }
701
+
702
+ return this.buildZodAST([
703
+ {
704
+ type: 'record',
705
+ args: [this.buildZodAST(['string']), this.buildZodAST(['unknown'])],
706
+ },
707
+ ]);
708
+ }
709
+
710
+ private buildArrayTypeFromSchema(schemaObj: {items?: unknown}): ts.CallExpression {
711
+ if (schemaObj.items) {
712
+ return this.buildZodAST([
713
+ {
714
+ type: 'array',
715
+ args: [this.buildSchemaFromLogicalOperator(schemaObj.items)],
716
+ },
717
+ ]);
718
+ }
719
+ return this.buildZodAST(['array']);
720
+ }
721
+
722
+ private isReference(reference: unknown): reference is ReferenceType {
723
+ if (typeof reference === 'object' && reference !== null && '$ref' in reference) {
724
+ const ref = reference as {$ref?: unknown};
725
+ return typeof ref.$ref === 'string' && ref.$ref.length > 0;
726
+ }
727
+ return false;
728
+ }
729
+
730
+ private buildFromReference(reference: ReferenceType): ts.Identifier {
731
+ const {$ref = ''} = Reference.parse(reference);
732
+ const refName = $ref.split('/').pop() ?? 'never';
733
+ return ts.factory.createIdentifier(this.typeBuilder.sanitizeIdentifier(refName));
734
+ }
735
+
736
+ private topologicalSort(schemas: Record<string, unknown>): string[] {
737
+ const visited = new Set<string>();
738
+ const visiting = new Set<string>();
739
+ const result: string[] = [];
740
+
741
+ const visit = (name: string) => {
742
+ if (visiting.has(name)) {
743
+ return;
744
+ }
745
+ if (visited.has(name)) {
746
+ return;
747
+ }
748
+
749
+ visiting.add(name);
750
+ const schema = schemas[name];
751
+ const dependencies = jp
752
+ .query(schema, '$..["$ref"]')
753
+ .filter((ref: string) => ref.startsWith('#/components/schemas/'))
754
+ .map((ref: string) => ref.replace('#/components/schemas/', ''));
755
+
756
+ for (const dep of dependencies) {
757
+ if (schemas[dep]) {
758
+ visit(dep);
759
+ }
760
+ }
761
+
762
+ visiting.delete(name);
763
+ visited.add(name);
764
+ result.push(name);
765
+ };
766
+
767
+ for (const name of Object.keys(schemas)) {
768
+ if (!visited.has(name)) {
769
+ visit(name);
770
+ }
771
+ }
772
+
773
+ return result;
774
+ }
775
+ }