mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Support variable server url
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Support variable server url
|
||||
@@ -12,6 +12,19 @@
|
||||
{
|
||||
"url": "https://example.com/api/v1",
|
||||
"description": "Example API"
|
||||
},
|
||||
{
|
||||
"url": "{protocol}://{domain}/api",
|
||||
"description": "The API is running at `/api` of your Fumadocs.",
|
||||
"variables": {
|
||||
"protocol": {
|
||||
"enum": ["http", "https"],
|
||||
"default": "https"
|
||||
},
|
||||
"domain": {
|
||||
"default": "fumadocs.example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-speakeasy-retries": {
|
||||
|
||||
@@ -54,7 +54,6 @@ export function Operation({
|
||||
|
||||
headingLevel?: number;
|
||||
}): ReactElement {
|
||||
const { baseUrls } = ctx;
|
||||
const body = method.requestBody;
|
||||
const security = method.security ?? ctx.document.security;
|
||||
let headNode: ReactNode = null;
|
||||
@@ -183,12 +182,7 @@ export function Operation({
|
||||
}
|
||||
|
||||
const info = (
|
||||
<ctx.renderer.APIInfo
|
||||
head={headNode}
|
||||
method={method.method}
|
||||
route={path}
|
||||
baseUrls={type === 'operation' ? baseUrls : []}
|
||||
>
|
||||
<ctx.renderer.APIInfo head={headNode} method={method.method} route={path}>
|
||||
{type === 'operation' ? (
|
||||
<Playground path={path} method={method} ctx={ctx} />
|
||||
) : null}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
ObjectCollapsible,
|
||||
APIPlayground,
|
||||
} from '@/ui';
|
||||
import type { RenderContext } from '@/types';
|
||||
import type { RenderContext, ServerObject } from '@/types';
|
||||
|
||||
export interface ResponsesProps {
|
||||
items: string[];
|
||||
@@ -27,7 +27,6 @@ export interface ResponseProps {
|
||||
export interface APIInfoProps {
|
||||
method: string;
|
||||
route: string;
|
||||
baseUrls: string[];
|
||||
|
||||
head: ReactNode;
|
||||
children: ReactNode;
|
||||
@@ -60,6 +59,7 @@ export interface ResponseTypeProps {
|
||||
|
||||
export interface RootProps {
|
||||
baseUrl?: string;
|
||||
servers: ServerObject[];
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type EndpointSample } from '@/utils/generate-sample';
|
||||
import { inputToString } from '@/utils/schema';
|
||||
import { inputToString } from '@/utils/input-to-string';
|
||||
|
||||
export function getSampleRequest(endpoint: EndpointSample): string {
|
||||
const s: string[] = [];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type EndpointSample } from '@/utils/generate-sample';
|
||||
import { inputToString } from '@/utils/schema';
|
||||
import { inputToString } from '@/utils/input-to-string';
|
||||
|
||||
export function getSampleRequest(endpoint: EndpointSample): string {
|
||||
const imports = ['fmt', 'net/http', 'io/ioutil'];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type EndpointSample } from '@/utils/generate-sample';
|
||||
import { inputToString } from '@/utils/schema';
|
||||
import { inputToString } from '@/utils/input-to-string';
|
||||
|
||||
export function getSampleRequest(endpoint: EndpointSample): string {
|
||||
const s: string[] = [];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type EndpointSample } from '@/utils/generate-sample';
|
||||
import { inputToString } from '@/utils/schema';
|
||||
import { inputToString } from '@/utils/input-to-string';
|
||||
|
||||
export function getSampleRequest(endpoint: EndpointSample): string {
|
||||
const headers = new Map<string, unknown>();
|
||||
|
||||
@@ -62,7 +62,7 @@ export async function APIPage(props: ApiPageProps) {
|
||||
const ctx = await getContext(processed, props);
|
||||
const { document } = processed;
|
||||
return (
|
||||
<ctx.renderer.Root baseUrl={ctx.baseUrl}>
|
||||
<ctx.renderer.Root baseUrl={ctx.baseUrl} servers={ctx.servers}>
|
||||
{operations?.map((item) => {
|
||||
const pathItem = document.paths?.[item.path];
|
||||
if (!pathItem) return null;
|
||||
@@ -127,7 +127,7 @@ export async function getContext(
|
||||
generateTypeScriptSchema: options.generateTypeScriptSchema,
|
||||
generateCodeSamples: options.generateCodeSamples,
|
||||
baseUrl: document.servers?.[0].url ?? 'https://example.com',
|
||||
baseUrls: document.servers?.map((s) => s.url) ?? [],
|
||||
servers: document.servers ?? [],
|
||||
slugger: new Slugger(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,13 +18,7 @@ export type SecuritySchemeObject = V3_1.SecuritySchemeObject;
|
||||
export type ReferenceObject = V3_1.ReferenceObject;
|
||||
export type PathItemObject = V3_1.PathItemObject;
|
||||
export type TagObject = V3_1.TagObject;
|
||||
|
||||
export interface RouteInformation {
|
||||
path: string;
|
||||
summary?: string;
|
||||
description?: string;
|
||||
methods: MethodInformation[];
|
||||
}
|
||||
export type ServerObject = NoReference<V3_1.ServerObject>;
|
||||
|
||||
export type MethodInformation = NoReference<OperationObject> & {
|
||||
method: string;
|
||||
@@ -51,7 +45,7 @@ export interface RenderContext {
|
||||
document: NoReference<Document>;
|
||||
|
||||
baseUrl: string;
|
||||
baseUrls: string[];
|
||||
servers: ServerObject[];
|
||||
|
||||
slugger: Slugger;
|
||||
|
||||
|
||||
@@ -7,9 +7,23 @@ import {
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { cn, useCopyButton, buttonVariants } from 'fumadocs-ui/components/api';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ApiProvider, useApiContext } from '@/ui/contexts/api';
|
||||
import {
|
||||
ApiProvider,
|
||||
useApiContext,
|
||||
useServerSelectContext,
|
||||
} from '@/ui/contexts/api';
|
||||
import { type RootProps } from '@/render/renderer';
|
||||
import { RenderContext } from '@/types';
|
||||
import type { RenderContext } from '@/types';
|
||||
import { labelVariants } from '@/ui/components/form';
|
||||
import { Input } from '@/ui/components/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/ui/components/select';
|
||||
import { getUrl } from '@/utils/server-url';
|
||||
|
||||
export const APIPlayground = dynamic(() =>
|
||||
import('./playground').then((mod) => mod.APIPlayground),
|
||||
@@ -20,6 +34,7 @@ export function Root({
|
||||
baseUrl,
|
||||
className,
|
||||
shikiOptions,
|
||||
servers,
|
||||
...props
|
||||
}: RootProps & {
|
||||
shikiOptions: RenderContext['shikiOptions'];
|
||||
@@ -32,7 +47,11 @@ export function Root({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ApiProvider shikiOptions={shikiOptions} defaultBaseUrl={baseUrl}>
|
||||
<ApiProvider
|
||||
servers={servers}
|
||||
shikiOptions={shikiOptions}
|
||||
defaultBaseUrl={baseUrl}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
</div>
|
||||
@@ -46,10 +65,12 @@ export function CopyRouteButton({
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
route: string;
|
||||
}): ReactNode {
|
||||
const { baseUrl } = useApiContext();
|
||||
const { serverRef } = useApiContext();
|
||||
|
||||
const [checked, onCopy] = useCopyButton(() => {
|
||||
void navigator.clipboard.writeText(`${baseUrl ?? ''}${route}`);
|
||||
void navigator.clipboard.writeText(
|
||||
`${serverRef.current ? getUrl(serverRef.current.url, serverRef.current.variables) : ''}${route}`,
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -74,28 +95,85 @@ export function CopyRouteButton({
|
||||
);
|
||||
}
|
||||
|
||||
export function BaseUrlSelect({ baseUrls }: { baseUrls: string[] }) {
|
||||
const { baseUrl, setBaseUrl } = useApiContext();
|
||||
export function ServerSelect() {
|
||||
const { servers } = useApiContext();
|
||||
const { server, setServer, setServerVariables } = useServerSelectContext();
|
||||
|
||||
if (baseUrls.length <= 1) return null;
|
||||
if (servers.length <= 1) return null;
|
||||
|
||||
const schema = server
|
||||
? servers.find((item) => item.url === server.url)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="mt-2 flex flex-row items-center gap-1 px-1">
|
||||
<span className="p-0.5 text-xs font-medium text-fd-muted-foreground">
|
||||
Server
|
||||
</span>
|
||||
<select
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
className="min-w-0 flex-1 bg-transparent text-xs text-fd-foreground outline-none"
|
||||
>
|
||||
{baseUrls.map((url) => (
|
||||
<option key={url} value={url}>
|
||||
{url}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<>
|
||||
<div className="not-prose mt-4 flex flex-row items-center gap-2">
|
||||
<span className="text-xs font-medium">Server</span>
|
||||
<select
|
||||
value={server?.url}
|
||||
onChange={(e) => setServer(e.target.value)}
|
||||
className="min-w-0 flex-1 bg-transparent text-xs text-fd-foreground outline-none"
|
||||
>
|
||||
{servers.map((item) => (
|
||||
<option key={item.url} value={item.url}>
|
||||
{item.url}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{schema && schema.variables && server?.variables ? (
|
||||
<div className="not-prose mt-2 flex flex-col gap-4">
|
||||
{schema.description ? (
|
||||
<p className="text-xs">{schema.description}</p>
|
||||
) : null}
|
||||
{Object.entries(schema.variables).map(([key, variable]) => {
|
||||
const id = `fd_server_select_${key}`;
|
||||
|
||||
return (
|
||||
<fieldset key={key} className="flex flex-col gap-1">
|
||||
<label className={cn(labelVariants())} htmlFor={id}>
|
||||
{key}
|
||||
</label>
|
||||
<p className="text-xs">{variable.description}</p>
|
||||
{variable.enum ? (
|
||||
<Select
|
||||
value={server.variables[key]}
|
||||
onValueChange={(v) =>
|
||||
setServerVariables({
|
||||
...server?.variables,
|
||||
[key]: v,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{variable.enum.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
id={id}
|
||||
value={server.variables[key]}
|
||||
onChange={(e) =>
|
||||
setServerVariables({
|
||||
...server?.variables,
|
||||
[key]: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ const FormItem = forwardRef<
|
||||
FormItem.displayName = 'FormItem';
|
||||
|
||||
export const labelVariants = cva(
|
||||
'text-xs font-medium text-fd-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
'font-mono text-xs text-fd-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
);
|
||||
|
||||
const FormLabel = forwardRef<
|
||||
|
||||
@@ -1,31 +1,47 @@
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
|
||||
import type { RenderContext } from '@/types';
|
||||
import type { RenderContext, ServerObject } from '@/types';
|
||||
|
||||
export interface ApiProviderProps {
|
||||
/**
|
||||
* Base URL for API requests
|
||||
*/
|
||||
defaultBaseUrl?: string;
|
||||
|
||||
servers: ServerObject[];
|
||||
shikiOptions: RenderContext['shikiOptions'];
|
||||
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface ApiContextType {
|
||||
baseUrl?: string;
|
||||
setBaseUrl: (value: string) => void;
|
||||
shikiOptions: RenderContext['shikiOptions'];
|
||||
interface SelectedServer {
|
||||
url: string;
|
||||
variables: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ApiContextType
|
||||
extends Omit<ApiProviderProps, 'children' | 'defaultBaseUrl'> {
|
||||
serverRef: RefObject<SelectedServer | null>;
|
||||
}
|
||||
|
||||
interface ServerSelectType {
|
||||
server: SelectedServer | null;
|
||||
setServer: (value: string) => void;
|
||||
setServerVariables: (value: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
const ApiContext = createContext<ApiContextType | undefined>(undefined);
|
||||
const ServerSelectContext = createContext<ServerSelectType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function useApiContext(): ApiContextType {
|
||||
const ctx = useContext(ApiContext);
|
||||
@@ -34,33 +50,95 @@ export function useApiContext(): ApiContextType {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function useServerSelectContext(): ServerSelectType {
|
||||
const ctx = useContext(ServerSelectContext);
|
||||
if (!ctx) throw new Error('Component must be used under <ApiProvider />');
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function ApiProvider({
|
||||
defaultBaseUrl,
|
||||
shikiOptions,
|
||||
children,
|
||||
}: ApiProviderProps): React.ReactElement {
|
||||
const [baseUrl, setBaseUrl] = useState(defaultBaseUrl);
|
||||
...props
|
||||
}: ApiProviderProps) {
|
||||
const [server, setServer] = useState<SelectedServer | null>(() => {
|
||||
const defaultItem = defaultBaseUrl
|
||||
? props.servers.find((item) => item.url === defaultBaseUrl)
|
||||
: undefined;
|
||||
|
||||
return defaultItem
|
||||
? {
|
||||
url: defaultItem.url,
|
||||
variables: getDefaultValues(defaultItem),
|
||||
}
|
||||
: null;
|
||||
});
|
||||
|
||||
const serverRef = useRef<SelectedServer | null>(server);
|
||||
serverRef.current = server;
|
||||
|
||||
useEffect(() => {
|
||||
setBaseUrl((prev) => localStorage.getItem('apiBaseUrl') ?? prev);
|
||||
}, []);
|
||||
const cached = localStorage.getItem('apiBaseUrl');
|
||||
if (!cached) return;
|
||||
|
||||
useOnChange(baseUrl, () => {
|
||||
if (baseUrl) localStorage.setItem('apiBaseUrl', baseUrl);
|
||||
});
|
||||
try {
|
||||
const obj = JSON.parse(cached);
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
|
||||
setServer(obj);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ApiContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
baseUrl,
|
||||
setBaseUrl,
|
||||
shikiOptions,
|
||||
...props,
|
||||
serverRef,
|
||||
}),
|
||||
[baseUrl, shikiOptions],
|
||||
[props],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<ServerSelectContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
server,
|
||||
setServerVariables(variables) {
|
||||
setServer((prev) => {
|
||||
if (!prev) return null;
|
||||
|
||||
const updated = { ...prev, variables };
|
||||
localStorage.setItem('apiBaseUrl', JSON.stringify(updated));
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
setServer(value) {
|
||||
const obj = props.servers.find((item) => item.url === value);
|
||||
if (!obj) return;
|
||||
|
||||
const result: SelectedServer = {
|
||||
url: value,
|
||||
variables: getDefaultValues(obj),
|
||||
};
|
||||
|
||||
localStorage.setItem('apiBaseUrl', JSON.stringify(result));
|
||||
setServer(result);
|
||||
},
|
||||
}),
|
||||
[server, props.servers],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ServerSelectContext.Provider>
|
||||
</ApiContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function getDefaultValues(server: ServerObject): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(server.variables ?? {}).map(([k, v]) => [k, v.default]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Fragment, type HTMLAttributes, type ReactNode } from 'react';
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { badgeVariants, getBadgeColor } from '@/ui/components/variants';
|
||||
import type { APIInfoProps, PropertyProps } from '@/render/renderer';
|
||||
import { BaseUrlSelect, CopyRouteButton } from '@/ui/client';
|
||||
import { ServerSelect, CopyRouteButton } from '@/ui/client';
|
||||
|
||||
export { Root, useSchemaContext, APIPlayground } from './client';
|
||||
|
||||
@@ -30,7 +30,6 @@ export function APIInfo({
|
||||
className,
|
||||
route,
|
||||
badgeClassname,
|
||||
baseUrls,
|
||||
method = 'GET',
|
||||
head,
|
||||
...props
|
||||
@@ -41,8 +40,8 @@ export function APIInfo({
|
||||
return (
|
||||
<div className={cn('min-w-0 flex-1', className)} {...props}>
|
||||
{head}
|
||||
<div className="sticky top-[var(--fd-api-info-top)] z-[4] mb-4 border-b border-fd-foreground/10 bg-fd-card/50 px-4 py-1.5 shadow-lg backdrop-blur-lg max-lg:-mx-3 max-md:-mx-4 md:rounded-xl md:border md:px-1.5">
|
||||
<div className="flex flex-row items-center gap-1.5">
|
||||
<div className="mb-4 border-b border-fd-foreground/10 bg-fd-card/50 px-4 py-1.5 shadow-lg md:rounded-xl md:border md:p-3">
|
||||
<div className="-mx-1 flex flex-row items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
badgeVariants({ color: getBadgeColor(method) }),
|
||||
@@ -54,7 +53,7 @@ export function APIInfo({
|
||||
<Route route={route} />
|
||||
<CopyRouteButton className="ms-auto size-6 p-1.5" route={route} />
|
||||
</div>
|
||||
<BaseUrlSelect baseUrls={baseUrls} />
|
||||
<ServerSelect />
|
||||
</div>
|
||||
<div className="prose-no-margin">{props.children}</div>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
import { CodeBlock } from '@/ui/components/codeblock';
|
||||
import { type DynamicField, SchemaContext } from '../contexts/schema';
|
||||
import { getStatusInfo } from '@/ui/playground/status-info';
|
||||
import { getUrl } from '@/utils/server-url';
|
||||
|
||||
interface FormValues {
|
||||
authorization: string;
|
||||
@@ -73,7 +74,7 @@ export function APIPlayground({
|
||||
body?: CustomField<'body', RequestSchema>;
|
||||
};
|
||||
} & HTMLAttributes<HTMLFormElement>) {
|
||||
const { baseUrl } = useApiContext();
|
||||
const { serverRef } = useApiContext();
|
||||
const dynamicRef = useRef(new Map<string, DynamicField>());
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
@@ -89,7 +90,10 @@ export function APIPlayground({
|
||||
const fetcher = await import('./fetcher').then((mod) =>
|
||||
mod.createBrowserFetcher(body, schemas),
|
||||
);
|
||||
const targetUrl = `${baseUrl ?? window.location.origin}${createPathnameFromInput(route, input.path, input.query)}`;
|
||||
const serverUrl = serverRef.current
|
||||
? getUrl(serverRef.current.url, serverRef.current.variables)
|
||||
: window.location.origin;
|
||||
const targetUrl = `${serverUrl}${createPathnameFromInput(route, input.path, input.query)}`;
|
||||
|
||||
let url: URL;
|
||||
if (proxyUrl) {
|
||||
|
||||
@@ -87,7 +87,7 @@ function InputContainer({
|
||||
...props
|
||||
}: InputContainerProps): React.ReactElement {
|
||||
return (
|
||||
<div {...props} className={cn('flex flex-col gap-2', props.className)}>
|
||||
<div {...props} className={cn('flex flex-col gap-1', props.className)}>
|
||||
<div className={cn(labelVariants(), 'inline-flex items-center gap-1')}>
|
||||
{name}
|
||||
{required ? <span className="text-red-500">*</span> : null}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { type ElementCompact, js2xml } from 'xml-js';
|
||||
|
||||
/**
|
||||
* Convert input value to hardcoded string (with quotes)
|
||||
*/
|
||||
export function inputToString(
|
||||
value: unknown,
|
||||
mediaType = 'application/json',
|
||||
multiLine: 'single-quote' | 'backtick' | 'none' = 'none',
|
||||
): string {
|
||||
const getStr = (v: string) => {
|
||||
if (multiLine === 'none') return JSON.stringify(v);
|
||||
|
||||
const delimit = multiLine === 'backtick' ? `\`` : `'`;
|
||||
return `${delimit}${v.replaceAll(delimit, `\\${delimit}`)}${delimit}`;
|
||||
};
|
||||
|
||||
if (typeof value === 'string') return getStr(value);
|
||||
|
||||
if (mediaType === 'application/json' || mediaType === 'multipart/form-data') {
|
||||
return getStr(JSON.stringify(value, null, 2));
|
||||
}
|
||||
|
||||
if (mediaType === 'application/xml') {
|
||||
return getStr(
|
||||
js2xml(value as ElementCompact, { compact: true, spaces: 2 }),
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported media type: ${mediaType}`);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ReferenceObject } from '@/types';
|
||||
import type { OpenAPIV3_1 } from 'openapi-types';
|
||||
import { js2xml, type ElementCompact } from 'xml-js';
|
||||
|
||||
export type NoReference<T> = T extends (infer I)[]
|
||||
? NoReference<I>[]
|
||||
@@ -22,36 +21,6 @@ export function getPreferredType<B extends Record<string, unknown>>(
|
||||
return Object.keys(body)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert input to string (with quotes)
|
||||
*/
|
||||
export function inputToString(
|
||||
value: unknown,
|
||||
mediaType = 'application/json',
|
||||
multiLine: 'single-quote' | 'backtick' | 'none' = 'none',
|
||||
): string {
|
||||
const getStr = (v: string) => {
|
||||
if (multiLine === 'none') return JSON.stringify(v);
|
||||
|
||||
const delimit = multiLine === 'backtick' ? `\`` : `'`;
|
||||
return `${delimit}${v.replaceAll(delimit, `\\${delimit}`)}${delimit}`;
|
||||
};
|
||||
|
||||
if (typeof value === 'string') return getStr(value);
|
||||
|
||||
if (mediaType === 'application/json' || mediaType === 'multipart/form-data') {
|
||||
return getStr(JSON.stringify(value, null, 2));
|
||||
}
|
||||
|
||||
if (mediaType === 'application/xml') {
|
||||
return getStr(
|
||||
js2xml(value as ElementCompact, { compact: true, spaces: 2 }),
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported media type: ${mediaType}`);
|
||||
}
|
||||
|
||||
export function isNullable(
|
||||
schema: NoReference<ParsedSchema>,
|
||||
includeOneOf = true,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export function getUrl(url: string, variables: Record<string, string>): string {
|
||||
let out = url;
|
||||
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
out = out.replaceAll(`{${key}}`, value);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -269,7 +269,7 @@ function SidebarHeaderItems({
|
||||
{nav.title ? (
|
||||
<Link
|
||||
href={nav.url ?? '/'}
|
||||
className="inline-flex items-center gap-2.5 py-1 font-medium"
|
||||
className="inline-flex items-center gap-2.5 py-1 font-medium md:px-2"
|
||||
>
|
||||
{nav.title}
|
||||
</Link>
|
||||
|
||||
Reference in New Issue
Block a user