@ygorazambuja/sauron 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.
@@ -0,0 +1,1599 @@
1
+ /**
2
+ * OpenAPI to TypeScript Converter Utilities
3
+ *
4
+ * This module provides utilities to convert OpenAPI/Swagger JSON schemas
5
+ * into TypeScript interface and type definitions. It handles:
6
+ *
7
+ * - Object schemas → TypeScript interfaces
8
+ * - Enum schemas → TypeScript union types
9
+ * - Primitive types (string, number, boolean)
10
+ * - Arrays with proper typing
11
+ * - Nullable properties
12
+ * - Schema references ($ref)
13
+ * - Date-time format conversion
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { readJsonFile, verifySwaggerComposition, createModels } from './utils';
18
+ *
19
+ * const swaggerData = await readJsonFile('swagger.json');
20
+ * const validatedSchema = verifySwaggerComposition(swaggerData);
21
+ * const typeDefinitions = createModels(validatedSchema);
22
+ *
23
+ * // typeDefinitions contains strings like:
24
+ * // "export interface User { id: number; name: string; }"
25
+ * // "export type Status = 'active' | 'inactive';"
26
+ * ```
27
+ *
28
+ * @since 1.0.0
29
+ */
30
+
31
+ import type { z } from "zod";
32
+ import { SwaggerOrOpenAPISchema } from "../schemas/swagger";
33
+
34
+ /**
35
+ * Represents an OpenAPI path operation (GET, POST, PUT, DELETE)
36
+ */
37
+ export type OpenApiOperation = {
38
+ tags?: string[];
39
+ operationId?: string;
40
+ parameters?: Array<{
41
+ name: string;
42
+ in: "query" | "path" | "header" | "cookie";
43
+ required?: boolean;
44
+ schema: OpenApiSchema;
45
+ }>;
46
+ requestBody?: {
47
+ content: Record<string, { schema: OpenApiSchema }>;
48
+ };
49
+ responses?: Record<
50
+ string,
51
+ {
52
+ description?: string;
53
+ content?: Record<string, { schema: OpenApiSchema }>;
54
+ }
55
+ >;
56
+ };
57
+
58
+ /**
59
+ * Represents an OpenAPI path with its operations
60
+ */
61
+ export type OpenApiPath = Record<string, OpenApiOperation>;
62
+
63
+ /**
64
+ * Represents an OpenAPI schema definition object
65
+ */
66
+ export type OpenApiSchema = Record<string, unknown> & {
67
+ type?: string;
68
+ properties?: Record<string, OpenApiSchema>;
69
+ required?: string[];
70
+ enum?: unknown[];
71
+ items?: OpenApiSchema;
72
+ anyOf?: OpenApiSchema[];
73
+ oneOf?: OpenApiSchema[];
74
+ allOf?: OpenApiSchema[];
75
+ $ref?: string;
76
+ nullable?: boolean;
77
+ format?: string;
78
+ };
79
+
80
+ export type OperationTypeInfo = {
81
+ requestType?: string;
82
+ responseType?: string;
83
+ };
84
+
85
+ export type OperationTypeMap = Record<
86
+ string,
87
+ Record<string, OperationTypeInfo>
88
+ >;
89
+
90
+ export type TypeNameMap = Map<string, string>;
91
+
92
+ /**
93
+ * Read json file.
94
+ * @param filePath Input parameter `filePath`.
95
+ * @returns Read json file output as `Promise<unknown>`.
96
+ * @example
97
+ * ```ts
98
+ * const result = await readJsonFile("value");
99
+ * // result: unknown
100
+ * ```
101
+ */
102
+ export async function readJsonFile(filePath: string): Promise<unknown> {
103
+ if (!filePath || typeof filePath !== "string") {
104
+ throw new Error("File path must be a non-empty string");
105
+ }
106
+
107
+ try {
108
+ const file = Bun.file(filePath);
109
+ const content = await file.text();
110
+ return JSON.parse(content);
111
+ } catch (error) {
112
+ throw new Error(
113
+ `Failed to read or parse JSON file "${filePath}": ${error}`,
114
+ );
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Fetch json from url.
120
+ * @param url Input parameter `url`.
121
+ * @returns Fetch json from url output as `Promise<unknown>`.
122
+ * @example
123
+ * ```ts
124
+ * const result = await fetchJsonFromUrl("value");
125
+ * // result: unknown
126
+ * ```
127
+ */
128
+ export async function fetchJsonFromUrl(url: string): Promise<unknown> {
129
+ if (!url || typeof url !== "string") {
130
+ throw new Error("URL must be a non-empty string");
131
+ }
132
+
133
+ try {
134
+ const response = await fetch(url);
135
+ if (!response.ok) {
136
+ throw new Error(`HTTP error! status: ${response.status}`);
137
+ }
138
+ const content = await response.text();
139
+ return JSON.parse(content);
140
+ } catch (error) {
141
+ throw new Error(`Failed to fetch or parse JSON from "${url}": ${error}`);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Verify swagger composition.
147
+ * @param swaggerData Input parameter `swaggerData`.
148
+ * @returns Verify swagger composition output as `z.infer<typeof SwaggerOrOpenAPISchema>`.
149
+ * @example
150
+ * ```ts
151
+ * const result = verifySwaggerComposition({});
152
+ * // result: z.infer<typeof SwaggerOrOpenAPISchema>
153
+ * ```
154
+ */
155
+ export function verifySwaggerComposition(
156
+ swaggerData: Record<string, unknown>,
157
+ ): z.infer<typeof SwaggerOrOpenAPISchema> {
158
+ if (!swaggerData || typeof swaggerData !== "object") {
159
+ throw new Error("Swagger data must be a valid object");
160
+ }
161
+
162
+ const { data, error } = SwaggerOrOpenAPISchema.safeParse(swaggerData);
163
+
164
+ if (error) {
165
+ throw new Error(`Invalid Swagger/OpenAPI schema: ${error.message}`);
166
+ }
167
+
168
+ return data;
169
+ }
170
+
171
+ /**
172
+ * Create models.
173
+ * @param data Input parameter `data`.
174
+ * @returns Create models output as `string[]`.
175
+ * @example
176
+ * ```ts
177
+ * const result = createModels({});
178
+ * // result: string[]
179
+ * ```
180
+ */
181
+ export function createModels(
182
+ data: z.infer<typeof SwaggerOrOpenAPISchema>,
183
+ ): string[] {
184
+ const { models } = createModelsWithOperationTypes(data);
185
+ return models;
186
+ }
187
+
188
+ /**
189
+ * Create models with operation types.
190
+ * @param data Input parameter `data`.
191
+ * @returns Create models with operation types output as `unknown`.
192
+ * @example
193
+ * ```ts
194
+ * const result = createModelsWithOperationTypes({});
195
+ * // result: unknown
196
+ * ```
197
+ */
198
+ export function createModelsWithOperationTypes(
199
+ data: z.infer<typeof SwaggerOrOpenAPISchema>,
200
+ ): {
201
+ models: string[];
202
+ operationTypes: OperationTypeMap;
203
+ typeNameMap: TypeNameMap;
204
+ } {
205
+ // Handle both OpenAPI 3.0+ (components.schemas) and Swagger 2.0 (definitions)
206
+ const schemas =
207
+ (data as any).components?.schemas || (data as any).definitions;
208
+ const schemaEntries = schemas ? Object.entries(schemas) : [];
209
+ const typeDefinitions: string[] = [];
210
+ const { typeNameMap, usedTypeNames } = createTypeNameMap(schemas);
211
+
212
+ if (!schemas) {
213
+ console.warn("Warning: No schema definitions found in OpenAPI components");
214
+ }
215
+
216
+ for (const [modelName, schemaDefinition] of schemaEntries) {
217
+ if (modelName && schemaDefinition) {
218
+ const typedSchemaDefinition = schemaDefinition as OpenApiSchema;
219
+ const resolvedName = resolveTypeName(modelName, typeNameMap);
220
+ const typeScriptCode = generateTypeScriptDefinition(
221
+ resolvedName,
222
+ typedSchemaDefinition,
223
+ typeNameMap,
224
+ );
225
+ typeDefinitions.push(typeScriptCode);
226
+ }
227
+ }
228
+
229
+ const { typeDefinitions: inlineDefinitions, operationTypes } =
230
+ collectInlineOperationTypes(data, usedTypeNames, typeNameMap);
231
+
232
+ if (typeDefinitions.length === 0 && inlineDefinitions.length === 0) {
233
+ console.warn("Warning: No schema definitions found in OpenAPI components");
234
+ return { models: [], operationTypes, typeNameMap };
235
+ }
236
+
237
+ return {
238
+ models: [...typeDefinitions, ...inlineDefinitions],
239
+ operationTypes,
240
+ typeNameMap,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Create angular http client methods.
246
+ * @param data Input parameter `data`.
247
+ * @param operationTypes Input parameter `operationTypes`.
248
+ * @param typeNameMap Input parameter `typeNameMap`.
249
+ * @returns Create angular http client methods output as `unknown`.
250
+ * @example
251
+ * ```ts
252
+ * const result = createAngularHttpClientMethods({}, {}, {});
253
+ * // result: unknown
254
+ * ```
255
+ */
256
+ export function createAngularHttpClientMethods(
257
+ data: z.infer<typeof SwaggerOrOpenAPISchema>,
258
+ operationTypes?: OperationTypeMap,
259
+ typeNameMap?: TypeNameMap,
260
+ ): { methods: string[]; imports: string[]; paramsInterfaces: string[] } {
261
+ if (!data.paths) {
262
+ throw new Error(
263
+ "No paths found in OpenAPI specification. Ensure your Swagger file has paths defined.",
264
+ );
265
+ }
266
+
267
+ const methods: string[] = [];
268
+ const paramsInterfaces: string[] = [];
269
+ const pathEntries = Object.entries(data.paths);
270
+ const usedMethodNames = new Set<string>();
271
+ const usedTypes = new Set<string>();
272
+ const resolvedTypeNameMap =
273
+ typeNameMap ??
274
+ createTypeNameMap(
275
+ ((data as any).components?.schemas || (data as any).definitions) as
276
+ | Record<string, OpenApiSchema>
277
+ | undefined,
278
+ ).typeNameMap;
279
+
280
+ if (pathEntries.length === 0) {
281
+ console.warn("Warning: No path definitions found in OpenAPI specification");
282
+ return { methods, imports: [], paramsInterfaces: [] };
283
+ }
284
+
285
+ for (const [path, pathItem] of pathEntries) {
286
+ const result = generateMethodsForPath(
287
+ path,
288
+ pathItem as OpenApiPath,
289
+ usedMethodNames,
290
+ data.components,
291
+ usedTypes,
292
+ operationTypes,
293
+ resolvedTypeNameMap,
294
+ );
295
+ methods.push(...result.methods);
296
+ paramsInterfaces.push(...result.paramsInterfaces);
297
+ }
298
+
299
+ // Generate imports for used types
300
+ const imports = Array.from(usedTypes).sort();
301
+
302
+ return { methods, imports, paramsInterfaces };
303
+ }
304
+
305
+ /**
306
+ * To pascal case.
307
+ * @param value Input parameter `value`.
308
+ * @returns To pascal case output as `string`.
309
+ * @example
310
+ * ```ts
311
+ * const result = toPascalCase("value");
312
+ * // result: string
313
+ * ```
314
+ */
315
+ function toPascalCase(value: string): string {
316
+ const sanitized = value
317
+ .replace(/[^a-zA-Z0-9]+/g, " ")
318
+ .split(" ")
319
+ .filter(Boolean)
320
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
321
+ .join("");
322
+
323
+ if (!sanitized) {
324
+ return "";
325
+ }
326
+
327
+ if (/^[0-9]/.test(sanitized)) {
328
+ return `Type${sanitized}`;
329
+ }
330
+
331
+ return sanitized;
332
+ }
333
+
334
+ /**
335
+ * Sanitize type name.
336
+ * @param value Input parameter `value`.
337
+ * @returns Sanitize type name output as `string`.
338
+ * @example
339
+ * ```ts
340
+ * const result = sanitizeTypeName("value");
341
+ * // result: string
342
+ * ```
343
+ */
344
+ function sanitizeTypeName(value: string): string {
345
+ const sanitized = toPascalCase(value);
346
+ return sanitized || "Type";
347
+ }
348
+
349
+ /**
350
+ * Resolve type name.
351
+ * @param value Input parameter `value`.
352
+ * @param typeNameMap Input parameter `typeNameMap`.
353
+ * @returns Resolve type name output as `string`.
354
+ * @example
355
+ * ```ts
356
+ * const result = resolveTypeName("value", {});
357
+ * // result: string
358
+ * ```
359
+ */
360
+ function resolveTypeName(value: string, typeNameMap?: TypeNameMap): string {
361
+ return typeNameMap?.get(value) ?? sanitizeTypeName(value);
362
+ }
363
+
364
+ /**
365
+ * Create type name map.
366
+ * @param schemas Input parameter `schemas`.
367
+ * @returns Create type name map output as `unknown`.
368
+ * @example
369
+ * ```ts
370
+ * const result = createTypeNameMap({});
371
+ * // result: unknown
372
+ * ```
373
+ */
374
+ function createTypeNameMap(schemas?: Record<string, OpenApiSchema>): {
375
+ typeNameMap: TypeNameMap;
376
+ usedTypeNames: Set<string>;
377
+ } {
378
+ const typeNameMap: TypeNameMap = new Map();
379
+ const usedTypeNames = new Set<string>();
380
+
381
+ if (!schemas) {
382
+ return { typeNameMap, usedTypeNames };
383
+ }
384
+
385
+ for (const modelName of Object.keys(schemas)) {
386
+ const sanitizedName = sanitizeTypeName(modelName);
387
+ const uniqueName = makeUniqueTypeName(sanitizedName, usedTypeNames);
388
+ typeNameMap.set(modelName, uniqueName);
389
+ }
390
+
391
+ return { typeNameMap, usedTypeNames };
392
+ }
393
+
394
+ /**
395
+ * Build inline base name.
396
+ * @param path Input parameter `path`.
397
+ * @param httpMethod Input parameter `httpMethod`.
398
+ * @param operation Input parameter `operation`.
399
+ * @returns Build inline base name output as `string`.
400
+ * @example
401
+ * ```ts
402
+ * const result = buildInlineBaseName("value", "value", {});
403
+ * // result: string
404
+ * ```
405
+ */
406
+ function buildInlineBaseName(
407
+ path: string,
408
+ httpMethod: string,
409
+ operation: OpenApiOperation,
410
+ ): string {
411
+ if (operation.operationId) {
412
+ const opName = toPascalCase(operation.operationId);
413
+ if (opName) {
414
+ return opName;
415
+ }
416
+ }
417
+
418
+ const pathParts = path.split("/").filter((part) => part && part !== "api");
419
+ const pathName =
420
+ pathParts.length > 0
421
+ ? pathParts
422
+ .map((part) => {
423
+ if (part.startsWith("{")) {
424
+ const param = part.slice(1, -1);
425
+ return `By${param.charAt(0).toUpperCase()}${param.slice(1)}`;
426
+ }
427
+ return part.charAt(0).toUpperCase() + part.slice(1);
428
+ })
429
+ .join("")
430
+ : "Api";
431
+
432
+ const methodPrefix = httpMethod.charAt(0).toUpperCase() + httpMethod.slice(1);
433
+ return `${methodPrefix}${pathName}`;
434
+ }
435
+
436
+ /**
437
+ * Make unique type name.
438
+ * @param name Input parameter `name`.
439
+ * @param usedNames Input parameter `usedNames`.
440
+ * @returns Make unique type name output as `string`.
441
+ * @example
442
+ * ```ts
443
+ * const result = makeUniqueTypeName("value", new Set());
444
+ * // result: string
445
+ * ```
446
+ */
447
+ function makeUniqueTypeName(name: string, usedNames: Set<string>): string {
448
+ if (!usedNames.has(name)) {
449
+ usedNames.add(name);
450
+ return name;
451
+ }
452
+
453
+ let counter = 2;
454
+ while (usedNames.has(`${name}${counter}`)) {
455
+ counter++;
456
+ }
457
+
458
+ const uniqueName = `${name}${counter}`;
459
+ usedNames.add(uniqueName);
460
+ return uniqueName;
461
+ }
462
+
463
+ /**
464
+ * Get preferred content schema.
465
+ * @example
466
+ * ```ts
467
+ * getPreferredContentSchema();
468
+ * ```
469
+ */
470
+ function getPreferredContentSchema(
471
+ content?: Record<string, { schema: OpenApiSchema }>,
472
+ ): OpenApiSchema | undefined {
473
+ if (!content) {
474
+ return undefined;
475
+ }
476
+
477
+ if (content["application/json"]?.schema) {
478
+ return content["application/json"].schema;
479
+ }
480
+
481
+ const firstKey = Object.keys(content)[0];
482
+ return firstKey ? content[firstKey]?.schema : undefined;
483
+ }
484
+
485
+ /**
486
+ * Get success response.
487
+ * @param operation Input parameter `operation`.
488
+ * @returns Get success response output as `unknown`.
489
+ * @example
490
+ * ```ts
491
+ * const result = getSuccessResponse({});
492
+ * // result: unknown
493
+ * ```
494
+ */
495
+ function getSuccessResponse(
496
+ operation: OpenApiOperation,
497
+ ): { content?: Record<string, { schema: OpenApiSchema }> } | undefined {
498
+ const responses = operation.responses || {};
499
+ if (responses["200"]) {
500
+ return responses["200"];
501
+ }
502
+ if (responses["201"]) {
503
+ return responses["201"];
504
+ }
505
+
506
+ const successKey = Object.keys(responses).find(
507
+ (key) => key.startsWith("2") && responses[key],
508
+ );
509
+ return successKey ? responses[successKey] : undefined;
510
+ }
511
+
512
+ /**
513
+ * Generate inline type definition.
514
+ * @param typeName Input parameter `typeName`.
515
+ * @param schema Input parameter `schema`.
516
+ * @param typeNameMap Input parameter `typeNameMap`.
517
+ * @returns Generate inline type definition output as `string`.
518
+ * @example
519
+ * ```ts
520
+ * const result = generateInlineTypeDefinition("value", {}, {});
521
+ * // result: string
522
+ * ```
523
+ */
524
+ function generateInlineTypeDefinition(
525
+ typeName: string,
526
+ schema: OpenApiSchema,
527
+ typeNameMap?: TypeNameMap,
528
+ ): string {
529
+ if (schema.type === "object" && schema.properties) {
530
+ return generateTypeScriptDefinition(typeName, schema, typeNameMap);
531
+ }
532
+
533
+ return `export type ${typeName} = ${convertSchemaToTypeScript(schema, typeNameMap)};`;
534
+ }
535
+
536
+ /**
537
+ * Resolve schema type name.
538
+ * @param schema Input parameter `schema`.
539
+ * @param typeName Input parameter `typeName`.
540
+ * @param usedTypeNames Input parameter `usedTypeNames`.
541
+ * @param typeDefinitions Input parameter `typeDefinitions`.
542
+ * @param typeNameMap Input parameter `typeNameMap`.
543
+ * @returns Resolve schema type name output as `string | undefined`.
544
+ * @example
545
+ * ```ts
546
+ * const result = resolveSchemaTypeName({}, "value", new Set(), [], {});
547
+ * // result: string | undefined
548
+ * ```
549
+ */
550
+ function resolveSchemaTypeName(
551
+ schema: OpenApiSchema | undefined,
552
+ typeName: string,
553
+ usedTypeNames: Set<string>,
554
+ typeDefinitions: string[],
555
+ typeNameMap?: TypeNameMap,
556
+ ): string | undefined {
557
+ if (!schema) {
558
+ return undefined;
559
+ }
560
+
561
+ if (schema.$ref && typeof schema.$ref === "string") {
562
+ const refParts = schema.$ref.split("/");
563
+ const rawName = refParts[refParts.length - 1];
564
+ return rawName ? resolveTypeName(rawName, typeNameMap) : undefined;
565
+ }
566
+
567
+ const safeTypeName = sanitizeTypeName(typeName);
568
+ const uniqueName = makeUniqueTypeName(safeTypeName, usedTypeNames);
569
+ const typeDefinition = generateInlineTypeDefinition(
570
+ uniqueName,
571
+ schema,
572
+ typeNameMap,
573
+ );
574
+ typeDefinitions.push(typeDefinition);
575
+ return uniqueName;
576
+ }
577
+
578
+ /**
579
+ * Collect inline operation types.
580
+ * @param data Input parameter `data`.
581
+ * @param usedTypeNames Input parameter `usedTypeNames`.
582
+ * @param typeNameMap Input parameter `typeNameMap`.
583
+ * @returns Collect inline operation types output as `unknown`.
584
+ * @example
585
+ * ```ts
586
+ * const result = collectInlineOperationTypes({}, new Set(), {});
587
+ * // result: unknown
588
+ * ```
589
+ */
590
+ function collectInlineOperationTypes(
591
+ data: z.infer<typeof SwaggerOrOpenAPISchema>,
592
+ usedTypeNames: Set<string>,
593
+ typeNameMap?: TypeNameMap,
594
+ ): { typeDefinitions: string[]; operationTypes: OperationTypeMap } {
595
+ const typeDefinitions: string[] = [];
596
+ const operationTypes: OperationTypeMap = {};
597
+
598
+ if (!data.paths) {
599
+ return { typeDefinitions, operationTypes };
600
+ }
601
+
602
+ const httpMethods = [
603
+ "get",
604
+ "post",
605
+ "put",
606
+ "delete",
607
+ "patch",
608
+ "head",
609
+ "options",
610
+ ] as const;
611
+
612
+ for (const [path, pathItem] of Object.entries(data.paths)) {
613
+ for (const httpMethod of httpMethods) {
614
+ const operation = (pathItem as OpenApiPath)[httpMethod];
615
+ if (!operation) {
616
+ continue;
617
+ }
618
+
619
+ const baseName = buildInlineBaseName(path, httpMethod, operation);
620
+ const requestSchema = getPreferredContentSchema(
621
+ operation.requestBody?.content,
622
+ );
623
+ const responseSchema = getPreferredContentSchema(
624
+ getSuccessResponse(operation)?.content,
625
+ );
626
+
627
+ const requestType = resolveSchemaTypeName(
628
+ requestSchema,
629
+ `${baseName}Request`,
630
+ usedTypeNames,
631
+ typeDefinitions,
632
+ typeNameMap,
633
+ );
634
+ const responseType = resolveSchemaTypeName(
635
+ responseSchema,
636
+ `${baseName}Response`,
637
+ usedTypeNames,
638
+ typeDefinitions,
639
+ typeNameMap,
640
+ );
641
+
642
+ if (requestType || responseType) {
643
+ if (!operationTypes[path]) {
644
+ operationTypes[path] = {};
645
+ }
646
+ operationTypes[path][httpMethod] = {
647
+ requestType,
648
+ responseType,
649
+ };
650
+ }
651
+ }
652
+ }
653
+
654
+ return { typeDefinitions, operationTypes };
655
+ }
656
+
657
+ /**
658
+ * Generate methods for path.
659
+ * @param path Input parameter `path`.
660
+ * @param operations Input parameter `operations`.
661
+ * @param usedMethodNames Input parameter `usedMethodNames`.
662
+ * @param components Input parameter `components`.
663
+ * @param usedTypes Input parameter `usedTypes`.
664
+ * @param operationTypes Input parameter `operationTypes`.
665
+ * @param typeNameMap Input parameter `typeNameMap`.
666
+ * @returns Generate methods for path output as `unknown`.
667
+ * @example
668
+ * ```ts
669
+ * const result = generateMethodsForPath("value", {}, new Set(), {}, new Set(), {}, {});
670
+ * // result: unknown
671
+ * ```
672
+ */
673
+ function generateMethodsForPath(
674
+ path: string,
675
+ operations: OpenApiPath,
676
+ usedMethodNames: Set<string>,
677
+ components: any,
678
+ usedTypes: Set<string>,
679
+ operationTypes?: OperationTypeMap,
680
+ typeNameMap?: TypeNameMap,
681
+ ): { methods: string[]; paramsInterfaces: string[] } {
682
+ const methods: string[] = [];
683
+ const paramsInterfaces: string[] = [];
684
+ const httpMethods = [
685
+ "get",
686
+ "post",
687
+ "put",
688
+ "delete",
689
+ "patch",
690
+ "head",
691
+ "options",
692
+ ] as const;
693
+
694
+ for (const httpMethod of httpMethods) {
695
+ if (operations[httpMethod]) {
696
+ const result = generateHttpMethod(
697
+ path,
698
+ httpMethod,
699
+ operations[httpMethod],
700
+ usedMethodNames,
701
+ components,
702
+ usedTypes,
703
+ operationTypes,
704
+ typeNameMap,
705
+ );
706
+ if (result) {
707
+ methods.push(result.method);
708
+ if (result.paramsInterface) {
709
+ paramsInterfaces.push(result.paramsInterface);
710
+ }
711
+ }
712
+ }
713
+ }
714
+
715
+ return { methods, paramsInterfaces };
716
+ }
717
+
718
+ /**
719
+ * Generate http method.
720
+ * @param path Input parameter `path`.
721
+ * @param httpMethod Input parameter `httpMethod`.
722
+ * @param operation Input parameter `operation`.
723
+ * @param usedMethodNames Input parameter `usedMethodNames`.
724
+ * @param components Input parameter `components`.
725
+ * @param usedTypes Input parameter `usedTypes`.
726
+ * @param operationTypes Input parameter `operationTypes`.
727
+ * @param typeNameMap Input parameter `typeNameMap`.
728
+ * @returns Generate http method output as `unknown`.
729
+ * @example
730
+ * ```ts
731
+ * const result = generateHttpMethod("value", "value", {}, new Set(), {}, new Set(), {}, {});
732
+ * // result: unknown
733
+ * ```
734
+ */
735
+ function generateHttpMethod(
736
+ path: string,
737
+ httpMethod: string,
738
+ operation: OpenApiOperation,
739
+ usedMethodNames: Set<string>,
740
+ components: any,
741
+ usedTypes: Set<string>,
742
+ operationTypes?: OperationTypeMap,
743
+ typeNameMap?: TypeNameMap,
744
+ ): { method: string; paramsInterface?: string } | null {
745
+ try {
746
+ const methodName = generateMethodName(path, httpMethod, operation);
747
+
748
+ // Ensure unique method name
749
+ let counter = 1;
750
+ let uniqueMethodName = methodName;
751
+ while (usedMethodNames.has(uniqueMethodName)) {
752
+ uniqueMethodName = `${methodName}${counter}`;
753
+ counter++;
754
+ }
755
+
756
+ usedMethodNames.add(uniqueMethodName);
757
+
758
+ const typeInfo = operationTypes?.[path]?.[httpMethod];
759
+ const parameters = extractMethodParameters(
760
+ path,
761
+ operation,
762
+ typeInfo,
763
+ components,
764
+ typeNameMap,
765
+ uniqueMethodName,
766
+ );
767
+
768
+ // Extract response type from operation
769
+ const requestType =
770
+ typeInfo?.requestType ??
771
+ extractRequestType(operation, components, typeNameMap);
772
+ let responseType =
773
+ typeInfo?.responseType ??
774
+ extractResponseType(operation, components, typeNameMap);
775
+ if (
776
+ responseType === "any" &&
777
+ requestType &&
778
+ ["post", "put", "patch"].includes(httpMethod)
779
+ ) {
780
+ responseType = requestType;
781
+ }
782
+ const returnType =
783
+ responseType !== "any"
784
+ ? `Observable<${responseType}>`
785
+ : "Observable<any>";
786
+
787
+ // Track used types for imports
788
+ if (requestType) {
789
+ usedTypes.add(requestType);
790
+ }
791
+ if (responseType !== "any" && !responseType.includes("[]")) {
792
+ // For single types (not arrays), add to imports
793
+ usedTypes.add(responseType);
794
+ } else if (responseType.includes("[]")) {
795
+ // For array types like "Type[]", extract "Type" and add to imports
796
+ const baseType = responseType.replace("[]", "");
797
+ usedTypes.add(baseType);
798
+ }
799
+
800
+ const methodBody = generateMethodBody(
801
+ path,
802
+ httpMethod,
803
+ operation,
804
+ responseType,
805
+ );
806
+
807
+ const paramInfo = buildParameterInfo(
808
+ path,
809
+ operation,
810
+ components,
811
+ typeNameMap,
812
+ );
813
+ const paramTypes = [
814
+ ...paramInfo.pathParams.map((param) => param.type),
815
+ ...paramInfo.queryParams.map((param) => param.type),
816
+ ];
817
+ addParamTypeImports(paramTypes, usedTypes);
818
+
819
+ let paramsInterface: string | undefined;
820
+ if (paramInfo.queryParams.length > 0) {
821
+ paramsInterface = generateParamsInterface(
822
+ uniqueMethodName,
823
+ paramInfo.queryParams,
824
+ );
825
+ }
826
+
827
+ return {
828
+ method: ` ${uniqueMethodName}(${parameters}): ${returnType} {
829
+ ${methodBody}
830
+ }`,
831
+ paramsInterface,
832
+ };
833
+ } catch (error) {
834
+ console.warn(
835
+ `Warning: Could not generate method for ${httpMethod.toUpperCase()} ${path}:`,
836
+ error,
837
+ );
838
+ return null;
839
+ }
840
+ }
841
+
842
+ /**
843
+ * Generate method name.
844
+ * @param path Input parameter `path`.
845
+ * @param httpMethod Input parameter `httpMethod`.
846
+ * @param operation Input parameter `operation`.
847
+ * @returns Generate method name output as `string`.
848
+ * @example
849
+ * ```ts
850
+ * const result = generateMethodName("value", "value", {});
851
+ * // result: string
852
+ * ```
853
+ */
854
+ function generateMethodName(
855
+ path: string,
856
+ httpMethod: string,
857
+ operation: OpenApiOperation,
858
+ ): string {
859
+ // Extract meaningful parts from path
860
+ const pathParts = path.split("/").filter((part) => part && part !== "api");
861
+ const tags = operation.tags || [];
862
+
863
+ // Create base name from tags or path parts
864
+ let baseName: string;
865
+ if (pathParts.length > 1) {
866
+ // Use path parts when there are multiple segments (more descriptive)
867
+ baseName = pathParts
868
+ .map((part) => {
869
+ if (part.startsWith("{")) {
870
+ return `By${part.slice(1, -1).charAt(0).toUpperCase()}${part.slice(2, -1)}`;
871
+ }
872
+ return part.charAt(0).toUpperCase() + part.slice(1);
873
+ })
874
+ .join("");
875
+ } else if (tags.length > 0) {
876
+ // Use tags as fallback
877
+ baseName = tags
878
+ .map((tag) => tag.charAt(0).toUpperCase() + tag.slice(1))
879
+ .join("");
880
+ } else {
881
+ baseName = "Api";
882
+ }
883
+
884
+ // Clean up base name (remove special chars)
885
+ baseName = baseName.replace(/[^a-zA-Z0-9]/g, "");
886
+
887
+ // Create HTTP method prefix
888
+ const methodPrefix = httpMethod.charAt(0).toUpperCase() + httpMethod.slice(1);
889
+
890
+ // For paths with parameters, add descriptive suffixes
891
+ const hasPathParams = path.includes("{");
892
+ const hasQueryParams =
893
+ operation.parameters?.some((p) => p.in === "query") || false;
894
+ const hasBody = !!operation.requestBody;
895
+
896
+ let additionalSuffix = "";
897
+ if (hasPathParams && httpMethod === "get") {
898
+ additionalSuffix = "";
899
+ } else if (hasQueryParams && httpMethod === "get") {
900
+ additionalSuffix = "WithParams";
901
+ } else if (hasBody && ["post", "put", "patch"].includes(httpMethod)) {
902
+ additionalSuffix = "Create";
903
+ }
904
+
905
+ return methodPrefix + baseName + additionalSuffix;
906
+ }
907
+
908
+ /**
909
+ * Build parameter info.
910
+ * @param path Input parameter `path`.
911
+ * @param operation Input parameter `operation`.
912
+ * @param components Input parameter `components`.
913
+ * @param typeNameMap Input parameter `typeNameMap`.
914
+ * @example
915
+ * ```ts
916
+ * buildParameterInfo("value", {}, {}, {});
917
+ * ```
918
+ */
919
+ function buildParameterInfo(
920
+ path: string,
921
+ operation: OpenApiOperation,
922
+ components?: any,
923
+ typeNameMap?: TypeNameMap,
924
+ ) {
925
+ const usedNames = new Set<string>();
926
+
927
+ const makeUniqueName = (base: string, suffix: string) => {
928
+ if (!usedNames.has(base)) {
929
+ usedNames.add(base);
930
+ return base;
931
+ }
932
+ const candidate = `${base}${suffix}`;
933
+ if (!usedNames.has(candidate)) {
934
+ usedNames.add(candidate);
935
+ return candidate;
936
+ }
937
+ let counter = 2;
938
+ while (usedNames.has(`${candidate}${counter}`)) {
939
+ counter++;
940
+ }
941
+ const unique = `${candidate}${counter}`;
942
+ usedNames.add(unique);
943
+ return unique;
944
+ };
945
+
946
+ const pathParams: Array<{ name: string; varName: string; type: string }> = [];
947
+ const queryParams: Array<{
948
+ name: string;
949
+ varName: string;
950
+ required: boolean;
951
+ type: string;
952
+ }> = [];
953
+ let bodyParam: { name: string; varName: string } | null = null;
954
+
955
+ // Extract path parameters (always required)
956
+ const pathParamMatches = path.match(/\{([^}]+)\}/g);
957
+ if (pathParamMatches) {
958
+ const pathParamSchemas =
959
+ operation.parameters?.filter((param) => param.in === "path") || [];
960
+ for (const match of pathParamMatches) {
961
+ const paramName = match.slice(1, -1); // Remove { }
962
+ usedNames.add(paramName);
963
+ const schema = pathParamSchemas.find(
964
+ (param) => param.name === paramName,
965
+ )?.schema;
966
+ const type = schema
967
+ ? convertParamSchemaToTypeScript(schema, components, typeNameMap)
968
+ : "any";
969
+ pathParams.push({ name: paramName, varName: paramName, type });
970
+ }
971
+ }
972
+
973
+ // Extract query parameters (may be required or optional)
974
+ if (operation.parameters) {
975
+ for (const param of operation.parameters) {
976
+ if (param.in === "query") {
977
+ const varName = makeUniqueName(param.name, "Query");
978
+ queryParams.push({
979
+ name: param.name,
980
+ varName,
981
+ required: !!param.required,
982
+ type: convertParamSchemaToTypeScript(
983
+ param.schema,
984
+ components,
985
+ typeNameMap,
986
+ ),
987
+ });
988
+ }
989
+ }
990
+ }
991
+
992
+ // Extract request body parameter for POST/PUT/PATCH (always required)
993
+ if (operation.requestBody) {
994
+ const varName = makeUniqueName("body", "Payload");
995
+ bodyParam = { name: "body", varName };
996
+ }
997
+
998
+ return { pathParams, queryParams, bodyParam };
999
+ }
1000
+
1001
+ /**
1002
+ * Generate params interface.
1003
+ * @example
1004
+ * ```ts
1005
+ * generateParamsInterface();
1006
+ * ```
1007
+ */
1008
+ function generateParamsInterface(
1009
+ methodName: string,
1010
+ queryParams: Array<{ name: string; required: boolean; type: string }>,
1011
+ ): string {
1012
+ const props = queryParams.map((param) => {
1013
+ const optional = param.required ? "" : "?";
1014
+ return ` ${param.name}${optional}: ${param.type};`;
1015
+ });
1016
+ return `export interface ${methodName}Params {\n${props.join("\n")}\n}`;
1017
+ }
1018
+
1019
+ /**
1020
+ * Extract method parameters.
1021
+ * @param path Input parameter `path`.
1022
+ * @param operation Input parameter `operation`.
1023
+ * @param typeInfo Input parameter `typeInfo`.
1024
+ * @param components Input parameter `components`.
1025
+ * @param typeNameMap Input parameter `typeNameMap`.
1026
+ * @param methodName Input parameter `methodName`.
1027
+ * @returns Extract method parameters output as `string`.
1028
+ * @example
1029
+ * ```ts
1030
+ * const result = extractMethodParameters("value", {}, {}, {}, {}, "value");
1031
+ * // result: string
1032
+ * ```
1033
+ */
1034
+ function extractMethodParameters(
1035
+ path: string,
1036
+ operation: OpenApiOperation,
1037
+ typeInfo?: OperationTypeInfo,
1038
+ components?: any,
1039
+ typeNameMap?: TypeNameMap,
1040
+ methodName?: string,
1041
+ ): string {
1042
+ const params: string[] = [];
1043
+ const optionalParams: string[] = [];
1044
+ const { pathParams, queryParams, bodyParam } = buildParameterInfo(
1045
+ path,
1046
+ operation,
1047
+ components,
1048
+ typeNameMap,
1049
+ );
1050
+
1051
+ for (const param of pathParams) {
1052
+ params.push(`${param.varName}: ${param.type}`);
1053
+ }
1054
+
1055
+ if (queryParams.length > 0 && methodName) {
1056
+ params.push(`params: ${methodName}Params`);
1057
+ } else {
1058
+ for (const param of queryParams) {
1059
+ if (param.required) {
1060
+ params.push(`${param.varName}: ${param.type}`);
1061
+ } else {
1062
+ optionalParams.push(`${param.varName}?: ${param.type}`);
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ if (bodyParam) {
1068
+ const bodyType =
1069
+ typeInfo?.requestType ??
1070
+ extractRequestType(operation, components, typeNameMap) ??
1071
+ "any";
1072
+ params.push(`${bodyParam.varName}: ${bodyType}`);
1073
+ }
1074
+
1075
+ return [...params, ...optionalParams].join(", ");
1076
+ }
1077
+
1078
+ /**
1079
+ * Extract request type.
1080
+ * @param operation Input parameter `operation`.
1081
+ * @param _components Input parameter `_components`.
1082
+ * @param typeNameMap Input parameter `typeNameMap`.
1083
+ * @returns Extract request type output as `string | undefined`.
1084
+ * @example
1085
+ * ```ts
1086
+ * const result = extractRequestType({}, {}, {});
1087
+ * // result: string | undefined
1088
+ * ```
1089
+ */
1090
+ function extractRequestType(
1091
+ operation: OpenApiOperation,
1092
+ _components?: any,
1093
+ typeNameMap?: TypeNameMap,
1094
+ ): string | undefined {
1095
+ const schema = getPreferredContentSchema(operation.requestBody?.content);
1096
+ if (!schema) {
1097
+ return undefined;
1098
+ }
1099
+
1100
+ if (schema.$ref && typeof schema.$ref === "string") {
1101
+ const refParts = schema.$ref.split("/");
1102
+ const rawName = refParts[refParts.length - 1];
1103
+ return rawName ? resolveTypeName(rawName, typeNameMap) : undefined;
1104
+ }
1105
+
1106
+ if (schema.type === "array" && schema.items?.$ref) {
1107
+ const refParts = schema.items.$ref.split("/");
1108
+ const itemTypeName = refParts[refParts.length - 1];
1109
+ return itemTypeName
1110
+ ? `${resolveTypeName(itemTypeName, typeNameMap)}[]`
1111
+ : undefined;
1112
+ }
1113
+
1114
+ return undefined;
1115
+ }
1116
+
1117
+ /**
1118
+ * Extract response type.
1119
+ * @param operation Input parameter `operation`.
1120
+ * @param _components Input parameter `_components`.
1121
+ * @param typeNameMap Input parameter `typeNameMap`.
1122
+ * @returns Extract response type output as `string`.
1123
+ * @example
1124
+ * ```ts
1125
+ * const result = extractResponseType({}, {}, {});
1126
+ * // result: string
1127
+ * ```
1128
+ */
1129
+ function extractResponseType(
1130
+ operation: OpenApiOperation,
1131
+ _components?: any,
1132
+ typeNameMap?: TypeNameMap,
1133
+ ): string {
1134
+ // Look for 200 response first, then any 2xx response
1135
+ const response =
1136
+ operation.responses?.["200"] ||
1137
+ operation.responses?.["201"] ||
1138
+ (Object.keys(operation.responses || {}).find(
1139
+ (key) => key.startsWith("2") && operation.responses?.[key],
1140
+ ) &&
1141
+ operation.responses?.[
1142
+ Object.keys(operation.responses).find((key) => key.startsWith("2"))!
1143
+ ]);
1144
+
1145
+ if (!response || typeof response !== "object") {
1146
+ return "any";
1147
+ }
1148
+
1149
+ // Check for content with application/json
1150
+ const content = (response as any).content;
1151
+ if (content?.["application/json"]?.schema) {
1152
+ const schema = content["application/json"].schema;
1153
+
1154
+ // Handle $ref
1155
+ if (schema.$ref && typeof schema.$ref === "string") {
1156
+ const refParts = schema.$ref.split("/");
1157
+ const typeName = refParts[refParts.length - 1];
1158
+ return typeName ? resolveTypeName(typeName, typeNameMap) : "any";
1159
+ }
1160
+
1161
+ // Handle direct type
1162
+ if (schema.type === "array" && schema.items?.$ref) {
1163
+ const refParts = schema.items.$ref.split("/");
1164
+ const itemTypeName = refParts[refParts.length - 1];
1165
+ return itemTypeName
1166
+ ? `${resolveTypeName(itemTypeName, typeNameMap)}[]`
1167
+ : "any[]";
1168
+ }
1169
+
1170
+ // Fallback to any for complex schemas
1171
+ return "any";
1172
+ }
1173
+
1174
+ return "any";
1175
+ }
1176
+
1177
+ /**
1178
+ * Generate method body.
1179
+ * @param path Input parameter `path`.
1180
+ * @param httpMethod Input parameter `httpMethod`.
1181
+ * @param operation Input parameter `operation`.
1182
+ * @param responseType Input parameter `responseType`.
1183
+ * @returns Generate method body output as `string`.
1184
+ * @example
1185
+ * ```ts
1186
+ * const result = generateMethodBody("value", "value", {}, "value");
1187
+ * // result: string
1188
+ * ```
1189
+ */
1190
+ function generateMethodBody(
1191
+ path: string,
1192
+ httpMethod: string,
1193
+ operation: OpenApiOperation,
1194
+ responseType: string,
1195
+ ): string {
1196
+ const paramInfo = buildParameterInfo(path, operation);
1197
+ // Replace path parameters with template literals
1198
+ let url = path.replace(/\{([^}]+)\}/g, "${$1}");
1199
+
1200
+ // Add backticks for template literal if there are path parameters
1201
+ const hasPathParams = path.includes("{");
1202
+ if (hasPathParams) {
1203
+ url = `\`${url}\``;
1204
+ } else {
1205
+ url = `"${url}"`;
1206
+ }
1207
+
1208
+ // Build HttpClient method call
1209
+ const httpClientMethod = `this.httpClient.${httpMethod}<${responseType}>`;
1210
+ const args = [url];
1211
+
1212
+ const queryParams = paramInfo.queryParams || [];
1213
+ const hasQueryParams = queryParams.length > 0;
1214
+ const hasBody = !!operation.requestBody;
1215
+ const requiresBody = ["post", "put", "patch"].includes(httpMethod);
1216
+
1217
+ if (hasBody) {
1218
+ args.push(paramInfo.bodyParam?.varName || "body");
1219
+ } else if (requiresBody) {
1220
+ args.push("null");
1221
+ }
1222
+
1223
+ if (hasQueryParams) {
1224
+ args.push(`{ params: { ...params } }`);
1225
+ }
1226
+
1227
+ return ` return ${httpClientMethod}(${args.join(", ")});`;
1228
+ }
1229
+
1230
+ /**
1231
+ * Convert param schema to type script.
1232
+ * @param schema Input parameter `schema`.
1233
+ * @param components Input parameter `components`.
1234
+ * @param typeNameMap Input parameter `typeNameMap`.
1235
+ * @returns Convert param schema to type script output as `string`.
1236
+ * @example
1237
+ * ```ts
1238
+ * const result = convertParamSchemaToTypeScript({}, {}, {});
1239
+ * // result: string
1240
+ * ```
1241
+ */
1242
+ function convertParamSchemaToTypeScript(
1243
+ schema: OpenApiSchema,
1244
+ components?: any,
1245
+ typeNameMap?: TypeNameMap,
1246
+ ): string {
1247
+ if (!schema || typeof schema !== "object") {
1248
+ return "any";
1249
+ }
1250
+
1251
+ // Handle JSON Schema $ref references
1252
+ if (schema.$ref && typeof schema.$ref === "string") {
1253
+ const referencePathParts = schema.$ref.split("/");
1254
+ const referencedTypeName =
1255
+ referencePathParts[referencePathParts.length - 1];
1256
+ if (!referencedTypeName) {
1257
+ return "any";
1258
+ }
1259
+
1260
+ const referencedSchema = components?.schemas?.[referencedTypeName] as
1261
+ | OpenApiSchema
1262
+ | undefined;
1263
+ if (
1264
+ referencedSchema?.type === "string" &&
1265
+ referencedSchema.format === "date-time"
1266
+ ) {
1267
+ return "string";
1268
+ }
1269
+
1270
+ return resolveTypeName(referencedTypeName, typeNameMap);
1271
+ }
1272
+
1273
+ // Handle enum values - convert to TypeScript union types
1274
+ if (Array.isArray(schema.enum)) {
1275
+ const unionValues = schema.enum
1276
+ .map((enumValue: unknown) => {
1277
+ if (typeof enumValue === "string") {
1278
+ return `"${enumValue}"`;
1279
+ }
1280
+ return String(enumValue);
1281
+ })
1282
+ .join(" | ");
1283
+
1284
+ return unionValues || "any";
1285
+ }
1286
+
1287
+ // Handle anyOf/oneOf schemas - convert to union types
1288
+ if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {
1289
+ const variants = (schema.anyOf || schema.oneOf || [])
1290
+ .map((variant) =>
1291
+ convertParamSchemaToTypeScript(variant, components, typeNameMap),
1292
+ )
1293
+ .filter(Boolean);
1294
+ const union = variants.join(" | ");
1295
+ return union || "any";
1296
+ }
1297
+
1298
+ // Handle allOf schemas - convert to intersection types
1299
+ if (Array.isArray(schema.allOf)) {
1300
+ const variants = schema.allOf
1301
+ .map((variant) =>
1302
+ convertParamSchemaToTypeScript(variant, components, typeNameMap),
1303
+ )
1304
+ .filter(Boolean);
1305
+ const intersection = variants.join(" & ");
1306
+ return intersection || "any";
1307
+ }
1308
+
1309
+ // Handle array types with item schema
1310
+ if (schema.type === "array" && schema.items) {
1311
+ const itemType = convertParamSchemaToTypeScript(
1312
+ schema.items,
1313
+ components,
1314
+ typeNameMap,
1315
+ );
1316
+ return `${itemType}[]`;
1317
+ }
1318
+
1319
+ // For params, date-time should be string
1320
+ if (schema.type === "string" && schema.format === "date-time") {
1321
+ return "string";
1322
+ }
1323
+
1324
+ return convertSchemaToTypeScript(schema, typeNameMap);
1325
+ }
1326
+
1327
+ /**
1328
+ * Add param type imports.
1329
+ * @param paramTypes Input parameter `paramTypes`.
1330
+ * @param usedTypes Input parameter `usedTypes`.
1331
+ * @example
1332
+ * ```ts
1333
+ * addParamTypeImports([], new Set());
1334
+ * ```
1335
+ */
1336
+ function addParamTypeImports(paramTypes: string[], usedTypes: Set<string>) {
1337
+ for (const type of paramTypes) {
1338
+ const parts = type.split(/[|&]/).map((part) => part.trim());
1339
+ for (let part of parts) {
1340
+ while (part.endsWith("[]")) {
1341
+ part = part.slice(0, -2);
1342
+ }
1343
+ if (!part) {
1344
+ continue;
1345
+ }
1346
+ if (
1347
+ part === "string" ||
1348
+ part === "number" ||
1349
+ part === "boolean" ||
1350
+ part === "any" ||
1351
+ part === "unknown" ||
1352
+ part === "object" ||
1353
+ part === "null" ||
1354
+ part === "undefined" ||
1355
+ part === "Date"
1356
+ ) {
1357
+ continue;
1358
+ }
1359
+ if (
1360
+ part.startsWith('"') ||
1361
+ part.startsWith("'") ||
1362
+ part.startsWith("{") ||
1363
+ /^[0-9]/.test(part)
1364
+ ) {
1365
+ continue;
1366
+ }
1367
+ usedTypes.add(part);
1368
+ }
1369
+ }
1370
+ }
1371
+
1372
+ /**
1373
+ * Convert schema to type script.
1374
+ * @param schema Input parameter `schema`.
1375
+ * @param typeNameMap Input parameter `typeNameMap`.
1376
+ * @returns Convert schema to type script output as `string`.
1377
+ * @example
1378
+ * ```ts
1379
+ * const result = convertSchemaToTypeScript({}, {});
1380
+ * // result: string
1381
+ * ```
1382
+ */
1383
+ function convertSchemaToTypeScript(
1384
+ schema: OpenApiSchema,
1385
+ typeNameMap?: TypeNameMap,
1386
+ ): string {
1387
+ if (!schema || typeof schema !== "object") {
1388
+ return "any";
1389
+ }
1390
+
1391
+ // Handle JSON Schema $ref references (e.g., "#/components/schemas/ModelName")
1392
+ if (schema.$ref && typeof schema.$ref === "string") {
1393
+ // Extract the referenced type name from the $ref path
1394
+ const referencePathParts = schema.$ref.split("/");
1395
+ const referencedTypeName =
1396
+ referencePathParts[referencePathParts.length - 1];
1397
+
1398
+ if (!referencedTypeName) {
1399
+ throw new Error(`Invalid $ref format: ${schema.$ref}`);
1400
+ }
1401
+
1402
+ return resolveTypeName(referencedTypeName, typeNameMap);
1403
+ }
1404
+
1405
+ // Handle enum values - convert to TypeScript union types
1406
+ if (Array.isArray(schema.enum)) {
1407
+ const unionValues = schema.enum
1408
+ .map((enumValue: unknown) => {
1409
+ // String enums need quotes, numbers stay as-is
1410
+ if (typeof enumValue === "string") {
1411
+ return `"${enumValue}"`;
1412
+ }
1413
+ return String(enumValue);
1414
+ })
1415
+ .join(" | ");
1416
+
1417
+ return unionValues || "any";
1418
+ }
1419
+
1420
+ // Handle anyOf/oneOf schemas - convert to union types
1421
+ if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {
1422
+ const variants = (schema.anyOf || schema.oneOf || [])
1423
+ .map((variant) => convertSchemaToTypeScript(variant, typeNameMap))
1424
+ .filter(Boolean);
1425
+ const union = variants.join(" | ");
1426
+ return union || "any";
1427
+ }
1428
+
1429
+ // Handle allOf schemas - convert to intersection types
1430
+ if (Array.isArray(schema.allOf)) {
1431
+ const variants = schema.allOf
1432
+ .map((variant) => convertSchemaToTypeScript(variant, typeNameMap))
1433
+ .filter(Boolean);
1434
+ const intersection = variants.join(" & ");
1435
+ return intersection || "any";
1436
+ }
1437
+
1438
+ // Handle array types with item schema
1439
+ if (schema.type === "array" && schema.items) {
1440
+ const itemType = convertSchemaToTypeScript(schema.items, typeNameMap);
1441
+ return `${itemType}[]`;
1442
+ }
1443
+
1444
+ // Handle inline object schemas
1445
+ if (schema.type === "object" && schema.properties) {
1446
+ const requiredProperties = Array.isArray(schema.required)
1447
+ ? schema.required
1448
+ : [];
1449
+ const hasExplicitRequiredList = requiredProperties.length > 0;
1450
+ const entries = Object.entries(
1451
+ schema.properties as Record<string, OpenApiSchema>,
1452
+ );
1453
+
1454
+ if (entries.length === 0) {
1455
+ return "{}";
1456
+ }
1457
+
1458
+ const propertyDefinitions = entries.map(
1459
+ ([propertyName, propertySchema]) => {
1460
+ const propertyType = convertSchemaToTypeScript(
1461
+ propertySchema,
1462
+ typeNameMap,
1463
+ );
1464
+ const isRequired = hasExplicitRequiredList
1465
+ ? requiredProperties.includes(propertyName)
1466
+ : true;
1467
+ const optionalMarker = isRequired ? "" : "?";
1468
+ return `${propertyName}${optionalMarker}: ${propertyType};`;
1469
+ },
1470
+ );
1471
+
1472
+ return `{ ${propertyDefinitions.join(" ")} }`;
1473
+ }
1474
+
1475
+ // Handle primitive OpenAPI types
1476
+ let typeScriptType: string;
1477
+ switch (schema.type) {
1478
+ case "string":
1479
+ // Special handling for date-time and numeric formats
1480
+ if (schema.format === "date-time") {
1481
+ typeScriptType = "string";
1482
+ } else if (schema.format === "numeric") {
1483
+ typeScriptType = "number";
1484
+ } else {
1485
+ typeScriptType = "string";
1486
+ }
1487
+ break;
1488
+ case "number":
1489
+ case "integer":
1490
+ // Both number and integer map to TypeScript number
1491
+ typeScriptType = "number";
1492
+ break;
1493
+ case "boolean":
1494
+ typeScriptType = "boolean";
1495
+ break;
1496
+ default:
1497
+ // Unknown or complex types default to any
1498
+ typeScriptType = "any";
1499
+ break;
1500
+ }
1501
+
1502
+ // Handle nullable properties (OpenAPI 3.0+)
1503
+ if (schema.nullable === true) {
1504
+ typeScriptType += " | null";
1505
+ }
1506
+
1507
+ return typeScriptType;
1508
+ }
1509
+
1510
+ /**
1511
+ * Generate type script definition.
1512
+ * @param modelName Input parameter `modelName`.
1513
+ * @param schema Input parameter `schema`.
1514
+ * @param typeNameMap Input parameter `typeNameMap`.
1515
+ * @returns Generate type script definition output as `string`.
1516
+ * @example
1517
+ * ```ts
1518
+ * const result = generateTypeScriptDefinition("value", {}, {});
1519
+ * // result: string
1520
+ * ```
1521
+ */
1522
+ function generateTypeScriptDefinition(
1523
+ modelName: string,
1524
+ schema: OpenApiSchema,
1525
+ typeNameMap?: TypeNameMap,
1526
+ ): string {
1527
+ if (!modelName || typeof modelName !== "string") {
1528
+ throw new Error("Model name must be a non-empty string");
1529
+ }
1530
+
1531
+ if (!schema || typeof schema !== "object") {
1532
+ throw new Error(`Invalid schema for model "${modelName}"`);
1533
+ }
1534
+
1535
+ // Handle enum schemas - generate TypeScript union types
1536
+ if (Array.isArray(schema.enum)) {
1537
+ const unionValues = schema.enum
1538
+ .map((enumValue: unknown) => {
1539
+ // String enums need quotes for TypeScript literal types
1540
+ if (typeof enumValue === "string") {
1541
+ return `"${enumValue}"`;
1542
+ }
1543
+ return String(enumValue);
1544
+ })
1545
+ .join(" | ");
1546
+
1547
+ return `export type ${modelName} = ${unionValues};`;
1548
+ }
1549
+
1550
+ // Handle object schemas - generate TypeScript interfaces
1551
+ if (schema.type === "object" && schema.properties) {
1552
+ const propertyDefinitions: string[] = [];
1553
+ const requiredProperties = Array.isArray(schema.required)
1554
+ ? schema.required
1555
+ : [];
1556
+ const hasExplicitRequiredList = requiredProperties.length > 0;
1557
+
1558
+ for (const [propertyName, propertySchema] of Object.entries(
1559
+ schema.properties as Record<string, OpenApiSchema>,
1560
+ )) {
1561
+ const propertyType = convertSchemaToTypeScript(
1562
+ propertySchema,
1563
+ typeNameMap,
1564
+ );
1565
+
1566
+ // Determine if property should be optional
1567
+ // OpenAPI Logic:
1568
+ // - If schema has NO required array: all defined properties are required
1569
+ // - If schema HAS required array: only properties in array are required
1570
+ // - Undefined properties are never included in generated interface
1571
+ const isRequired = hasExplicitRequiredList
1572
+ ? requiredProperties.includes(propertyName)
1573
+ : true;
1574
+
1575
+ const optionalMarker = isRequired ? "" : "?";
1576
+ if (
1577
+ propertySchema.type === "string" &&
1578
+ propertySchema.format === "date-time"
1579
+ ) {
1580
+ propertyDefinitions.push(" // openapi Date -> String");
1581
+ }
1582
+ propertyDefinitions.push(` ${propertyName}${optionalMarker}: ${propertyType};`);
1583
+ }
1584
+
1585
+ const propertiesString = propertyDefinitions.join("\n");
1586
+ return `export interface ${modelName} {\n${propertiesString}\n}`;
1587
+ }
1588
+
1589
+ const inlineType = convertSchemaToTypeScript(schema, typeNameMap);
1590
+ if (inlineType !== "any") {
1591
+ return `export type ${modelName} = ${inlineType};`;
1592
+ }
1593
+
1594
+ // Fallback for unsupported schema types
1595
+ console.warn(
1596
+ `Warning: Unsupported schema type for "${modelName}". Using fallback type.`,
1597
+ );
1598
+ return `export type ${modelName} = any;`;
1599
+ }