OpenAPI: Add showResponseSchema option to show the full response schema

This commit is contained in:
Fuma Nama
2025-01-11 12:19:51 +08:00
parent 041f23040b
commit 056ab2c553
5 changed files with 101 additions and 78 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Add `showResponseSchema` option to show the full response schema
+88 -51
View File
@@ -4,11 +4,12 @@ import * as CURL from '@/requests/curl';
import * as JS from '@/requests/javascript';
import * as Go from '@/requests/go';
import * as Python from '@/requests/python';
import {
type MethodInformation,
import type {
CallbackObject,
MethodInformation,
OperationObject,
type RenderContext,
type SecurityRequirementObject,
RenderContext,
SecurityRequirementObject,
} from '@/types';
import { getPreferredType, NoReference } from '@/utils/schema';
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
@@ -50,14 +51,15 @@ export function Operation({
path: string;
method: MethodInformation;
ctx: RenderContext;
hasHead?: boolean;
hasHead?: boolean;
headingLevel?: number;
}): ReactElement {
const body = method.requestBody;
const security = method.security ?? ctx.document.security;
let headNode: ReactNode = null;
let bodyNode: ReactNode = null;
let responseNode: ReactNode = null;
let callbacksNode: ReactNode = null;
if (hasHead) {
@@ -104,6 +106,42 @@ export function Operation({
);
}
if (method.responses && ctx.showResponseSchema) {
responseNode = (
<>
{heading(headingLevel, 'Response Body', ctx)}
{Object.entries(method.responses).map(([status, response]) => {
if (!response.content) return;
const mediaType = getPreferredType(response.content);
if (!mediaType) return null;
const content = response.content[mediaType];
if (!content.schema) return null;
return (
<Fragment key={status}>
{heading(headingLevel + 1, status, ctx)}
<Markdown text={response.description} />
<Schema
name="response"
schema={content.schema}
ctx={{
render: ctx,
writeOnly: false,
readOnly: true,
required: true,
}}
/>
</Fragment>
);
})}
</>
);
}
const parameterGroups = new Map<string, ReactNode[]>();
const endpoint = generateSample(path, method, ctx);
@@ -149,34 +187,14 @@ export function Operation({
callbacksNode = (
<>
{heading(headingLevel, 'Webhooks', ctx)}
{Object.entries(method.callbacks).map(([name, callback]) => {
const nodes = Object.entries(callback).map(([path, pathItem]) => {
const pathNodes = methodKeys.map((method) => {
const operation = pathItem[method];
if (!operation) return null;
return (
<Operation
key={method}
type="webhook"
hasHead
path={path}
headingLevel={headingLevel + 1}
method={createMethod(
method,
pathItem,
operation as NoReference<OperationObject>,
)}
ctx={ctx}
/>
);
});
return <Fragment key={path}>{pathNodes}</Fragment>;
});
return <Fragment key={name}>{nodes}</Fragment>;
})}
{Object.entries(method.callbacks).map(([name, callback]) => (
<WebhookCallback
key={name}
callback={callback}
ctx={ctx}
headingLevel={headingLevel}
/>
))}
</>
);
}
@@ -201,6 +219,7 @@ export function Operation({
</Fragment>
);
})}
{responseNode}
{callbacksNode}
</ctx.renderer.APIInfo>
);
@@ -298,6 +317,41 @@ async function APIExample({
return <renderer.APIExample>{children}</renderer.APIExample>;
}
function WebhookCallback({
callback,
ctx,
headingLevel,
}: {
callback: CallbackObject;
ctx: RenderContext;
headingLevel: number;
}) {
return Object.entries(callback).map(([path, pathItem]) => {
const pathNodes = methodKeys.map((method) => {
const operation = pathItem[method];
if (!operation) return null;
return (
<Operation
key={method}
type="webhook"
hasHead
path={path}
headingLevel={headingLevel + 1}
method={createMethod(
method,
pathItem,
operation as NoReference<OperationObject>,
)}
ctx={ctx}
/>
);
});
return <Fragment key={path}>{pathNodes}</Fragment>;
});
}
/**
* Remove duplicated labels
*/
@@ -334,24 +388,7 @@ function AuthSection({
</p>
) : null;
if (schema.type === 'http') {
info.push(
<renderer.Property
key={id++}
name="Authorization"
type={prefix ? `${prefix} <token>` : '<token>'}
required
>
{schema.description ? <Markdown text={schema.description} /> : null}
<p>
In: <code>header</code>
{scopeElement}
</p>
</renderer.Property>,
);
}
if (schema.type === 'oauth2') {
if (schema.type === 'http' || schema.type === 'oauth2') {
info.push(
<renderer.Property
key={id++}
+2
View File
@@ -15,6 +15,7 @@ type ApiPageContextProps = Pick<
| 'generateTypeScriptSchema'
| 'generateCodeSamples'
| 'proxyUrl'
| 'showResponseSchema'
>;
export interface ApiPageProps extends ApiPageContextProps {
@@ -126,6 +127,7 @@ export async function getContext(
document: document,
dereferenceMap,
proxyUrl: options.proxyUrl,
showResponseSchema: options.showResponseSchema,
renderer: {
...createRenders(options.shikiOptions),
...options.renderer,
+6
View File
@@ -19,6 +19,7 @@ export type ReferenceObject = V3_1.ReferenceObject;
export type PathItemObject = V3_1.PathItemObject;
export type TagObject = V3_1.TagObject;
export type ServerObject = NoReference<V3_1.ServerObject>;
export type CallbackObject = NoReference<V3_1.CallbackObject>;
export type MethodInformation = NoReference<OperationObject> & {
method: string;
@@ -70,4 +71,9 @@ export interface RenderContext {
shikiOptions?: Omit<CodeToHastOptionsCommon, 'lang'> &
CodeOptionsThemes<BuiltinTheme>;
/**
* Show full response schema instead of only example response & Typescript definitions
*/
showResponseSchema?: boolean;
}
-27
View File
@@ -60,31 +60,4 @@ const defaultMdxComponents = {
Callout,
};
/**
* **Server Component Only**
*
* Sometimes, if you directly pass a client component to MDX Components, it will throw an error
*
* To solve this, you can re-create the component in a server component like: `(props) => <Component {...props} />`
*
* This function does that for you
*
* @param c - MDX Components
* @returns MDX Components with re-created client components
* @deprecated no longer used
*/
export function createComponents<
Components extends Record<string, FC<unknown>>,
>(c: Components): Components {
const mapped = Object.entries(c).map(([k, V]) => {
// Client components are empty objects
return [
k,
Object.keys(V).length === 0 ? (props: object) => <V {...props} /> : V,
];
});
return Object.fromEntries(mapped) as Components;
}
export { defaultMdxComponents as default };