mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +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 alternatives of the same type into a single schema.
|
||||||
* - Merge string enums
|
* - Merge string enums
|
||||||
|
* - Safely merge object schemas with compatible properties
|
||||||
*/
|
*/
|
||||||
function mergeAlternatives(
|
function mergeAlternatives(
|
||||||
alternativeType: AlternativeType,
|
alternativeType: AlternativeType,
|
||||||
@@ -590,24 +620,40 @@ function mergeAlternatives(
|
|||||||
|
|
||||||
if (latest && latest.type === 'object' && schemaOrRef.type === 'object') {
|
if (latest && latest.type === 'object' && schemaOrRef.type === 'object') {
|
||||||
const keys = Object.keys(schemaOrRef);
|
const keys = Object.keys(schemaOrRef);
|
||||||
if (
|
|
||||||
keys.every((key) =>
|
if (isSafeToMerge(schemaOrRef)) {
|
||||||
['type', 'properties', 'required', 'nullable'].includes(key)
|
const safeKeys = keys.filter((key) => safeExtensions.includes(key));
|
||||||
)
|
const vendorKeys = keys.filter((key) => key.startsWith('x-'));
|
||||||
) {
|
|
||||||
latest.properties = {
|
latest.properties = {
|
||||||
...latest.properties,
|
...(latest.properties || {}),
|
||||||
...schemaOrRef.properties,
|
...(schemaOrRef.properties || {}),
|
||||||
};
|
};
|
||||||
latest.required = Array.from(
|
latest.required = Array.from(
|
||||||
new Set([
|
new Set([
|
||||||
...(Array.isArray(latest.required) ? latest.required : []),
|
...(latest.required && Array.isArray(latest.required)
|
||||||
...(Array.isArray(schemaOrRef.required)
|
? latest.required
|
||||||
|
: []),
|
||||||
|
...(schemaOrRef.required && Array.isArray(schemaOrRef.required)
|
||||||
? schemaOrRef.required
|
? schemaOrRef.required
|
||||||
: []),
|
: []),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
latest.nullable = latest.nullable || schemaOrRef.nullable;
|
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;
|
return acc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user