OpenAPI: Improve cURL generator

This commit is contained in:
fuma-nama
2024-08-06 00:16:10 +08:00
parent 61ef42c323
commit 4bf9851a43
8 changed files with 93 additions and 26 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': minor
---
Support to group pages by tags
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Improve Curl example generator
+46 -16
View File
@@ -32,10 +32,18 @@ export interface Config extends GenerateOptions {
/**
* Group output using folders (Only works on `operation` mode)
*
* @deprecated Use `groupBy` instead
* @defaultValue false
*/
groupByFolder?: boolean;
/**
* Group output using folders (Only works on `operation` mode)
*
* @defaultValue 'none'
*/
groupBy?: 'tag' | 'route' | 'none';
cwd?: string;
}
@@ -45,7 +53,7 @@ export async function generateFiles({
name: nameFn,
per = 'file',
cwd = process.cwd(),
groupByFolder = false,
groupBy = 'none',
...options
}: Config): Promise<void> {
const outputDir = join(cwd, output);
@@ -66,27 +74,46 @@ export async function generateFiles({
}
if (per === 'operation') {
const routeFolders = new Set<string>();
const metaFiles = new Set<string>();
const results = await generateOperations(path, options);
await Promise.all(
results.map(async (result) => {
const outPath = groupByFolder
? join(
outputDir,
filename,
result.route.summary
? getFilename(result.route.summary)
: getFilenameFromRoute(result.route.path),
`${getFilename(result.id)}.mdx`,
)
: join(outputDir, filename, `${getFilename(result.id)}.mdx`);
let outPath;
if (!result.method.operationId) return;
const id =
result.method.operationId.split('.').at(-1) ??
result.method.operationId;
if (groupByFolder && !routeFolders.has(dirname(outPath))) {
routeFolders.add(dirname(outPath));
if (
groupBy === 'tag' &&
result.method.tags &&
result.method.tags.length > 0
) {
if (result.method.tags.length > 1)
console.warn(
`${result.route.path} has more than 1 tag, which isn't allowed under 'groupBy: tag'. Only the first tag will be considered.`,
);
if (result.route.summary) {
const metaFile = join(dirname(outPath), 'meta.json');
outPath = join(
outputDir,
filename,
result.method.tags[0],
`${getFilename(id)}.mdx`,
);
} else if (groupBy === 'route') {
outPath = join(
outputDir,
filename,
result.route.summary
? getFilename(result.route.summary)
: getFilenameFromRoute(result.route.path),
`${getFilename(id)}.mdx`,
);
const metaFile = join(dirname(outPath), 'meta.json');
if (result.route.summary && !metaFiles.has(metaFile)) {
metaFiles.add(metaFile);
await write(
metaFile,
@@ -96,6 +123,8 @@ export async function generateFiles({
);
console.log(`Generated Meta: ${metaFile}`);
}
} else {
outPath = join(outputDir, filename, `${getFilename(id)}.mdx`);
}
await write(outPath, result.content);
@@ -122,6 +151,7 @@ export async function generateFiles({
function getFilenameFromRoute(path: string): string {
return (
path
.replaceAll('.', '/')
.split('/')
.filter((v) => !v.startsWith('{') && !v.endsWith('}'))
.at(-1) ?? ''
+3 -3
View File
@@ -70,9 +70,9 @@ export interface GenerateTagOutput {
}
export interface GenerateOperationOutput {
id: string;
content: string;
method: MethodInformation;
route: RouteInformation;
}
@@ -124,7 +124,7 @@ export async function generateOperations(
]),
options,
{
title: method.summary ?? method.method,
title: method.summary ?? idToTitle(method.operationId),
description: method.description,
context: {
type: 'operation',
@@ -135,9 +135,9 @@ export async function generateOperations(
);
return {
id: method.operationId,
content,
route,
method,
} satisfies GenerateOperationOutput;
});
}),
+28 -4
View File
@@ -12,11 +12,35 @@ export function getSampleRequest(endpoint: Endpoint): string {
s.push(`-H "${header}"`);
}
if (param.in === 'cookie') {
const cookie = JSON.stringify(
`${param.name}=${toSampleInput(param.sample)}`,
);
s.push(`--cookie ${cookie}`);
}
}
if (endpoint.body?.mediaType === 'multipart/form-data')
console.warn("Curl sample with form data body isn't supported.");
if (endpoint.body) s.push(`-d '${toSampleInput(endpoint.body.sample)}'`);
if (endpoint.body?.mediaType === 'multipart/form-data') {
const sample = endpoint.body.sample;
return s.join(' \\\n ');
if (sample && typeof sample === 'object') {
for (const [key, value] of Object.entries(sample)) {
s.push(`-F ${key}=${JSON.stringify(value, null, 2)}`);
}
}
} else if (endpoint.body) {
s.push(`-H "Content-Type: application/json"`);
s.push(`-d '${toSampleInput(endpoint.body.sample)}'`);
}
return s
.flatMap((v, i) =>
v
.split('\n')
.map((line) => (i > 0 ? ` ${line}` : line))
.join('\n'),
)
.join(' \\\n');
}
+1 -1
View File
@@ -38,7 +38,7 @@ function ApiIndicator({ method }: { method: string }): ReactNode {
return React.createElement(
'span',
{
className: badgeVariants({ className: 'ms-auto', color }),
className: badgeVariants({ className: 'ms-auto text-nowrap', color }),
},
method,
);
+4 -1
View File
@@ -1,7 +1,10 @@
export function idToTitle(id: string): string {
const result = [];
let result: string[] = [];
for (const c of id) {
if (result.length === 0) result.push(c.toLocaleUpperCase());
// ignore the other parts surrounded with '.', like 'migrations.dev' -> 'dev'
else if (c === '.') result = [];
else if (/^[A-Z]$/.test(c) && result.at(-1) !== ' ') result.push(' ', c);
else if (c === '-') result.push(' ');
else result.push(c);
+1 -1
View File
@@ -19,7 +19,7 @@ export const buttonVariants = cva(
);
export const itemVariants = cva(
'flex w-full flex-row items-center gap-2 rounded-md px-2 py-1.5 text-fd-muted-foreground transition-colors duration-100 [&_svg]:size-4',
'flex w-full flex-row items-center gap-2 rounded-md px-2 py-1.5 text-fd-muted-foreground transition-colors duration-100 [overflow-wrap:anywhere] [&_svg]:size-4',
{
variants: {
active: {