Skip to content

Commit

Permalink
OpenAPI: Add showResponseSchema option to show the full response sc…
Browse files Browse the repository at this point in the history
…hema
  • Loading branch information
fuma-nama committed Jan 11, 2025
1 parent 041f230 commit 056ab2c
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 78 deletions.
5 changes: 5 additions & 0 deletions .changeset/mighty-experts-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---

Add `showResponseSchema` option to show the full response schema
139 changes: 88 additions & 51 deletions packages/openapi/src/render/operation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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}
/>
))}
</>
);
}
Expand All @@ -201,6 +219,7 @@ export function Operation({
</Fragment>
);
})}
{responseNode}
{callbacksNode}
</ctx.renderer.APIInfo>
);
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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++}
Expand Down
2 changes: 2 additions & 0 deletions packages/openapi/src/server/api-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type ApiPageContextProps = Pick<
| 'generateTypeScriptSchema'
| 'generateCodeSamples'
| 'proxyUrl'
| 'showResponseSchema'
>;

export interface ApiPageProps extends ApiPageContextProps {
Expand Down Expand Up @@ -126,6 +127,7 @@ export async function getContext(
document: document,
dereferenceMap,
proxyUrl: options.proxyUrl,
showResponseSchema: options.showResponseSchema,
renderer: {
...createRenders(options.shikiOptions),
...options.renderer,
Expand Down
6 changes: 6 additions & 0 deletions packages/openapi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 changes: 0 additions & 27 deletions packages/ui/src/mdx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

0 comments on commit 056ab2c

Please sign in to comment.