docusaurus-plugin-openapi-docs
Version:
OpenAPI plugin for Docusaurus.
863 lines (795 loc) • 23.7 kB
text/typescript
/* ============================================================================
* Copyright (c) Palo Alto Networks
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* ========================================================================== */
// eslint-disable-next-line import/no-extraneous-dependencies
import { merge } from "allof-merge";
import clsx from "clsx";
import isEmpty from "lodash/isEmpty";
import {
createClosingArrayBracket,
createOpeningArrayBracket,
} from "./createArrayBracket";
import { createDescription } from "./createDescription";
import { createDetails } from "./createDetails";
import { createDetailsSummary } from "./createDetailsSummary";
import { getQualifierMessage, getSchemaName } from "./schema";
import { create, guard } from "./utils";
import { SchemaObject } from "../openapi/types";
let SCHEMA_TYPE: "request" | "response";
/**
* Returns a merged representation of allOf array of schemas.
*/
export function mergeAllOf(allOf: SchemaObject) {
const onMergeError = (msg: string) => {
console.warn(msg);
};
const mergedSchemas = merge(allOf, { onMergeError }) as SchemaObject;
return mergedSchemas;
}
/**
* For handling nested anyOf/oneOf.
*/
function createAnyOneOf(schema: SchemaObject): any {
const type = schema.oneOf ? "oneOf" : "anyOf";
return create("div", {
children: [
create("span", {
className: "badge badge--info",
children: type,
style: { marginBottom: "1rem" },
}),
create("SchemaTabs", {
children: schema[type]!.map((anyOneSchema, index) => {
const label = anyOneSchema.title
? anyOneSchema.title
: `MOD${index + 1}`;
const anyOneChildren = [];
if (anyOneSchema.description) {
anyOneChildren.push(
create("div", {
style: { marginTop: ".5rem", marginBottom: ".5rem" },
className: "openapi-schema__summary",
children: createDescription(anyOneSchema.description),
})
);
}
if (
anyOneSchema.type === "object" &&
!anyOneSchema.properties &&
!anyOneSchema.allOf &&
!anyOneSchema.items
) {
anyOneChildren.push(createNodes(anyOneSchema, SCHEMA_TYPE));
}
if (anyOneSchema.properties !== undefined) {
anyOneChildren.push(createProperties(anyOneSchema));
delete anyOneSchema.properties;
}
if (anyOneSchema.allOf !== undefined) {
anyOneChildren.push(createNodes(anyOneSchema, SCHEMA_TYPE));
delete anyOneSchema.allOf;
}
if (anyOneSchema.oneOf !== undefined) {
anyOneChildren.push(createNodes(anyOneSchema, SCHEMA_TYPE));
delete anyOneSchema.oneOf;
}
if (anyOneSchema.items !== undefined) {
anyOneChildren.push(createItems(anyOneSchema));
delete anyOneSchema.items;
}
if (
anyOneSchema.type === "string" ||
anyOneSchema.type === "number" ||
anyOneSchema.type === "integer" ||
anyOneSchema.type === "boolean"
) {
anyOneChildren.push(createNodes(anyOneSchema, SCHEMA_TYPE));
}
if (anyOneChildren.length) {
if (schema.type === "array") {
return create("TabItem", {
label: label,
value: `${index}-item-properties`,
children: [
createOpeningArrayBracket(),
anyOneChildren,
createClosingArrayBracket(),
]
.filter(Boolean)
.flat(),
});
}
return create("TabItem", {
label: label,
value: `${index}-item-properties`,
children: anyOneChildren.filter(Boolean).flat(),
});
}
return undefined;
}),
}),
],
});
}
/**
* For handling properties.
*/
function createProperties(schema: SchemaObject) {
const discriminator = schema.discriminator;
if (Object.keys(schema.properties!).length === 0) {
return create("SchemaItem", {
collapsible: false,
name: "",
required: false,
schemaName: "object",
qualifierMessage: undefined,
schema: {},
});
}
return Object.entries(schema.properties!).map(([key, val]) => {
return createEdges({
name: key,
schema: val,
required: Array.isArray(schema.required)
? schema.required.includes(key)
: false,
discriminator,
});
});
}
/**
* For handling additionalProperties.
*/
function createAdditionalProperties(schema: SchemaObject) {
const additionalProperties = schema.additionalProperties;
if (!additionalProperties) return [];
// Handle free-form objects
if (additionalProperties === true || isEmpty(additionalProperties)) {
return create("SchemaItem", {
name: "property name*",
required: false,
schemaName: "any",
qualifierMessage: getQualifierMessage(schema),
schema: schema,
collapsible: false,
discriminator: false,
});
}
// objects, arrays, complex schemas
if (
additionalProperties.properties ||
additionalProperties.items ||
additionalProperties.allOf ||
additionalProperties.additionalProperties ||
additionalProperties.oneOf ||
additionalProperties.anyOf
) {
const title = additionalProperties.title as string;
const schemaName = getSchemaName(additionalProperties);
const required = schema.required ?? false;
return createDetailsNode(
"property name*",
title ?? schemaName,
additionalProperties,
required,
schema.nullable
);
}
// primitive types
if (
additionalProperties.type === "string" ||
additionalProperties.type === "boolean" ||
additionalProperties.type === "integer" ||
additionalProperties.type === "number"
) {
const schemaName = getSchemaName(additionalProperties);
return create("SchemaItem", {
name: "property name*",
required: false,
schemaName: schemaName,
qualifierMessage: getQualifierMessage(schema),
schema: additionalProperties,
collapsible: false,
discriminator: false,
});
}
// unknown
return [];
}
/**
* For handling items.
*/
function createItems(schema: SchemaObject) {
if (schema.items?.properties !== undefined) {
return [
createOpeningArrayBracket(),
createProperties(schema.items),
createClosingArrayBracket(),
].flat();
}
if (schema.items?.additionalProperties !== undefined) {
return [
createOpeningArrayBracket(),
createAdditionalProperties(schema.items),
createClosingArrayBracket(),
].flat();
}
if (schema.items?.oneOf !== undefined || schema.items?.anyOf !== undefined) {
return [
createOpeningArrayBracket(),
createAnyOneOf(schema.items!),
createClosingArrayBracket(),
].flat();
}
if (schema.items?.allOf !== undefined) {
// TODO: figure out if and how we should pass merged required array
const mergedSchemas = mergeAllOf(schema.items) as SchemaObject;
// Handles combo anyOf/oneOf + properties
if (
(mergedSchemas.oneOf !== undefined ||
mergedSchemas.anyOf !== undefined) &&
mergedSchemas.properties
) {
return [
createOpeningArrayBracket(),
createAnyOneOf(mergedSchemas),
createProperties(mergedSchemas),
createClosingArrayBracket(),
].flat();
}
// Handles only anyOf/oneOf
if (
mergedSchemas.oneOf !== undefined ||
mergedSchemas.anyOf !== undefined
) {
return [
createOpeningArrayBracket(),
createAnyOneOf(mergedSchemas),
createClosingArrayBracket(),
].flat();
}
// Handles properties
if (mergedSchemas.properties !== undefined) {
return [
createOpeningArrayBracket(),
createProperties(mergedSchemas),
createClosingArrayBracket(),
].flat();
}
}
if (
schema.items?.type === "string" ||
schema.items?.type === "number" ||
schema.items?.type === "integer" ||
schema.items?.type === "boolean" ||
schema.items?.type === "object"
) {
return [
createOpeningArrayBracket(),
createNodes(schema.items, SCHEMA_TYPE),
createClosingArrayBracket(),
].flat();
}
// TODO: clean this up or eliminate it?
return [
createOpeningArrayBracket(),
Object.entries(schema.items!).map(([key, val]) =>
createEdges({
name: key,
schema: val,
required: Array.isArray(schema.required)
? schema.required.includes(key)
: false,
})
),
createClosingArrayBracket(),
].flat();
}
/**
* For handling nested properties.
*/
function createDetailsNode(
name: string,
schemaName: string,
schema: SchemaObject,
required: string[] | boolean,
nullable: boolean | unknown
): any {
return create("SchemaItem", {
collapsible: true,
className: "schemaItem",
children: [
createDetails({
className: "openapi-markdown__details",
children: [
createDetailsSummary({
children: [
create("span", {
className: "openapi-schema__container",
children: [
create("strong", {
className: clsx("openapi-schema__property", {
"openapi-schema__strikethrough": schema.deprecated,
}),
children: name,
}),
create("span", {
className: "openapi-schema__name",
children: ` ${schemaName}`,
}),
guard(
(Array.isArray(required)
? required.includes(name)
: required === true) ||
schema.deprecated ||
nullable,
() => [
create("span", {
className: "openapi-schema__divider",
}),
]
),
guard(nullable, () => [
create("span", {
className: "openapi-schema__nullable",
children: "nullable",
}),
]),
guard(
Array.isArray(required)
? required.includes(name)
: required === true,
() => [
create("span", {
className: "openapi-schema__required",
children: "required",
}),
]
),
guard(schema.deprecated, () => [
create("span", {
className: "openapi-schema__deprecated",
children: "deprecated",
}),
]),
],
}),
],
}),
create("div", {
style: { marginLeft: "1rem" },
children: [
guard(schema.description, (description) =>
create("div", {
style: { marginTop: ".5rem", marginBottom: ".5rem" },
children: createDescription(description),
})
),
guard(getQualifierMessage(schema), (message) =>
create("div", {
style: { marginTop: ".5rem", marginBottom: ".5rem" },
children: createDescription(message),
})
),
createNodes(schema, SCHEMA_TYPE),
],
}),
],
}),
],
});
}
/**
* For handling anyOf/oneOf properties.
*/
// function createAnyOneOfProperty(
// name: string,
// schemaName: string,
// schema: SchemaObject,
// required: string[] | boolean,
// nullable: boolean | unknown
// ): any {
// return create("SchemaItem", {
// collapsible: true,
// className: "schemaItem",
// children: [
// createDetails({
// className: "openapi-markdown__details",
// children: [
// createDetailsSummary({
// children: [
// create("strong", { children: name }),
// create("span", {
// style: { opacity: "0.6" },
// children: ` ${schemaName}`,
// }),
// guard(
// (schema.nullable && schema.nullable === true) ||
// (nullable && nullable === true),
// () => [
// create("strong", {
// style: {
// fontSize: "var(--ifm-code-font-size)",
// color: "var(--openapi-nullable)",
// },
// children: " nullable",
// }),
// ]
// ),
// guard(
// Array.isArray(required)
// ? required.includes(name)
// : required === true,
// () => [
// create("strong", {
// style: {
// fontSize: "var(--ifm-code-font-size)",
// color: "var(--openapi-required)",
// },
// children: " required",
// }),
// ]
// ),
// ],
// }),
// create("div", {
// style: { marginLeft: "1rem" },
// children: [
// guard(getQualifierMessage(schema), (message) =>
// create("div", {
// style: { marginTop: ".5rem", marginBottom: ".5rem" },
// children: createDescription(message),
// })
// ),
// guard(schema.description, (description) =>
// create("div", {
// style: { marginTop: ".5rem", marginBottom: ".5rem" },
// children: createDescription(description),
// })
// ),
// ],
// }),
// createAnyOneOf(schema),
// ],
// }),
// ],
// });
// }
/**
* For handling discriminators that map to a same-level property (like 'petType').
* Note: These should only be encountered while iterating through properties.
*/
function createPropertyDiscriminator(
name: string,
schemaName: string,
schema: SchemaObject,
discriminator: any,
required: string[] | boolean
): any {
if (schema === undefined) {
return undefined;
}
// render as a simple property if there's no mapping
if (discriminator.mapping === undefined) {
return createEdges({ name, schema, required });
}
return create("div", {
className: "openapi-discriminator__item openapi-schema__list-item",
children: create("div", {
children: [
create("span", {
className: "openapi-schema__container",
children: [
create("strong", {
className: "openapi-discriminator__name openapi-schema__property",
children: name,
}),
guard(schemaName, (name) =>
create("span", {
className: "openapi-schema__name",
children: ` ${schemaName}`,
})
),
guard(required, () => [
create("span", {
className: "openapi-schema__required",
children: "required",
}),
]),
],
}),
guard(schema.description, (description) =>
create("div", {
style: {
paddingLeft: "1rem",
},
children: createDescription(description),
})
),
guard(getQualifierMessage(discriminator), (message) =>
create("div", {
style: {
paddingLeft: "1rem",
},
children: createDescription(message),
})
),
create("DiscriminatorTabs", {
className: "openapi-tabs__discriminator",
children: Object.keys(discriminator?.mapping!).map((key, index) => {
const label = key;
return create("TabItem", {
// className: "openapi-tabs__discriminator-item",
label: label,
value: `${index}-item-discriminator`,
children: [createNodes(discriminator?.mapping[key], SCHEMA_TYPE)],
});
}),
}),
],
}),
});
}
interface EdgeProps {
name: string;
schema: SchemaObject;
required: string[] | boolean;
discriminator?: any | unknown;
}
/**
* Creates the edges or "leaves" of a schema tree. Edges can branch into sub-nodes with createDetails().
*/
function createEdges({
name,
schema,
required,
discriminator,
}: EdgeProps): any {
if (SCHEMA_TYPE === "request") {
if (schema.readOnly && schema.readOnly === true) {
return undefined;
}
}
if (SCHEMA_TYPE === "response") {
if (schema.writeOnly && schema.writeOnly === true) {
return undefined;
}
}
const schemaName = getSchemaName(schema);
if (discriminator !== undefined && discriminator.propertyName === name) {
return createPropertyDiscriminator(
name,
"string",
schema,
discriminator,
required
);
}
if (schema.oneOf !== undefined || schema.anyOf !== undefined) {
return createDetailsNode(
name,
schemaName,
schema,
required,
schema.nullable
);
}
if (schema.properties !== undefined) {
return createDetailsNode(
name,
schemaName,
schema,
required,
schema.nullable
);
}
if (schema.additionalProperties !== undefined) {
return createDetailsNode(
name,
schemaName,
schema,
required,
schema.nullable
);
}
// array of objects
if (schema.items?.properties !== undefined) {
return createDetailsNode(
name,
schemaName,
schema,
required,
schema.nullable
);
}
if (schema.items?.anyOf !== undefined || schema.items?.oneOf !== undefined) {
return createDetailsNode(
name,
schemaName,
schema,
required,
schema.nullable
);
}
if (schema.items?.allOf !== undefined) {
const mergedSchemas = mergeAllOf(schema.items);
if (SCHEMA_TYPE === "request") {
if (mergedSchemas.readOnly && mergedSchemas.readOnly === true) {
return undefined;
}
}
if (SCHEMA_TYPE === "response") {
if (mergedSchemas.writeOnly && mergedSchemas.writeOnly === true) {
return undefined;
}
}
const mergedSchemaName = getSchemaName(mergedSchemas);
if (
mergedSchemas.oneOf !== undefined ||
mergedSchemas.anyOf !== undefined
) {
return createDetailsNode(
name,
mergedSchemaName,
mergedSchemas,
required,
mergedSchemas.nullable
);
}
if (mergedSchemas.properties !== undefined) {
return createDetailsNode(
name,
mergedSchemaName,
mergedSchemas,
required,
mergedSchemas.nullable
);
}
if (mergedSchemas.additionalProperties !== undefined) {
return createDetailsNode(
name,
mergedSchemaName,
mergedSchemas,
required,
mergedSchemas.nullable
);
}
// array of objects
if (mergedSchemas.items?.properties !== undefined) {
return createDetailsNode(
name,
mergedSchemaName,
mergedSchemas,
required,
mergedSchemas.nullable
);
}
return create("SchemaItem", {
collapsible: false,
name,
required: Array.isArray(required) ? required.includes(name) : required,
schemaName: mergedSchemaName,
qualifierMessage: getQualifierMessage(mergedSchemas),
schema: mergedSchemas,
});
}
// primitives and array of non-objects
return create("SchemaItem", {
collapsible: false,
name,
required: Array.isArray(required) ? required.includes(name) : required,
schemaName: schemaName,
qualifierMessage: getQualifierMessage(schema),
schema: schema,
});
}
/**
* Creates a hierarchical level of a schema tree. Nodes produce edges that can branch into sub-nodes with edges, recursively.
*/
export function createNodes(
schema: SchemaObject,
schemaType: "request" | "response"
): any {
SCHEMA_TYPE = schemaType;
if (SCHEMA_TYPE === "request") {
if (schema.readOnly && schema.readOnly === true) {
return undefined;
}
}
if (SCHEMA_TYPE === "response") {
if (schema.writeOnly && schema.writeOnly === true) {
return undefined;
}
}
const nodes = [];
// if (schema.discriminator !== undefined) {
// return createDiscriminator(schema);
// }
if (schema.oneOf !== undefined || schema.anyOf !== undefined) {
nodes.push(createAnyOneOf(schema));
}
if (schema.properties !== undefined) {
nodes.push(createProperties(schema));
}
if (schema.additionalProperties !== undefined) {
nodes.push(createAdditionalProperties(schema));
}
// TODO: figure out how to handle array of objects
if (schema.items !== undefined) {
nodes.push(createItems(schema));
}
if (schema.allOf !== undefined) {
const mergedSchemas = mergeAllOf(schema) as SchemaObject;
if (
mergedSchemas.oneOf !== undefined ||
mergedSchemas.anyOf !== undefined
) {
nodes.push(createAnyOneOf(mergedSchemas));
}
if (mergedSchemas.properties !== undefined) {
nodes.push(createProperties(mergedSchemas));
}
}
if (nodes.length && nodes.length > 0) {
return nodes.filter(Boolean).flat();
}
// primitive
if (schema.type !== undefined) {
if (schema.allOf) {
//handle circular result in allOf
if (schema.allOf.length && typeof schema.allOf[0] === "string") {
return create("div", {
style: {
marginTop: ".5rem",
marginBottom: ".5rem",
marginLeft: "1rem",
},
children: createDescription(schema.allOf[0]),
});
}
}
return create("div", {
style: {
marginTop: ".5rem",
marginBottom: ".5rem",
},
children: [
createDescription(schema.type),
guard(getQualifierMessage(schema), (message) =>
create("div", {
style: {
paddingTop: "1rem",
},
children: createDescription(message),
})
),
],
});
}
// handle circular references
if (typeof schema === "string") {
return create("div", {
style: {
marginTop: ".5rem",
marginBottom: ".5rem",
},
children: [
createDescription(schema),
guard(getQualifierMessage(schema), (message) =>
create("div", {
style: {
paddingTop: "1rem",
},
children: createDescription(message),
})
),
],
});
}
// Unknown node/schema type should return undefined
// So far, haven't seen this hit in testing
return "any";
}