fumadocs-openapi
Version:
Generate MDX docs for your OpenAPI spec
411 lines (409 loc) • 14.9 kB
JavaScript
import { idToTitle } from "../../utils/id-to-title.js";
import { createMethod, methodKeys } from "../../utils/schema.js";
import { isMediaTypeSupported } from "../../requests/media/resolve-adapter.js";
import "../../requests/media/adapter.js";
import { cn } from "../../utils/cn.js";
import { MethodLabel } from "../components/method-label.js";
import { APIPlayground } from "../../playground/index.js";
import { Schema } from "../schema/index.js";
import { UsageTabsProviderLazy } from "./usage-tabs/lazy.js";
import { AccordionContent, AccordionHeader, AccordionItem, AccordionTrigger, Accordions } from "../components/accordion.js";
import { UsageTabs } from "./usage-tabs/index.js";
import { getTypescriptSchema } from "../../utils/get-typescript-schema.js";
import { CopyResponseTypeScript, SelectTab, SelectTabTrigger, SelectTabs } from "./client.js";
import { RequestTabs, getExampleRequests } from "./request-tabs.js";
import { Fragment } from "react";
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
//#region src/ui/operation/index.tsx
const ParamTypes = {
path: "Path Parameters",
query: "Query Parameters",
header: "Header Parameters",
cookie: "Cookie Parameters"
};
async function Operation({ type = "operation", path, method, ctx, showTitle, showDescription, headingLevel = 2 }) {
const { schema: { dereferenced } } = ctx;
const body = method.requestBody;
let headNode = null;
const descriptionNode = showDescription && method.description && ctx.renderMarkdown(method.description);
let bodyNode = null;
let authNode = null;
let responseNode = null;
let callbacksNode = null;
if (showTitle) {
const title = method.summary || (method.operationId ? idToTitle(method.operationId) : path);
headNode = ctx.renderHeading(headingLevel, title);
headingLevel++;
}
const contentTypes = body ? Object.entries(body.content) : null;
if (body && contentTypes && contentTypes.length > 0) {
const items = contentTypes.map(([key]) => ({
label: /* @__PURE__ */ jsx("code", {
className: "text-xs",
children: key
}),
value: key
}));
bodyNode = /* @__PURE__ */ jsxs(SelectTabs, {
defaultValue: items[0].value,
children: [
/* @__PURE__ */ jsxs("div", {
className: "flex gap-2 items-center justify-between mt-10",
children: [ctx.renderHeading(headingLevel, "Request Body", { className: "my-0!" }), contentTypes.length > 1 ? /* @__PURE__ */ jsx(SelectTabTrigger, {
items,
className: "font-medium"
}) : /* @__PURE__ */ jsx("p", {
className: "text-fd-muted-foreground not-prose",
children: items[0].label
})]
}),
body.description && ctx.renderMarkdown(body.description),
contentTypes.map(([type$1, content]) => {
if (!isMediaTypeSupported(type$1, ctx.mediaAdapters)) throw new Error(`Media type ${type$1} is not supported (in ${path})`);
return /* @__PURE__ */ jsx(SelectTab, {
value: type$1,
children: /* @__PURE__ */ jsx(Schema, {
client: {
name: "body",
as: "body",
required: body.required
},
root: content.schema ?? {},
readOnly: method.method === "GET",
writeOnly: method.method !== "GET",
ctx
})
}, type$1);
})
]
});
}
if (method.responses && ctx.showResponseSchema !== false) {
const statuses = Object.keys(method.responses);
responseNode = /* @__PURE__ */ jsxs(Fragment$1, { children: [ctx.renderHeading(headingLevel, "Response Body"), /* @__PURE__ */ jsx(Accordions, {
type: "multiple",
children: statuses.map((status) => /* @__PURE__ */ jsx(ResponseAccordion, {
status,
operation: method,
ctx
}, status))
})] });
}
const parameterNode = Object.entries(ParamTypes).map(([type$1, title]) => {
const params = method.parameters?.filter((param) => param.in === type$1);
if (!params || params.length === 0) return;
return /* @__PURE__ */ jsxs(Fragment, { children: [ctx.renderHeading(headingLevel, title), /* @__PURE__ */ jsx("div", {
className: "flex flex-col",
children: params.map((param) => /* @__PURE__ */ jsx(Schema, {
client: {
name: param.name,
required: param.required
},
root: {
...param.schema,
description: param.description ?? param.schema?.description,
deprecated: (param.deprecated ?? false) || (param.schema?.deprecated ?? false)
},
readOnly: method.method === "GET",
writeOnly: method.method !== "GET",
ctx
}, param.name))
})] }, type$1);
});
const securities = (method.security ?? dereferenced.security ?? []).filter((v) => Object.keys(v).length > 0);
if (type === "operation" && securities.length > 0) {
const securitySchemes = dereferenced.components?.securitySchemes;
const items = securities.map((security, i) => {
return {
value: String(i),
label: /* @__PURE__ */ jsx("div", {
className: "flex flex-col text-xs min-w-0",
children: Object.entries(security).map(([key, scopes]) => /* @__PURE__ */ jsxs("code", {
className: "truncate",
children: [
/* @__PURE__ */ jsx("span", {
className: "font-medium",
children: key
}),
" ",
scopes.length > 0 && /* @__PURE__ */ jsx("span", {
className: "text-fd-muted-foreground",
children: scopes.join(", ")
})
]
}, key))
})
};
});
authNode = /* @__PURE__ */ jsxs(SelectTabs, {
defaultValue: items[0].value,
children: [/* @__PURE__ */ jsxs("div", {
className: "flex items-start justify-between gap-2 mt-10",
children: [ctx.renderHeading(headingLevel, "Authorization", { className: "my-0!" }), items.length > 1 ? /* @__PURE__ */ jsx(SelectTabTrigger, { items }) : /* @__PURE__ */ jsx("div", {
className: "not-prose",
children: items[0].label
})]
}), securities.map((security, i) => /* @__PURE__ */ jsx(SelectTab, {
value: items[i].value,
children: Object.entries(security).map(([key, scopes]) => {
const scheme = securitySchemes?.[key];
if (!scheme) return;
return /* @__PURE__ */ jsx(AuthScheme, {
scheme,
scopes,
ctx
}, key);
})
}, i))]
});
}
const callbacks = method.callbacks ? Object.entries(method.callbacks) : null;
if (callbacks && callbacks.length > 0) {
const items = callbacks.map(([key]) => ({
label: /* @__PURE__ */ jsx("code", {
className: "text-xs",
children: key
}),
value: key
}));
callbacksNode = /* @__PURE__ */ jsxs(SelectTabs, {
defaultValue: items[0].value,
children: [/* @__PURE__ */ jsxs("div", {
className: "flex justify-between gap-2 items-end mt-10",
children: [ctx.renderHeading(headingLevel, "Callbacks", { className: "my-0!" }), callbacks.length > 1 ? /* @__PURE__ */ jsx(SelectTabTrigger, {
items,
className: "font-medium"
}) : /* @__PURE__ */ jsx("p", {
className: "text-fd-muted-foreground not-prose",
children: items[0].label
})]
}), callbacks.map(([name, callback]) => /* @__PURE__ */ jsx(SelectTab, {
value: name,
children: /* @__PURE__ */ jsx(WebhookCallback, {
callback,
ctx,
headingLevel
})
}, name))]
});
}
let { renderOperationLayout, renderWebhookLayout } = ctx.content ?? {};
if (type === "operation") {
renderOperationLayout ??= (slots) => {
return /* @__PURE__ */ jsxs("div", {
className: "flex flex-col gap-x-6 gap-y-4 @4xl:flex-row @4xl:items-start",
children: [/* @__PURE__ */ jsxs("div", {
className: "min-w-0 flex-1",
children: [
slots.header,
slots.apiPlayground,
slots.description,
slots.authSchemes,
slots.paremeters,
slots.body,
slots.responses,
slots.callbacks
]
}), /* @__PURE__ */ jsx("div", {
className: "@4xl:sticky @4xl:top-[calc(var(--fd-docs-row-1,2rem)+1rem)] @4xl:w-[400px]",
children: slots.apiExample
})]
});
};
const playgroundEnabled = ctx.playground?.enabled ?? true;
const content = await renderOperationLayout({
header: headNode,
description: descriptionNode,
authSchemes: authNode,
body: bodyNode,
callbacks: callbacksNode,
paremeters: parameterNode,
responses: responseNode,
apiPlayground: playgroundEnabled ? /* @__PURE__ */ jsx(APIPlayground, {
path,
method,
ctx
}) : /* @__PURE__ */ jsxs("div", {
className: "flex flex-row items-center gap-2.5 p-3 rounded-xl border bg-fd-card text-fd-card-foreground not-prose",
children: [/* @__PURE__ */ jsx(MethodLabel, {
className: "text-xs",
children: method.method
}), /* @__PURE__ */ jsx("code", {
className: "flex-1 overflow-auto text-nowrap text-[0.8125rem] text-fd-muted-foreground",
children: path
})]
}),
apiExample: /* @__PURE__ */ jsx(UsageTabs, {
method,
ctx
})
}, ctx, method);
return /* @__PURE__ */ jsx(UsageTabsProviderLazy, {
defaultExampleId: method["x-exclusiveCodeSample"] ?? method["x-selectedCodeSample"],
route: path,
examples: getExampleRequests(path, method, ctx),
children: content
});
} else {
renderWebhookLayout ??= (slots) => /* @__PURE__ */ jsxs("div", {
className: "flex flex-col-reverse gap-x-6 gap-y-4 @4xl:flex-row @4xl:items-start",
children: [/* @__PURE__ */ jsxs("div", {
className: "min-w-0 flex-1",
children: [
slots.header,
slots.description,
slots.authSchemes,
slots.paremeters,
slots.body,
slots.responses,
slots.callbacks
]
}), /* @__PURE__ */ jsx("div", {
className: "@4xl:sticky @4xl:top-[calc(var(--fd-docs-row-1,2rem)+1rem)] @4xl:w-[400px]",
children: slots.requests
})]
});
return renderWebhookLayout({
header: headNode,
description: descriptionNode,
authSchemes: authNode,
body: bodyNode,
callbacks: callbacksNode,
paremeters: parameterNode,
responses: responseNode,
requests: /* @__PURE__ */ jsx(RequestTabs, {
path,
operation: method,
ctx
})
});
}
}
async function ResponseAccordion({ status, operation, ctx }) {
const response = operation.responses[status];
const { generateTypeScriptSchema } = ctx;
const contentTypes = response.content ? Object.entries(response.content) : [];
let wrapper = (children) => children;
let selectorNode = null;
if (contentTypes.length > 0) {
const items = contentTypes.map(([key]) => ({
label: /* @__PURE__ */ jsx("code", {
className: "text-xs",
children: key
}),
value: key
}));
selectorNode = items.length === 1 ? /* @__PURE__ */ jsx("p", {
className: "text-fd-muted-foreground not-prose",
children: items[0].label
}) : /* @__PURE__ */ jsx(SelectTabTrigger, { items });
wrapper = (children) => /* @__PURE__ */ jsx(SelectTabs, {
defaultValue: items[0].value,
children
});
}
return wrapper(/* @__PURE__ */ jsxs(AccordionItem, {
value: status,
children: [/* @__PURE__ */ jsxs(AccordionHeader, { children: [/* @__PURE__ */ jsx(AccordionTrigger, {
className: "font-mono",
children: status
}), selectorNode] }), /* @__PURE__ */ jsxs(AccordionContent, {
className: "ps-4.5",
children: [response.description && /* @__PURE__ */ jsx("div", {
className: "prose-no-margin mb-2",
children: ctx.renderMarkdown(response.description)
}), contentTypes.map(async ([type, resType]) => {
const schema = resType.schema;
let ts;
if (generateTypeScriptSchema) ts = await generateTypeScriptSchema(operation, status);
else if (generateTypeScriptSchema === void 0 && schema) ts = await getTypescriptSchema(schema, ctx);
return /* @__PURE__ */ jsxs(SelectTab, {
value: type,
className: "mb-2",
children: [ts && /* @__PURE__ */ jsx(CopyResponseTypeScript, { code: ts }), schema && /* @__PURE__ */ jsx("div", {
className: "border px-3 py-2 rounded-lg",
children: /* @__PURE__ */ jsx(Schema, {
client: {
name: "response",
as: "body"
},
root: schema,
readOnly: true,
ctx
})
})]
}, type);
})]
})]
}));
}
function WebhookCallback({ callback, ctx, headingLevel }) {
return /* @__PURE__ */ jsx(Accordions, {
type: "single",
collapsible: true,
children: Object.entries(callback).map(([path, pathItem]) => {
const pathNodes = methodKeys.map((method) => {
const operation = pathItem[method];
if (!operation) return null;
return /* @__PURE__ */ jsx("div", {
className: "border p-3 my-2 @container prose-no-margin rounded-lg",
children: /* @__PURE__ */ jsx(Operation, {
type: "webhook",
path,
headingLevel: headingLevel + 1,
method: createMethod(method, pathItem, operation),
ctx
})
}, method);
});
return /* @__PURE__ */ jsxs(AccordionItem, {
value: path,
children: [/* @__PURE__ */ jsx(AccordionHeader, { children: /* @__PURE__ */ jsx(AccordionTrigger, {
className: "font-mono",
children: path
}) }), /* @__PURE__ */ jsx(AccordionContent, { children: pathNodes })]
}, path);
})
});
}
function AuthScheme({ scheme: schema, scopes, ctx }) {
if (schema.type === "http" || schema.type === "oauth2") return /* @__PURE__ */ jsxs(AuthProperty, {
name: "Authorization",
type: schema.type === "http" && schema.scheme === "basic" ? `Basic <token>` : "Bearer <token>",
scopes,
children: [schema.description && ctx.renderMarkdown(schema.description), /* @__PURE__ */ jsxs("p", { children: ["In: ", /* @__PURE__ */ jsx("code", { children: "header" })] })]
});
if (schema.type === "apiKey") return /* @__PURE__ */ jsxs(AuthProperty, {
name: schema.name,
type: "<token>",
scopes,
children: [schema.description && ctx.renderMarkdown(schema.description), /* @__PURE__ */ jsxs("p", { children: ["In: ", /* @__PURE__ */ jsx("code", { children: schema.in })] })]
});
if (schema.type === "openIdConnect") return /* @__PURE__ */ jsx(AuthProperty, {
name: "OpenID Connect",
type: "<token>",
scopes,
children: schema.description && ctx.renderMarkdown(schema.description)
});
}
function AuthProperty({ name, type, scopes = [], className, ...props }) {
return /* @__PURE__ */ jsxs("div", {
className: cn("text-sm border-t my-4 first:border-t-0", className),
children: [/* @__PURE__ */ jsxs("div", {
className: "flex flex-wrap items-center gap-3 not-prose",
children: [/* @__PURE__ */ jsx("span", {
className: "font-medium font-mono text-fd-primary",
children: name
}), /* @__PURE__ */ jsx("span", {
className: "text-sm font-mono text-fd-muted-foreground",
children: type
})]
}), /* @__PURE__ */ jsxs("div", {
className: "prose-no-margin pt-2.5 empty:hidden",
children: [props.children, scopes.length > 0 && /* @__PURE__ */ jsxs("p", { children: ["Scope: ", /* @__PURE__ */ jsx("code", { children: scopes.join(", ") })] })]
})]
});
}
//#endregion
export { Operation };
//# sourceMappingURL=index.js.map