mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
OpenAPI: Support Parameter Serialization
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
'fumadocs-openapi': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
**Support Parameter Serialization**
|
||||||
|
|
||||||
|
Maybe need to update your code if you've added custom media adapters.
|
||||||
@@ -5,16 +5,14 @@ description: Support other media types
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
A media adapter in Fumadocs handle:
|
A media adapter in Fumadocs supports:
|
||||||
|
|
||||||
- Encode: convert form data into `fetch()` body with corresponding media type.
|
- Converting value into `fetch()` body compatible with corresponding media type.
|
||||||
- Example: generate code example based on different programming language/tool.
|
- Generate code example based on different programming language/tool.
|
||||||
|
|
||||||
Put your media adapters in a separate file with `use client` directive.
|
Put your media adapters in a separate file.
|
||||||
|
|
||||||
```ts title="lib/media-adapters.ts" twoslash
|
|
||||||
'use client';
|
|
||||||
|
|
||||||
|
```ts tab="lib/media-adapters.ts" twoslash
|
||||||
import type { MediaAdapter } from 'fumadocs-openapi';
|
import type { MediaAdapter } from 'fumadocs-openapi';
|
||||||
|
|
||||||
export const myAdapter: MediaAdapter = {
|
export const myAdapter: MediaAdapter = {
|
||||||
@@ -40,15 +38,28 @@ export const myAdapter: MediaAdapter = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```ts tab="lib/media-adapters.client.ts"
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
// forward them so that Fumadocs can also use your media adapter in a client component
|
||||||
|
export { myAdapter } from './media-adapters';
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass the adapter.
|
||||||
|
|
||||||
```ts title="lib/source.ts"
|
```ts title="lib/source.ts"
|
||||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||||
import { myAdapter } from './media-adapters';
|
import * as Adapters from './media-adapters';
|
||||||
|
import * as ClientAdapters from './media-adapters.client';
|
||||||
|
|
||||||
export const openapi = createOpenAPI({
|
export const openapi = createOpenAPI({
|
||||||
proxyUrl: '/api/proxy',
|
proxyUrl: '/api/proxy',
|
||||||
mediaAdapters: {
|
mediaAdapters: {
|
||||||
// [!code ++] override the default adapter of `application/json`
|
// [!code ++:4] override the default adapter of `application/json`
|
||||||
'application/json': myAdapter,
|
'application/json': {
|
||||||
|
...Adapters.myAdapter,
|
||||||
|
client: ClientAdapters.myAdapter,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
|
||||||
import { escapeString, inputToString } from '@/utils/input-to-string';
|
import { escapeString, inputToString } from '@/utils/input-to-string';
|
||||||
import type { RequestData } from '@/requests/_shared';
|
// @ts-expect-error -- untyped
|
||||||
|
import { js2xml } from 'xml-js/lib/js2xml';
|
||||||
|
|
||||||
interface BaseContext {
|
interface BaseContext {
|
||||||
/**
|
/**
|
||||||
@@ -38,14 +38,21 @@ export type MediaContext =
|
|||||||
|
|
||||||
export interface MediaAdapter {
|
export interface MediaAdapter {
|
||||||
/**
|
/**
|
||||||
* encode request data into body for `fetch()`.
|
* the same adapter that's passed from a client component.
|
||||||
|
*
|
||||||
|
* It is needed for client-side serialization of values.
|
||||||
|
*/
|
||||||
|
client?: MediaAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* encode data into specified media type for `fetch()`.
|
||||||
*
|
*
|
||||||
* Return the encoded form of `data.body` property.
|
* Return the encoded form of `data.body` property.
|
||||||
*/
|
*/
|
||||||
encode: (data: RequestData) => BodyInit | Promise<BodyInit>;
|
encode: (data: { body: unknown }) => BodyInit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* generate code for usage examples in a given programming language.
|
* generate code example for creating the body in a given programming language.
|
||||||
*
|
*
|
||||||
* @param data - request data.
|
* @param data - request data.
|
||||||
* @param lang - name of programming language.
|
* @param lang - name of programming language.
|
||||||
@@ -53,7 +60,10 @@ export interface MediaAdapter {
|
|||||||
*
|
*
|
||||||
* @returns code that inits a `body` variable, or undefined if not supported (skip example for that 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;
|
generateExample: (
|
||||||
|
data: { body: unknown },
|
||||||
|
ctx: MediaContext,
|
||||||
|
) => string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultAdapters = {
|
export const defaultAdapters = {
|
||||||
@@ -66,10 +76,7 @@ export const defaultAdapters = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'application/xml': {
|
'application/xml': {
|
||||||
async encode(data) {
|
encode(data) {
|
||||||
// @ts-expect-error -- untyped
|
|
||||||
const { js2xml } = await import('xml-js/lib/js2xml');
|
|
||||||
|
|
||||||
return js2xml(data.body as Record<string, unknown>, {
|
return js2xml(data.body as Record<string, unknown>, {
|
||||||
compact: true,
|
compact: true,
|
||||||
spaces: 2,
|
spaces: 2,
|
||||||
@@ -84,6 +91,7 @@ export const defaultAdapters = {
|
|||||||
if (Array.isArray(data.body)) {
|
if (Array.isArray(data.body)) {
|
||||||
return data.body.map((v) => JSON.stringify(v)).join('\n');
|
return data.body.map((v) => JSON.stringify(v)).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.stringify(data.body);
|
return JSON.stringify(data.body);
|
||||||
},
|
},
|
||||||
generateExample(data, ctx) {
|
generateExample(data, ctx) {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import {
|
|||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from 'fumadocs-ui/components/ui/collapsible';
|
} from 'fumadocs-ui/components/ui/collapsible';
|
||||||
import { ChevronDown, LoaderCircle } from 'lucide-react';
|
import { ChevronDown, LoaderCircle } from 'lucide-react';
|
||||||
import type { RequestData } from '@/requests/_shared';
|
import { encodeRequestData, type RequestData } from '@/requests/_shared';
|
||||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||||
import { cn } from 'fumadocs-ui/utils/cn';
|
import { cn } from 'fumadocs-ui/utils/cn';
|
||||||
import {
|
import {
|
||||||
@@ -68,13 +68,16 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/ui/components/select';
|
} from '@/ui/components/select';
|
||||||
import { labelVariants } from '@/ui/components/input';
|
import { labelVariants } from '@/ui/components/input';
|
||||||
|
import type { ParsedSchema } from '@/utils/schema';
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
path: Record<string, string>;
|
path: Record<string, unknown>;
|
||||||
query: Record<string, string>;
|
query: Record<string, unknown>;
|
||||||
header: Record<string, string>;
|
header: Record<string, unknown>;
|
||||||
cookie: Record<string, string>;
|
cookie: Record<string, unknown>;
|
||||||
body: unknown;
|
body: unknown;
|
||||||
|
|
||||||
|
_encoded?: RequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomField<TName extends FieldPath<FormValues>, Info> {
|
export interface CustomField<TName extends FieldPath<FormValues>, Info> {
|
||||||
@@ -120,22 +123,6 @@ export interface ClientProps extends HTMLAttributes<HTMLFormElement> {
|
|||||||
|
|
||||||
const AuthPrefix = '__fumadocs_auth';
|
const AuthPrefix = '__fumadocs_auth';
|
||||||
|
|
||||||
function toRequestData(
|
|
||||||
method: string,
|
|
||||||
mediaType: string | undefined,
|
|
||||||
value: FormValues,
|
|
||||||
): RequestData {
|
|
||||||
return {
|
|
||||||
path: value.path,
|
|
||||||
method,
|
|
||||||
header: value.header,
|
|
||||||
body: value.body,
|
|
||||||
bodyMediaType: mediaType,
|
|
||||||
cookie: value.cookie,
|
|
||||||
query: value.query,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const ServerSelect = lazy(() => import('@/ui/server-select'));
|
const ServerSelect = lazy(() => import('@/ui/server-select'));
|
||||||
const OauthDialog = lazy(() =>
|
const OauthDialog = lazy(() =>
|
||||||
import('./auth/oauth-dialog').then((mod) => ({
|
import('./auth/oauth-dialog').then((mod) => ({
|
||||||
@@ -152,7 +139,7 @@ export default function Client({
|
|||||||
route,
|
route,
|
||||||
method = 'GET',
|
method = 'GET',
|
||||||
securities,
|
securities,
|
||||||
parameters,
|
parameters = [],
|
||||||
body,
|
body,
|
||||||
fields,
|
fields,
|
||||||
references,
|
references,
|
||||||
@@ -187,7 +174,12 @@ export default function Client({
|
|||||||
const fetcher = await import('./fetcher').then((mod) =>
|
const fetcher = await import('./fetcher').then((mod) =>
|
||||||
mod.createBrowserFetcher(mediaAdapters),
|
mod.createBrowserFetcher(mediaAdapters),
|
||||||
);
|
);
|
||||||
const data = toRequestData(method, body?.mediaType, input);
|
|
||||||
|
input._encoded ??= encodeRequestData(
|
||||||
|
{ ...mapInputs(input), method, bodyMediaType: body?.mediaType },
|
||||||
|
mediaAdapters,
|
||||||
|
parameters,
|
||||||
|
);
|
||||||
|
|
||||||
return fetcher.fetch(
|
return fetcher.fetch(
|
||||||
joinURL(
|
joinURL(
|
||||||
@@ -195,11 +187,11 @@ export default function Client({
|
|||||||
server ? resolveServerUrl(server.url, server.variables) : '/',
|
server ? resolveServerUrl(server.url, server.variables) : '/',
|
||||||
window.location.origin,
|
window.location.origin,
|
||||||
),
|
),
|
||||||
resolveRequestData(route, data),
|
resolveRequestData(route, input._encoded),
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
proxyUrl,
|
proxyUrl,
|
||||||
...data,
|
...input._encoded,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -254,7 +246,13 @@ export default function Client({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updater.setData(toRequestData(method, body?.mediaType, mapInputs(values)));
|
const data = {
|
||||||
|
...mapInputs(values),
|
||||||
|
method,
|
||||||
|
bodyMediaType: body?.mediaType,
|
||||||
|
};
|
||||||
|
values._encoded ??= encodeRequestData(data, mediaAdapters, parameters);
|
||||||
|
updater.setData(data, values._encoded);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -265,6 +263,9 @@ export default function Client({
|
|||||||
values: true,
|
values: true,
|
||||||
},
|
},
|
||||||
callback({ values }) {
|
callback({ values }) {
|
||||||
|
// remove cached encoded request data
|
||||||
|
delete values._encoded;
|
||||||
|
|
||||||
if (timer) window.clearTimeout(timer);
|
if (timer) window.clearTimeout(timer);
|
||||||
timer = window.setTimeout(
|
timer = window.setTimeout(
|
||||||
() => onUpdateDebounced(values),
|
() => onUpdateDebounced(values),
|
||||||
@@ -425,11 +426,16 @@ function FormBody({
|
|||||||
<CollapsiblePanel key={name} title={name}>
|
<CollapsiblePanel key={name} title={name}>
|
||||||
{param.map((field) => {
|
{param.map((field) => {
|
||||||
const fieldName = `${type}.${field.name}` as const;
|
const fieldName = `${type}.${field.name}` as const;
|
||||||
|
const schema = (
|
||||||
|
field.content
|
||||||
|
? field.content[Object.keys(field.content)[0]].schema
|
||||||
|
: field.schema
|
||||||
|
) as ParsedSchema;
|
||||||
|
|
||||||
if (fields?.parameter) {
|
if (fields?.parameter) {
|
||||||
return renderCustomField(
|
return renderCustomField(
|
||||||
fieldName,
|
fieldName,
|
||||||
field.schema,
|
schema,
|
||||||
fields.parameter,
|
fields.parameter,
|
||||||
field.name,
|
field.name,
|
||||||
);
|
);
|
||||||
@@ -440,7 +446,7 @@ function FormBody({
|
|||||||
key={fieldName}
|
key={fieldName}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
field={field.schema}
|
field={schema}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ export function createBrowserFetcher(
|
|||||||
headers.append('Content-Type', options.bodyMediaType);
|
headers.append('Content-Type', options.bodyMediaType);
|
||||||
|
|
||||||
for (const key in options.header) {
|
for (const key in options.header) {
|
||||||
const paramValue = options.header[key];
|
const param = options.header[key];
|
||||||
|
|
||||||
if (paramValue.length > 0) headers.append(key, paramValue.toString());
|
if (!Array.isArray(param.value)) {
|
||||||
|
headers.append(key, param.value);
|
||||||
|
} else {
|
||||||
|
headers.append(key, param.value.join(','));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyUrl = options.proxyUrl
|
const proxyUrl = options.proxyUrl
|
||||||
@@ -54,34 +58,19 @@ export function createBrowserFetcher(
|
|||||||
data: `[Fumadocs] No adapter for ${options.bodyMediaType}, you need to specify one from 'createOpenAPI()'.`,
|
data: `[Fumadocs] No adapter for ${options.bodyMediaType}, you need to specify one from 'createOpenAPI()'.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
body = await adapter.encode(options);
|
body = adapter.encode(options as { body: unknown });
|
||||||
}
|
}
|
||||||
|
|
||||||
// cookies
|
// cookies
|
||||||
for (const key in options.cookie) {
|
for (const key in options.cookie) {
|
||||||
const value = options.cookie[key];
|
const param = options.cookie[key];
|
||||||
if (!value) continue;
|
const segs: string[] = [`${key}=${param.value}`];
|
||||||
|
|
||||||
const cookie = {
|
if (proxyUrl && proxyUrl.origin !== window.location.origin)
|
||||||
[key]: value,
|
segs.push(`domain=${proxyUrl.host}`);
|
||||||
domain:
|
segs.push('path=/', 'max-age=30');
|
||||||
proxyUrl && proxyUrl.origin !== window.location.origin
|
|
||||||
? `domain=${proxyUrl.host}`
|
|
||||||
: undefined,
|
|
||||||
path: '/',
|
|
||||||
'max-age': 30,
|
|
||||||
};
|
|
||||||
|
|
||||||
let str = '';
|
document.cookie = segs.join('; ');
|
||||||
for (const [key, value] of Object.entries(cookie)) {
|
|
||||||
if (value) {
|
|
||||||
if (str.length > 0) str += '; ';
|
|
||||||
|
|
||||||
str += `${key}=${value}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.cookie = str;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import type {
|
import type {
|
||||||
MethodInformation,
|
MethodInformation,
|
||||||
|
ParameterObject,
|
||||||
RenderContext,
|
RenderContext,
|
||||||
SecuritySchemeObject,
|
SecuritySchemeObject,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { getPreferredType, type ParsedSchema } from '@/utils/schema';
|
import {
|
||||||
|
getPreferredType,
|
||||||
|
type NoReference,
|
||||||
|
type ParsedSchema,
|
||||||
|
} from '@/utils/schema';
|
||||||
import { type ClientProps } from './client';
|
import { type ClientProps } from './client';
|
||||||
import { ClientLazy } from '@/ui/lazy';
|
import { ClientLazy } from '@/ui/lazy';
|
||||||
|
|
||||||
export type ParameterField = {
|
export type ParameterField = NoReference<ParameterObject> & {
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
schema: ParsedSchema;
|
schema: ParsedSchema;
|
||||||
in: 'cookie' | 'header' | 'query' | 'path';
|
in: 'cookie' | 'header' | 'query' | 'path';
|
||||||
};
|
};
|
||||||
@@ -59,12 +62,13 @@ export async function APIPlayground({
|
|||||||
securities: parseSecurities(method, ctx),
|
securities: parseSecurities(method, ctx),
|
||||||
method: method.method,
|
method: method.method,
|
||||||
route: path,
|
route: path,
|
||||||
parameters: method.parameters?.map((v) => ({
|
parameters: method.parameters?.map(
|
||||||
name: v.name,
|
(v) =>
|
||||||
in: v.in as ParameterField['in'],
|
({
|
||||||
schema: writeReferences((v.schema ?? true) as ParsedSchema, context),
|
...v,
|
||||||
description: v.description,
|
schema: writeReferences((v.schema ?? true) as ParsedSchema, context),
|
||||||
})),
|
}) as ParameterField,
|
||||||
|
),
|
||||||
body:
|
body:
|
||||||
bodyContent && mediaType
|
bodyContent && mediaType
|
||||||
? ({
|
? ({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
processDocument,
|
processDocument,
|
||||||
type ProcessedDocument,
|
type ProcessedDocument,
|
||||||
} from '@/utils/process-document';
|
} from '@/utils/process-document';
|
||||||
import type { defaultAdapters } from '@/media/adapter';
|
import { defaultAdapters } from '@/media/adapter';
|
||||||
|
|
||||||
type ApiPageContextProps = Pick<
|
type ApiPageContextProps = Pick<
|
||||||
Partial<RenderContext>,
|
Partial<RenderContext>,
|
||||||
@@ -145,14 +145,7 @@ export async function getContext(
|
|||||||
generateCodeSamples: options.generateCodeSamples,
|
generateCodeSamples: options.generateCodeSamples,
|
||||||
servers,
|
servers,
|
||||||
mediaAdapters: {
|
mediaAdapters: {
|
||||||
...({
|
...defaultAdapters,
|
||||||
'application/octet-stream': true,
|
|
||||||
'application/json': true,
|
|
||||||
'multipart/form-data': true,
|
|
||||||
'application/xml': true,
|
|
||||||
'application/x-ndjson': true,
|
|
||||||
'application/x-www-form-urlencoded': true,
|
|
||||||
} satisfies Record<keyof typeof defaultAdapters, true>),
|
|
||||||
...options.mediaAdapters,
|
...options.mediaAdapters,
|
||||||
},
|
},
|
||||||
slugger: new Slugger(),
|
slugger: new Slugger(),
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import { CodeExample, CodeExampleProvider } from '@/ui/lazy';
|
|||||||
import { getPreferredType, type NoReference } from '@/utils/schema';
|
import { getPreferredType, type NoReference } from '@/utils/schema';
|
||||||
import { getRequestData } from '@/render/operation/get-request-data';
|
import { getRequestData } from '@/render/operation/get-request-data';
|
||||||
import { sample } from 'openapi-sampler';
|
import { sample } from 'openapi-sampler';
|
||||||
import type { RequestData } from '@/requests/_shared';
|
import {
|
||||||
|
encodeRequestData,
|
||||||
|
type RawRequestData,
|
||||||
|
type RequestData,
|
||||||
|
} from '@/requests/_shared';
|
||||||
|
|
||||||
const defaultSamples: CodeSample[] = [
|
const defaultSamples: CodeSample[] = [
|
||||||
{
|
{
|
||||||
@@ -47,7 +51,8 @@ interface CodeExampleItem {
|
|||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
data: RequestData;
|
data: RawRequestData;
|
||||||
|
encoded: RequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function APIExampleProvider({
|
export function APIExampleProvider({
|
||||||
@@ -67,10 +72,7 @@ export function APIExampleProvider({
|
|||||||
<CodeExampleProvider
|
<CodeExampleProvider
|
||||||
initialKey={exclusiveSampleKey}
|
initialKey={exclusiveSampleKey}
|
||||||
route={route}
|
route={route}
|
||||||
examples={examples.map((example) => ({
|
examples={examples}
|
||||||
key: example.key,
|
|
||||||
data: example.data,
|
|
||||||
}))}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</CodeExampleProvider>
|
</CodeExampleProvider>
|
||||||
@@ -91,24 +93,36 @@ export function getAPIExamples(
|
|||||||
const result: CodeExampleItem[] = [];
|
const result: CodeExampleItem[] = [];
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(bodyOfType.examples)) {
|
for (const [key, value] of Object.entries(bodyOfType.examples)) {
|
||||||
|
const data = getRequestData(path, method, key, ctx);
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
key,
|
key,
|
||||||
name: value.summary ?? key,
|
name: value.summary ?? key,
|
||||||
description: value.description,
|
description: value.description,
|
||||||
|
data,
|
||||||
data: getRequestData(path, method, key, ctx),
|
encoded: encodeRequestData(
|
||||||
|
data,
|
||||||
|
ctx.mediaAdapters,
|
||||||
|
method.parameters ?? [],
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.length > 0) return result;
|
if (result.length > 0) return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = getRequestData(path, method, null, ctx);
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: '_default',
|
key: '_default',
|
||||||
name: 'Default',
|
name: 'Default',
|
||||||
description: bodyOfType?.schema?.description,
|
description: bodyOfType?.schema?.description,
|
||||||
data: getRequestData(path, method, null, ctx),
|
data,
|
||||||
|
encoded: encodeRequestData(
|
||||||
|
data,
|
||||||
|
ctx.mediaAdapters,
|
||||||
|
method.parameters ?? [],
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import type { MethodInformation, RenderContext } from '@/types';
|
|||||||
import type { ResolvedSchema } from '@/utils/schema';
|
import type { ResolvedSchema } from '@/utils/schema';
|
||||||
import { getPreferredType } from '@/utils/schema';
|
import { getPreferredType } from '@/utils/schema';
|
||||||
import { sample } from 'openapi-sampler';
|
import { sample } from 'openapi-sampler';
|
||||||
import type { RequestData } from '@/requests/_shared';
|
import { type RawRequestData } from '@/requests/_shared';
|
||||||
|
|
||||||
export function getRequestData(
|
export function getRequestData(
|
||||||
path: string,
|
path: string,
|
||||||
method: MethodInformation,
|
method: MethodInformation,
|
||||||
sampleKey: string | null,
|
sampleKey: string | null,
|
||||||
_ctx: RenderContext,
|
_ctx: RenderContext,
|
||||||
): RequestData {
|
): RawRequestData {
|
||||||
const result: RequestData = {
|
const result: RawRequestData = {
|
||||||
path: {},
|
path: {},
|
||||||
cookie: {},
|
cookie: {},
|
||||||
header: {},
|
header: {},
|
||||||
@@ -55,7 +55,7 @@ export function getRequestData(
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
`Cannot find body schema for ${path} ${method.method}: missing media type`,
|
`Cannot find body schema for ${path} ${method.method}: missing media type`,
|
||||||
);
|
);
|
||||||
result.bodyMediaType = type as RequestData['bodyMediaType'];
|
result.bodyMediaType = type as RawRequestData['bodyMediaType'];
|
||||||
const bodyOfType = body[type];
|
const bodyOfType = body[type];
|
||||||
|
|
||||||
if (bodyOfType.examples && sampleKey) {
|
if (bodyOfType.examples && sampleKey) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { MediaAdapter } from '@/media/adapter';
|
import type { MediaAdapter } from '@/media/adapter';
|
||||||
|
import type { NoReference } from '@/utils/schema';
|
||||||
|
import type { ParameterObject } from '@/types';
|
||||||
|
|
||||||
export type SampleGenerator = (
|
export type SampleGenerator = (
|
||||||
url: string,
|
url: string,
|
||||||
@@ -8,15 +10,31 @@ export type SampleGenerator = (
|
|||||||
},
|
},
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
|
export interface RawRequestData {
|
||||||
|
method: string;
|
||||||
|
|
||||||
|
path: Record<string, unknown>;
|
||||||
|
query: Record<string, unknown>;
|
||||||
|
header: Record<string, unknown>;
|
||||||
|
cookie: Record<string, unknown>;
|
||||||
|
|
||||||
|
body?: unknown;
|
||||||
|
bodyMediaType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedParameter {
|
||||||
|
readonly value: string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface RequestData {
|
export interface RequestData {
|
||||||
method: string;
|
method: string;
|
||||||
|
|
||||||
path: Record<string, string>;
|
path: Record<string, EncodedParameter>;
|
||||||
query: Record<string, string>;
|
query: Record<string, EncodedParameter>;
|
||||||
header: Record<string, string>;
|
header: Record<string, EncodedParameter>;
|
||||||
cookie: Record<string, string>;
|
cookie: Record<string, EncodedParameter>;
|
||||||
body?: unknown;
|
|
||||||
|
|
||||||
|
body?: unknown;
|
||||||
bodyMediaType?: string;
|
bodyMediaType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,3 +44,127 @@ export function ident(code: string, tab: number = 1) {
|
|||||||
.map((v) => ' '.repeat(tab) + v)
|
.map((v) => ' '.repeat(tab) + v)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function encodeRequestData(
|
||||||
|
from: RawRequestData,
|
||||||
|
adapters: Record<string, MediaAdapter>,
|
||||||
|
parameters: NoReference<ParameterObject>[],
|
||||||
|
): RequestData {
|
||||||
|
function getMediaEncoder(field: NoReference<ParameterObject>) {
|
||||||
|
if (!field.content) return;
|
||||||
|
|
||||||
|
for (const k in field.content) {
|
||||||
|
if (k in adapters) {
|
||||||
|
return (v: unknown) => String(adapters[k].encode({ body: v }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeObject(
|
||||||
|
parentKey: string,
|
||||||
|
value: object,
|
||||||
|
swallow: boolean,
|
||||||
|
output: Record<string, EncodedParameter>,
|
||||||
|
) {
|
||||||
|
for (const k in value) {
|
||||||
|
const prop: unknown = value[k as keyof object];
|
||||||
|
if (prop == null) continue;
|
||||||
|
|
||||||
|
const key = swallow ? k : `${parentKey}[${k}]`;
|
||||||
|
if (swallow || typeof prop !== 'object') {
|
||||||
|
output[key] = {
|
||||||
|
value: String(prop),
|
||||||
|
};
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeObject(key, value, swallow, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function write(
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
field: NoReference<ParameterObject>,
|
||||||
|
output: Record<string, EncodedParameter> = {},
|
||||||
|
): Record<string, EncodedParameter> {
|
||||||
|
const encoder = getMediaEncoder(field);
|
||||||
|
if (encoder) {
|
||||||
|
output[key] = { value: encoder(value) };
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
const explode = field.explode ?? true;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// header & cookie doesn't support explode for array values
|
||||||
|
if (explode && field.in !== 'header' && field.in !== 'cookie') {
|
||||||
|
output[key] = {
|
||||||
|
value: value.map(String),
|
||||||
|
};
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sep =
|
||||||
|
{
|
||||||
|
spaceDelimited: ' ',
|
||||||
|
pipeDelimited: '|',
|
||||||
|
}[field.style ?? 'form'] ?? ',';
|
||||||
|
output[key] = {
|
||||||
|
value: value.map(String).join(sep),
|
||||||
|
};
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && value) {
|
||||||
|
// header uses the original key
|
||||||
|
if (explode && field.in === 'header') {
|
||||||
|
output[key] = {
|
||||||
|
value: Object.entries(value)
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join(','),
|
||||||
|
};
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (explode || field.style === 'deepObject') {
|
||||||
|
writeObject(key, value, explode, output);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
output[key] = {
|
||||||
|
value: Object.entries(value).flat().join(','),
|
||||||
|
};
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
output[key] = {
|
||||||
|
value: String(value),
|
||||||
|
};
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Partial<RequestData> = {
|
||||||
|
method: from.method,
|
||||||
|
body: from.body,
|
||||||
|
bodyMediaType: from.bodyMediaType,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const type of ['cookie', 'query', 'header', 'path'] as const) {
|
||||||
|
const out = {};
|
||||||
|
for (const k in from[type]) {
|
||||||
|
const value = from[type][k];
|
||||||
|
if (value == null) continue;
|
||||||
|
|
||||||
|
const field = parameters.find((p) => p.name === k && p.in === type);
|
||||||
|
if (!field) continue;
|
||||||
|
|
||||||
|
write(k, value, field, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
result[type] = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result as RequestData;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import type { SampleGenerator } from '@/requests/_shared';
|
import type { SampleGenerator } from '@/requests/_shared';
|
||||||
import { resolveRequestData } from '@/utils/url';
|
|
||||||
|
|
||||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||||
const s: string[] = [];
|
const s: string[] = [];
|
||||||
@@ -10,12 +9,15 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
|||||||
// Handle request body
|
// Handle request body
|
||||||
let body: string | undefined;
|
let body: string | undefined;
|
||||||
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
||||||
body = mediaAdapters[data.bodyMediaType].generateExample(data, {
|
body = mediaAdapters[data.bodyMediaType].generateExample(
|
||||||
lang: 'csharp',
|
data as { body: unknown },
|
||||||
addImport(from) {
|
{
|
||||||
imports.add(from);
|
lang: 'csharp',
|
||||||
|
addImport(from) {
|
||||||
|
imports.add(from);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const specifier of imports) {
|
for (const specifier of imports) {
|
||||||
@@ -29,31 +31,36 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.push('var client = new HttpClient();');
|
s.push('var client = new HttpClient();');
|
||||||
|
const headerLines: string[] = [];
|
||||||
|
|
||||||
|
function addHeader(key: string, value: unknown) {
|
||||||
|
headerLines.push(
|
||||||
|
`client.DefaultRequestHeaders.Add("${key}", ${JSON.stringify(value)});`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const k in headers) {
|
||||||
|
addHeader(k, headers[k].value);
|
||||||
|
}
|
||||||
|
|
||||||
// Add cookie header if cookies are present
|
// Add cookie header if cookies are present
|
||||||
if (Object.keys(data.cookie).length > 0) {
|
if (Object.keys(data.cookie).length > 0) {
|
||||||
headers['cookie'] = Object.entries(data.cookie)
|
const cookie = Object.entries(data.cookie)
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
.map(([key, param]) => `${key}=${param.value}`)
|
||||||
.join('; ');
|
.join('; ');
|
||||||
}
|
|
||||||
|
|
||||||
// Add headers
|
addHeader('cookie', cookie);
|
||||||
const headerLines = Object.entries(headers).map(
|
}
|
||||||
([key, value]) =>
|
|
||||||
`client.DefaultRequestHeaders.Add("${key}", ${JSON.stringify(value)});`,
|
|
||||||
);
|
|
||||||
|
|
||||||
s.push(...headerLines);
|
s.push(...headerLines);
|
||||||
|
|
||||||
// Build the request
|
// Build the request
|
||||||
const resolvedUrl = resolveRequestData(url, data);
|
|
||||||
const method =
|
const method =
|
||||||
data.method[0].toUpperCase() + data.method.slice(1).toLowerCase() + 'Async';
|
data.method[0].toUpperCase() + data.method.slice(1).toLowerCase() + 'Async';
|
||||||
|
|
||||||
if (body) {
|
if (body) {
|
||||||
s.push(`var response = await client.${method}("${resolvedUrl}", body);`);
|
s.push(`var response = await client.${method}("${url}", body);`);
|
||||||
} else {
|
} else {
|
||||||
s.push(`var response = await client.${method}("${resolvedUrl}");`);
|
s.push(`var response = await client.${method}("${url}");`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add response handling
|
// Add response handling
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { escapeString, inputToString } from '@/utils/input-to-string';
|
import { escapeString, inputToString } from '@/utils/input-to-string';
|
||||||
import { ident, type SampleGenerator } from '@/requests/_shared';
|
import { ident, type SampleGenerator } from '@/requests/_shared';
|
||||||
import { resolveRequestData } from '@/utils/url';
|
|
||||||
|
|
||||||
export const generator: SampleGenerator = (url, data) => {
|
export const generator: SampleGenerator = (url, data) => {
|
||||||
const s: string[] = [];
|
const s: string[] = [];
|
||||||
s.push(`curl -X ${data.method} "${resolveRequestData(url, data)}"`);
|
s.push(`curl -X ${data.method} "${url}"`);
|
||||||
|
|
||||||
for (const header in data.header) {
|
for (const header in data.header) {
|
||||||
const value = `${header}: ${data.header[header]}`;
|
const value = `${header}: ${data.header[header].value}`;
|
||||||
|
|
||||||
s.push(`-H "${value}"`);
|
s.push(`-H "${value}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const cookie in data.cookie) {
|
for (const k in data.cookie) {
|
||||||
const value = JSON.stringify(`${cookie}=${data.cookie[cookie]}`);
|
const param = data.cookie[k];
|
||||||
|
|
||||||
s.push(`--cookie ${value}`);
|
s.push(`--cookie ${JSON.stringify(`${k}=${param.value}`)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.body && data.bodyMediaType === 'multipart/form-data') {
|
if (data.body && data.bodyMediaType === 'multipart/form-data') {
|
||||||
|
|||||||
@@ -1,34 +1,39 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { ident, SampleGenerator } from '@/requests/_shared';
|
import { ident, type SampleGenerator } from '@/requests/_shared';
|
||||||
import { resolveRequestData } from '@/utils/url';
|
|
||||||
|
|
||||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||||
const imports = ['fmt', 'net/http', 'io/ioutil'];
|
const imports = ['fmt', 'net/http', 'io/ioutil'];
|
||||||
const headers = new Map<string, string>();
|
const headers = new Map<string, string>();
|
||||||
const variables = new Map<string, string>();
|
const variables = new Map<string, string>();
|
||||||
variables.set('url', JSON.stringify(resolveRequestData(url, data)));
|
variables.set('url', JSON.stringify(url));
|
||||||
|
|
||||||
for (const header in data.header) {
|
for (const header in data.header) {
|
||||||
headers.set(header, JSON.stringify(data.header[header]));
|
headers.set(header, JSON.stringify(data.header[header].value));
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookies = Object.keys(data.cookie);
|
const cookies = Object.entries(data.cookie);
|
||||||
if (cookies.length > 0)
|
if (cookies.length > 0) {
|
||||||
headers.set(
|
headers.set(
|
||||||
'Cookie',
|
'Cookie',
|
||||||
JSON.stringify(cookies.map((p) => `${p}=${data.cookie[p]}`).join('; ')),
|
JSON.stringify(
|
||||||
|
cookies.map(([k, param]) => `${k}=${param.value}`).join('; '),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let body: string | undefined;
|
let body: string | undefined;
|
||||||
|
|
||||||
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
||||||
headers.set('Content-Type', `"${data.bodyMediaType}"`);
|
headers.set('Content-Type', `"${data.bodyMediaType}"`);
|
||||||
body = mediaAdapters[data.bodyMediaType].generateExample(data, {
|
body = mediaAdapters[data.bodyMediaType].generateExample(
|
||||||
lang: 'go',
|
data as { body: unknown },
|
||||||
addImport(from) {
|
{
|
||||||
imports.push(from);
|
lang: 'go',
|
||||||
|
addImport(from) {
|
||||||
|
imports.push(from);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `package main
|
return `package main
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { ident, type SampleGenerator } from '@/requests/_shared';
|
import { ident, type SampleGenerator } from '@/requests/_shared';
|
||||||
import { resolveRequestData } from '@/utils/url';
|
|
||||||
|
|
||||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||||
const s: string[] = [];
|
const s: string[] = [];
|
||||||
@@ -19,7 +18,7 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
|||||||
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
||||||
const adapter = mediaAdapters[data.bodyMediaType];
|
const adapter = mediaAdapters[data.bodyMediaType];
|
||||||
|
|
||||||
body = adapter.generateExample(data, {
|
body = adapter.generateExample(data as { body: unknown }, {
|
||||||
lang: 'java',
|
lang: 'java',
|
||||||
addImport(specifier) {
|
addImport(specifier) {
|
||||||
imports.add(specifier);
|
imports.add(specifier);
|
||||||
@@ -44,13 +43,13 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
|||||||
|
|
||||||
// Build request
|
// Build request
|
||||||
s.push('HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()');
|
s.push('HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()');
|
||||||
s.push(
|
s.push(ident(`.uri(URI.create(${JSON.stringify(url)}))`));
|
||||||
ident(`.uri(URI.create(${JSON.stringify(resolveRequestData(url, data))}))`),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add headers
|
// Add headers
|
||||||
for (const [key, value] of Object.entries(headers)) {
|
for (const [key, param] of Object.entries(headers)) {
|
||||||
s.push(ident(`.header(${JSON.stringify(key)}, ${JSON.stringify(value)})`));
|
s.push(
|
||||||
|
ident(`.header(${JSON.stringify(key)}, ${JSON.stringify(param.value)})`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.bodyMediaType) {
|
if (data.bodyMediaType) {
|
||||||
@@ -58,11 +57,13 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add cookies if present
|
// Add cookies if present
|
||||||
const cookieString = Object.entries(data.cookie)
|
const cookies = Object.entries(data.cookie);
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
|
||||||
.join('; ');
|
if (cookies.length > 0) {
|
||||||
|
const cookieString = cookies
|
||||||
|
.map(([key, param]) => `${key}=${param.value}`)
|
||||||
|
.join('; ');
|
||||||
|
|
||||||
if (cookieString.length > 0) {
|
|
||||||
s.push(ident(`.header("Cookie", ${JSON.stringify(cookieString)})`));
|
s.push(ident(`.header("Cookie", ${JSON.stringify(cookieString)})`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { ident, type SampleGenerator } from '@/requests/_shared';
|
import { ident, type SampleGenerator } from '@/requests/_shared';
|
||||||
import { resolveRequestData } from '@/utils/url';
|
|
||||||
|
|
||||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||||
const s: string[] = [];
|
const s: string[] = [];
|
||||||
const options = new Map<string, string>();
|
const options = new Map<string, string>();
|
||||||
const headers = { ...data.header };
|
const headers: Record<string, string> = {};
|
||||||
|
|
||||||
if (Object.keys(data.cookie).length > 0) {
|
for (const [k, v] of Object.entries(data.header)) {
|
||||||
headers['cookie'] = Object.entries(data.cookie)
|
headers[k] = v.value as string;
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
}
|
||||||
|
|
||||||
|
const cookies = Object.entries(data.cookie);
|
||||||
|
if (cookies.length > 0) {
|
||||||
|
headers['cookie'] = cookies
|
||||||
|
.map(([key, param]) => `${key}=${param.value}`)
|
||||||
.join('; ');
|
.join('; ');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,12 +23,15 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
|||||||
|
|
||||||
let body: string | undefined;
|
let body: string | undefined;
|
||||||
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
||||||
body = mediaAdapters[data.bodyMediaType].generateExample(data, {
|
body = mediaAdapters[data.bodyMediaType].generateExample(
|
||||||
lang: 'js',
|
data as { body: unknown },
|
||||||
addImport(from, name) {
|
{
|
||||||
s.unshift(`import { ${name} } from "${from}"`);
|
lang: 'js',
|
||||||
|
addImport(from, name) {
|
||||||
|
s.unshift(`import { ${name} } from "${from}"`);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body) {
|
if (body) {
|
||||||
@@ -32,7 +39,7 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
|||||||
options.set('body', 'body');
|
options.set('body', 'body');
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = [JSON.stringify(resolveRequestData(url, data))];
|
const params = [JSON.stringify(url)];
|
||||||
if (options.size > 0) {
|
if (options.size > 0) {
|
||||||
const str = Array.from(options.entries())
|
const str = Array.from(options.entries())
|
||||||
.map(([k, v]) => ident(k === v ? k : `${k}: ${v}`))
|
.map(([k, v]) => ident(k === v ? k : `${k}: ${v}`))
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { type SampleGenerator } from '@/requests/_shared';
|
import { type SampleGenerator } from '@/requests/_shared';
|
||||||
import { resolveRequestData } from '@/utils/url';
|
|
||||||
|
|
||||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||||
const headers = { ...data.header };
|
const headers: Record<string, string> = {};
|
||||||
const params = [`"${data.method}"`, 'url'];
|
const params = [`"${data.method}"`, 'url'];
|
||||||
let body: string | undefined;
|
let body: string | undefined;
|
||||||
|
|
||||||
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
if (data.body && data.bodyMediaType && data.bodyMediaType in mediaAdapters) {
|
||||||
headers['Content-Type'] = data.bodyMediaType;
|
headers['Content-Type'] = data.bodyMediaType;
|
||||||
|
|
||||||
body = mediaAdapters[data.bodyMediaType].generateExample(data, {
|
body = mediaAdapters[data.bodyMediaType].generateExample(
|
||||||
lang: 'python',
|
data as { body: unknown },
|
||||||
});
|
{
|
||||||
|
lang: 'python',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (body && data.bodyMediaType === 'application/json') {
|
if (body && data.bodyMediaType === 'application/json') {
|
||||||
params.push('json = body');
|
params.push('json = body');
|
||||||
@@ -21,17 +23,28 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(data.header)) {
|
||||||
|
headers[k] = v.value as string;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(headers).length > 0) {
|
if (Object.keys(headers).length > 0) {
|
||||||
params.push(`headers = ${JSON.stringify(headers, null, 2)}`);
|
params.push(`headers = ${JSON.stringify(headers, null, 2)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(data.cookie).length > 0) {
|
const inputCookies = Object.entries(data.cookie);
|
||||||
params.push(`cookies = ${JSON.stringify(data.cookie, null, 2)}`);
|
if (inputCookies.length > 0) {
|
||||||
|
const cookies: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const [k, v] of inputCookies) {
|
||||||
|
cookies[k] = v.value as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
params.push(`cookies = ${JSON.stringify(cookies, null, 2)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `import requests
|
return `import requests
|
||||||
|
|
||||||
url = ${JSON.stringify(resolveRequestData(url, data))}
|
url = ${JSON.stringify(url)}
|
||||||
${body ?? ''}
|
${body ?? ''}
|
||||||
response = requests.request(${params.join(', ')})
|
response = requests.request(${params.join(', ')})
|
||||||
|
|
||||||
|
|||||||
@@ -85,5 +85,5 @@ export interface RenderContext {
|
|||||||
*/
|
*/
|
||||||
showResponseSchema?: boolean;
|
showResponseSchema?: boolean;
|
||||||
|
|
||||||
mediaAdapters: Record<string, MediaAdapter | true>;
|
mediaAdapters: Record<string, MediaAdapter>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import {
|
|||||||
import type { RenderContext, ServerObject } from '@/types';
|
import type { RenderContext, ServerObject } from '@/types';
|
||||||
import { defaultAdapters, type MediaAdapter } from '@/media/adapter';
|
import { defaultAdapters, type MediaAdapter } from '@/media/adapter';
|
||||||
|
|
||||||
export interface ApiProviderProps extends ApiContextType {
|
export interface ApiProviderProps
|
||||||
|
extends Omit<ApiContextType, 'mediaAdapters'> {
|
||||||
/**
|
/**
|
||||||
* Base URL for API requests
|
* Base URL for API requests
|
||||||
*/
|
*/
|
||||||
defaultBaseUrl?: string;
|
defaultBaseUrl?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
mediaAdapters?: Record<string, MediaAdapter>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectedServer {
|
export interface SelectedServer {
|
||||||
|
|||||||
@@ -21,23 +21,29 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/ui/components/select';
|
} from '@/ui/components/select';
|
||||||
import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
|
import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
|
||||||
import { joinURL, resolveServerUrl, withBase } from '@/utils/url';
|
import {
|
||||||
import type { RequestData } from '@/requests/_shared';
|
joinURL,
|
||||||
|
resolveRequestData,
|
||||||
|
resolveServerUrl,
|
||||||
|
withBase,
|
||||||
|
} from '@/utils/url';
|
||||||
|
import type { RawRequestData, RequestData } from '@/requests/_shared';
|
||||||
import { defaultSamples } from '@/requests';
|
import { defaultSamples } from '@/requests';
|
||||||
|
|
||||||
type UpdateListener = (data: RequestData) => void;
|
type UpdateListener = (data: RawRequestData, encoded: RequestData) => void;
|
||||||
|
|
||||||
const CodeExampleContext = createContext<{
|
const CodeExampleContext = createContext<{
|
||||||
route: string;
|
route: string;
|
||||||
examples: {
|
examples: {
|
||||||
key: string;
|
key: string;
|
||||||
data: RequestData;
|
data: RawRequestData;
|
||||||
|
encoded: RequestData;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
key: string;
|
key: string;
|
||||||
setKey: (key: string) => void;
|
setKey: (key: string) => void;
|
||||||
|
|
||||||
setData: (data: RequestData) => void;
|
setData: (data: RawRequestData, encoded: RequestData) => void;
|
||||||
|
|
||||||
addListener: (listener: UpdateListener) => void;
|
addListener: (listener: UpdateListener) => void;
|
||||||
removeListener: (listener: UpdateListener) => void;
|
removeListener: (listener: UpdateListener) => void;
|
||||||
@@ -52,7 +58,8 @@ export function CodeExampleProvider({
|
|||||||
route: string;
|
route: string;
|
||||||
examples: {
|
examples: {
|
||||||
key: string;
|
key: string;
|
||||||
data: RequestData;
|
data: RawRequestData;
|
||||||
|
encoded: RequestData;
|
||||||
}[];
|
}[];
|
||||||
initialKey?: string;
|
initialKey?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -60,33 +67,38 @@ export function CodeExampleProvider({
|
|||||||
const [key, setKey] = useState(initialKey ?? examples[0].key);
|
const [key, setKey] = useState(initialKey ?? examples[0].key);
|
||||||
const listeners = useRef<UpdateListener[]>([]);
|
const listeners = useRef<UpdateListener[]>([]);
|
||||||
|
|
||||||
const setData = useEffectEvent((newData: RequestData) => {
|
const setData = useEffectEvent(
|
||||||
for (const example of examples) {
|
(data: RawRequestData, encoded: RequestData) => {
|
||||||
if (example.key === key) {
|
for (const example of examples) {
|
||||||
// persistent changes
|
if (example.key === key) {
|
||||||
example.data = newData;
|
// persistent changes
|
||||||
|
example.data = data;
|
||||||
|
example.encoded = encoded;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (const listener of listeners.current) {
|
for (const listener of listeners.current) {
|
||||||
listener(newData);
|
listener(data, encoded);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const updateKey = useEffectEvent((newKey: string) => {
|
const updateKey = useEffectEvent((newKey: string) => {
|
||||||
const data = examples.find((example) => example.key === newKey)?.data;
|
const example = examples.find((example) => example.key === newKey);
|
||||||
if (!data) return;
|
if (!example) return;
|
||||||
|
|
||||||
setKey(newKey);
|
setKey(newKey);
|
||||||
for (const listener of listeners.current) {
|
for (const listener of listeners.current) {
|
||||||
listener(data);
|
listener(example.data, example.encoded);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const addListener = useEffectEvent((listener: UpdateListener) => {
|
const addListener = useEffectEvent((listener: UpdateListener) => {
|
||||||
// initial call to listeners to ensure their data is the latest
|
// initial call to listeners to ensure their data is the latest
|
||||||
// this is necessary to avoid race conditions between `useEffect()`
|
// this is necessary to avoid race conditions between `useEffect()`
|
||||||
listener(examples.find((example) => example.key === key)!.data);
|
const example = examples.find((example) => example.key === key)!;
|
||||||
|
listener(example.data, example.encoded);
|
||||||
listeners.current.push(listener);
|
listeners.current.push(listener);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,7 +132,7 @@ export function CodeExample(props: CodeSample) {
|
|||||||
useContext(CodeExampleContext)!;
|
useContext(CodeExampleContext)!;
|
||||||
const { server } = useServerSelectContext();
|
const { server } = useServerSelectContext();
|
||||||
const [data, setData] = useState(() => {
|
const [data, setData] = useState(() => {
|
||||||
return examples.find((example) => example.key === key)!.data;
|
return examples.find((example) => example.key === key)!.encoded;
|
||||||
});
|
});
|
||||||
|
|
||||||
const sample = props.source
|
const sample = props.source
|
||||||
@@ -128,7 +140,7 @@ export function CodeExample(props: CodeSample) {
|
|||||||
: defaultSamples.find((item) => item.label === props.label);
|
: defaultSamples.find((item) => item.label === props.label);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = setData;
|
const listener: UpdateListener = (_, encoded) => setData(encoded);
|
||||||
|
|
||||||
addListener(listener);
|
addListener(listener);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -148,7 +160,7 @@ export function CodeExample(props: CodeSample) {
|
|||||||
? window.location.origin
|
? window.location.origin
|
||||||
: 'https://loading',
|
: 'https://loading',
|
||||||
),
|
),
|
||||||
route,
|
resolveRequestData(route, data),
|
||||||
),
|
),
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,16 +18,17 @@ export function Root({
|
|||||||
ctx,
|
ctx,
|
||||||
...props
|
...props
|
||||||
}: RootProps & HTMLAttributes<HTMLDivElement>) {
|
}: RootProps & HTMLAttributes<HTMLDivElement>) {
|
||||||
|
const mediaAdapters: Record<string, MediaAdapter> = {};
|
||||||
|
for (const k in ctx.mediaAdapters) {
|
||||||
|
const adapter = ctx.mediaAdapters[k];
|
||||||
|
|
||||||
|
if (adapter.client) mediaAdapters[k] = adapter.client;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-col gap-24 text-sm', className)} {...props}>
|
<div className={cn('flex flex-col gap-24 text-sm', className)} {...props}>
|
||||||
<ApiProvider
|
<ApiProvider
|
||||||
mediaAdapters={
|
mediaAdapters={mediaAdapters}
|
||||||
Object.fromEntries(
|
|
||||||
Object.entries(ctx.mediaAdapters).filter(
|
|
||||||
([_, v]) => typeof v !== 'boolean',
|
|
||||||
),
|
|
||||||
) as Record<string, MediaAdapter>
|
|
||||||
}
|
|
||||||
servers={ctx.servers}
|
servers={ctx.servers}
|
||||||
shikiOptions={ctx.shikiOptions}
|
shikiOptions={ctx.shikiOptions}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -36,25 +36,25 @@ export function resolveRequestData(
|
|||||||
{ path, query }: RequestData,
|
{ path, query }: RequestData,
|
||||||
): string {
|
): string {
|
||||||
for (const key in path) {
|
for (const key in path) {
|
||||||
if (path[key] === '') continue;
|
const param = path[key];
|
||||||
|
|
||||||
if (Array.isArray(path[key])) {
|
if (Array.isArray(param.value)) {
|
||||||
pathname = pathname.replace(`{${key}}`, path[key].join('/'));
|
pathname = pathname.replace(`{${key}}`, param.value.join('/'));
|
||||||
} else {
|
} else {
|
||||||
pathname = pathname.replace(`{${key}}`, String(path[key]));
|
pathname = pathname.replace(`{${key}}`, param.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
for (const key in query) {
|
for (const key in query) {
|
||||||
if (Array.isArray(query[key])) {
|
const param = query[key];
|
||||||
for (const value of query[key]) {
|
|
||||||
if (!query[key]) continue;
|
|
||||||
|
|
||||||
searchParams.append(key, String(value));
|
if (Array.isArray(param.value)) {
|
||||||
|
for (const item of param.value) {
|
||||||
|
searchParams.append(key, item);
|
||||||
}
|
}
|
||||||
} else if (query[key] && query[key] !== '') {
|
} else {
|
||||||
searchParams.set(key, String(query[key]));
|
searchParams.append(key, param.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import * as Curl from '@/requests/curl';
|
|||||||
import * as JS from '@/requests/javascript';
|
import * as JS from '@/requests/javascript';
|
||||||
import type { RequestData } from '@/requests/_shared';
|
import type { RequestData } from '@/requests/_shared';
|
||||||
import { defaultAdapters } from '@/media/adapter';
|
import { defaultAdapters } from '@/media/adapter';
|
||||||
|
import { resolveRequestData } from '@/utils/url';
|
||||||
|
|
||||||
describe('Code Sample Generators', () => {
|
describe('Code Sample Generators', () => {
|
||||||
const url = 'http://localhost:8080/{test}';
|
|
||||||
const data: RequestData = {
|
const data: RequestData = {
|
||||||
path: {
|
path: {
|
||||||
test: 'hello_world',
|
test: { value: 'hello_world' },
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
@@ -18,15 +18,16 @@ describe('Code Sample Generators', () => {
|
|||||||
bodyMediaType: 'application/json',
|
bodyMediaType: 'application/json',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
cookie: {
|
cookie: {
|
||||||
mode: 'light',
|
mode: { value: 'light' },
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
authorization: 'Bearer',
|
authorization: { value: 'Bearer' },
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
search: 'ai',
|
search: { value: 'ai' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const url = resolveRequestData('http://localhost:8080/{test}', data);
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
mediaAdapters: defaultAdapters,
|
mediaAdapters: defaultAdapters,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ body = {
|
|||||||
"id": "id"
|
"id": "id"
|
||||||
}
|
}
|
||||||
response = requests.request("GET", url, json = body, headers = {
|
response = requests.request("GET", url, json = body, headers = {
|
||||||
"authorization": "Bearer",
|
"Content-Type": "application/json",
|
||||||
"Content-Type": "application/json"
|
"authorization": "Bearer"
|
||||||
}, cookies = {
|
}, cookies = {
|
||||||
"mode": "light"
|
"mode": "light"
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user