OpenAPI: Support response examples

This commit is contained in:
Fuma Nama
2025-02-20 19:42:08 +08:00
parent 6ecde7a2dc
commit 0a13c45d79
5 changed files with 73 additions and 45 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Support response examples
@@ -7,8 +7,8 @@ import type { EndpointSample } from '@/utils/generate-sample';
import { type ReactNode } from 'react';
import { Markdown } from '@/render/markdown';
import { type CodeSample } from '@/render/operation';
import type { ResponseTypeProps } from '@/render/renderer';
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
import { CodeBlock } from '@/render/codeblock';
const defaultSamples: CodeSample[] = [
{
@@ -111,16 +111,14 @@ export async function APIExample({
);
}
const exclusiveCodeSamples = method['x-exclusiveCodeSample'];
if (
(samples.size === 1 && samples.has('_default')) ||
(method['x-exclusiveCodeSample'] &&
samples.has(method['x-exclusiveCodeSample']))
(exclusiveCodeSamples && samples.has(exclusiveCodeSamples))
) {
// if exclusiveSampleKey is present, we don't use tabs
// if only the fallback or non described openapi legacy example is present, we don't use tabs
children = renderRequest(
samples.get(method['x-exclusiveCodeSample'] ?? '_default')!,
);
children = renderRequest(samples.get(exclusiveCodeSamples ?? '_default')!);
} else if (samples.size > 0) {
const entries = Array.from(samples.entries());
@@ -133,7 +131,7 @@ export async function APIExample({
) : null,
value: key,
}))}
defaultValue={method['x-selectedCodeSample']}
defaultValue={exclusiveCodeSamples}
>
{entries.map(([key, sample]) => (
<renderer.Sample key={key} value={key}>
@@ -180,19 +178,13 @@ function ResponseTabs({
if (!operation.responses) return null;
async function renderResponse(code: string) {
const types: ResponseTypeProps[] = [];
const response =
code in endpoint.responses ? endpoint.responses[code] : null;
let description = operation.responses?.[code].description;
if (!description && code in endpoint.responses)
description = endpoint.responses[code].schema.description ?? '';
if (code in endpoint.responses) {
types.push({
lang: 'json',
label: 'Response',
code: JSON.stringify(endpoint.responses[code].sample, null, 2),
});
}
const description =
operation.responses?.[code].description ??
response?.schema.description ??
'';
let ts: string | undefined;
if (generateTypeScriptSchema) {
@@ -201,24 +193,53 @@ function ResponseTabs({
ts = await getTypescriptSchema(endpoint, code, schema.dereferenceMap);
}
if (ts) {
types.push({
code: ts,
lang: 'ts',
label: 'TypeScript',
const values: string[] = [];
let exampleSlot: ReactNode;
if (response?.samples._default) {
values.push('Response');
exampleSlot = (
<renderer.ResponseType label="Response">
<CodeBlock
lang="json"
code={JSON.stringify(
endpoint.responses[code].samples._default,
null,
2,
)}
/>
</renderer.ResponseType>
);
} else if (response) {
exampleSlot = Object.entries(response.samples).map(([key, sample], i) => {
const title = sample?.summary ?? `Example ${i + 1}`;
values.push(title);
return (
<renderer.ResponseType key={key} label={title}>
{sample?.description ? (
<Markdown text={sample.description} />
) : null}
<CodeBlock lang="json" code={JSON.stringify(sample, null, 2)} />
</renderer.ResponseType>
);
});
}
return (
<renderer.Response value={code}>
{description ? <Markdown text={description} /> : null}
{types.length > 0 ? (
<renderer.ResponseTypes>
{types.map((type) => (
<renderer.ResponseType key={type.lang} {...type} />
))}
{response && (
<renderer.ResponseTypes defaultValue={values[0]}>
{exampleSlot}
{ts ? (
<renderer.ResponseType label="TypeScript">
<CodeBlock code={ts} lang="ts" />
</renderer.ResponseType>
) : null}
</renderer.ResponseTypes>
) : null}
)}
</renderer.Response>
);
}
+4 -7
View File
@@ -67,9 +67,8 @@ export interface SamplesProps {
}
export interface ResponseTypeProps {
lang: string;
code: string;
label: string;
children: ReactNode;
}
export interface RootProps {
@@ -92,7 +91,7 @@ export interface Renderer {
Samples: ComponentType<SamplesProps>;
Requests: ComponentType<{ items: string[]; children: ReactNode }>;
Request: ComponentType<RequestProps>;
ResponseTypes: ComponentType<{ children: ReactNode }>;
ResponseTypes: ComponentType<{ defaultValue?: string; children: ReactNode }>;
ResponseType: ComponentType<ResponseTypeProps>;
/**
@@ -137,15 +136,13 @@ export function createRenders(
<Accordions
type="single"
className="!-m-4 border-none pt-2"
defaultValue="Response"
defaultValue={props.defaultValue}
>
{props.children}
</Accordions>
),
ResponseType: (props) => (
<Accordion title={props.label}>
<CodeBlock code={props.code} lang={props.lang} options={shikiOptions} />
</Accordion>
<Accordion title={props.label}>{props.children}</Accordion>
),
Property,
ObjectCollapsible,
+1 -1
View File
@@ -22,7 +22,7 @@ export function Samples({
return (
<>
<Select value={value} onValueChange={setValue}>
<SelectTrigger className="not-prose">
<SelectTrigger className="not-prose mb-2">
<SelectValue
placeholder={
defaultItem ? <SelectDisplay {...defaultItem} /> : null
+12 -7
View File
@@ -8,14 +8,15 @@ import {
import { getSecurities, getSecurityPrefix } from '@/utils/get-security';
import type { OpenAPIV3_1 } from 'openapi-types';
export interface Samples {
[key: string]: {
export type Samples = {
[key in '_default' | (string & {})]?: {
value?: unknown;
description?: string;
summary?: string;
externalValue?: string;
};
}
};
/**
* Sample info of endpoint
*/
@@ -36,7 +37,7 @@ export interface EndpointSample {
interface ResponseSample {
mediaType: string;
sample: unknown;
samples: Samples;
schema: ParsedSchema;
}
@@ -134,11 +135,15 @@ export function generateSample(
const responseSchema = content[mediaType].schema;
if (!responseSchema) continue;
const examples = content[mediaType].examples ?? content.examples;
const example = content[mediaType].example ?? content.example;
responses[code] = {
mediaType,
sample:
content[mediaType].example ??
generateBody(method.method, responseSchema),
samples: examples
? examples
: {
_default: example ?? generateBody(method.method, responseSchema),
},
schema: responseSchema,
};
}