@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.
- package/README.md +570 -0
- package/bin.ts +15 -0
- package/docs/plugins.md +154 -0
- package/package.json +59 -0
- package/src/cli/args.ts +196 -0
- package/src/cli/config.ts +228 -0
- package/src/cli/main.ts +276 -0
- package/src/cli/project.ts +113 -0
- package/src/cli/types.ts +22 -0
- package/src/generators/angular.ts +76 -0
- package/src/generators/fetch.ts +994 -0
- package/src/generators/missing-definitions.ts +776 -0
- package/src/generators/type-coverage.ts +938 -0
- package/src/index.ts +47 -0
- package/src/plugins/builtin/angular.ts +146 -0
- package/src/plugins/builtin/axios.ts +307 -0
- package/src/plugins/builtin/fetch.ts +140 -0
- package/src/plugins/registry.ts +84 -0
- package/src/plugins/runner.ts +174 -0
- package/src/plugins/types.ts +97 -0
- package/src/schemas/swagger.ts +134 -0
- package/src/utils/index.ts +1599 -0
|
@@ -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
|
+
}
|