Skip to content

Commit 0a13c45

Browse files
committed
OpenAPI: Support response examples
1 parent 6ecde7a commit 0a13c45

File tree

5 files changed

+73
-45
lines changed

5 files changed

+73
-45
lines changed

.changeset/loud-eagles-stay.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'fumadocs-openapi': patch
3+
---
4+
5+
Support response examples

packages/openapi/src/render/operation/api-example.tsx

+51-30
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import type { EndpointSample } from '@/utils/generate-sample';
77
import { type ReactNode } from 'react';
88
import { Markdown } from '@/render/markdown';
99
import { type CodeSample } from '@/render/operation';
10-
import type { ResponseTypeProps } from '@/render/renderer';
1110
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
11+
import { CodeBlock } from '@/render/codeblock';
1212

1313
const defaultSamples: CodeSample[] = [
1414
{
@@ -111,16 +111,14 @@ export async function APIExample({
111111
);
112112
}
113113

114+
const exclusiveCodeSamples = method['x-exclusiveCodeSample'];
114115
if (
115116
(samples.size === 1 && samples.has('_default')) ||
116-
(method['x-exclusiveCodeSample'] &&
117-
samples.has(method['x-exclusiveCodeSample']))
117+
(exclusiveCodeSamples && samples.has(exclusiveCodeSamples))
118118
) {
119119
// if exclusiveSampleKey is present, we don't use tabs
120120
// if only the fallback or non described openapi legacy example is present, we don't use tabs
121-
children = renderRequest(
122-
samples.get(method['x-exclusiveCodeSample'] ?? '_default')!,
123-
);
121+
children = renderRequest(samples.get(exclusiveCodeSamples ?? '_default')!);
124122
} else if (samples.size > 0) {
125123
const entries = Array.from(samples.entries());
126124

@@ -133,7 +131,7 @@ export async function APIExample({
133131
) : null,
134132
value: key,
135133
}))}
136-
defaultValue={method['x-selectedCodeSample']}
134+
defaultValue={exclusiveCodeSamples}
137135
>
138136
{entries.map(([key, sample]) => (
139137
<renderer.Sample key={key} value={key}>
@@ -180,19 +178,13 @@ function ResponseTabs({
180178
if (!operation.responses) return null;
181179

182180
async function renderResponse(code: string) {
183-
const types: ResponseTypeProps[] = [];
181+
const response =
182+
code in endpoint.responses ? endpoint.responses[code] : null;
184183

185-
let description = operation.responses?.[code].description;
186-
if (!description && code in endpoint.responses)
187-
description = endpoint.responses[code].schema.description ?? '';
188-
189-
if (code in endpoint.responses) {
190-
types.push({
191-
lang: 'json',
192-
label: 'Response',
193-
code: JSON.stringify(endpoint.responses[code].sample, null, 2),
194-
});
195-
}
184+
const description =
185+
operation.responses?.[code].description ??
186+
response?.schema.description ??
187+
'';
196188

197189
let ts: string | undefined;
198190
if (generateTypeScriptSchema) {
@@ -201,24 +193,53 @@ function ResponseTabs({
201193
ts = await getTypescriptSchema(endpoint, code, schema.dereferenceMap);
202194
}
203195

204-
if (ts) {
205-
types.push({
206-
code: ts,
207-
lang: 'ts',
208-
label: 'TypeScript',
196+
const values: string[] = [];
197+
let exampleSlot: ReactNode;
198+
199+
if (response?.samples._default) {
200+
values.push('Response');
201+
202+
exampleSlot = (
203+
<renderer.ResponseType label="Response">
204+
<CodeBlock
205+
lang="json"
206+
code={JSON.stringify(
207+
endpoint.responses[code].samples._default,
208+
null,
209+
2,
210+
)}
211+
/>
212+
</renderer.ResponseType>
213+
);
214+
} else if (response) {
215+
exampleSlot = Object.entries(response.samples).map(([key, sample], i) => {
216+
const title = sample?.summary ?? `Example ${i + 1}`;
217+
218+
values.push(title);
219+
return (
220+
<renderer.ResponseType key={key} label={title}>
221+
{sample?.description ? (
222+
<Markdown text={sample.description} />
223+
) : null}
224+
<CodeBlock lang="json" code={JSON.stringify(sample, null, 2)} />
225+
</renderer.ResponseType>
226+
);
209227
});
210228
}
211229

212230
return (
213231
<renderer.Response value={code}>
214232
{description ? <Markdown text={description} /> : null}
215-
{types.length > 0 ? (
216-
<renderer.ResponseTypes>
217-
{types.map((type) => (
218-
<renderer.ResponseType key={type.lang} {...type} />
219-
))}
233+
{response && (
234+
<renderer.ResponseTypes defaultValue={values[0]}>
235+
{exampleSlot}
236+
{ts ? (
237+
<renderer.ResponseType label="TypeScript">
238+
<CodeBlock code={ts} lang="ts" />
239+
</renderer.ResponseType>
240+
) : null}
220241
</renderer.ResponseTypes>
221-
) : null}
242+
)}
222243
</renderer.Response>
223244
);
224245
}

packages/openapi/src/render/renderer.tsx

+4-7
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,8 @@ export interface SamplesProps {
6767
}
6868

6969
export interface ResponseTypeProps {
70-
lang: string;
71-
code: string;
7270
label: string;
71+
children: ReactNode;
7372
}
7473

7574
export interface RootProps {
@@ -92,7 +91,7 @@ export interface Renderer {
9291
Samples: ComponentType<SamplesProps>;
9392
Requests: ComponentType<{ items: string[]; children: ReactNode }>;
9493
Request: ComponentType<RequestProps>;
95-
ResponseTypes: ComponentType<{ children: ReactNode }>;
94+
ResponseTypes: ComponentType<{ defaultValue?: string; children: ReactNode }>;
9695
ResponseType: ComponentType<ResponseTypeProps>;
9796

9897
/**
@@ -137,15 +136,13 @@ export function createRenders(
137136
<Accordions
138137
type="single"
139138
className="!-m-4 border-none pt-2"
140-
defaultValue="Response"
139+
defaultValue={props.defaultValue}
141140
>
142141
{props.children}
143142
</Accordions>
144143
),
145144
ResponseType: (props) => (
146-
<Accordion title={props.label}>
147-
<CodeBlock code={props.code} lang={props.lang} options={shikiOptions} />
148-
</Accordion>
145+
<Accordion title={props.label}>{props.children}</Accordion>
149146
),
150147
Property,
151148
ObjectCollapsible,

packages/openapi/src/ui/sample-select.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function Samples({
2222
return (
2323
<>
2424
<Select value={value} onValueChange={setValue}>
25-
<SelectTrigger className="not-prose">
25+
<SelectTrigger className="not-prose mb-2">
2626
<SelectValue
2727
placeholder={
2828
defaultItem ? <SelectDisplay {...defaultItem} /> : null

packages/openapi/src/utils/generate-sample.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import {
88
import { getSecurities, getSecurityPrefix } from '@/utils/get-security';
99
import type { OpenAPIV3_1 } from 'openapi-types';
1010

11-
export interface Samples {
12-
[key: string]: {
11+
export type Samples = {
12+
[key in '_default' | (string & {})]?: {
1313
value?: unknown;
1414
description?: string;
1515
summary?: string;
1616
externalValue?: string;
1717
};
18-
}
18+
};
19+
1920
/**
2021
* Sample info of endpoint
2122
*/
@@ -36,7 +37,7 @@ export interface EndpointSample {
3637

3738
interface ResponseSample {
3839
mediaType: string;
39-
sample: unknown;
40+
samples: Samples;
4041
schema: ParsedSchema;
4142
}
4243

@@ -134,11 +135,15 @@ export function generateSample(
134135
const responseSchema = content[mediaType].schema;
135136
if (!responseSchema) continue;
136137

138+
const examples = content[mediaType].examples ?? content.examples;
139+
const example = content[mediaType].example ?? content.example;
137140
responses[code] = {
138141
mediaType,
139-
sample:
140-
content[mediaType].example ??
141-
generateBody(method.method, responseSchema),
142+
samples: examples
143+
? examples
144+
: {
145+
_default: example ?? generateBody(method.method, responseSchema),
146+
},
142147
schema: responseSchema,
143148
};
144149
}

0 commit comments

Comments
 (0)