OpenAPI: Support XML request body

This commit is contained in:
Fuma Nama
2025-01-03 22:15:19 +08:00
parent cae8863313
commit 0e8be0e779
9 changed files with 126 additions and 59 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': minor
---
Support XML request body
+2 -1
View File
@@ -54,7 +54,8 @@
"react-hook-form": "^7.54.2",
"remark": "^15.0.1",
"remark-rehype": "^11.1.1",
"shiki": "^1.26.1"
"shiki": "^1.26.1",
"xml-js": "^1.6.11"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
+8 -8
View File
@@ -1,5 +1,5 @@
import { type EndpointSample } from '@/utils/generate-sample';
import { toSampleInput } from '@/utils/schema';
import { inputToString } from '@/utils/schema';
export function getSampleRequest(endpoint: EndpointSample): string {
const s: string[] = [];
@@ -8,15 +8,13 @@ export function getSampleRequest(endpoint: EndpointSample): string {
for (const param of endpoint.parameters) {
if (param.in === 'header') {
const header = `${param.name}: ${toSampleInput(param.sample)}`;
const header = `${param.name}: ${param.sample}`;
s.push(`-H "${header}"`);
}
if (param.in === 'cookie') {
const cookie = JSON.stringify(
`${param.name}=${toSampleInput(param.sample)}`,
);
const cookie = JSON.stringify(`${param.name}=${param.sample}`);
s.push(`--cookie ${cookie}`);
}
@@ -27,12 +25,14 @@ export function getSampleRequest(endpoint: EndpointSample): string {
if (sample && typeof sample === 'object') {
for (const [key, value] of Object.entries(sample)) {
s.push(`-F ${key}=${JSON.stringify(value, null, 2)}`);
s.push(`-F ${key}=${inputToString(value)}`);
}
}
} else if (endpoint.body) {
s.push(`-H "Content-Type: application/json"`);
s.push(`-d '${toSampleInput(endpoint.body.sample)}'`);
s.push(`-H "Content-Type: ${endpoint.body.mediaType}"`);
s.push(
`-d ${inputToString(endpoint.body.sample, endpoint.body.mediaType, 'single-quote')}`,
);
}
return s
+16 -21
View File
@@ -1,10 +1,9 @@
import { type EndpointSample } from '@/utils/generate-sample';
import { toSampleInput } from '@/utils/schema';
import { inputToString } from '@/utils/schema';
export function getSampleRequest(endpoint: EndpointSample): string {
const imports = ['fmt', 'net/http', 'io/ioutil'];
const headers = new Map<string, string>();
const cookies = new Map<string, string>();
const variables = new Map<string, string>();
// additional lines before initializing request
@@ -12,35 +11,21 @@ export function getSampleRequest(endpoint: EndpointSample): string {
for (const p of endpoint.parameters) {
if (p.in === 'header') headers.set(p.name, JSON.stringify(p.sample));
if (p.in === 'cookie') cookies.set(p.name, toSampleInput(p.sample));
}
const cookies = endpoint.parameters.filter((p) => p.in === 'cookie');
variables.set('url', JSON.stringify(endpoint.url));
if (cookies.size > 0)
if (cookies.length > 0)
headers.set(
'Cookie',
JSON.stringify(
Array.from(cookies.entries())
.map(([key, value]) => `${key}=${value}`)
.join('; '),
),
JSON.stringify(cookies.map((p) => `${p.name}=${p.sample}`).join('; ')),
);
if (endpoint.body) {
headers.set('Content-Type', `"${endpoint.body.mediaType}"`);
if (endpoint.body.mediaType === 'application/json') {
imports.push('strings');
variables.set(
'payload',
`strings.NewReader(\`${JSON.stringify(
endpoint.body.sample,
null,
2,
).replaceAll('\n', '\n ')}\`)`,
);
}
if (
endpoint.body.mediaType === 'multipart/form-data' &&
typeof endpoint.body.sample === 'object'
@@ -52,9 +37,19 @@ export function getSampleRequest(endpoint: EndpointSample): string {
for (const [key, value] of Object.entries(endpoint.body.sample ?? {})) {
additional.push(
`mp.WriteField("${key}", ${JSON.stringify(toSampleInput(value))})`,
`mp.WriteField("${key}", ${inputToString(value, undefined, 'backtick')})`,
);
}
} else {
imports.push('strings');
variables.set(
'payload',
`strings.NewReader(${inputToString(
endpoint.body.sample,
endpoint.body.mediaType,
'backtick',
).replaceAll('\n', '\n ')})`,
);
}
}
+26 -15
View File
@@ -1,5 +1,5 @@
import { type EndpointSample } from '@/utils/generate-sample';
import { toSampleInput } from '@/utils/schema';
import { inputToString } from '@/utils/schema';
export function getSampleRequest(endpoint: EndpointSample): string {
const s: string[] = [];
@@ -21,7 +21,7 @@ export function getSampleRequest(endpoint: EndpointSample): string {
headers.set(
'cookie',
Array.from(cookies.entries())
.map(([key, value]) => `${key}=${toSampleInput(value)}`)
.map(([key, value]) => `${key}=${value}`)
.join('; '),
);
}
@@ -29,11 +29,10 @@ export function getSampleRequest(endpoint: EndpointSample): string {
if (headers.size > 0) {
options.set(
'headers',
JSON.stringify(
Object.fromEntries(headers.entries()),
undefined,
2,
).replaceAll('\n', '\n '),
JSON.stringify(Object.fromEntries(headers.entries()), null, 2).replaceAll(
'\n',
'\n ',
),
);
}
@@ -45,18 +44,30 @@ export function getSampleRequest(endpoint: EndpointSample): string {
s.push(`const formData = new FormData();`);
for (const [key, value] of Object.entries(endpoint.body.sample))
s.push(`formData.set(${key}, ${JSON.stringify(value)})`);
s.push(`formData.set(${key}, ${inputToString(value)})`);
options.set('body', 'formData');
} else if (endpoint.body) {
options.set(
'body',
`JSON.stringify(${JSON.stringify(
let code: string;
if (endpoint.body.mediaType === 'application/json') {
code =
typeof endpoint.body.sample === 'string'
? inputToString(
endpoint.body.sample,
endpoint.body.mediaType,
'backtick',
)
: `JSON.stringify(${JSON.stringify(endpoint.body.sample, null, 2)})`;
} else {
code = inputToString(
endpoint.body.sample,
null,
2,
).replaceAll('\n', '\n ')})`,
);
endpoint.body.mediaType,
'backtick',
);
}
options.set('body', code.replaceAll('\n', '\n '));
}
const optionsStr = Array.from(options.entries())
+24 -7
View File
@@ -1,4 +1,5 @@
import { type EndpointSample } from '@/utils/generate-sample';
import { inputToString } from '@/utils/schema';
export function getSampleRequest(endpoint: EndpointSample): string {
const headers = new Map<string, unknown>();
@@ -10,6 +11,29 @@ export function getSampleRequest(endpoint: EndpointSample): string {
if (param.in === 'cookie') cookies.set(param.name, param.sample);
}
if (endpoint.body) {
switch (endpoint.body.mediaType) {
case 'application/json':
variables.set('json', JSON.stringify(endpoint.body.sample, null, 2));
break;
case 'multipart/form-data':
headers.set('Content-Type', endpoint.body.mediaType);
variables.set('data', JSON.stringify(endpoint.body.sample, null, 2));
break;
default:
headers.set('Content-Type', endpoint.body.mediaType);
variables.set(
'data',
inputToString(
endpoint.body.sample,
endpoint.body.mediaType,
'backtick',
),
);
}
}
if (headers.size > 0) {
variables.set(
'headers',
@@ -24,13 +48,6 @@ export function getSampleRequest(endpoint: EndpointSample): string {
);
}
if (endpoint.body) {
variables.set(
endpoint.body.mediaType === 'multipart/form-data' ? 'data' : 'json',
JSON.stringify(endpoint.body.sample, null, 2),
);
}
return `import requests
url = ${JSON.stringify(endpoint.url)}
@@ -1,7 +1,6 @@
import { sample } from 'openapi-sampler';
import type { MethodInformation, RenderContext } from '@/types';
import {
toSampleInput,
getPreferredType,
type ParsedSchema,
type NoReference,
@@ -131,13 +130,12 @@ export function generateSample(
for (const param of params) {
const value = generateBody(method.method, param.schema);
if (param.in === 'query')
queryParams.append(param.name, toSampleInput(value));
if (param.in === 'query') queryParams.append(param.name, String(value));
if (param.in === 'path')
pathWithParameters = pathWithParameters.replace(
`{${param.name}}`,
toSampleInput(value),
String(value),
);
}
+27 -3
View File
@@ -1,5 +1,6 @@
import type { ReferenceObject } from '@/types';
import type { OpenAPIV3_1 } from 'openapi-types';
import { js2xml, type ElementCompact } from 'xml-js';
export type NoReference<T> = T extends (infer I)[]
? NoReference<I>[]
@@ -22,10 +23,33 @@ export function getPreferredType<B extends Record<string, unknown>>(
}
/**
* Convert to JSON string if necessary
* Convert input to string (with quotes)
*/
export function toSampleInput(value: unknown): string {
return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
export function inputToString(
value: unknown,
mediaType = 'application/json',
multiLine: 'single-quote' | 'backtick' | 'none' = 'none',
): string {
const getStr = (v: string) => {
if (multiLine === 'none') return JSON.stringify(v);
const delimit = multiLine === 'backtick' ? `\`` : `'`;
return `${delimit}${v.replaceAll(delimit, `\\${delimit}`)}${delimit}`;
};
if (typeof value === 'string') return getStr(value);
if (mediaType === 'application/json' || mediaType === 'multipart/form-data') {
return getStr(JSON.stringify(value, null, 2));
}
if (mediaType === 'application/xml') {
return getStr(
js2xml(value as ElementCompact, { compact: true, spaces: 2 }),
);
}
throw new Error(`Unsupported media type: ${mediaType}`);
}
export function isNullable(
+16
View File
@@ -955,6 +955,9 @@ importers:
shiki:
specifier: ^1.26.1
version: 1.26.1
xml-js:
specifier: ^1.6.11
version: 1.6.11
devDependencies:
'@types/js-yaml':
specifier: ^4.0.9
@@ -5809,6 +5812,9 @@ packages:
[email protected]:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
[email protected]:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
[email protected]:
resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
@@ -6550,6 +6556,10 @@ packages:
utf-8-validate:
optional: true
[email protected]:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
[email protected]:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -11855,6 +11865,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -12729,6 +12741,10 @@ snapshots:
optionalDependencies:
bufferutil: 4.0.9
[email protected]:
dependencies:
sax: 1.4.1
[email protected]: {}
[email protected]: {}