mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 06:31:49 +00:00
OpenAPI: Support media adapter API
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Support media adapter API
|
||||
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
import { inputToString } from '@/utils/input-to-string';
|
||||
import type { MediaAdapter, MediaContext } from '@/media/adapter';
|
||||
|
||||
export const json: MediaAdapter = {
|
||||
encode(data) {
|
||||
return JSON.stringify(data.body);
|
||||
},
|
||||
generateExample(data, ctx) {
|
||||
return str(data.body, 'json', ctx);
|
||||
},
|
||||
};
|
||||
export const xml: MediaAdapter = {
|
||||
async encode(data) {
|
||||
const { js2xml } = await import('xml-js');
|
||||
|
||||
return js2xml(data.body as Record<string, unknown>, {
|
||||
compact: true,
|
||||
spaces: 2,
|
||||
});
|
||||
},
|
||||
generateExample(data, ctx) {
|
||||
return str(data.body, 'xml', ctx);
|
||||
},
|
||||
};
|
||||
|
||||
export const url: MediaAdapter = {
|
||||
encode(data) {
|
||||
if (typeof data.body !== 'object')
|
||||
throw new Error(
|
||||
`Input value must be object, received: ${typeof data.body}`,
|
||||
);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
for (const key in data.body) {
|
||||
params.set(key, String(data.body[key as keyof object]));
|
||||
}
|
||||
|
||||
return params;
|
||||
},
|
||||
generateExample(data, ctx) {
|
||||
if (ctx.lang === 'js') {
|
||||
return `const body = new URLSearchParams(${JSON.stringify(data.body, null, 2)})`;
|
||||
}
|
||||
|
||||
return str(data.body, 'url', ctx);
|
||||
},
|
||||
};
|
||||
|
||||
export const formData: MediaAdapter = {
|
||||
encode(data) {
|
||||
const formData = new FormData();
|
||||
const body = data.body as Record<string, unknown>;
|
||||
|
||||
if (typeof body !== 'object' || !body) {
|
||||
throw new Error(
|
||||
`Unsupported body type: ${typeof body}, expected: object`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in body) {
|
||||
const prop = body[key];
|
||||
|
||||
if (typeof prop === 'object' && prop instanceof File) {
|
||||
formData.set(key, prop);
|
||||
}
|
||||
|
||||
if (Array.isArray(prop) && prop.every((item) => item instanceof File)) {
|
||||
for (const item of prop) {
|
||||
formData.append(key, item);
|
||||
}
|
||||
}
|
||||
|
||||
if (prop && !(prop instanceof File)) {
|
||||
formData.set(key, JSON.stringify(prop));
|
||||
}
|
||||
}
|
||||
|
||||
return formData;
|
||||
},
|
||||
generateExample(data, ctx) {
|
||||
const s: string[] = [];
|
||||
if (ctx.lang === 'js') {
|
||||
s.push(`const body = new FormData();`);
|
||||
|
||||
for (const [key, value] of Object.entries(data.body as object)) {
|
||||
s.push(`body.set(${key}, ${inputToString(value)})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.lang === 'go' && 'addImport' in ctx) {
|
||||
ctx.addImport('mime/multipart');
|
||||
ctx.addImport('bytes');
|
||||
|
||||
s.push('body := new(bytes.Buffer)');
|
||||
s.push('mp := multipart.NewWriter(payload)');
|
||||
|
||||
for (const [key, value] of Object.entries(data.body as object)) {
|
||||
s.push(
|
||||
`mp.WriteField("${key}", ${inputToString(value, 'json', 'backtick')})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (s.length > 0) return s.join('\n');
|
||||
},
|
||||
};
|
||||
|
||||
function str(init: unknown, format: 'xml' | 'json' | 'url', ctx: MediaContext) {
|
||||
if (ctx.lang === 'js') {
|
||||
if (format === 'json') {
|
||||
return `const body = JSON.stringify(${JSON.stringify(init, null, 2)})`;
|
||||
}
|
||||
return `const body = ${inputToString(init, format, 'backtick')}`;
|
||||
}
|
||||
if (ctx.lang === 'python') {
|
||||
return `body = ${inputToString(init, format, 'python')}`;
|
||||
}
|
||||
if (ctx.lang === 'go' && 'addImport' in ctx) {
|
||||
ctx.addImport('strings');
|
||||
|
||||
return `body := strings.NewReader(${inputToString(init, format, 'backtick')})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { RequestData } from '@/requests/_shared';
|
||||
import * as adapters from './adapter.client';
|
||||
|
||||
interface GoContext {
|
||||
lang: 'go';
|
||||
addImport: (name: string) => void;
|
||||
}
|
||||
|
||||
interface JavaScriptContext {
|
||||
lang: 'js';
|
||||
addImport: (pkg: string, name: string) => void;
|
||||
}
|
||||
|
||||
export type MediaContext =
|
||||
| GoContext
|
||||
| JavaScriptContext
|
||||
| {
|
||||
lang: string;
|
||||
/**
|
||||
* Passed by your custom example generator, for your custom media adapter to receive.
|
||||
*/
|
||||
customData?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export interface MediaAdapter {
|
||||
/**
|
||||
* encode request data into body for `fetch()`.
|
||||
*
|
||||
* Return the encoded form of `data.body` property.
|
||||
*/
|
||||
encode: (data: RequestData) => BodyInit | Promise<BodyInit>;
|
||||
|
||||
/**
|
||||
* generate code for usage examples in a given programming language.
|
||||
*
|
||||
* @param data - request data.
|
||||
* @param lang - name of programming language.
|
||||
* @param ctx - context passed from the generator of programming language.
|
||||
*
|
||||
* @returns code that inits a `body` variable, or undefined if not supported (skip example for that language).
|
||||
*/
|
||||
generateExample: (data: RequestData, ctx: MediaContext) => string | undefined;
|
||||
}
|
||||
|
||||
export const defaultAdapters: Record<string, MediaAdapter> = {
|
||||
'application/json': adapters.json,
|
||||
'application/xml': adapters.xml,
|
||||
'application/x-www-form-urlencoded': adapters.url,
|
||||
'multipart/form-data': adapters.formData,
|
||||
};
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
SchemaProvider,
|
||||
useResolvedSchema,
|
||||
} from '@/playground/schema';
|
||||
import type { MediaAdapter } from '@/media/adapter';
|
||||
|
||||
interface FormValues {
|
||||
authorization:
|
||||
@@ -104,6 +105,8 @@ export type ClientProps = HTMLAttributes<HTMLFormElement> & {
|
||||
components?: Partial<{
|
||||
ResultDisplay: FC<{ data: FetchResult }>;
|
||||
}>;
|
||||
|
||||
mediaAdapters: Record<string, MediaAdapter>;
|
||||
};
|
||||
|
||||
function toRequestData(
|
||||
@@ -116,7 +119,7 @@ function toRequestData(
|
||||
method,
|
||||
header: value.header,
|
||||
body: value.body,
|
||||
bodyMediaType: mediaType as RequestData['bodyMediaType'],
|
||||
bodyMediaType: mediaType,
|
||||
cookie: value.cookie,
|
||||
query: value.query,
|
||||
};
|
||||
@@ -133,6 +136,7 @@ export default function Client({
|
||||
fields,
|
||||
references,
|
||||
proxyUrl,
|
||||
mediaAdapters,
|
||||
components: { ResultDisplay = DefaultResultDisplay } = {},
|
||||
...rest
|
||||
}: ClientProps) {
|
||||
@@ -158,7 +162,7 @@ export default function Client({
|
||||
|
||||
const testQuery = useQuery(async (input: FormValues) => {
|
||||
const fetcher = await import('./fetcher').then((mod) =>
|
||||
mod.createBrowserFetcher(),
|
||||
mod.createBrowserFetcher(mediaAdapters),
|
||||
);
|
||||
|
||||
const serverUrl = server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getPathnameFromInput } from '@/utils/get-pathname-from-input';
|
||||
import { js2xml } from 'xml-js';
|
||||
import { RequestData } from '@/requests/_shared';
|
||||
import type { RequestData } from '@/requests/_shared';
|
||||
import type { MediaAdapter } from '@/media/adapter';
|
||||
|
||||
export interface FetchOptions extends RequestData {
|
||||
proxyUrl?: string;
|
||||
@@ -16,7 +16,9 @@ export interface Fetcher {
|
||||
fetch: (route: string, options: FetchOptions) => Promise<FetchResult>;
|
||||
}
|
||||
|
||||
export function createBrowserFetcher(): Fetcher {
|
||||
export function createBrowserFetcher(
|
||||
adapters: Record<string, MediaAdapter>,
|
||||
): Fetcher {
|
||||
return {
|
||||
async fetch(route, options) {
|
||||
const headers = new Headers();
|
||||
@@ -61,14 +63,21 @@ export function createBrowserFetcher(): Fetcher {
|
||||
url = proxyUrl.toString();
|
||||
}
|
||||
|
||||
let body: BodyInit | undefined = undefined;
|
||||
if (options.bodyMediaType && options.body) {
|
||||
const adapter = adapters[options.bodyMediaType];
|
||||
if (!adapter)
|
||||
throw new Error(
|
||||
`No adapter for ${options.bodyMediaType}, you need to specify one from 'createOpenAPI()'.`,
|
||||
);
|
||||
body = await adapter.encode(options);
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
method: options.method,
|
||||
cache: 'no-cache',
|
||||
headers,
|
||||
body:
|
||||
options.bodyMediaType && options.body
|
||||
? await createBodyFromValue(options.bodyMediaType, options.body)
|
||||
: undefined,
|
||||
body,
|
||||
signal: AbortSignal.timeout(10 * 1000),
|
||||
})
|
||||
.then(async (res) => {
|
||||
@@ -99,59 +108,3 @@ export function createBrowserFetcher(): Fetcher {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create request body from value
|
||||
*/
|
||||
export async function createBodyFromValue(
|
||||
mediaType: Required<RequestData>['bodyMediaType'],
|
||||
value: unknown,
|
||||
): Promise<BodyInit> {
|
||||
if (mediaType === 'application/json') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
if (mediaType === 'application/xml') {
|
||||
return js2xml(value as Record<string, unknown>, {
|
||||
compact: true,
|
||||
spaces: 2,
|
||||
});
|
||||
}
|
||||
|
||||
if (mediaType === 'application/x-www-form-urlencoded') {
|
||||
if (typeof value !== 'object')
|
||||
throw new Error(`Input value must be object, received: ${typeof value}`);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
for (const key in value) {
|
||||
params.set(key, String(value[key as keyof object]));
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
if (typeof value !== 'object' || !value) {
|
||||
throw new Error(`Unsupported body type: ${typeof value}, expected: object`);
|
||||
}
|
||||
|
||||
for (const key in value) {
|
||||
const prop: unknown = value[key as keyof object];
|
||||
|
||||
if (typeof prop === 'object' && prop instanceof File) {
|
||||
formData.set(key, prop);
|
||||
}
|
||||
|
||||
if (Array.isArray(prop) && prop.every((item) => item instanceof File)) {
|
||||
for (const item of prop) {
|
||||
formData.append(key, item);
|
||||
}
|
||||
}
|
||||
|
||||
if (prop && !(prop instanceof File)) {
|
||||
formData.set(key, JSON.stringify(prop));
|
||||
}
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
@@ -14,11 +14,9 @@ export type ParameterField = {
|
||||
export type RequestSchema = ParsedSchema;
|
||||
|
||||
interface Context {
|
||||
allowFile: boolean;
|
||||
references: Record<string, RequestSchema>;
|
||||
registered: WeakMap<Exclude<ParsedSchema, boolean>, string>;
|
||||
nextId: () => string;
|
||||
render: RenderContext;
|
||||
}
|
||||
|
||||
export interface APIPlaygroundProps {
|
||||
@@ -42,19 +40,18 @@ export async function APIPlayground({
|
||||
const mediaType = bodyContent ? getPreferredType(bodyContent) : undefined;
|
||||
|
||||
const context: Context = {
|
||||
allowFile: mediaType === 'multipart/form-data',
|
||||
references: {},
|
||||
nextId() {
|
||||
return String(currentId++);
|
||||
},
|
||||
registered: new WeakMap(),
|
||||
render: ctx,
|
||||
};
|
||||
|
||||
const props: ClientProps = {
|
||||
authorization: getAuthorizationField(method, ctx),
|
||||
method: method.method,
|
||||
route: path,
|
||||
mediaAdapters: ctx.mediaAdapters,
|
||||
parameters: method.parameters?.map((v) => ({
|
||||
name: v.name,
|
||||
in: v.in as ParameterField['in'],
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type ProcessedDocument,
|
||||
} from '@/utils/process-document';
|
||||
import { getUrl } from '@/utils/server-url';
|
||||
import { defaultAdapters } from '@/media/adapter';
|
||||
|
||||
type ApiPageContextProps = Pick<
|
||||
Partial<RenderContext>,
|
||||
@@ -19,6 +20,7 @@ type ApiPageContextProps = Pick<
|
||||
| 'proxyUrl'
|
||||
| 'showResponseSchema'
|
||||
| 'disablePlayground'
|
||||
| 'mediaAdapters'
|
||||
>;
|
||||
|
||||
export interface ApiPageProps extends ApiPageContextProps {
|
||||
@@ -63,7 +65,7 @@ export async function APIPage(props: ApiPageProps) {
|
||||
const { document } = processed;
|
||||
|
||||
return (
|
||||
<ctx.renderer.Root baseUrl={ctx.baseUrl} servers={ctx.servers}>
|
||||
<ctx.renderer.Root ctx={ctx}>
|
||||
{operations?.map((item) => {
|
||||
const pathItem = document.paths?.[item.path];
|
||||
if (!pathItem) return null;
|
||||
@@ -129,7 +131,7 @@ export async function getContext(
|
||||
disablePlayground: options.disablePlayground,
|
||||
showResponseSchema: options.showResponseSchema,
|
||||
renderer: {
|
||||
...createRenders(options.shikiOptions),
|
||||
...createRenders(),
|
||||
...options.renderer,
|
||||
},
|
||||
shikiOptions: options.shikiOptions,
|
||||
@@ -144,6 +146,10 @@ export async function getContext(
|
||||
: {},
|
||||
),
|
||||
servers,
|
||||
mediaAdapters: {
|
||||
...defaultAdapters,
|
||||
...options.mediaAdapters,
|
||||
},
|
||||
slugger: new Slugger(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
getAPIExamples,
|
||||
} from '@/render/operation/api-example';
|
||||
import { MethodLabel } from '@/ui/components/method-label';
|
||||
import { type RequestData, supportedMediaTypes } from '@/requests/_shared';
|
||||
import { type SampleGenerator } from '@/requests/_shared';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
|
||||
import { CopyResponseTypeScript } from '@/ui/client';
|
||||
@@ -32,10 +32,7 @@ import { CopyResponseTypeScript } from '@/ui/client';
|
||||
export interface CodeSample {
|
||||
lang: string;
|
||||
label: string;
|
||||
source?:
|
||||
| string
|
||||
| ((url: string, data: RequestData) => string | undefined)
|
||||
| false;
|
||||
source?: string | SampleGenerator | false;
|
||||
}
|
||||
|
||||
const ParamTypes = {
|
||||
@@ -83,12 +80,7 @@ export function Operation({
|
||||
|
||||
if (body) {
|
||||
const type = getPreferredType(body.content);
|
||||
if (
|
||||
!type ||
|
||||
!supportedMediaTypes.includes(
|
||||
String(type) as (typeof supportedMediaTypes)[number],
|
||||
)
|
||||
)
|
||||
if (!type || !(type in ctx.mediaAdapters))
|
||||
throw new Error(
|
||||
`No supported media type for body content: ${path}, received: ${type}`,
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Property,
|
||||
Root,
|
||||
} from '@/ui';
|
||||
import type { RenderContext, ServerObject } from '@/types';
|
||||
import type { RenderContext } from '@/types';
|
||||
import { APIPlayground, type APIPlaygroundProps } from '@/playground';
|
||||
import { CodeExampleSelector } from '@/ui/contexts/code-example.lazy';
|
||||
|
||||
@@ -64,10 +64,7 @@ export interface ResponseTypeProps {
|
||||
}
|
||||
|
||||
export interface RootProps {
|
||||
baseUrl?: string;
|
||||
shikiOptions?: RenderContext['shikiOptions'];
|
||||
|
||||
servers: ServerObject[];
|
||||
ctx: RenderContext;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@@ -93,15 +90,9 @@ export interface Renderer {
|
||||
APIPlayground: ComponentType<APIPlaygroundProps>;
|
||||
}
|
||||
|
||||
export function createRenders(
|
||||
shikiOptions: RenderContext['shikiOptions'],
|
||||
): Renderer {
|
||||
export function createRenders(): Renderer {
|
||||
return {
|
||||
Root: (props) => (
|
||||
<Root shikiOptions={shikiOptions} {...props}>
|
||||
{props.children}
|
||||
</Root>
|
||||
),
|
||||
Root,
|
||||
API,
|
||||
APIInfo: ({ children, head }) => (
|
||||
<APIInfo>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { getPathnameFromInput } from '@/utils/get-pathname-from-input';
|
||||
import type { MediaAdapter } from '@/media/adapter';
|
||||
|
||||
export const supportedMediaTypes = [
|
||||
'multipart/form-data',
|
||||
'application/json',
|
||||
'application/xml',
|
||||
'application/x-www-form-urlencoded',
|
||||
] as const;
|
||||
export type SampleGenerator = (
|
||||
url: string,
|
||||
data: RequestData,
|
||||
context: {
|
||||
mediaAdapters: Record<string, MediaAdapter>;
|
||||
},
|
||||
) => string;
|
||||
|
||||
export interface RequestData {
|
||||
method: string;
|
||||
@@ -16,15 +18,9 @@ export interface RequestData {
|
||||
cookie: Record<string, string>;
|
||||
body?: unknown;
|
||||
|
||||
bodyMediaType?: (typeof supportedMediaTypes)[number];
|
||||
bodyMediaType?: string;
|
||||
}
|
||||
|
||||
export const MediaTypeFormatMap = {
|
||||
'application/json': 'json',
|
||||
'application/xml': 'xml',
|
||||
'application/x-www-form-urlencoded': 'url',
|
||||
} as const;
|
||||
|
||||
export function getUrl(url: string, data: RequestData): string {
|
||||
return getPathnameFromInput(url, data.path, data.query);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
'use client';
|
||||
import { inputToString } from '@/utils/input-to-string';
|
||||
import {
|
||||
getUrl,
|
||||
ident,
|
||||
MediaTypeFormatMap,
|
||||
type RequestData,
|
||||
} from '@/requests/_shared';
|
||||
import { getUrl, ident, type SampleGenerator } from '@/requests/_shared';
|
||||
|
||||
export function getSampleRequest(url: string, data: RequestData): string {
|
||||
const MediaTypeFormatMap = {
|
||||
'application/json': 'json',
|
||||
'application/xml': 'xml',
|
||||
'application/x-www-form-urlencoded': 'url',
|
||||
} as const;
|
||||
|
||||
export const generator: SampleGenerator = (url, data) => {
|
||||
const s: string[] = [];
|
||||
s.push(`curl -X ${data.method} "${getUrl(url, data)}"`);
|
||||
|
||||
@@ -23,23 +24,30 @@ export function getSampleRequest(url: string, data: RequestData): string {
|
||||
s.push(`--cookie ${value}`);
|
||||
}
|
||||
|
||||
if (data.bodyMediaType === 'multipart/form-data') {
|
||||
if (data.body && typeof data.body === 'object') {
|
||||
for (const [key, value] of Object.entries(data.body)) {
|
||||
s.push(`-F ${key}=${inputToString(value)}`);
|
||||
}
|
||||
if (data.body && data.bodyMediaType === 'multipart/form-data') {
|
||||
if (typeof data.body !== 'object')
|
||||
throw new Error('[CURL] request body must be an object.');
|
||||
|
||||
for (const [key, value] of Object.entries(data.body)) {
|
||||
s.push(`-F ${key}=${inputToString(value)}`);
|
||||
}
|
||||
} else if (data.body && data.bodyMediaType) {
|
||||
} else if (
|
||||
data.body &&
|
||||
data.bodyMediaType &&
|
||||
data.bodyMediaType in MediaTypeFormatMap
|
||||
) {
|
||||
s.push(`-H "Content-Type: ${data.bodyMediaType}"`);
|
||||
|
||||
s.push(
|
||||
`-d ${inputToString(
|
||||
data.body,
|
||||
MediaTypeFormatMap[data.bodyMediaType],
|
||||
MediaTypeFormatMap[
|
||||
data.bodyMediaType as keyof typeof MediaTypeFormatMap
|
||||
],
|
||||
'single-quote',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return s.flatMap((v, i) => ident(v, i > 0 ? 1 : 0)).join(' \\\n');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
'use client';
|
||||
import { inputToString } from '@/utils/input-to-string';
|
||||
import {
|
||||
getUrl,
|
||||
ident,
|
||||
MediaTypeFormatMap,
|
||||
type RequestData,
|
||||
} from '@/requests/_shared';
|
||||
import { getUrl, ident, SampleGenerator } from '@/requests/_shared';
|
||||
|
||||
export function getSampleRequest(url: string, data: RequestData): string {
|
||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
const imports = ['fmt', 'net/http', 'io/ioutil'];
|
||||
const headers = new Map<string, string>();
|
||||
const variables = new Map<string, string>();
|
||||
variables.set('url', JSON.stringify(getUrl(url, data)));
|
||||
|
||||
// additional lines before initializing request
|
||||
const additional: string[] = [];
|
||||
|
||||
for (const header in data.header) {
|
||||
headers.set(header, JSON.stringify(data.header[header]));
|
||||
}
|
||||
@@ -27,31 +18,16 @@ export function getSampleRequest(url: string, data: RequestData): string {
|
||||
JSON.stringify(cookies.map((p) => `${p}=${data.cookie[p]}`).join('; ')),
|
||||
);
|
||||
|
||||
if (data.body && data.bodyMediaType) {
|
||||
let body: string | undefined;
|
||||
|
||||
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
||||
headers.set('Content-Type', `"${data.bodyMediaType}"`);
|
||||
|
||||
if (data.bodyMediaType === 'multipart/form-data') {
|
||||
imports.push('mime/multipart', 'bytes');
|
||||
|
||||
variables.set('payload', `new(bytes.Buffer)`);
|
||||
variables.set('mp', 'multipart.NewWriter(payload)');
|
||||
|
||||
for (const [key, value] of Object.entries(data.body)) {
|
||||
additional.push(
|
||||
`mp.WriteField("${key}", ${inputToString(value, 'json', 'backtick')})`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
imports.push('strings');
|
||||
variables.set(
|
||||
'payload',
|
||||
`strings.NewReader(${inputToString(
|
||||
data.body,
|
||||
MediaTypeFormatMap[data.bodyMediaType],
|
||||
'backtick',
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
body = mediaAdapters[data.bodyMediaType].generateExample(data, {
|
||||
lang: 'go',
|
||||
addImport(from) {
|
||||
imports.push(from);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return `package main
|
||||
@@ -64,8 +40,8 @@ func main() {
|
||||
${Array.from(variables.entries())
|
||||
.map(([k, v]) => ident(`${k} := ${v}`))
|
||||
.join('\n')}
|
||||
${ident(additional.join('\n'))}
|
||||
req, _ := http.NewRequest("${data.method}", url, ${variables.has('payload') ? 'payload' : 'nil'})
|
||||
${body ? ident(body) : ''}
|
||||
req, _ := http.NewRequest("${data.method}", url, ${body ? 'body' : 'nil'})
|
||||
${ident(
|
||||
Array.from(headers.entries())
|
||||
.map(([key, value]) => `req.Header.Add("${key}", ${value})`)
|
||||
@@ -78,4 +54,4 @@ ${ident(
|
||||
fmt.Println(res)
|
||||
fmt.Println(string(body))
|
||||
}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,22 +7,22 @@ import * as Python from '@/requests/python';
|
||||
export const defaultSamples: CodeSample[] = [
|
||||
{
|
||||
label: 'cURL',
|
||||
source: CURL.getSampleRequest,
|
||||
source: CURL.generator,
|
||||
lang: 'bash',
|
||||
},
|
||||
{
|
||||
label: 'JavaScript',
|
||||
source: JS.getSampleRequest,
|
||||
source: JS.generator,
|
||||
lang: 'js',
|
||||
},
|
||||
{
|
||||
label: 'Go',
|
||||
source: Go.getSampleRequest,
|
||||
source: Go.generator,
|
||||
lang: 'go',
|
||||
},
|
||||
{
|
||||
label: 'Python',
|
||||
source: Python.getSampleRequest,
|
||||
source: Python.generator,
|
||||
lang: 'python',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
import { inputToString } from '@/utils/input-to-string';
|
||||
import { getUrl, ident, type RequestData } from '@/requests/_shared';
|
||||
import { getUrl, ident, type SampleGenerator } from '@/requests/_shared';
|
||||
|
||||
export function getSampleRequest(url: string, data: RequestData): string {
|
||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
const s: string[] = [];
|
||||
const options = new Map<string, string>();
|
||||
const headers = { ...data.header };
|
||||
@@ -17,41 +16,26 @@ export function getSampleRequest(url: string, data: RequestData): string {
|
||||
options.set('headers', JSON.stringify(headers, null, 2));
|
||||
}
|
||||
|
||||
if (data.bodyMediaType === 'multipart/form-data' && data.body) {
|
||||
s.push(`const formData = new FormData();`);
|
||||
let body: string | undefined;
|
||||
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
||||
body = mediaAdapters[data.bodyMediaType].generateExample(data, {
|
||||
lang: 'js',
|
||||
addImport(from, name) {
|
||||
s.unshift(`import { ${name} } from "${from}"`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(data.body))
|
||||
s.push(`formData.set(${key}, ${inputToString(value)})`);
|
||||
|
||||
options.set('body', 'formData');
|
||||
} else if (
|
||||
data.body &&
|
||||
data.bodyMediaType === 'application/x-www-form-urlencoded'
|
||||
) {
|
||||
options.set(
|
||||
'body',
|
||||
`new URLSearchParams(${JSON.stringify(data.body, null, 2)})`,
|
||||
);
|
||||
} else if (data.body) {
|
||||
let code: string;
|
||||
|
||||
if (data.bodyMediaType === 'application/json') {
|
||||
code =
|
||||
typeof data.body === 'string'
|
||||
? inputToString(data.body, 'json', 'backtick')
|
||||
: `JSON.stringify(${JSON.stringify(data.body, null, 2)})`;
|
||||
} else {
|
||||
code = inputToString(data.body, 'xml', 'backtick');
|
||||
}
|
||||
|
||||
options.set('body', code);
|
||||
if (body) {
|
||||
s.push(body);
|
||||
options.set('body', 'body');
|
||||
}
|
||||
|
||||
const optionsStr = Array.from(options.entries())
|
||||
.map(([k, v]) => ident(`${k}: ${v}`))
|
||||
.map(([k, v]) => ident(k === v ? k : `${k}: ${v}`))
|
||||
.join(',\n');
|
||||
|
||||
s.push(`fetch(${JSON.stringify(getUrl(url, data))}, {\n${optionsStr}\n});`);
|
||||
|
||||
return s.join('\n\n');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,59 +1,44 @@
|
||||
'use client';
|
||||
import { inputToString } from '@/utils/input-to-string';
|
||||
import {
|
||||
getUrl,
|
||||
MediaTypeFormatMap,
|
||||
type RequestData,
|
||||
} from '@/requests/_shared';
|
||||
import { getUrl, type SampleGenerator } from '@/requests/_shared';
|
||||
|
||||
export function getSampleRequest(url: string, data: RequestData): string {
|
||||
const variables = new Map<string, string>();
|
||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
const headers = { ...data.header };
|
||||
const params = [`"${data.method}"`, 'url'];
|
||||
let body: string | undefined;
|
||||
|
||||
if (data.body && data.bodyMediaType) {
|
||||
switch (data.bodyMediaType) {
|
||||
case 'application/json':
|
||||
variables.set('json', JSON.stringify(data.body, null, 2));
|
||||
break;
|
||||
case 'multipart/form-data':
|
||||
headers['Content-Type'] = data.bodyMediaType;
|
||||
variables.set('data', JSON.stringify(data.body, null, 2));
|
||||
break;
|
||||
default:
|
||||
headers['Content-Type'] = data.bodyMediaType;
|
||||
if (data.bodyMediaType === 'application/json') {
|
||||
params.push(`json = ${JSON.stringify(data.body, null, 2)}`);
|
||||
} else if (data.bodyMediaType === 'multipart/form-data') {
|
||||
headers['Content-Type'] = data.bodyMediaType;
|
||||
|
||||
variables.set(
|
||||
'data',
|
||||
inputToString(
|
||||
data.body,
|
||||
MediaTypeFormatMap[data.bodyMediaType],
|
||||
'python',
|
||||
),
|
||||
);
|
||||
params.push(`data = ${JSON.stringify(data.body, null, 2)}`);
|
||||
} else if (data.bodyMediaType in mediaAdapters) {
|
||||
headers['Content-Type'] = data.bodyMediaType;
|
||||
|
||||
body = mediaAdapters[data.bodyMediaType].generateExample(data, {
|
||||
lang: 'python',
|
||||
});
|
||||
|
||||
if (body) {
|
||||
params.push('data = body');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(headers).length > 0) {
|
||||
variables.set('headers', JSON.stringify(headers, null, 2));
|
||||
params.push(`headers = ${JSON.stringify(headers, null, 2)}`);
|
||||
}
|
||||
|
||||
if (Object.keys(data.cookie).length > 0) {
|
||||
variables.set('cookies', JSON.stringify(data.cookie, null, 2));
|
||||
params.push(`cookies = ${JSON.stringify(data.cookie, null, 2)}`);
|
||||
}
|
||||
|
||||
const params = [
|
||||
`"${data.method}"`,
|
||||
'url',
|
||||
...Array.from(variables.keys()).map((k) => `${k}=${k}`),
|
||||
];
|
||||
|
||||
return `import requests
|
||||
|
||||
url = ${JSON.stringify(getUrl(url, data))}
|
||||
${Array.from(variables.entries())
|
||||
.map(([k, v]) => `${k} = ${v}`)
|
||||
.join('\n')}
|
||||
${body ?? ''}
|
||||
response = requests.request(${params.join(', ')})
|
||||
|
||||
print(response.text)`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from 'shiki';
|
||||
import type { NoReference } from '@/utils/schema';
|
||||
import type { ProcessedDocument } from '@/utils/process-document';
|
||||
import type { MediaAdapter } from '@/media/adapter';
|
||||
|
||||
export type Document = V3_1.Document;
|
||||
export type OperationObject = V3_1.OperationObject;
|
||||
@@ -84,4 +85,6 @@ export interface RenderContext {
|
||||
* @default true
|
||||
*/
|
||||
showResponseSchema?: boolean;
|
||||
|
||||
mediaAdapters: Record<string, MediaAdapter>;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { RenderContext, ServerObject } from '@/types';
|
||||
import type { MediaAdapter } from '@/media/adapter';
|
||||
|
||||
export interface ApiProviderProps {
|
||||
/**
|
||||
@@ -17,6 +18,7 @@ export interface ApiProviderProps {
|
||||
|
||||
servers: ServerObject[];
|
||||
shikiOptions: RenderContext['shikiOptions'];
|
||||
mediaAdapters: Record<string, MediaAdapter>;
|
||||
|
||||
children?: ReactNode;
|
||||
}
|
||||
@@ -34,10 +36,8 @@ interface ServerSelectType {
|
||||
setServerVariables: (value: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
const ApiContext = createContext<ApiContextType | undefined>(undefined);
|
||||
const ServerSelectContext = createContext<ServerSelectType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const ApiContext = createContext<ApiContextType | null>(null);
|
||||
const ServerSelectContext = createContext<ServerSelectType | null>(null);
|
||||
|
||||
export function useApiContext(): ApiContextType {
|
||||
const ctx = useContext(ApiContext);
|
||||
|
||||
@@ -111,7 +111,7 @@ export function CodeExampleProvider({
|
||||
}
|
||||
|
||||
export function CodeExample(props: CodeSample) {
|
||||
const { shikiOptions } = useApiContext();
|
||||
const { shikiOptions, mediaAdapters } = useApiContext();
|
||||
const { examples, key, route, addListener, removeListener } =
|
||||
useContext(CodeExampleContext)!;
|
||||
const { server } = useServerSelectContext();
|
||||
@@ -139,8 +139,11 @@ export function CodeExample(props: CodeSample) {
|
||||
return sample.source(
|
||||
`${server ? getUrl(server.url, server.variables) : '/'}${route}`,
|
||||
data,
|
||||
{
|
||||
mediaAdapters,
|
||||
},
|
||||
);
|
||||
}, [sample, server, route, data]);
|
||||
}, [mediaAdapters, sample, server, route, data]);
|
||||
|
||||
if (!code || !sample) return null;
|
||||
|
||||
|
||||
@@ -13,10 +13,8 @@ import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
baseUrl,
|
||||
className,
|
||||
shikiOptions,
|
||||
servers,
|
||||
ctx,
|
||||
...props
|
||||
}: RootProps & HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
@@ -28,9 +26,10 @@ export function Root({
|
||||
{...props}
|
||||
>
|
||||
<ApiProvider
|
||||
servers={servers}
|
||||
shikiOptions={shikiOptions}
|
||||
defaultBaseUrl={baseUrl}
|
||||
mediaAdapters={ctx.mediaAdapters}
|
||||
servers={ctx.servers}
|
||||
shikiOptions={ctx.shikiOptions}
|
||||
defaultBaseUrl={ctx.baseUrl}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as Go from '@/requests/go';
|
||||
import * as Curl from '@/requests/curl';
|
||||
import * as JS from '@/requests/javascript';
|
||||
import type { RequestData } from '@/requests/_shared';
|
||||
import { defaultAdapters } from '@/media/adapter';
|
||||
|
||||
describe('Code Sample Generators', () => {
|
||||
const url = 'http://localhost:8080/{test}';
|
||||
@@ -27,26 +28,30 @@ describe('Code Sample Generators', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
mediaAdapters: defaultAdapters,
|
||||
};
|
||||
|
||||
test(`Go`, async () => {
|
||||
await expect(Go.getSampleRequest(url, data)).toMatchFileSnapshot(
|
||||
await expect(Go.generator(url, data, context)).toMatchFileSnapshot(
|
||||
`./out/samples/1.go`,
|
||||
);
|
||||
});
|
||||
|
||||
test(`Curl`, async () => {
|
||||
await expect(Curl.getSampleRequest(url, data)).toMatchFileSnapshot(
|
||||
await expect(Curl.generator(url, data, context)).toMatchFileSnapshot(
|
||||
`./out/samples/1.bash`,
|
||||
);
|
||||
});
|
||||
|
||||
test(`Python`, async () => {
|
||||
await expect(Python.getSampleRequest(url, data)).toMatchFileSnapshot(
|
||||
await expect(Python.generator(url, data, context)).toMatchFileSnapshot(
|
||||
`./out/samples/1.py`,
|
||||
);
|
||||
});
|
||||
|
||||
test(`JavaScript`, async () => {
|
||||
await expect(JS.getSampleRequest(url, data)).toMatchFileSnapshot(
|
||||
await expect(JS.generator(url, data, context)).toMatchFileSnapshot(
|
||||
`./out/samples/1.js`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,11 +9,10 @@ import (
|
||||
|
||||
func main() {
|
||||
url := "http://localhost:8080/hello_world?search=ai"
|
||||
payload := strings.NewReader(`{
|
||||
body := strings.NewReader(`{
|
||||
"id": "id"
|
||||
}`)
|
||||
|
||||
req, _ := http.NewRequest("GET", url, payload)
|
||||
req, _ := http.NewRequest("GET", url, body)
|
||||
req.Header.Add("authorization", "Bearer")
|
||||
req.Header.Add("Cookie", "mode=light")
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const body = JSON.stringify({
|
||||
"id": "id"
|
||||
})
|
||||
|
||||
fetch("http://localhost:8080/hello_world?search=ai", {
|
||||
headers: {
|
||||
"authorization": "Bearer",
|
||||
"cookie": "mode=light"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"id": "id"
|
||||
})
|
||||
body
|
||||
});
|
||||
@@ -1,15 +1,13 @@
|
||||
import requests
|
||||
|
||||
url = "http://localhost:8080/hello_world?search=ai"
|
||||
json = {
|
||||
|
||||
response = requests.request("GET", url, json = {
|
||||
"id": "id"
|
||||
}
|
||||
headers = {
|
||||
}, headers = {
|
||||
"authorization": "Bearer"
|
||||
}
|
||||
cookies = {
|
||||
}, cookies = {
|
||||
"mode": "light"
|
||||
}
|
||||
response = requests.request("GET", url, json=json, headers=headers, cookies=cookies)
|
||||
})
|
||||
|
||||
print(response.text)
|
||||
Reference in New Issue
Block a user