OpenAPI: Support media adapter API

This commit is contained in:
Fuma Nama
2025-05-04 19:47:05 +08:00
parent 6ea1718167
commit 3e69302ae0
23 changed files with 344 additions and 264 deletions
+5
View File
@@ -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')})`;
}
}
+50
View File
@@ -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,
};
+6 -2
View File
@@ -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
+16 -63
View File
@@ -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;
}
+1 -4
View File
@@ -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'],
+8 -2
View File
@@ -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}`,
);
+4 -13
View File
@@ -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>
+9 -13
View File
@@ -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);
}
+23 -15
View File
@@ -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');
}
};
+14 -38
View File
@@ -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))
}`;
}
};
+4 -4
View File
@@ -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',
},
];
+16 -32
View File
@@ -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');
}
};
+23 -38
View File
@@ -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)`;
}
};
+3
View File
@@ -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>;
}
+4 -4
View File
@@ -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;
+5 -6
View File
@@ -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>
+9 -4
View File
@@ -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`,
);
});
+2 -3
View File
@@ -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")
+5 -3
View File
@@ -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
});
+5 -7
View File
@@ -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)