mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Highlight discriminator properties in oneOf, allOf, anyOf objects (#3691)
This commit is contained in:
@@ -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,
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: '无内容',
|
||||
|
||||
Reference in New Issue
Block a user