Highlight discriminator properties in oneOf, allOf, anyOf objects (#3691)

This commit is contained in:
spastorelli
2025-09-29 17:09:21 +02:00
committed by GitHub
parent 1e4e54e752
commit 8e99871004
14 changed files with 153 additions and 84 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Highlight discriminator properties in oneOf, allOf, anyOf objects
@@ -192,7 +192,7 @@
.openapi-schema-name {
/* To make double click on the property name select only the name,
we disable selection on the parent and re-enable it on the children. */
@apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5;
@apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5 items-center;
}
.openapi-schema-name .openapi-deprecated {
@@ -207,6 +207,10 @@
@apply line-through opacity-9;
}
.openapi-schema-discriminator {
@apply text-primary-subtle/9 text-[0.813rem] lowercase;
}
.openapi-schema-required {
@apply text-warning-subtle text-[0.813rem] lowercase;
}
@@ -22,17 +22,20 @@ describe('getSchemaAlternatives', () => {
},
],
})
).toEqual([
{
type: 'number',
},
{
type: 'boolean',
},
{
type: 'string',
},
]);
).toEqual({
type: 'oneOf',
schemas: [
{
type: 'number',
},
{
type: 'boolean',
},
{
type: 'string',
},
],
});
});
it('merges string enum', () => {
@@ -54,13 +57,16 @@ describe('getSchemaAlternatives', () => {
},
],
})
).toEqual([
{
type: 'string',
enum: ['a', 'b', 'c', 'd'],
nullable: true,
},
]);
).toEqual({
type: 'oneOf',
schemas: [
{
type: 'string',
enum: ['a', 'b', 'c', 'd'],
nullable: true,
},
],
});
});
it('merges objects with allOf', () => {
@@ -93,26 +99,29 @@ describe('getSchemaAlternatives', () => {
},
],
})
).toEqual([
{
type: 'object',
properties: {
name: {
type: 'string',
},
map: {
type: 'string',
},
description: {
type: 'string',
},
externalId: {
type: 'string',
).toEqual({
type: 'allOf',
schemas: [
{
type: 'object',
properties: {
name: {
type: 'string',
},
map: {
type: 'string',
},
description: {
type: 'string',
},
externalId: {
type: 'string',
},
},
required: ['name', 'map', 'externalId'],
},
required: ['name', 'map', 'externalId'],
},
]);
],
});
});
it('should not flatten oneOf and allOf', () => {
@@ -134,21 +143,24 @@ describe('getSchemaAlternatives', () => {
},
],
})
).toEqual([
{
allOf: [
{
type: 'number',
},
{
type: 'boolean',
},
],
},
{
type: 'string',
},
]);
).toEqual({
type: 'oneOf',
schemas: [
{
allOf: [
{
type: 'number',
},
{
type: 'boolean',
},
],
},
{
type: 'string',
},
],
});
});
it('should stop at circular references', () => {
@@ -162,11 +174,14 @@ describe('getSchemaAlternatives', () => {
a.anyOf?.push(a);
expect(getSchemaAlternatives(a)).toEqual([
{
type: 'string',
},
a,
]);
expect(getSchemaAlternatives(a)).toEqual({
type: 'anyOf',
schemas: [
{
type: 'string',
},
a,
],
});
});
});
+49 -24
View File
@@ -23,6 +23,7 @@ type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
export interface OpenAPISchemaPropertyEntry {
propertyName?: string;
required?: boolean | null;
isDiscriminatorProperty?: boolean;
schema: OpenAPIV3.SchemaObject;
}
@@ -69,17 +70,19 @@ function OpenAPISchemaProperty(
);
}
if (alternatives) {
if (alternatives?.schemas) {
const { schemas, discriminator } = alternatives;
return (
<div className="openapi-schema-alternatives">
{alternatives.map((alternativeSchema, index) => (
{schemas.map((alternativeSchema, index) => (
<div key={index} className="openapi-schema-alternative">
<OpenAPISchemaAlternative
schema={alternativeSchema}
discriminator={discriminator}
circularRefs={circularRefs}
context={context}
/>
{index < alternatives.length - 1 ? (
{index < schemas.length - 1 ? (
<OpenAPISchemaAlternativeSeparator
schema={schema}
context={context}
@@ -228,11 +231,12 @@ export function OpenAPIRootSchemaFromServer(props: {
*/
function OpenAPISchemaAlternative(props: {
schema: OpenAPIV3.SchemaObject;
discriminator: OpenAPIV3.DiscriminatorObject | undefined;
circularRefs: CircularRefsIds;
context: OpenAPIClientContext;
}) {
const { schema, circularRefs, context } = props;
const properties = getSchemaProperties(schema);
const { schema, discriminator, circularRefs, context } = props;
const properties = getSchemaProperties(schema, discriminator);
return properties?.length ? (
<OpenAPIDisclosure
@@ -359,7 +363,7 @@ export function OpenAPISchemaPresentation(props: {
context: OpenAPIClientContext;
}) {
const {
property: { schema, propertyName, required },
property: { schema, propertyName, required, isDiscriminatorProperty },
context,
} = props;
@@ -372,6 +376,7 @@ export function OpenAPISchemaPresentation(props: {
schema={schema}
type={getSchemaTitle(schema)}
propertyName={propertyName}
isDiscriminatorProperty={isDiscriminatorProperty}
required={required}
context={context}
/>
@@ -414,13 +419,19 @@ export function OpenAPISchemaPresentation(props: {
/**
* Get the sub-properties of a schema.
*/
function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISchemaPropertyEntry[] {
function getSchemaProperties(
schema: OpenAPIV3.SchemaObject,
discriminator?: OpenAPIV3.DiscriminatorObject | undefined
): null | OpenAPISchemaPropertyEntry[] {
// check array AND schema.items as this is sometimes null despite what the type indicates
if (schema.type === 'array' && schema.items && !checkIsReference(schema.items)) {
const items = schema.items;
const itemProperties = getSchemaProperties(items);
if (itemProperties) {
return itemProperties;
return itemProperties.map((prop) => ({
...prop,
isDiscriminatorProperty: discriminator?.propertyName === prop.propertyName,
}));
}
// If the items are a primitive type, we don't need to display them
@@ -451,6 +462,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
required: Array.isArray(schema.required)
? schema.required.includes(propertyName)
: undefined,
isDiscriminatorProperty: discriminator?.propertyName === propertyName,
schema: propertySchema,
});
});
@@ -471,14 +483,20 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
type AlternativeType = 'oneOf' | 'allOf' | 'anyOf';
type SchemaAlternatives = {
type: AlternativeType;
schemas: OpenAPIV3.SchemaObject[];
discriminator?: OpenAPIV3.DiscriminatorObject;
} | null;
/**
* Get the alternatives to display for a schema.
*/
export function getSchemaAlternatives(
schema: OpenAPIV3.SchemaObject,
ancestors: Set<OpenAPIV3.SchemaObject> = new Set()
): OpenAPIV3.SchemaObject[] | null {
// Search for alternatives in the items property if it exists
): SchemaAlternatives {
// Check for nested alternatives in `items`
if (
schema.items &&
('oneOf' in schema.items || 'allOf' in schema.items || 'anyOf' in schema.items)
@@ -487,20 +505,21 @@ export function getSchemaAlternatives(
}
const alternatives:
| [AlternativeType, (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[]]
| [
AlternativeType,
(OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[],
OpenAPIV3.DiscriminatorObject?,
]
| null = (() => {
if (schema.anyOf) {
return ['anyOf', schema.anyOf];
return ['anyOf', schema.anyOf, schema.discriminator];
}
if (schema.oneOf) {
return ['oneOf', schema.oneOf];
return ['oneOf', schema.oneOf, schema.discriminator];
}
if (schema.allOf) {
return ['allOf', schema.allOf];
return ['allOf', schema.allOf, schema.discriminator];
}
return null;
})();
@@ -508,11 +527,17 @@ export function getSchemaAlternatives(
return null;
}
const [type, schemas] = alternatives;
return mergeAlternatives(
const [type, schemas, discriminator] = alternatives;
return {
type,
flattenAlternatives(type, schemas, new Set(ancestors).add(schema))
);
schemas:
mergeAlternatives(
type,
flattenAlternatives(type, schemas, new Set(ancestors).add(schema))
) ?? [],
discriminator,
};
}
/**
@@ -610,10 +635,10 @@ function flattenAlternatives(
}
if (schemaOrRef[alternativeType] && !ancestors.has(schemaOrRef)) {
const schemas = getSchemaAlternatives(schemaOrRef, ancestors);
if (schemas) {
const alternatives = getSchemaAlternatives(schemaOrRef, ancestors);
if (alternatives?.schemas) {
acc.push(
...schemas.map((schema) => ({
...alternatives.schemas.map((schema) => ({
...schema,
required: mergeRequiredFields(schema, latestAncestor),
}))
@@ -7,6 +7,7 @@ interface OpenAPISchemaNameProps {
schema?: OpenAPIV3.SchemaObject;
propertyName?: string | React.JSX.Element;
required?: boolean | null;
isDiscriminatorProperty?: boolean;
type?: string;
context: OpenAPIClientContext;
}
@@ -16,7 +17,7 @@ interface OpenAPISchemaNameProps {
* It includes the property name, type, required and deprecated status.
*/
export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
const { schema, type, propertyName, required, context } = props;
const { schema, type, propertyName, required, isDiscriminatorProperty, context } = props;
const additionalItems = schema && getAdditionalItems(schema, context);
@@ -27,9 +28,18 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
{propertyName}
</span>
) : null}
{isDiscriminatorProperty ? (
<span className="openapi-schema-discriminator">
{t(context.translation, 'discriminator')}
</span>
) : null}
{type || additionalItems ? (
<span>
{type ? <span className="openapi-schema-type">{type}</span> : null}
{schema?.const ? (
<span className="openapi-schema-type">const: {schema?.const}</span>
) : type ? (
<span className="openapi-schema-type">{type}</span>
) : null}
{additionalItems ? (
<span className="openapi-schema-type">{additionalItems}</span>
) : null}
@@ -5,6 +5,7 @@ export const de = {
stability_experimental: 'Experimentell',
stability_alpha: 'Alpha',
stability_beta: 'Beta',
discriminator: 'Diskriminator',
copy_to_clipboard: 'In die Zwischenablage kopieren',
copied: 'Kopiert',
no_content: 'Kein Inhalt',
@@ -5,6 +5,7 @@ export const en = {
stability_experimental: 'Experimental',
stability_alpha: 'Alpha',
stability_beta: 'Beta',
discriminator: 'Discriminator',
copy_to_clipboard: 'Copy to clipboard',
copied: 'Copied',
no_content: 'No content',
@@ -5,6 +5,7 @@ export const es = {
stability_experimental: 'Experimental',
stability_alpha: 'Alfa',
stability_beta: 'Beta',
discriminator: 'Discriminador',
copy_to_clipboard: 'Copiar al portapapeles',
copied: 'Copiado',
no_content: 'Sin contenido',
@@ -5,6 +5,7 @@ export const fr = {
stability_experimental: 'Expérimental',
stability_alpha: 'Alpha',
stability_beta: 'Bêta',
discriminator: 'Discriminateur',
copy_to_clipboard: 'Copier dans le presse-papiers',
copied: 'Copié',
no_content: 'Aucun contenu',
@@ -5,6 +5,7 @@ export const ja = {
stability_experimental: '実験的',
stability_alpha: 'アルファ',
stability_beta: 'ベータ',
discriminator: '識別子',
copy_to_clipboard: 'クリップボードにコピー',
copied: 'コピー済み',
no_content: 'コンテンツなし',
@@ -5,6 +5,7 @@ export const nl = {
stability_experimental: 'Experimenteel',
stability_alpha: 'Alfa',
stability_beta: 'Bèta',
discriminator: 'Discriminator',
copy_to_clipboard: 'Kopiëren naar klembord',
copied: 'Gekopieerd',
no_content: 'Geen inhoud',
@@ -5,6 +5,7 @@ export const no = {
stability_experimental: 'Eksperimentell',
stability_alpha: 'Alfa',
stability_beta: 'Beta',
discriminator: 'Diskriminator',
copy_to_clipboard: 'Kopier til utklippstavle',
copied: 'Kopiert',
no_content: 'Ingen innhold',
@@ -5,6 +5,7 @@ export const pt_br = {
stability_experimental: 'Experimental',
stability_alpha: 'Alfa',
stability_beta: 'Beta',
discriminator: 'Discriminador',
copy_to_clipboard: 'Copiar para a área de transferência',
copied: 'Copiado',
no_content: 'Sem conteúdo',
@@ -5,6 +5,7 @@ export const zh = {
stability_experimental: '实验性',
stability_alpha: 'Alpha',
stability_beta: 'Beta',
discriminator: '判别器',
copy_to_clipboard: '复制到剪贴板',
copied: '已复制',
no_content: '无内容',