mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Fix merge logic for allOf alternatives (#3515)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Fix merge logic for allOf alternatives
|
||||
@@ -184,4 +184,203 @@ describe('getSchemaAlternatives', () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('safe merging with allOf', () => {
|
||||
it('should merge objects with safe extensions', () => {
|
||||
expect(
|
||||
getSchemaAlternatives({
|
||||
allOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
description: 'Base schema',
|
||||
title: 'Base',
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['email'],
|
||||
description: 'Extended schema',
|
||||
example: { email: 'test@example.com' },
|
||||
deprecated: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual({
|
||||
type: 'allOf',
|
||||
schemas: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['name', 'email'],
|
||||
description: 'Extended schema',
|
||||
title: 'Base',
|
||||
example: { email: 'test@example.com' },
|
||||
deprecated: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge objects with vendor extensions', () => {
|
||||
expect(
|
||||
getSchemaAlternatives({
|
||||
allOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
'x-internal': true,
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
'x-version': '1.0',
|
||||
'x-internal': false,
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual({
|
||||
type: 'allOf',
|
||||
schemas: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
'x-internal': false,
|
||||
'x-version': '1.0',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge objects with nullable', () => {
|
||||
expect(
|
||||
getSchemaAlternatives({
|
||||
allOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
field1: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
field2: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
nullable: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual({
|
||||
type: 'allOf',
|
||||
schemas: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
field1: {
|
||||
type: 'string',
|
||||
},
|
||||
field2: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
nullable: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT merge objects with unsafe properties', () => {
|
||||
expect(
|
||||
getSchemaAlternatives({
|
||||
allOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
value: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
// oneOf is not a safe property to merge
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual({
|
||||
type: 'allOf',
|
||||
schemas: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
value: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -540,9 +540,39 @@ export function getSchemaAlternatives(
|
||||
};
|
||||
}
|
||||
|
||||
// These extensions are safe to merge
|
||||
const safeExtensions = [
|
||||
'description',
|
||||
'title',
|
||||
'example',
|
||||
'examples',
|
||||
'default',
|
||||
'readOnly',
|
||||
'writeOnly',
|
||||
'deprecated',
|
||||
];
|
||||
|
||||
/**
|
||||
* Determine if a schema is safe to merge based on its properties
|
||||
*/
|
||||
function isSafeToMerge(schema: OpenAPIV3.SchemaObject): boolean {
|
||||
const keys = Object.keys(schema);
|
||||
|
||||
const coreProperties = ['type', 'properties', 'required', 'nullable'];
|
||||
|
||||
const coreKeys = keys.filter((key) => coreProperties.includes(key));
|
||||
const unknownKeys = keys.filter(
|
||||
(key) =>
|
||||
!coreProperties.includes(key) && !safeExtensions.includes(key) && !key.startsWith('x-')
|
||||
);
|
||||
|
||||
return coreKeys.length > 0 && unknownKeys.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge alternatives of the same type into a single schema.
|
||||
* - Merge string enums
|
||||
* - Safely merge object schemas with compatible properties
|
||||
*/
|
||||
function mergeAlternatives(
|
||||
alternativeType: AlternativeType,
|
||||
@@ -590,24 +620,40 @@ function mergeAlternatives(
|
||||
|
||||
if (latest && latest.type === 'object' && schemaOrRef.type === 'object') {
|
||||
const keys = Object.keys(schemaOrRef);
|
||||
if (
|
||||
keys.every((key) =>
|
||||
['type', 'properties', 'required', 'nullable'].includes(key)
|
||||
)
|
||||
) {
|
||||
|
||||
if (isSafeToMerge(schemaOrRef)) {
|
||||
const safeKeys = keys.filter((key) => safeExtensions.includes(key));
|
||||
const vendorKeys = keys.filter((key) => key.startsWith('x-'));
|
||||
|
||||
latest.properties = {
|
||||
...latest.properties,
|
||||
...schemaOrRef.properties,
|
||||
...(latest.properties || {}),
|
||||
...(schemaOrRef.properties || {}),
|
||||
};
|
||||
latest.required = Array.from(
|
||||
new Set([
|
||||
...(Array.isArray(latest.required) ? latest.required : []),
|
||||
...(Array.isArray(schemaOrRef.required)
|
||||
...(latest.required && Array.isArray(latest.required)
|
||||
? latest.required
|
||||
: []),
|
||||
...(schemaOrRef.required && Array.isArray(schemaOrRef.required)
|
||||
? schemaOrRef.required
|
||||
: []),
|
||||
])
|
||||
);
|
||||
latest.nullable = latest.nullable || schemaOrRef.nullable;
|
||||
|
||||
// Preserve safe extensions and vendor extensions
|
||||
// Always overwrite (last schema has priority)
|
||||
[...vendorKeys, ...safeKeys].forEach((key) => {
|
||||
if (
|
||||
typeof latest[key] === 'object' &&
|
||||
typeof schemaOrRef[key] === 'object'
|
||||
) {
|
||||
latest[key] = { ...latest[key], ...schemaOrRef[key] };
|
||||
} else {
|
||||
latest[key] = schemaOrRef[key];
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user