mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Support response examples
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user