|
1 | 1 | import type {OpenAPIV3} from "openapi-types"; |
2 | 2 | import {traverse} from './utils/OpenApiTraverser'; |
3 | 3 | import isEqual from 'lodash.isequal'; |
4 | | -import {compressMultipleUnderscores, isPrimitiveType, resolveObj, isReferenceObject, isEmptyObjectSchema} from './utils/helper'; |
| 4 | +import {compressMultipleUnderscores, isPrimitiveType, resolveObj, isReferenceObject, isEmptyObjectSchema, is_simple_ref} from './utils/helper'; |
5 | 5 | import Logger from "./utils/logger"; |
6 | 6 |
|
7 | 7 |
|
@@ -47,6 +47,7 @@ export class SchemaModifier { |
47 | 47 | if (!schema || isReferenceObject(schema)) return; |
48 | 48 | this.simplifySingleMapSchema(schema, visit) |
49 | 49 | this.handleAdditionalPropertiesUndefined(schema) |
| 50 | + this.markOneOfExtensions(schema); |
50 | 51 | }, |
51 | 52 | }); |
52 | 53 | return this.root |
@@ -475,4 +476,84 @@ export class SchemaModifier { |
475 | 476 | } |
476 | 477 | } |
477 | 478 | } |
| 479 | + |
| 480 | + /** |
| 481 | + * Marks schemas and properties with oneOf extensions. |
| 482 | + * Adds x-oneof-property to properties when schema has maxProperties=1. |
| 483 | + * Adds x-oneof-schema to schemas that have the max pattern AND to parent schemas |
| 484 | + **/ |
| 485 | + markOneOfExtensions(schema: OpenAPIV3.SchemaObject): void { |
| 486 | + const hasDirectPattern = schema.maxProperties === 1; |
| 487 | + const hasNestedPattern = this.hasNestedOneOfPattern(schema); |
| 488 | + |
| 489 | + if (!hasDirectPattern && !hasNestedPattern) { |
| 490 | + return; |
| 491 | + } |
| 492 | + |
| 493 | + if (hasDirectPattern) { |
| 494 | + if (schema.properties) { |
| 495 | + for (const propName in schema.properties) { |
| 496 | + const prop = schema.properties[propName] as any; |
| 497 | + if (prop && typeof prop === 'object') { |
| 498 | + prop['x-oneof-property'] = true; |
| 499 | + |
| 500 | + if ('$ref' in prop) { |
| 501 | + this.markReferencedSchemaAsOneof(prop.$ref); |
| 502 | + } |
| 503 | + } |
| 504 | + } |
| 505 | + } |
| 506 | + (schema as any)['x-oneof-schema'] = true; |
| 507 | + this.logger.info(`Added x-oneof-property to properties and marked schema with x-oneof-schema`); |
| 508 | + } else if (hasNestedPattern) { |
| 509 | + (schema as any)['x-oneof-schema'] = true; |
| 510 | + this.logger.info(`Marked parent schema with x-oneof-schema (contains nested oneOf pattern)`); |
| 511 | + } |
| 512 | + } |
| 513 | + |
| 514 | + /** |
| 515 | + * Recursively marks $ref schemas that are part of a oneOf, but stops when reaching a schema with actual content. |
| 516 | + **/ |
| 517 | + private markReferencedSchemaAsOneof(ref: string, visited: Set<string> = new Set()): void { |
| 518 | + if (visited.has(ref)) { |
| 519 | + return; |
| 520 | + } |
| 521 | + visited.add(ref); |
| 522 | + |
| 523 | + const schemaName = ref.split('/').pop(); |
| 524 | + if (!schemaName || !this.root.components?.schemas) { |
| 525 | + return; |
| 526 | + } |
| 527 | + |
| 528 | + const schema = this.root.components.schemas[schemaName]; |
| 529 | + if (!schema) { |
| 530 | + return; |
| 531 | + } |
| 532 | + |
| 533 | + if (is_simple_ref(schema)) { |
| 534 | + (schema as any)['x-oneof-property'] = true; |
| 535 | + this.markReferencedSchemaAsOneof((schema as any).$ref, visited); |
| 536 | + } |
| 537 | + } |
| 538 | + |
| 539 | + /** |
| 540 | + * Checks if schema has nested items (in allOf/anyOf/oneOf) with oneOf pattern. |
| 541 | + **/ |
| 542 | + private hasNestedOneOfPattern(schema: OpenAPIV3.SchemaObject): boolean { |
| 543 | + const composedKeys = ['allOf', 'anyOf', 'oneOf'] as const; |
| 544 | + for (const key of composedKeys) { |
| 545 | + const items = schema[key]; |
| 546 | + if (Array.isArray(items)) { |
| 547 | + for (const item of items) { |
| 548 | + if (item && typeof item === 'object' && !('$ref' in item)) { |
| 549 | + const itemSchema = item as any; |
| 550 | + if (itemSchema.maxProperties === 1) { |
| 551 | + return true; |
| 552 | + } |
| 553 | + } |
| 554 | + } |
| 555 | + } |
| 556 | + } |
| 557 | + return false; |
| 558 | + } |
478 | 559 | } |
0 commit comments