@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,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
+ }