@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,938 @@
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
+ * Type coverage location.
12
+ */
13
+ export type TypeCoverageLocation =
14
+ | "path.parameter"
15
+ | "query.parameter"
16
+ | "request.body"
17
+ | "response.body";
18
+
19
+ /**
20
+ * Type coverage issue.
21
+ */
22
+ export type TypeCoverageIssue = {
23
+ path: string;
24
+ method: string;
25
+ location: TypeCoverageLocation;
26
+ field?: string;
27
+ reason: string;
28
+ };
29
+
30
+ /**
31
+ * Type coverage metrics.
32
+ */
33
+ export type TypeCoverageMetrics = {
34
+ total: number;
35
+ typed: number;
36
+ untyped: number;
37
+ coveragePercentage: number;
38
+ };
39
+
40
+ /**
41
+ * Type coverage operation summary.
42
+ */
43
+ export type TypeCoverageOperationSummary = {
44
+ path: string;
45
+ method: string;
46
+ total: number;
47
+ typed: number;
48
+ untyped: number;
49
+ coveragePercentage: number;
50
+ untypedLocations: TypeCoverageLocation[];
51
+ };
52
+
53
+ /**
54
+ * Type coverage report.
55
+ */
56
+ export type TypeCoverageReport = {
57
+ generatedAt: string;
58
+ totalOperations: number;
59
+ totals: TypeCoverageMetrics;
60
+ summary: {
61
+ pathParameters: TypeCoverageMetrics;
62
+ queryParameters: TypeCoverageMetrics;
63
+ requestBodies: TypeCoverageMetrics;
64
+ responseBodies: TypeCoverageMetrics;
65
+ };
66
+ operations: TypeCoverageOperationSummary[];
67
+ issues: TypeCoverageIssue[];
68
+ };
69
+
70
+ type CoverageEntry = {
71
+ path: string;
72
+ method: string;
73
+ location: TypeCoverageLocation;
74
+ field?: string;
75
+ isTyped: boolean;
76
+ reason?: string;
77
+ };
78
+
79
+ /**
80
+ * Create type coverage report.
81
+ * @param data Input parameter `data`.
82
+ * @param operationTypes Input parameter `operationTypes`.
83
+ * @returns Create type coverage report output as `TypeCoverageReport`.
84
+ * @example
85
+ * ```ts
86
+ * const result = createTypeCoverageReport({ paths: {} } as never, {});
87
+ * // result: TypeCoverageReport
88
+ * ```
89
+ */
90
+ export function createTypeCoverageReport(
91
+ data: z.infer<typeof SwaggerOrOpenAPISchema>,
92
+ operationTypes?: OperationTypeMap,
93
+ ): TypeCoverageReport {
94
+ const entries = collectCoverageEntries(data, operationTypes);
95
+ const operations = buildOperationSummary(entries);
96
+ const issues = buildTypeCoverageIssues(entries);
97
+ return {
98
+ generatedAt: new Date().toISOString(),
99
+ totalOperations: operations.length,
100
+ totals: buildMetrics(entries),
101
+ summary: {
102
+ pathParameters: buildMetricsByLocation(entries, "path.parameter"),
103
+ queryParameters: buildMetricsByLocation(entries, "query.parameter"),
104
+ requestBodies: buildMetricsByLocation(entries, "request.body"),
105
+ responseBodies: buildMetricsByLocation(entries, "response.body"),
106
+ },
107
+ operations,
108
+ issues,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Generate type coverage report file.
114
+ * @param report Input parameter `report`.
115
+ * @returns Generate type coverage report file output as `string`.
116
+ * @example
117
+ * ```ts
118
+ * const result = generateTypeCoverageReportFile({
119
+ * generatedAt: "",
120
+ * totalOperations: 0,
121
+ * totals: { total: 0, typed: 0, untyped: 0, coveragePercentage: 100 },
122
+ * summary: {
123
+ * pathParameters: { total: 0, typed: 0, untyped: 0, coveragePercentage: 100 },
124
+ * queryParameters: { total: 0, typed: 0, untyped: 0, coveragePercentage: 100 },
125
+ * requestBodies: { total: 0, typed: 0, untyped: 0, coveragePercentage: 100 },
126
+ * responseBodies: { total: 0, typed: 0, untyped: 0, coveragePercentage: 100 },
127
+ * },
128
+ * operations: [],
129
+ * issues: [],
130
+ * });
131
+ * // result: string
132
+ * ```
133
+ */
134
+ export function generateTypeCoverageReportFile(
135
+ report: TypeCoverageReport,
136
+ ): string {
137
+ return `${JSON.stringify(report, null, 2)}\n`;
138
+ }
139
+
140
+ /**
141
+ * Collect coverage entries.
142
+ * @param data Input parameter `data`.
143
+ * @param operationTypes Input parameter `operationTypes`.
144
+ * @returns Collect coverage entries output as `CoverageEntry[]`.
145
+ * @example
146
+ * ```ts
147
+ * const result = collectCoverageEntries({ paths: {} } as never, {});
148
+ * // result: CoverageEntry[]
149
+ * ```
150
+ */
151
+ function collectCoverageEntries(
152
+ data: z.infer<typeof SwaggerOrOpenAPISchema>,
153
+ operationTypes?: OperationTypeMap,
154
+ ): CoverageEntry[] {
155
+ if (!data.paths) {
156
+ return [];
157
+ }
158
+
159
+ const entries: CoverageEntry[] = [];
160
+ const httpMethods = [
161
+ "get",
162
+ "post",
163
+ "put",
164
+ "delete",
165
+ "patch",
166
+ "head",
167
+ "options",
168
+ ] as const;
169
+
170
+ for (const [path, pathItem] of Object.entries(data.paths)) {
171
+ for (const httpMethod of httpMethods) {
172
+ const operation = (pathItem as OpenApiPath)[httpMethod];
173
+ if (!operation) {
174
+ continue;
175
+ }
176
+
177
+ const typeInfo = operationTypes?.[path]?.[httpMethod];
178
+ const operationEntries = collectOperationEntries(
179
+ path,
180
+ httpMethod,
181
+ operation,
182
+ typeInfo,
183
+ );
184
+ entries.push(...operationEntries);
185
+ }
186
+ }
187
+
188
+ return entries;
189
+ }
190
+
191
+ /**
192
+ * Collect operation coverage entries.
193
+ * @param path Input parameter `path`.
194
+ * @param httpMethod Input parameter `httpMethod`.
195
+ * @param operation Input parameter `operation`.
196
+ * @param typeInfo Input parameter `typeInfo`.
197
+ * @returns Collect operation coverage entries output as `CoverageEntry[]`.
198
+ * @example
199
+ * ```ts
200
+ * const result = collectOperationEntries("/users", "get", {}, undefined);
201
+ * // result: CoverageEntry[]
202
+ * ```
203
+ */
204
+ function collectOperationEntries(
205
+ path: string,
206
+ httpMethod: string,
207
+ operation: OpenApiOperation,
208
+ typeInfo?: OperationTypeInfo,
209
+ ): CoverageEntry[] {
210
+ const entries: CoverageEntry[] = [];
211
+ const parameterEntries = collectParameterEntries(path, httpMethod, operation);
212
+ entries.push(...parameterEntries);
213
+
214
+ const requestEntry = collectRequestBodyEntry(path, httpMethod, operation, typeInfo);
215
+ if (requestEntry) {
216
+ entries.push(requestEntry);
217
+ }
218
+
219
+ const responseEntry = collectResponseBodyEntry(path, httpMethod, operation, typeInfo);
220
+ entries.push(responseEntry);
221
+
222
+ return entries;
223
+ }
224
+
225
+ /**
226
+ * Collect parameter coverage entries.
227
+ * @param path Input parameter `path`.
228
+ * @param httpMethod Input parameter `httpMethod`.
229
+ * @param operation Input parameter `operation`.
230
+ * @returns Collect parameter coverage entries output as `CoverageEntry[]`.
231
+ * @example
232
+ * ```ts
233
+ * const result = collectParameterEntries("/users/{id}", "get", {});
234
+ * // result: CoverageEntry[]
235
+ * ```
236
+ */
237
+ function collectParameterEntries(
238
+ path: string,
239
+ httpMethod: string,
240
+ operation: OpenApiOperation,
241
+ ): CoverageEntry[] {
242
+ const entries: CoverageEntry[] = [];
243
+ const method = httpMethod.toUpperCase();
244
+ const parameters = Array.isArray(operation.parameters)
245
+ ? operation.parameters
246
+ : [];
247
+
248
+ const pathPlaceholders = getPathPlaceholders(path);
249
+ for (const placeholder of pathPlaceholders) {
250
+ const pathParameter = parameters.find(
251
+ (parameter) => parameter.in === "path" && parameter.name === placeholder,
252
+ );
253
+
254
+ if (!pathParameter) {
255
+ entries.push({
256
+ path,
257
+ method,
258
+ location: "path.parameter",
259
+ field: placeholder,
260
+ isTyped: false,
261
+ reason: "Path parameter is missing from operation.parameters.",
262
+ });
263
+ continue;
264
+ }
265
+
266
+ const isTyped = !isSchemaAny(pathParameter.schema);
267
+ entries.push({
268
+ path,
269
+ method,
270
+ location: "path.parameter",
271
+ field: placeholder,
272
+ isTyped,
273
+ reason: isTyped
274
+ ? undefined
275
+ : "Path parameter schema is missing or unresolved.",
276
+ });
277
+ }
278
+
279
+ for (const parameter of parameters) {
280
+ if (parameter.in !== "query") {
281
+ continue;
282
+ }
283
+
284
+ const isTyped = !isSchemaAny(parameter.schema);
285
+ entries.push({
286
+ path,
287
+ method,
288
+ location: "query.parameter",
289
+ field: parameter.name,
290
+ isTyped,
291
+ reason: isTyped
292
+ ? undefined
293
+ : "Query parameter schema is missing or unresolved.",
294
+ });
295
+ }
296
+
297
+ return entries;
298
+ }
299
+
300
+ /**
301
+ * Collect request body coverage entry.
302
+ * @param path Input parameter `path`.
303
+ * @param httpMethod Input parameter `httpMethod`.
304
+ * @param operation Input parameter `operation`.
305
+ * @param typeInfo Input parameter `typeInfo`.
306
+ * @returns Collect request body coverage entry output as `CoverageEntry | undefined`.
307
+ * @example
308
+ * ```ts
309
+ * const result = collectRequestBodyEntry("/users", "post", {}, undefined);
310
+ * // result: CoverageEntry | undefined
311
+ * ```
312
+ */
313
+ function collectRequestBodyEntry(
314
+ path: string,
315
+ httpMethod: string,
316
+ operation: OpenApiOperation,
317
+ typeInfo?: OperationTypeInfo,
318
+ ): CoverageEntry | undefined {
319
+ if (!operation.requestBody) {
320
+ return undefined;
321
+ }
322
+
323
+ const requestType = typeInfo?.requestType ?? extractRequestType(operation) ?? "any";
324
+ const isTyped = !containsAnyType(requestType);
325
+ const schema = getPreferredContentSchema(operation.requestBody.content);
326
+
327
+ return {
328
+ path,
329
+ method: httpMethod.toUpperCase(),
330
+ location: "request.body",
331
+ isTyped,
332
+ reason: resolveRequestBodyReason(schema, isTyped),
333
+ };
334
+ }
335
+
336
+ /**
337
+ * Resolve request body reason.
338
+ * @param schema Input parameter `schema`.
339
+ * @param isTyped Input parameter `isTyped`.
340
+ * @returns Resolve request body reason output as `string | undefined`.
341
+ * @example
342
+ * ```ts
343
+ * const result = resolveRequestBodyReason(undefined, false);
344
+ * // result: string | undefined
345
+ * ```
346
+ */
347
+ function resolveRequestBodyReason(
348
+ schema: Record<string, unknown> | undefined,
349
+ isTyped: boolean,
350
+ ): string | undefined {
351
+ if (isTyped) {
352
+ return undefined;
353
+ }
354
+ if (!schema) {
355
+ return "Request body exists but no schema was documented in content.";
356
+ }
357
+ return "Request body schema could not be resolved to a concrete model type.";
358
+ }
359
+
360
+ /**
361
+ * Collect response body coverage entry.
362
+ * @param path Input parameter `path`.
363
+ * @param httpMethod Input parameter `httpMethod`.
364
+ * @param operation Input parameter `operation`.
365
+ * @param typeInfo Input parameter `typeInfo`.
366
+ * @returns Collect response body coverage entry output as `CoverageEntry`.
367
+ * @example
368
+ * ```ts
369
+ * const result = collectResponseBodyEntry("/users", "get", {}, undefined);
370
+ * // result: CoverageEntry
371
+ * ```
372
+ */
373
+ function collectResponseBodyEntry(
374
+ path: string,
375
+ httpMethod: string,
376
+ operation: OpenApiOperation,
377
+ typeInfo?: OperationTypeInfo,
378
+ ): CoverageEntry {
379
+ const requestType = typeInfo?.requestType ?? extractRequestType(operation) ?? "any";
380
+ let responseType = typeInfo?.responseType ?? extractResponseType(operation);
381
+ if (!responseType) {
382
+ responseType = "any";
383
+ }
384
+
385
+ const isMutatingMethod = ["post", "put", "patch"].includes(httpMethod);
386
+ if (containsAnyType(responseType) && isMutatingMethod && !containsAnyType(requestType)) {
387
+ responseType = requestType;
388
+ }
389
+
390
+ const isTyped = !containsAnyType(responseType);
391
+ const successResponse = getSuccessResponse(operation);
392
+ const schema = getPreferredContentSchema(successResponse?.content);
393
+
394
+ return {
395
+ path,
396
+ method: httpMethod.toUpperCase(),
397
+ location: "response.body",
398
+ isTyped,
399
+ reason: resolveResponseBodyReason(successResponse, schema, isTyped),
400
+ };
401
+ }
402
+
403
+ /**
404
+ * Resolve response body reason.
405
+ * @param successResponse Input parameter `successResponse`.
406
+ * @param schema Input parameter `schema`.
407
+ * @param isTyped Input parameter `isTyped`.
408
+ * @returns Resolve response body reason output as `string | undefined`.
409
+ * @example
410
+ * ```ts
411
+ * const result = resolveResponseBodyReason(undefined, undefined, false);
412
+ * // result: string | undefined
413
+ * ```
414
+ */
415
+ function resolveResponseBodyReason(
416
+ successResponse:
417
+ | { content?: Record<string, { schema: Record<string, unknown> }> }
418
+ | undefined,
419
+ schema: Record<string, unknown> | undefined,
420
+ isTyped: boolean,
421
+ ): string | undefined {
422
+ if (isTyped) {
423
+ return undefined;
424
+ }
425
+ if (!successResponse) {
426
+ return "No 2xx success response is documented for this operation.";
427
+ }
428
+ if (!schema) {
429
+ return "Success response exists but no response schema was documented in content.";
430
+ }
431
+ return "Response schema could not be resolved to a concrete model type.";
432
+ }
433
+
434
+ /**
435
+ * Build type coverage issues.
436
+ * @param entries Input parameter `entries`.
437
+ * @returns Build type coverage issues output as `TypeCoverageIssue[]`.
438
+ * @example
439
+ * ```ts
440
+ * const result = buildTypeCoverageIssues([]);
441
+ * // result: TypeCoverageIssue[]
442
+ * ```
443
+ */
444
+ function buildTypeCoverageIssues(entries: CoverageEntry[]): TypeCoverageIssue[] {
445
+ return entries
446
+ .filter((entry) => !entry.isTyped)
447
+ .map((entry) => ({
448
+ path: entry.path,
449
+ method: entry.method,
450
+ location: entry.location,
451
+ field: entry.field,
452
+ reason: entry.reason ?? "Type could not be resolved.",
453
+ }));
454
+ }
455
+
456
+ /**
457
+ * Build operation summary.
458
+ * @param entries Input parameter `entries`.
459
+ * @returns Build operation summary output as `TypeCoverageOperationSummary[]`.
460
+ * @example
461
+ * ```ts
462
+ * const result = buildOperationSummary([]);
463
+ * // result: TypeCoverageOperationSummary[]
464
+ * ```
465
+ */
466
+ function buildOperationSummary(
467
+ entries: CoverageEntry[],
468
+ ): TypeCoverageOperationSummary[] {
469
+ const byOperation = new Map<string, CoverageEntry[]>();
470
+ for (const entry of entries) {
471
+ const key = `${entry.method} ${entry.path}`;
472
+ const list = byOperation.get(key);
473
+ if (list) {
474
+ list.push(entry);
475
+ continue;
476
+ }
477
+ byOperation.set(key, [entry]);
478
+ }
479
+
480
+ const operations: TypeCoverageOperationSummary[] = [];
481
+ for (const [key, operationEntries] of byOperation) {
482
+ const [method, ...pathParts] = key.split(" ");
483
+ const path = pathParts.join(" ");
484
+ const total = operationEntries.length;
485
+ const typed = operationEntries.filter((entry) => entry.isTyped).length;
486
+ const untyped = total - typed;
487
+ const untypedLocations = operationEntries
488
+ .filter((entry) => !entry.isTyped)
489
+ .map((entry) => entry.location);
490
+
491
+ operations.push({
492
+ path,
493
+ method,
494
+ total,
495
+ typed,
496
+ untyped,
497
+ coveragePercentage: calculateCoveragePercentage(typed, total),
498
+ untypedLocations,
499
+ });
500
+ }
501
+
502
+ return operations;
503
+ }
504
+
505
+ /**
506
+ * Build metrics by location.
507
+ * @param entries Input parameter `entries`.
508
+ * @param location Input parameter `location`.
509
+ * @returns Build metrics by location output as `TypeCoverageMetrics`.
510
+ * @example
511
+ * ```ts
512
+ * const result = buildMetricsByLocation([], "query.parameter");
513
+ * // result: TypeCoverageMetrics
514
+ * ```
515
+ */
516
+ function buildMetricsByLocation(
517
+ entries: CoverageEntry[],
518
+ location: TypeCoverageLocation,
519
+ ): TypeCoverageMetrics {
520
+ const filtered = entries.filter((entry) => entry.location === location);
521
+ return buildMetrics(filtered);
522
+ }
523
+
524
+ /**
525
+ * Build metrics.
526
+ * @param entries Input parameter `entries`.
527
+ * @returns Build metrics output as `TypeCoverageMetrics`.
528
+ * @example
529
+ * ```ts
530
+ * const result = buildMetrics([]);
531
+ * // result: TypeCoverageMetrics
532
+ * ```
533
+ */
534
+ function buildMetrics(entries: CoverageEntry[]): TypeCoverageMetrics {
535
+ const total = entries.length;
536
+ const typed = entries.filter((entry) => entry.isTyped).length;
537
+ const untyped = total - typed;
538
+ return {
539
+ total,
540
+ typed,
541
+ untyped,
542
+ coveragePercentage: calculateCoveragePercentage(typed, total),
543
+ };
544
+ }
545
+
546
+ /**
547
+ * Calculate coverage percentage.
548
+ * @param typed Input parameter `typed`.
549
+ * @param total Input parameter `total`.
550
+ * @returns Calculate coverage percentage output as `number`.
551
+ * @example
552
+ * ```ts
553
+ * const result = calculateCoveragePercentage(1, 2);
554
+ * // result: number
555
+ * ```
556
+ */
557
+ function calculateCoveragePercentage(typed: number, total: number): number {
558
+ if (total === 0) {
559
+ return 100;
560
+ }
561
+
562
+ const percentage = (typed / total) * 100;
563
+ return Number(percentage.toFixed(2));
564
+ }
565
+
566
+ /**
567
+ * Get path placeholders.
568
+ * @param path Input parameter `path`.
569
+ * @returns Get path placeholders output as `string[]`.
570
+ * @example
571
+ * ```ts
572
+ * const result = getPathPlaceholders("/users/{id}");
573
+ * // result: string[]
574
+ * ```
575
+ */
576
+ function getPathPlaceholders(path: string): string[] {
577
+ const matches = path.match(/\{([^}]+)\}/g);
578
+ if (!matches) {
579
+ return [];
580
+ }
581
+
582
+ return matches.map((match) => match.slice(1, -1));
583
+ }
584
+
585
+ /**
586
+ * Get preferred content schema.
587
+ * @param content Input parameter `content`.
588
+ * @returns Get preferred content schema output as `Record<string, unknown> | undefined`.
589
+ * @example
590
+ * ```ts
591
+ * const result = getPreferredContentSchema(undefined);
592
+ * // result: Record<string, unknown> | undefined
593
+ * ```
594
+ */
595
+ function getPreferredContentSchema(
596
+ content?: Record<string, { schema: Record<string, unknown> }>,
597
+ ): Record<string, unknown> | undefined {
598
+ if (!content) {
599
+ return undefined;
600
+ }
601
+
602
+ const jsonSchema = content["application/json"]?.schema;
603
+ if (jsonSchema && typeof jsonSchema === "object") {
604
+ return jsonSchema;
605
+ }
606
+
607
+ const firstKey = Object.keys(content)[0];
608
+ if (!firstKey) {
609
+ return undefined;
610
+ }
611
+
612
+ const firstSchema = content[firstKey]?.schema;
613
+ if (!firstSchema || typeof firstSchema !== "object") {
614
+ return undefined;
615
+ }
616
+
617
+ return firstSchema;
618
+ }
619
+
620
+ /**
621
+ * Get success response.
622
+ * @param operation Input parameter `operation`.
623
+ * @returns Get success response output as `{ content?: Record<string, { schema: Record<string, unknown> }> } | undefined`.
624
+ * @example
625
+ * ```ts
626
+ * const result = getSuccessResponse({});
627
+ * // result: { content?: Record<string, { schema: Record<string, unknown> }> } | undefined
628
+ * ```
629
+ */
630
+ function getSuccessResponse(
631
+ operation: OpenApiOperation,
632
+ ): { content?: Record<string, { schema: Record<string, unknown> }> } | undefined {
633
+ const responses = operation.responses ?? {};
634
+
635
+ const response200 = responses["200"];
636
+ if (response200) {
637
+ return response200 as {
638
+ content?: Record<string, { schema: Record<string, unknown> }>;
639
+ };
640
+ }
641
+
642
+ const response201 = responses["201"];
643
+ if (response201) {
644
+ return response201 as {
645
+ content?: Record<string, { schema: Record<string, unknown> }>;
646
+ };
647
+ }
648
+
649
+ const successStatus = Object.keys(responses).find(
650
+ (statusCode) => statusCode.startsWith("2") && responses[statusCode],
651
+ );
652
+ if (!successStatus) {
653
+ return undefined;
654
+ }
655
+
656
+ return responses[successStatus] as {
657
+ content?: Record<string, { schema: Record<string, unknown> }>;
658
+ };
659
+ }
660
+
661
+ /**
662
+ * Extract request type.
663
+ * @param operation Input parameter `operation`.
664
+ * @returns Extract request type output as `string | undefined`.
665
+ * @example
666
+ * ```ts
667
+ * const result = extractRequestType({});
668
+ * // result: string | undefined
669
+ * ```
670
+ */
671
+ function extractRequestType(operation: OpenApiOperation): string | undefined {
672
+ const schema = getPreferredContentSchema(operation.requestBody?.content as never);
673
+ if (!schema) {
674
+ return undefined;
675
+ }
676
+
677
+ const schemaRef = getSchemaRef(schema);
678
+ if (schemaRef) {
679
+ return schemaRef;
680
+ }
681
+
682
+ const schemaType = schema.type;
683
+ if (schemaType !== "array") {
684
+ return undefined;
685
+ }
686
+
687
+ const items = schema.items;
688
+ if (!items || typeof items !== "object") {
689
+ return undefined;
690
+ }
691
+
692
+ const itemRef = getSchemaRef(items);
693
+ if (!itemRef) {
694
+ return undefined;
695
+ }
696
+
697
+ return `${itemRef}[]`;
698
+ }
699
+
700
+ /**
701
+ * Extract response type.
702
+ * @param operation Input parameter `operation`.
703
+ * @returns Extract response type output as `string`.
704
+ * @example
705
+ * ```ts
706
+ * const result = extractResponseType({});
707
+ * // result: string
708
+ * ```
709
+ */
710
+ function extractResponseType(operation: OpenApiOperation): string {
711
+ const response = getSuccessResponse(operation);
712
+ if (!response) {
713
+ return "any";
714
+ }
715
+
716
+ const schema = getPreferredContentSchema(response.content);
717
+ if (!schema) {
718
+ return "any";
719
+ }
720
+
721
+ const schemaRef = getSchemaRef(schema);
722
+ if (schemaRef) {
723
+ return schemaRef;
724
+ }
725
+
726
+ const schemaType = schema.type;
727
+ if (schemaType !== "array") {
728
+ return "any";
729
+ }
730
+
731
+ const items = schema.items;
732
+ if (!items || typeof items !== "object") {
733
+ return "any";
734
+ }
735
+
736
+ const itemRef = getSchemaRef(items);
737
+ if (!itemRef) {
738
+ return "any[]";
739
+ }
740
+
741
+ return `${itemRef}[]`;
742
+ }
743
+
744
+ /**
745
+ * Get schema reference name.
746
+ * @param schema Input parameter `schema`.
747
+ * @returns Get schema reference name output as `string | undefined`.
748
+ * @example
749
+ * ```ts
750
+ * const result = getSchemaRef({ $ref: "#/components/schemas/User" });
751
+ * // result: string | undefined
752
+ * ```
753
+ */
754
+ function getSchemaRef(schema: Record<string, unknown>): string | undefined {
755
+ const schemaReference = schema.$ref;
756
+ if (typeof schemaReference !== "string") {
757
+ return undefined;
758
+ }
759
+
760
+ const referencePathParts = schemaReference.split("/");
761
+ const referenceName = referencePathParts[referencePathParts.length - 1];
762
+ if (!referenceName) {
763
+ return undefined;
764
+ }
765
+
766
+ return referenceName;
767
+ }
768
+
769
+ /**
770
+ * Check if schema resolves to any.
771
+ * @param schema Input parameter `schema`.
772
+ * @returns Check if schema resolves to any output as `boolean`.
773
+ * @example
774
+ * ```ts
775
+ * const result = isSchemaAny(undefined);
776
+ * // result: boolean
777
+ * ```
778
+ */
779
+ function isSchemaAny(schema: unknown): boolean {
780
+ const resolvedType = resolveSchemaType(schema);
781
+ return containsAnyType(resolvedType);
782
+ }
783
+
784
+ /**
785
+ * Resolve schema type.
786
+ * @param schema Input parameter `schema`.
787
+ * @returns Resolve schema type output as `string`.
788
+ * @example
789
+ * ```ts
790
+ * const result = resolveSchemaType({ type: "string" });
791
+ * // result: string
792
+ * ```
793
+ */
794
+ function resolveSchemaType(schema: unknown): string {
795
+ if (!schema || typeof schema !== "object") {
796
+ return "any";
797
+ }
798
+
799
+ const typedSchema = schema as Record<string, unknown>;
800
+ const schemaRef = getSchemaRef(typedSchema);
801
+ if (schemaRef) {
802
+ return schemaRef;
803
+ }
804
+
805
+ const schemaEnum = typedSchema.enum;
806
+ if (Array.isArray(schemaEnum)) {
807
+ const union = schemaEnum
808
+ .map((enumValue) =>
809
+ typeof enumValue === "string" ? `\"${enumValue}\"` : String(enumValue),
810
+ )
811
+ .join(" | ");
812
+ if (!union) {
813
+ return "any";
814
+ }
815
+ return union;
816
+ }
817
+
818
+ const anyOfType = resolveUnionType(typedSchema.anyOf);
819
+ if (anyOfType) {
820
+ return anyOfType;
821
+ }
822
+
823
+ const oneOfType = resolveUnionType(typedSchema.oneOf);
824
+ if (oneOfType) {
825
+ return oneOfType;
826
+ }
827
+
828
+ const allOfType = resolveIntersectionType(typedSchema.allOf);
829
+ if (allOfType) {
830
+ return allOfType;
831
+ }
832
+
833
+ const schemaType = typedSchema.type;
834
+ if (schemaType === "array") {
835
+ const itemType = resolveSchemaType(typedSchema.items);
836
+ return `${itemType}[]`;
837
+ }
838
+
839
+ if (schemaType === "object" && typedSchema.properties) {
840
+ return "object";
841
+ }
842
+
843
+ if (schemaType === "string") {
844
+ const format = typedSchema.format;
845
+ if (format === "numeric") {
846
+ return "number";
847
+ }
848
+ return "string";
849
+ }
850
+
851
+ if (schemaType === "number") {
852
+ return "number";
853
+ }
854
+
855
+ if (schemaType === "integer") {
856
+ return "number";
857
+ }
858
+
859
+ if (schemaType === "boolean") {
860
+ return "boolean";
861
+ }
862
+
863
+ return "any";
864
+ }
865
+
866
+ /**
867
+ * Resolve union type.
868
+ * @param schemaVariants Input parameter `schemaVariants`.
869
+ * @returns Resolve union type output as `string | undefined`.
870
+ * @example
871
+ * ```ts
872
+ * const result = resolveUnionType([{ type: "string" }]);
873
+ * // result: string | undefined
874
+ * ```
875
+ */
876
+ function resolveUnionType(schemaVariants: unknown): string | undefined {
877
+ if (!Array.isArray(schemaVariants)) {
878
+ return undefined;
879
+ }
880
+
881
+ const variants = schemaVariants
882
+ .map((variant) => resolveSchemaType(variant))
883
+ .filter(Boolean);
884
+ if (variants.length === 0) {
885
+ return undefined;
886
+ }
887
+
888
+ return variants.join(" | ");
889
+ }
890
+
891
+ /**
892
+ * Resolve intersection type.
893
+ * @param schemaVariants Input parameter `schemaVariants`.
894
+ * @returns Resolve intersection type output as `string | undefined`.
895
+ * @example
896
+ * ```ts
897
+ * const result = resolveIntersectionType([{ type: "string" }]);
898
+ * // result: string | undefined
899
+ * ```
900
+ */
901
+ function resolveIntersectionType(schemaVariants: unknown): string | undefined {
902
+ if (!Array.isArray(schemaVariants)) {
903
+ return undefined;
904
+ }
905
+
906
+ const variants = schemaVariants
907
+ .map((variant) => resolveSchemaType(variant))
908
+ .filter(Boolean);
909
+ if (variants.length === 0) {
910
+ return undefined;
911
+ }
912
+
913
+ return variants.join(" & ");
914
+ }
915
+
916
+ /**
917
+ * Check if type includes any.
918
+ * @param typeName Input parameter `typeName`.
919
+ * @returns Check if type includes any output as `boolean`.
920
+ * @example
921
+ * ```ts
922
+ * const result = containsAnyType("any[]");
923
+ * // result: boolean
924
+ * ```
925
+ */
926
+ function containsAnyType(typeName: string): boolean {
927
+ const normalized = typeName.trim();
928
+ if (normalized === "any") {
929
+ return true;
930
+ }
931
+
932
+ if (normalized === "any[]") {
933
+ return true;
934
+ }
935
+
936
+ const tokens = normalized.split(/[|&]/).map((token) => token.trim());
937
+ return tokens.some((token) => token === "any" || token === "any[]");
938
+ }