@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,776 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { SwaggerOrOpenAPISchema } from "../schemas/swagger";
|
|
3
|
+
import type {
|
|
4
|
+
OpenApiOperation,
|
|
5
|
+
OpenApiPath,
|
|
6
|
+
OperationTypeInfo,
|
|
7
|
+
OperationTypeMap,
|
|
8
|
+
} from "../utils";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Missing Swagger definition issue.
|
|
12
|
+
*/
|
|
13
|
+
export type MissingSwaggerDefinitionIssue = {
|
|
14
|
+
path: string;
|
|
15
|
+
method: string;
|
|
16
|
+
location: "path.parameter" | "query.parameter" | "request.body" | "response.body";
|
|
17
|
+
field?: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
recommendedDefinition: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Missing Swagger definitions report.
|
|
24
|
+
*/
|
|
25
|
+
export type MissingSwaggerDefinitionsReport = {
|
|
26
|
+
generatedAt: string;
|
|
27
|
+
totalIssues: number;
|
|
28
|
+
summary: {
|
|
29
|
+
pathParameters: number;
|
|
30
|
+
queryParameters: number;
|
|
31
|
+
requestBodies: number;
|
|
32
|
+
responseBodies: number;
|
|
33
|
+
};
|
|
34
|
+
issues: MissingSwaggerDefinitionIssue[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create missing Swagger definitions report.
|
|
39
|
+
* @param data Input parameter `data`.
|
|
40
|
+
* @param operationTypes Input parameter `operationTypes`.
|
|
41
|
+
* @returns Create missing Swagger definitions report output as `MissingSwaggerDefinitionsReport`.
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const result = createMissingSwaggerDefinitionsReport({ paths: {} } as never, {});
|
|
45
|
+
* // result: MissingSwaggerDefinitionsReport
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function createMissingSwaggerDefinitionsReport(
|
|
49
|
+
data: z.infer<typeof SwaggerOrOpenAPISchema>,
|
|
50
|
+
operationTypes?: OperationTypeMap,
|
|
51
|
+
): MissingSwaggerDefinitionsReport {
|
|
52
|
+
const issues = collectMissingSwaggerDefinitionIssues(data, operationTypes);
|
|
53
|
+
return {
|
|
54
|
+
generatedAt: new Date().toISOString(),
|
|
55
|
+
totalIssues: issues.length,
|
|
56
|
+
summary: buildSummary(issues),
|
|
57
|
+
issues,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate missing Swagger definitions file content.
|
|
63
|
+
* @param report Input parameter `report`.
|
|
64
|
+
* @returns Generate missing Swagger definitions file content output as `string`.
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const result = generateMissingSwaggerDefinitionsFile({ generatedAt: "", totalIssues: 0, summary: { pathParameters: 0, queryParameters: 0, requestBodies: 0, responseBodies: 0 }, issues: [] });
|
|
68
|
+
* // result: string
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function generateMissingSwaggerDefinitionsFile(
|
|
72
|
+
report: MissingSwaggerDefinitionsReport,
|
|
73
|
+
): string {
|
|
74
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Collect missing Swagger definition issues.
|
|
79
|
+
* @param data Input parameter `data`.
|
|
80
|
+
* @param operationTypes Input parameter `operationTypes`.
|
|
81
|
+
* @returns Collect missing Swagger definition issues output as `MissingSwaggerDefinitionIssue[]`.
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const result = collectMissingSwaggerDefinitionIssues({ paths: {} } as never, {});
|
|
85
|
+
* // result: MissingSwaggerDefinitionIssue[]
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
function collectMissingSwaggerDefinitionIssues(
|
|
89
|
+
data: z.infer<typeof SwaggerOrOpenAPISchema>,
|
|
90
|
+
operationTypes?: OperationTypeMap,
|
|
91
|
+
): MissingSwaggerDefinitionIssue[] {
|
|
92
|
+
if (!data.paths) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const issues: MissingSwaggerDefinitionIssue[] = [];
|
|
97
|
+
const httpMethods = [
|
|
98
|
+
"get",
|
|
99
|
+
"post",
|
|
100
|
+
"put",
|
|
101
|
+
"delete",
|
|
102
|
+
"patch",
|
|
103
|
+
"head",
|
|
104
|
+
"options",
|
|
105
|
+
] as const;
|
|
106
|
+
|
|
107
|
+
for (const [path, pathItem] of Object.entries(data.paths)) {
|
|
108
|
+
for (const httpMethod of httpMethods) {
|
|
109
|
+
const operation = (pathItem as OpenApiPath)[httpMethod];
|
|
110
|
+
if (!operation) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const typeInfo = operationTypes?.[path]?.[httpMethod];
|
|
115
|
+
const operationIssues = collectOperationIssues(
|
|
116
|
+
path,
|
|
117
|
+
httpMethod,
|
|
118
|
+
operation,
|
|
119
|
+
typeInfo,
|
|
120
|
+
);
|
|
121
|
+
issues.push(...operationIssues);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return issues;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Collect operation issues.
|
|
130
|
+
* @param path Input parameter `path`.
|
|
131
|
+
* @param httpMethod Input parameter `httpMethod`.
|
|
132
|
+
* @param operation Input parameter `operation`.
|
|
133
|
+
* @param typeInfo Input parameter `typeInfo`.
|
|
134
|
+
* @returns Collect operation issues output as `MissingSwaggerDefinitionIssue[]`.
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* const result = collectOperationIssues("/users", "get", {}, undefined);
|
|
138
|
+
* // result: MissingSwaggerDefinitionIssue[]
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
function collectOperationIssues(
|
|
142
|
+
path: string,
|
|
143
|
+
httpMethod: string,
|
|
144
|
+
operation: OpenApiOperation,
|
|
145
|
+
typeInfo?: OperationTypeInfo,
|
|
146
|
+
): MissingSwaggerDefinitionIssue[] {
|
|
147
|
+
const issues: MissingSwaggerDefinitionIssue[] = [];
|
|
148
|
+
|
|
149
|
+
const parameterIssues = collectParameterIssues(path, httpMethod, operation);
|
|
150
|
+
issues.push(...parameterIssues);
|
|
151
|
+
|
|
152
|
+
const requestIssue = collectRequestBodyIssue(path, httpMethod, operation, typeInfo);
|
|
153
|
+
if (requestIssue) {
|
|
154
|
+
issues.push(requestIssue);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const responseIssue = collectResponseBodyIssue(path, httpMethod, operation, typeInfo);
|
|
158
|
+
if (responseIssue) {
|
|
159
|
+
issues.push(responseIssue);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return issues;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Collect parameter issues.
|
|
167
|
+
* @param path Input parameter `path`.
|
|
168
|
+
* @param httpMethod Input parameter `httpMethod`.
|
|
169
|
+
* @param operation Input parameter `operation`.
|
|
170
|
+
* @returns Collect parameter issues output as `MissingSwaggerDefinitionIssue[]`.
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* const result = collectParameterIssues("/users/{id}", "get", {});
|
|
174
|
+
* // result: MissingSwaggerDefinitionIssue[]
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
function collectParameterIssues(
|
|
178
|
+
path: string,
|
|
179
|
+
httpMethod: string,
|
|
180
|
+
operation: OpenApiOperation,
|
|
181
|
+
): MissingSwaggerDefinitionIssue[] {
|
|
182
|
+
const issues: MissingSwaggerDefinitionIssue[] = [];
|
|
183
|
+
const parameters = Array.isArray(operation.parameters)
|
|
184
|
+
? operation.parameters
|
|
185
|
+
: [];
|
|
186
|
+
|
|
187
|
+
const pathPlaceholders = getPathPlaceholders(path);
|
|
188
|
+
for (const placeholder of pathPlaceholders) {
|
|
189
|
+
const pathParameter = parameters.find(
|
|
190
|
+
(parameter) => parameter.in === "path" && parameter.name === placeholder,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (!pathParameter) {
|
|
194
|
+
issues.push({
|
|
195
|
+
path,
|
|
196
|
+
method: httpMethod.toUpperCase(),
|
|
197
|
+
location: "path.parameter",
|
|
198
|
+
field: placeholder,
|
|
199
|
+
reason: "Path parameter is missing from operation.parameters.",
|
|
200
|
+
recommendedDefinition:
|
|
201
|
+
"Add a path parameter definition with schema.type or schema.$ref.",
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!isSchemaAny(pathParameter.schema)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
issues.push({
|
|
211
|
+
path,
|
|
212
|
+
method: httpMethod.toUpperCase(),
|
|
213
|
+
location: "path.parameter",
|
|
214
|
+
field: placeholder,
|
|
215
|
+
reason: "Path parameter schema is missing or unresolved.",
|
|
216
|
+
recommendedDefinition:
|
|
217
|
+
"Define parameter.schema with a primitive type, enum, object, array, or valid $ref.",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const parameter of parameters) {
|
|
222
|
+
if (parameter.in !== "query") {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!isSchemaAny(parameter.schema)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
issues.push({
|
|
231
|
+
path,
|
|
232
|
+
method: httpMethod.toUpperCase(),
|
|
233
|
+
location: "query.parameter",
|
|
234
|
+
field: parameter.name,
|
|
235
|
+
reason: "Query parameter schema is missing or unresolved.",
|
|
236
|
+
recommendedDefinition:
|
|
237
|
+
"Define query parameter schema.type, schema.enum, schema.items, anyOf/oneOf/allOf, or schema.$ref.",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return issues;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Collect request body issue.
|
|
246
|
+
* @param path Input parameter `path`.
|
|
247
|
+
* @param httpMethod Input parameter `httpMethod`.
|
|
248
|
+
* @param operation Input parameter `operation`.
|
|
249
|
+
* @param typeInfo Input parameter `typeInfo`.
|
|
250
|
+
* @returns Collect request body issue output as `MissingSwaggerDefinitionIssue | undefined`.
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* const result = collectRequestBodyIssue("/users", "post", {}, undefined);
|
|
254
|
+
* // result: MissingSwaggerDefinitionIssue | undefined
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
function collectRequestBodyIssue(
|
|
258
|
+
path: string,
|
|
259
|
+
httpMethod: string,
|
|
260
|
+
operation: OpenApiOperation,
|
|
261
|
+
typeInfo?: OperationTypeInfo,
|
|
262
|
+
): MissingSwaggerDefinitionIssue | undefined {
|
|
263
|
+
if (!operation.requestBody) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const requestType = typeInfo?.requestType ?? extractRequestType(operation) ?? "any";
|
|
268
|
+
if (!containsAnyType(requestType)) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const schema = getPreferredContentSchema(operation.requestBody.content);
|
|
273
|
+
if (!schema) {
|
|
274
|
+
return {
|
|
275
|
+
path,
|
|
276
|
+
method: httpMethod.toUpperCase(),
|
|
277
|
+
location: "request.body",
|
|
278
|
+
reason: "Request body exists but no schema was documented in content.",
|
|
279
|
+
recommendedDefinition:
|
|
280
|
+
"Add requestBody.content['application/json'].schema with type/object/array or $ref.",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
path,
|
|
286
|
+
method: httpMethod.toUpperCase(),
|
|
287
|
+
location: "request.body",
|
|
288
|
+
reason: "Request body schema could not be resolved to a concrete model type.",
|
|
289
|
+
recommendedDefinition:
|
|
290
|
+
"Reference a schema with $ref or define a complete inline schema in requestBody.content.",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Collect response body issue.
|
|
296
|
+
* @param path Input parameter `path`.
|
|
297
|
+
* @param httpMethod Input parameter `httpMethod`.
|
|
298
|
+
* @param operation Input parameter `operation`.
|
|
299
|
+
* @param typeInfo Input parameter `typeInfo`.
|
|
300
|
+
* @returns Collect response body issue output as `MissingSwaggerDefinitionIssue | undefined`.
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* const result = collectResponseBodyIssue("/users", "get", {}, undefined);
|
|
304
|
+
* // result: MissingSwaggerDefinitionIssue | undefined
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
function collectResponseBodyIssue(
|
|
308
|
+
path: string,
|
|
309
|
+
httpMethod: string,
|
|
310
|
+
operation: OpenApiOperation,
|
|
311
|
+
typeInfo?: OperationTypeInfo,
|
|
312
|
+
): MissingSwaggerDefinitionIssue | undefined {
|
|
313
|
+
const requestType = typeInfo?.requestType ?? extractRequestType(operation) ?? "any";
|
|
314
|
+
let responseType = typeInfo?.responseType ?? extractResponseType(operation);
|
|
315
|
+
if (!responseType) {
|
|
316
|
+
responseType = "any";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const isMutatingMethod = ["post", "put", "patch"].includes(httpMethod);
|
|
320
|
+
if (containsAnyType(responseType) && isMutatingMethod && !containsAnyType(requestType)) {
|
|
321
|
+
responseType = requestType;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!containsAnyType(responseType)) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const successResponse = getSuccessResponse(operation);
|
|
329
|
+
if (!successResponse) {
|
|
330
|
+
return {
|
|
331
|
+
path,
|
|
332
|
+
method: httpMethod.toUpperCase(),
|
|
333
|
+
location: "response.body",
|
|
334
|
+
reason: "No 2xx success response is documented for this operation.",
|
|
335
|
+
recommendedDefinition:
|
|
336
|
+
"Add a 200/201 (or any 2xx) response with content schema for the HTTP client return type.",
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const schema = getPreferredContentSchema(successResponse.content);
|
|
341
|
+
if (!schema) {
|
|
342
|
+
return {
|
|
343
|
+
path,
|
|
344
|
+
method: httpMethod.toUpperCase(),
|
|
345
|
+
location: "response.body",
|
|
346
|
+
reason: "Success response exists but no response schema was documented in content.",
|
|
347
|
+
recommendedDefinition:
|
|
348
|
+
"Add response.content['application/json'].schema using $ref or a fully defined inline schema.",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
path,
|
|
354
|
+
method: httpMethod.toUpperCase(),
|
|
355
|
+
location: "response.body",
|
|
356
|
+
reason: "Response schema could not be resolved to a concrete model type.",
|
|
357
|
+
recommendedDefinition:
|
|
358
|
+
"Use $ref to a schema in components.schemas/definitions or define response schema details explicitly.",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Build summary.
|
|
364
|
+
* @param issues Input parameter `issues`.
|
|
365
|
+
* @returns Build summary output as `MissingSwaggerDefinitionsReport["summary"]`.
|
|
366
|
+
* @example
|
|
367
|
+
* ```ts
|
|
368
|
+
* const result = buildSummary([]);
|
|
369
|
+
* // result: MissingSwaggerDefinitionsReport["summary"]
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
function buildSummary(
|
|
373
|
+
issues: MissingSwaggerDefinitionIssue[],
|
|
374
|
+
): MissingSwaggerDefinitionsReport["summary"] {
|
|
375
|
+
const summary = {
|
|
376
|
+
pathParameters: 0,
|
|
377
|
+
queryParameters: 0,
|
|
378
|
+
requestBodies: 0,
|
|
379
|
+
responseBodies: 0,
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
for (const issue of issues) {
|
|
383
|
+
if (issue.location === "path.parameter") {
|
|
384
|
+
summary.pathParameters += 1;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (issue.location === "query.parameter") {
|
|
389
|
+
summary.queryParameters += 1;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (issue.location === "request.body") {
|
|
394
|
+
summary.requestBodies += 1;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
summary.responseBodies += 1;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return summary;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get path placeholders.
|
|
406
|
+
* @param path Input parameter `path`.
|
|
407
|
+
* @returns Get path placeholders output as `string[]`.
|
|
408
|
+
* @example
|
|
409
|
+
* ```ts
|
|
410
|
+
* const result = getPathPlaceholders("/users/{id}");
|
|
411
|
+
* // result: string[]
|
|
412
|
+
* ```
|
|
413
|
+
*/
|
|
414
|
+
function getPathPlaceholders(path: string): string[] {
|
|
415
|
+
const matches = path.match(/\{([^}]+)\}/g);
|
|
416
|
+
if (!matches) {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return matches.map((match) => match.slice(1, -1));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Get preferred content schema.
|
|
425
|
+
* @param content Input parameter `content`.
|
|
426
|
+
* @returns Get preferred content schema output as `Record<string, unknown> | undefined`.
|
|
427
|
+
* @example
|
|
428
|
+
* ```ts
|
|
429
|
+
* const result = getPreferredContentSchema(undefined);
|
|
430
|
+
* // result: Record<string, unknown> | undefined
|
|
431
|
+
* ```
|
|
432
|
+
*/
|
|
433
|
+
function getPreferredContentSchema(
|
|
434
|
+
content?: Record<string, { schema: Record<string, unknown> }>,
|
|
435
|
+
): Record<string, unknown> | undefined {
|
|
436
|
+
if (!content) {
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const jsonSchema = content["application/json"]?.schema;
|
|
441
|
+
if (jsonSchema && typeof jsonSchema === "object") {
|
|
442
|
+
return jsonSchema;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const firstKey = Object.keys(content)[0];
|
|
446
|
+
if (!firstKey) {
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const firstSchema = content[firstKey]?.schema;
|
|
451
|
+
if (!firstSchema || typeof firstSchema !== "object") {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return firstSchema;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get success response.
|
|
460
|
+
* @param operation Input parameter `operation`.
|
|
461
|
+
* @returns Get success response output as `{ content?: Record<string, { schema: Record<string, unknown> }> } | undefined`.
|
|
462
|
+
* @example
|
|
463
|
+
* ```ts
|
|
464
|
+
* const result = getSuccessResponse({});
|
|
465
|
+
* // result: { content?: Record<string, { schema: Record<string, unknown> }> } | undefined
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
468
|
+
function getSuccessResponse(
|
|
469
|
+
operation: OpenApiOperation,
|
|
470
|
+
): { content?: Record<string, { schema: Record<string, unknown> }> } | undefined {
|
|
471
|
+
const responses = operation.responses ?? {};
|
|
472
|
+
|
|
473
|
+
const response200 = responses["200"];
|
|
474
|
+
if (response200) {
|
|
475
|
+
return response200 as {
|
|
476
|
+
content?: Record<string, { schema: Record<string, unknown> }>;
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const response201 = responses["201"];
|
|
481
|
+
if (response201) {
|
|
482
|
+
return response201 as {
|
|
483
|
+
content?: Record<string, { schema: Record<string, unknown> }>;
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const successStatus = Object.keys(responses).find(
|
|
488
|
+
(statusCode) => statusCode.startsWith("2") && responses[statusCode],
|
|
489
|
+
);
|
|
490
|
+
if (!successStatus) {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return responses[successStatus] as {
|
|
495
|
+
content?: Record<string, { schema: Record<string, unknown> }>;
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Extract request type.
|
|
501
|
+
* @param operation Input parameter `operation`.
|
|
502
|
+
* @returns Extract request type output as `string | undefined`.
|
|
503
|
+
* @example
|
|
504
|
+
* ```ts
|
|
505
|
+
* const result = extractRequestType({});
|
|
506
|
+
* // result: string | undefined
|
|
507
|
+
* ```
|
|
508
|
+
*/
|
|
509
|
+
function extractRequestType(operation: OpenApiOperation): string | undefined {
|
|
510
|
+
const schema = getPreferredContentSchema(operation.requestBody?.content as never);
|
|
511
|
+
if (!schema) {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const schemaRef = getSchemaRef(schema);
|
|
516
|
+
if (schemaRef) {
|
|
517
|
+
return schemaRef;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const schemaType = schema.type;
|
|
521
|
+
if (schemaType !== "array") {
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const items = schema.items;
|
|
526
|
+
if (!items || typeof items !== "object") {
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const itemRef = getSchemaRef(items);
|
|
531
|
+
if (!itemRef) {
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return `${itemRef}[]`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Extract response type.
|
|
540
|
+
* @param operation Input parameter `operation`.
|
|
541
|
+
* @returns Extract response type output as `string`.
|
|
542
|
+
* @example
|
|
543
|
+
* ```ts
|
|
544
|
+
* const result = extractResponseType({});
|
|
545
|
+
* // result: string
|
|
546
|
+
* ```
|
|
547
|
+
*/
|
|
548
|
+
function extractResponseType(operation: OpenApiOperation): string {
|
|
549
|
+
const response = getSuccessResponse(operation);
|
|
550
|
+
if (!response) {
|
|
551
|
+
return "any";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const schema = getPreferredContentSchema(response.content);
|
|
555
|
+
if (!schema) {
|
|
556
|
+
return "any";
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const schemaRef = getSchemaRef(schema);
|
|
560
|
+
if (schemaRef) {
|
|
561
|
+
return schemaRef;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const schemaType = schema.type;
|
|
565
|
+
if (schemaType !== "array") {
|
|
566
|
+
return "any";
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const items = schema.items;
|
|
570
|
+
if (!items || typeof items !== "object") {
|
|
571
|
+
return "any";
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const itemRef = getSchemaRef(items);
|
|
575
|
+
if (!itemRef) {
|
|
576
|
+
return "any[]";
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return `${itemRef}[]`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Get schema reference name.
|
|
584
|
+
* @param schema Input parameter `schema`.
|
|
585
|
+
* @returns Get schema reference name output as `string | undefined`.
|
|
586
|
+
* @example
|
|
587
|
+
* ```ts
|
|
588
|
+
* const result = getSchemaRef({ $ref: "#/components/schemas/User" });
|
|
589
|
+
* // result: string | undefined
|
|
590
|
+
* ```
|
|
591
|
+
*/
|
|
592
|
+
function getSchemaRef(schema: Record<string, unknown>): string | undefined {
|
|
593
|
+
const schemaReference = schema.$ref;
|
|
594
|
+
if (typeof schemaReference !== "string") {
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const referencePathParts = schemaReference.split("/");
|
|
599
|
+
const referenceName = referencePathParts[referencePathParts.length - 1];
|
|
600
|
+
if (!referenceName) {
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return referenceName;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Check if schema resolves to any.
|
|
609
|
+
* @param schema Input parameter `schema`.
|
|
610
|
+
* @returns Check if schema resolves to any output as `boolean`.
|
|
611
|
+
* @example
|
|
612
|
+
* ```ts
|
|
613
|
+
* const result = isSchemaAny(undefined);
|
|
614
|
+
* // result: boolean
|
|
615
|
+
* ```
|
|
616
|
+
*/
|
|
617
|
+
function isSchemaAny(schema: unknown): boolean {
|
|
618
|
+
const resolvedType = resolveSchemaType(schema);
|
|
619
|
+
return containsAnyType(resolvedType);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Resolve schema type.
|
|
624
|
+
* @param schema Input parameter `schema`.
|
|
625
|
+
* @returns Resolve schema type output as `string`.
|
|
626
|
+
* @example
|
|
627
|
+
* ```ts
|
|
628
|
+
* const result = resolveSchemaType({ type: "string" });
|
|
629
|
+
* // result: string
|
|
630
|
+
* ```
|
|
631
|
+
*/
|
|
632
|
+
function resolveSchemaType(schema: unknown): string {
|
|
633
|
+
if (!schema || typeof schema !== "object") {
|
|
634
|
+
return "any";
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const typedSchema = schema as Record<string, unknown>;
|
|
638
|
+
const schemaRef = getSchemaRef(typedSchema);
|
|
639
|
+
if (schemaRef) {
|
|
640
|
+
return schemaRef;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const schemaEnum = typedSchema.enum;
|
|
644
|
+
if (Array.isArray(schemaEnum)) {
|
|
645
|
+
const union = schemaEnum
|
|
646
|
+
.map((enumValue) =>
|
|
647
|
+
typeof enumValue === "string" ? `\"${enumValue}\"` : String(enumValue),
|
|
648
|
+
)
|
|
649
|
+
.join(" | ");
|
|
650
|
+
if (!union) {
|
|
651
|
+
return "any";
|
|
652
|
+
}
|
|
653
|
+
return union;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const anyOfType = resolveUnionType(typedSchema.anyOf);
|
|
657
|
+
if (anyOfType) {
|
|
658
|
+
return anyOfType;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const oneOfType = resolveUnionType(typedSchema.oneOf);
|
|
662
|
+
if (oneOfType) {
|
|
663
|
+
return oneOfType;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const allOfType = resolveIntersectionType(typedSchema.allOf);
|
|
667
|
+
if (allOfType) {
|
|
668
|
+
return allOfType;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const schemaType = typedSchema.type;
|
|
672
|
+
if (schemaType === "array") {
|
|
673
|
+
const itemType = resolveSchemaType(typedSchema.items);
|
|
674
|
+
return `${itemType}[]`;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (schemaType === "object" && typedSchema.properties) {
|
|
678
|
+
return "object";
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (schemaType === "string") {
|
|
682
|
+
const format = typedSchema.format;
|
|
683
|
+
if (format === "numeric") {
|
|
684
|
+
return "number";
|
|
685
|
+
}
|
|
686
|
+
return "string";
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (schemaType === "number") {
|
|
690
|
+
return "number";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (schemaType === "integer") {
|
|
694
|
+
return "number";
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (schemaType === "boolean") {
|
|
698
|
+
return "boolean";
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return "any";
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Resolve union type.
|
|
706
|
+
* @param schemaVariants Input parameter `schemaVariants`.
|
|
707
|
+
* @returns Resolve union type output as `string | undefined`.
|
|
708
|
+
* @example
|
|
709
|
+
* ```ts
|
|
710
|
+
* const result = resolveUnionType([{ type: "string" }]);
|
|
711
|
+
* // result: string | undefined
|
|
712
|
+
* ```
|
|
713
|
+
*/
|
|
714
|
+
function resolveUnionType(schemaVariants: unknown): string | undefined {
|
|
715
|
+
if (!Array.isArray(schemaVariants)) {
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const variants = schemaVariants
|
|
720
|
+
.map((variant) => resolveSchemaType(variant))
|
|
721
|
+
.filter(Boolean);
|
|
722
|
+
if (variants.length === 0) {
|
|
723
|
+
return undefined;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return variants.join(" | ");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Resolve intersection type.
|
|
731
|
+
* @param schemaVariants Input parameter `schemaVariants`.
|
|
732
|
+
* @returns Resolve intersection type output as `string | undefined`.
|
|
733
|
+
* @example
|
|
734
|
+
* ```ts
|
|
735
|
+
* const result = resolveIntersectionType([{ type: "string" }]);
|
|
736
|
+
* // result: string | undefined
|
|
737
|
+
* ```
|
|
738
|
+
*/
|
|
739
|
+
function resolveIntersectionType(schemaVariants: unknown): string | undefined {
|
|
740
|
+
if (!Array.isArray(schemaVariants)) {
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const variants = schemaVariants
|
|
745
|
+
.map((variant) => resolveSchemaType(variant))
|
|
746
|
+
.filter(Boolean);
|
|
747
|
+
if (variants.length === 0) {
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return variants.join(" & ");
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Check if type includes any.
|
|
756
|
+
* @param typeName Input parameter `typeName`.
|
|
757
|
+
* @returns Check if type includes any output as `boolean`.
|
|
758
|
+
* @example
|
|
759
|
+
* ```ts
|
|
760
|
+
* const result = containsAnyType("any[]");
|
|
761
|
+
* // result: boolean
|
|
762
|
+
* ```
|
|
763
|
+
*/
|
|
764
|
+
function containsAnyType(typeName: string): boolean {
|
|
765
|
+
const normalized = typeName.trim();
|
|
766
|
+
if (normalized === "any") {
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (normalized === "any[]") {
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const tokens = normalized.split(/[|&]/).map((token) => token.trim());
|
|
775
|
+
return tokens.some((token) => token === "any" || token === "any[]");
|
|
776
|
+
}
|