Files
gitbook/packages/react-openapi/src/OpenAPISchema.tsx
T
Samy Pessé 85ec1884e6 OpenAPI blocks (#74)
* Start methods to resolve refs

* bun

* Start package

* Add code to resolve ref

* Continue

* Deref

* Start monorepo

* Continue

* Fix ref resolver

* Style a bit more

* Start toggling section

* Continue

* Start displaying query/path/headers params

* Hide respo se if empty

* Fix recursive refs

* Simplify interactive and styling

* Display enums

* Format

* Improve type name being displayed

* Improve naming

* Render oneOf/anyOf/allOf

* Handle circular references

* Start variable in server url

* Use client component for the spec part

* Improve CSS sizing

* Display open api blocks in aside

* Make aside an overlay

* Improve stickiness of openapi

* Align document on the left when api page

* Improve general layout

* Better align

* Fix padding in aside

* Use syntax highlighting

* Show sample of response

* Improve code generation

* Improve code generated

* Format

* Format

* Rename to OpenAPI

* Skip deprecated properties and handle additionalProperties

* Use discriminator for naming

* Better name enum

* Make entire header toggeable

* Format and lint

* Add curl example

* Improve curl

* style pass

* Start securities

* Bun

* Fix TS

* Improve label

* Start markdown

* Improve code samples

* Test

* Use custom skeleton for api block

* Format

* Add support for redocly code samples

* Fix api blocks in aside

* Use typography for markdown

* Format

* Fix spacing in markdown

* Render headers

* Format

* Format

---------

Co-authored-by: Sebastian Graz <graz@live.se>
2024-01-30 10:15:58 +01:00

362 lines
11 KiB
TypeScript

import classNames from 'classnames';
import { OpenAPIV3 } from 'openapi-types';
import { noReference } from './utils';
import { OpenAPIClientContext } from './types';
import { InteractiveSection } from './InteractiveSection';
import { SYMBOL_REF_RESOLVED } from './fetchOpenAPIOperation';
import React, { useId } from 'react';
import { Markdown } from './Markdown';
type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
interface OpenAPISchemaPropertyEntry {
propertyName?: string;
required?: boolean;
schema: OpenAPIV3.SchemaObject;
}
/**
* Render a property of an OpenAPI schema.
*/
export function OpenAPISchemaProperty(
props: OpenAPISchemaPropertyEntry & {
/** Set of objects already observed as parents */
circularRefs?: CircularRefsIds;
context: OpenAPIClientContext;
},
) {
const {
propertyName,
required,
schema,
circularRefs: parentCircularRefs = new Map<OpenAPIV3.SchemaObject, string>(),
context,
} = props;
const id = useId();
const parentCircularRef = parentCircularRefs.get(schema);
const circularRefs = new Map(parentCircularRefs).set(schema, id);
// Avoid recursing infinitely, and instead render a link to the parent schema
const properties = parentCircularRef ? null : getSchemaProperties(schema);
const alternatives = parentCircularRef
? null
: getSchemaAlternatives(schema, new Set(circularRefs.keys()));
return (
<InteractiveSection
id={id}
className={classNames('openapi-schema')}
toggeable={!!properties || !!alternatives}
defaultOpened={false}
toggleOpenIcon={context.icons.chevronRight}
toggleCloseIcon={context.icons.chevronDown}
tabs={alternatives?.[0].map((alternative, index) => ({
key: `${index}`,
label: getSchemaTitle(alternative, alternatives[1]),
body: circularRefs.has(alternative) ? (
<OpenAPISchemaCircularRef
id={circularRefs.get(alternative)!}
schema={alternative}
/>
) : (
<OpenAPISchemaAlternative
schema={alternative}
circularRefs={circularRefs}
context={context}
/>
),
}))}
header={
<div className={classNames('openapi-schema-presentation')}>
<div className={classNames('openapi-schema-name')}>
{propertyName ? (
<span className={classNames('openapi-schema-propertyname')}>
{propertyName}
</span>
) : null}
{required ? (
<span className={classNames('openapi-schema-required')}>*</span>
) : null}
<span className={classNames('openapi-schema-type')}>
{getSchemaTitle(schema)}
</span>
</div>
{schema.description ? (
<Markdown
source={schema.description}
className="openapi-schema-description"
/>
) : null}
</div>
}
>
{(properties && properties.length > 0) ||
(schema.enum && schema.enum.length > 0) ||
parentCircularRef ? (
<>
{properties?.length ? (
<OpenAPISchemaProperties
properties={properties}
circularRefs={circularRefs}
context={context}
/>
) : null}
{schema.enum && schema.enum.length > 0 ? (
<OpenAPISchemaEnum enumValues={schema.enum} />
) : null}
{parentCircularRef ? (
<OpenAPISchemaCircularRef id={parentCircularRef} schema={schema} />
) : null}
</>
) : null}
</InteractiveSection>
);
}
/**
* Render a set of properties of an OpenAPI schema.
*/
export function OpenAPISchemaProperties(props: {
id?: string;
properties: OpenAPISchemaPropertyEntry[];
circularRefs?: CircularRefsIds;
context: OpenAPIClientContext;
}) {
const { id, properties, circularRefs, context } = props;
if (!properties.length) {
return null;
}
return (
<div id={id} className={classNames('openapi-schema-properties')}>
{properties.map((property) => (
<OpenAPISchemaProperty
key={property.propertyName}
circularRefs={circularRefs}
{...property}
context={context}
/>
))}
</div>
);
}
/**
* Render a tab for an alternative schema.
*/
export function OpenAPISchemaAlternative(props: {
schema: OpenAPIV3.SchemaObject;
circularRefs?: CircularRefsIds;
context: OpenAPIClientContext;
}) {
const { schema, circularRefs, context } = props;
const id = useId();
return (
<OpenAPISchemaProperties
id={id}
properties={getSchemaProperties(schema) ?? []}
circularRefs={new Map(circularRefs).set(schema, id)}
context={context}
/>
);
}
/**
* Render a circular reference to a schema.
*/
function OpenAPISchemaCircularRef(props: { id: string; schema: OpenAPIV3.SchemaObject }) {
const { id, schema } = props;
return (
<div className="openapi-schema-circular">
Circular reference to <a href={`#${id}`}>{getSchemaTitle(schema)}</a>{' '}
<span className="openapi-schema-circular-glyph">↩</span>
</div>
);
}
/**
* Render the enum value for a schema.
*/
export function OpenAPISchemaEnum(props: { enumValues: any[] }) {
const { enumValues } = props;
return (
<div className="openapi-schema-enum">
{enumValues.map((value, index) => (
<span key={index} className="openapi-schema-enum-value">{`${value}`}</span>
))}
</div>
);
}
/**
* Get the sub-properties of a schema.
*/
export function getSchemaProperties(
schema: OpenAPIV3.SchemaObject,
): null | OpenAPISchemaPropertyEntry[] {
if (schema.allOf) {
return schema.allOf.reduce((acc, subSchema) => {
const properties = getSchemaProperties(noReference(subSchema)) ?? [
{
schema: noReference(subSchema),
},
];
return [...acc, ...properties];
}, [] as OpenAPISchemaPropertyEntry[]);
}
if (schema.type === 'array') {
const items = noReference(schema.items);
const itemProperties = getSchemaProperties(items);
if (itemProperties) {
return itemProperties;
}
return [
{
propertyName: 'items',
schema: items,
},
];
}
if (schema.type === 'object' || schema.properties) {
const result: OpenAPISchemaPropertyEntry[] = [];
if (schema.properties) {
Object.entries(schema.properties).forEach(([propertyName, rawPropertySchema]) => {
const propertySchema = noReference(rawPropertySchema);
if (propertySchema.deprecated) {
return;
}
result.push({
propertyName,
required: schema.required?.includes(propertyName),
schema: propertySchema,
});
});
}
if (schema.additionalProperties) {
const additionalProperties = noReference(schema.additionalProperties);
result.push({
propertyName: 'Other properties',
schema: additionalProperties === true ? {} : additionalProperties,
});
}
return result;
}
return null;
}
/**
* Get the alternatives to display for a schema.
*/
export function getSchemaAlternatives(
schema: OpenAPIV3.SchemaObject,
ancestors: Set<OpenAPIV3.SchemaObject> = new Set(),
): null | [OpenAPIV3.SchemaObject[], OpenAPIV3.DiscriminatorObject | undefined] {
const downAncestors = new Set(ancestors).add(schema);
if (schema.anyOf) {
return [
flattenAlternatives('anyOf', schema.anyOf.map(noReference), downAncestors),
noReference(schema.discriminator),
];
}
if (schema.oneOf) {
return [
flattenAlternatives('oneOf', schema.oneOf.map(noReference), downAncestors),
noReference(schema.discriminator),
];
}
if (schema.allOf) {
// allOf is managed in `getSchemaProperties`
return null;
}
return null;
}
function flattenAlternatives(
alternativeType: 'oneOf' | 'allOf' | 'anyOf',
alternatives: OpenAPIV3.SchemaObject[],
ancestors: Set<OpenAPIV3.SchemaObject>,
): OpenAPIV3.SchemaObject[] {
return alternatives.reduce((acc, alternative) => {
if (!!alternative[alternativeType] && !ancestors.has(alternative)) {
return [...acc, ...(getSchemaAlternatives(alternative, ancestors)?.[0] || [])];
}
return [...acc, alternative];
}, [] as OpenAPIV3.SchemaObject[]);
}
function getSchemaTitle(
schema: OpenAPIV3.SchemaObject,
/** If the title is inferred in a oneOf with discriminator, we can use it to optimize the title */
discriminator?: OpenAPIV3.DiscriminatorObject,
): string {
if (schema.title) {
// If the schema has a title, use it
return schema.title;
}
// Try using the discriminator
if (discriminator && schema.properties) {
const discriminatorProperty = noReference(schema.properties[discriminator.propertyName]);
if (discriminatorProperty) {
if (discriminatorProperty.enum) {
return discriminatorProperty.enum.map((value) => value.toString()).join(' | ');
}
}
}
// Otherwise try to infer a nice title
let type = 'any';
if (schema.enum) {
type = 'enum';
} else if (schema.type === 'array') {
type = `array of ${getSchemaTitle(noReference(schema.items))}`;
} else if (schema.type) {
type = schema.type;
if (schema.type === 'object' && SYMBOL_REF_RESOLVED in schema) {
type = `${schema[SYMBOL_REF_RESOLVED]}`;
}
if (schema.format) {
type += ` (${schema.format})`;
}
} else if ('anyOf' in schema) {
type = 'any of';
} else if ('oneOf' in schema) {
type = 'one of';
} else if ('allOf' in schema) {
type = 'all of';
} else if ('not' in schema) {
type = 'not';
}
if (schema.nullable) {
type = `nullable ${type}`;
}
return type;
}