OpenAPI: Fix gaps of property components under parameters section

This commit is contained in:
Fuma Nama
2025-02-11 17:44:48 +08:00
parent 3b437e4f41
commit f118e2462a
9 changed files with 100 additions and 94 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Fix gaps of property components under parameters section
+1 -6
View File
@@ -2,7 +2,6 @@ import { createMDXSource } from 'fumadocs-mdx';
import { loader } from 'fumadocs-core/source';
import { attachFile, createOpenAPI } from 'fumadocs-openapi/server';
import { docs, meta } from '@/.source';
import { APIPlayground } from 'fumadocs-openapi/scalar';
export const source = loader({
baseUrl: '/docs',
@@ -12,8 +11,4 @@ export const source = loader({
},
});
export const openapi = createOpenAPI({
renderer: {
APIPlayground,
},
});
export const openapi = createOpenAPI();
@@ -3,14 +3,25 @@ import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
/**
* Shared layout configurations
*
* you can configure layouts individually from:
* you can customise layouts individually from:
* Home Layout: app/(home)/layout.tsx
* Docs Layout: app/docs/layout.tsx
*/
export const baseOptions: BaseLayoutProps = {
nav: {
// can be JSX too!
title: 'My App',
title: (
<>
<svg
width="24"
height="24"
xmlns="http://www.w3.org/2000/svg"
aria-label="Logo"
>
<circle cx={12} cy={12} r={12} fill="currentColor" />
</svg>
My App
</>
),
},
links: [
{
+40 -44
View File
@@ -27,6 +27,13 @@ export interface CodeSample {
| false;
}
const ParamTypes = {
path: 'Path Parameters',
query: 'Query Parameters',
header: 'Header Parameters',
cookie: 'Cookie Parameters',
};
export function Operation({
type = 'operation',
path,
@@ -130,46 +137,42 @@ export function Operation({
);
}
const parameterGroups = new Map<string, ReactNode[]>();
const endpoint = generateSample(path, method, ctx);
for (const param of method.parameters ?? []) {
const pInfo = endpoint.parameters.find(
(item) => item.name === param.name && item.in === param.in,
const parameterNode = Object.entries(ParamTypes).map(([type, title]) => {
const params = endpoint.parameters.filter(
(param) => param.in === type && !param.isAuthOnly,
);
if (!pInfo) continue;
if (params.length === 0) return;
const schema = pInfo.schema;
const groupName =
{
path: 'Path Parameters',
query: 'Query Parameters',
header: 'Header Parameters',
cookie: 'Cookie Parameters',
}[param.in] ?? 'Other Parameters';
const group = parameterGroups.get(groupName) ?? [];
group.push(
<Schema
key={param.name}
name={param.name}
schema={{
...schema,
description: param.description ?? schema.description,
deprecated:
(param.deprecated ?? false) || (schema.deprecated ?? false),
}}
ctx={{
parseObject: false,
readOnly: method.method === 'GET',
writeOnly: method.method !== 'GET',
required: param.required ?? false,
render: ctx,
}}
/>,
return (
<Fragment key={type}>
{heading(headingLevel, title, ctx)}
<div className="flex flex-col gap-4">
{params.map((param) => (
<Schema
key={param.name}
name={param.name}
schema={{
...param.schema,
description: param.description ?? param.schema?.description,
deprecated:
(param.deprecated ?? false) ||
(param.schema?.deprecated ?? false),
}}
ctx={{
parseObject: false,
readOnly: method.method === 'GET',
writeOnly: method.method !== 'GET',
required: param.required ?? false,
render: ctx,
}}
/>
))}
</div>
</Fragment>
);
parameterGroups.set(groupName, group);
}
});
if (method.callbacks) {
callbacksNode = (
@@ -208,14 +211,7 @@ export function Operation({
</>
) : null}
{bodyNode}
{Array.from(parameterGroups.entries()).map(([group, params]) => {
return (
<Fragment key={group}>
{heading(headingLevel, group, ctx)}
{params}
</Fragment>
);
})}
{parameterNode}
{responseNode}
{callbacksNode}
</ctx.renderer.APIInfo>
@@ -277,7 +273,7 @@ function AuthSection({
}: {
requirements: SecurityRequirementObject[];
ctx: RenderContext;
}): ReactNode {
}) {
let id = 0;
const info: ReactNode[] = [];
+1 -1
View File
@@ -125,7 +125,7 @@ export function Schema({
);
}
let footer: ReactNode = null;
let footer: ReactNode;
const fields: {
key: string;
value: string;
+1 -1
View File
@@ -45,7 +45,7 @@ export function getSampleRequest(
inputToString(
endpoint.body.samples[sampleKey]?.value ?? '',
endpoint.body.mediaType,
'backtick',
'python',
),
);
}
+1 -1
View File
@@ -78,7 +78,7 @@ export function Property({
}: PropertyProps) {
return (
<div className="rounded-xl border bg-fd-card p-3 prose-no-margin">
<div className="flex flex-row flex-wrap items-center gap-4">
<div className="flex flex-row flex-wrap items-center gap-4 mb-2">
<code>{name}</code>
{required ? (
<Badge color="red" className="text-xs">
+32 -36
View File
@@ -6,7 +6,9 @@ import {
type NoReference,
} from '@/utils/schema';
import { getSecurities, getSecurityPrefix } from '@/utils/get-security';
export interface EndpointSamples {
import type { OpenAPIV3_1 } from 'openapi-types';
export interface Samples {
[key: string]: {
value?: unknown;
description?: string;
@@ -26,7 +28,7 @@ export interface EndpointSample {
body?: {
schema: ParsedSchema;
mediaType: string;
samples: EndpointSamples;
samples: Samples;
};
responses: Record<string, ResponseSample>;
parameters: ParameterSample[];
@@ -38,11 +40,9 @@ interface ResponseSample {
schema: ParsedSchema;
}
interface ParameterSample {
name: string;
in: string;
schema: ParsedSchema;
interface ParameterSample extends NoReference<OpenAPIV3_1.ParameterObject> {
sample: unknown;
isAuthOnly: boolean;
}
export function generateSample(
@@ -54,35 +54,34 @@ export function generateSample(
const responses: EndpointSample['responses'] = {};
for (const param of method.parameters ?? []) {
if (param.schema) {
let value = param.example ?? sample(param.schema as object);
if (param.schema.type && param.schema.type === value) {
// if no example is defined make sure its visible that there is still a placeholder, equal to auth <token>
value = `<${value}>`;
}
params.push({
name: param.name,
in: param.in,
schema: param.schema,
sample: value,
});
} else if (param.content) {
const key = getPreferredType(param.content);
const content = key ? param.content[key] : undefined;
let schema = param.schema,
value;
if (!key || !content)
if (!schema && param.content) {
const key = getPreferredType(param.content);
const content = key ? param.content[key] : undefined;
if (!content)
throw new Error(
`Cannot find parameter schema for ${param.name} in ${path} ${method.method}`,
);
params.push({
name: param.name,
in: param.in,
schema: content.schema ?? {},
sample:
content.example ?? param.example ?? sample(content.schema as object),
});
schema = content.schema;
value = content.example ?? param.example ?? sample(schema as object);
} else {
value = param.example ?? sample(schema as object);
}
if (schema?.type && param.schema?.type === value) {
// if no example is defined make sure its visible that there is still a placeholder, equal to auth <token>
value = `<${value}>`;
}
params.push({
...param,
sample: value,
isAuthOnly: false,
});
}
const requirements = method.security ?? document.security;
@@ -97,6 +96,7 @@ export function generateSample(
},
sample: prefix ? `${prefix} <token>` : '<token>',
in: 'header',
isAuthOnly: true,
});
}
}
@@ -147,17 +147,13 @@ export function generateSample(
const queryParams = new URLSearchParams();
for (const param of params) {
let value = generateBody(method.method, param.schema);
if (param.schema.type && param.schema.type === value) {
// if no example is defined make sure its visible that there is still a placeholder, equal to auth <token>
value = `<${value}>`;
}
if (param.in === 'query') queryParams.append(param.name, String(value));
if (param.in === 'query')
queryParams.append(param.name, String(param.sample));
if (param.in === 'path')
pathWithParameters = pathWithParameters.replace(
`{${param.name}}`,
String(value),
String(param.sample),
);
}
@@ -6,12 +6,15 @@ import { type ElementCompact, js2xml } from 'xml-js';
export function inputToString(
value: unknown,
mediaType = 'application/json',
multiLine: 'single-quote' | 'backtick' | 'none' = 'none',
multiLine: 'single-quote' | 'backtick' | 'python' | 'none' = 'none',
): string {
const getStr = (v: string) => {
if (multiLine === 'none') return JSON.stringify(v);
const delimit = multiLine === 'backtick' ? `\`` : `'`;
const delimit = { backtick: `\``, 'single-quote': `'`, python: `"""` }[
multiLine
];
return `${delimit}${v.replaceAll(delimit, `\\${delimit}`)}${delimit}`;
};