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