Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/dirty-mice-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---

Highlight discriminator properties in oneOf, allOf, anyOf objects
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
129 changes: 72 additions & 57 deletions packages/react-openapi/src/OpenAPISchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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,
],
});
});
});
73 changes: 49 additions & 24 deletions packages/react-openapi/src/OpenAPISchema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
export interface OpenAPISchemaPropertyEntry {
propertyName?: string;
required?: boolean | null;
isDiscriminatorProperty?: boolean;
schema: OpenAPIV3.SchemaObject;
}

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -359,7 +363,7 @@ export function OpenAPISchemaPresentation(props: {
context: OpenAPIClientContext;
}) {
const {
property: { schema, propertyName, required },
property: { schema, propertyName, required, isDiscriminatorProperty },
context,
} = props;

Expand All @@ -372,6 +376,7 @@ export function OpenAPISchemaPresentation(props: {
schema={schema}
type={getSchemaTitle(schema)}
propertyName={propertyName}
isDiscriminatorProperty={isDiscriminatorProperty}
required={required}
context={context}
/>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
});
});
Expand All @@ -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)
Expand All @@ -487,32 +505,39 @@ 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;
})();

if (!alternatives) {
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,
};
}

/**
Expand Down Expand Up @@ -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),
}))
Expand Down
Loading