From 3363a188564f6c2e7915ba28b56dc3b90ef4afd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Wed, 16 Apr 2025 11:14:12 +0200 Subject: [PATCH] Merge simple alternatives (#3165) --- .changeset/kind-lobsters-grab.md | 5 ++ .../react-openapi/src/OpenAPISchema.test.ts | 80 +++++++++++++++++ packages/react-openapi/src/OpenAPISchema.tsx | 86 ++++++++++++++++++- 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 .changeset/kind-lobsters-grab.md diff --git a/.changeset/kind-lobsters-grab.md b/.changeset/kind-lobsters-grab.md new file mode 100644 index 000000000..c4247d6d4 --- /dev/null +++ b/.changeset/kind-lobsters-grab.md @@ -0,0 +1,5 @@ +--- +"@gitbook/react-openapi": patch +--- + +Merge simple alternatives diff --git a/packages/react-openapi/src/OpenAPISchema.test.ts b/packages/react-openapi/src/OpenAPISchema.test.ts index e3fdc754c..4d2adcfc0 100644 --- a/packages/react-openapi/src/OpenAPISchema.test.ts +++ b/packages/react-openapi/src/OpenAPISchema.test.ts @@ -35,6 +35,86 @@ describe('getSchemaAlternatives', () => { ]); }); + it('merges string enum', () => { + expect( + getSchemaAlternatives({ + oneOf: [ + { + oneOf: [ + { + type: 'string', + enum: ['a', 'b'], + }, + { + type: 'string', + enum: ['c', 'd'], + nullable: true, + }, + ], + }, + ], + }) + ).toEqual([ + { + type: 'string', + enum: ['a', 'b', 'c', 'd'], + nullable: true, + }, + ]); + }); + + it('merges objects with allOf', () => { + expect( + getSchemaAlternatives({ + allOf: [ + { + type: 'object', + properties: { + name: { + type: 'string', + }, + map: { + type: 'string', + }, + description: { + type: 'string', + }, + }, + required: ['name'], + }, + { + type: 'object', + properties: { + externalId: { + type: 'string', + }, + }, + required: ['map', 'externalId'], + }, + ], + }) + ).toEqual([ + { + type: 'object', + properties: { + name: { + type: 'string', + }, + map: { + type: 'string', + }, + description: { + type: 'string', + }, + externalId: { + type: 'string', + }, + }, + required: ['name', 'map', 'externalId'], + }, + ]); + }); + it('should not flatten oneOf and allOf', () => { expect( getSchemaAlternatives({ diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index 0b76efc14..0c2593432 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -450,7 +450,91 @@ export function getSchemaAlternatives( } const [type, schemas] = alternatives; - return flattenAlternatives(type, schemas, new Set(ancestors).add(schema)); + return mergeAlternatives( + type, + flattenAlternatives(type, schemas, new Set(ancestors).add(schema)) + ); +} + +/** + * Merge alternatives of the same type into a single schema. + * - Merge string enums + */ +function mergeAlternatives( + alternativeType: AlternativeType, + schemasOrRefs: OpenAPIV3.SchemaObject[] +): OpenAPIV3.SchemaObject[] | null { + switch (alternativeType) { + case 'oneOf': { + return schemasOrRefs.reduce((acc, schemaOrRef) => { + const latest = acc.at(-1); + + if ( + latest && + latest.type === 'string' && + latest.enum && + schemaOrRef.type === 'string' && + schemaOrRef.enum + ) { + latest.enum = Array.from(new Set([...latest.enum, ...schemaOrRef.enum])); + latest.nullable = latest.nullable || schemaOrRef.nullable; + return acc; + } + + acc.push(schemaOrRef); + return acc; + }, []); + } + case 'allOf': { + return schemasOrRefs.reduce((acc, schemaOrRef) => { + const latest = acc.at(-1); + + if ( + latest && + latest.type === 'string' && + latest.enum && + schemaOrRef.type === 'string' && + schemaOrRef.enum + ) { + const keys = Object.keys(schemaOrRef); + if (keys.every((key) => ['type', 'enum', 'nullable'].includes(key))) { + latest.enum = Array.from(new Set([...latest.enum, ...schemaOrRef.enum])); + latest.nullable = latest.nullable || schemaOrRef.nullable; + return acc; + } + } + + if (latest && latest.type === 'object' && schemaOrRef.type === 'object') { + const keys = Object.keys(schemaOrRef); + if ( + keys.every((key) => + ['type', 'properties', 'required', 'nullable'].includes(key) + ) + ) { + latest.properties = { + ...latest.properties, + ...schemaOrRef.properties, + }; + latest.required = Array.from( + new Set([ + ...(Array.isArray(latest.required) ? latest.required : []), + ...(Array.isArray(schemaOrRef.required) + ? schemaOrRef.required + : []), + ]) + ); + latest.nullable = latest.nullable || schemaOrRef.nullable; + return acc; + } + } + + acc.push(schemaOrRef); + return acc; + }, []); + } + default: + return schemasOrRefs; + } } function flattenAlternatives(