mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 06:31:49 +00:00
OpenAPI: improve performance
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Improve performance
|
||||
@@ -61,7 +61,6 @@
|
||||
"@scalar/openapi-upgrader": "^0.2.2",
|
||||
"ajv": "^8.18.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"dereference-json-schema": "^0.2.2",
|
||||
"github-slugger": "^2.0.0",
|
||||
"hast-util-to-jsx-runtime": "^2.3.6",
|
||||
"js-yaml": "^4.1.1",
|
||||
|
||||
@@ -38,7 +38,6 @@ import { labelVariants } from '@/ui/components/input';
|
||||
import type { ParsedSchema } from '@/utils/schema';
|
||||
import ServerSelect from './components/server-select';
|
||||
import { useStorageKey } from '@/ui/client/storage-key';
|
||||
import { useExampleRequests } from '@/ui/operation/usage-tabs/client';
|
||||
import {
|
||||
FieldKey,
|
||||
Stf,
|
||||
@@ -53,6 +52,7 @@ import { FieldInput, FieldSet, JsonInput, ObjectInput } from './components/input
|
||||
import type { ParameterObject } from '@/types';
|
||||
import { ClientCodeBlock } from '@/ui/components/codeblock';
|
||||
import { useTranslations } from '@/ui/client/i18n';
|
||||
import { useOperationContext } from '@/ui/operation/client';
|
||||
|
||||
export interface FormValues extends Record<string, unknown> {
|
||||
path: Record<string, unknown>;
|
||||
@@ -141,7 +141,8 @@ export default function PlaygroundClient({
|
||||
...rest
|
||||
}: PlaygroundClientProps) {
|
||||
const t = useTranslations();
|
||||
const { example: exampleId, examples, setExampleData } = useExampleRequests();
|
||||
const { example: exampleId, examples, setExampleData } = useOperationContext();
|
||||
const { server } = useServerContext();
|
||||
const storageKeys = useStorageKey();
|
||||
const {
|
||||
mediaAdapters,
|
||||
@@ -153,7 +154,6 @@ export default function PlaygroundClient({
|
||||
} = {},
|
||||
},
|
||||
} = useApiContext();
|
||||
const { serverRef } = useServerContext();
|
||||
const [securityId, setSecurityId] = useState(() => {
|
||||
const idx = securities.findIndex((s) => s.every((entry) => !entry.deprecated));
|
||||
return idx === -1 ? 0 : idx;
|
||||
@@ -182,7 +182,6 @@ export default function PlaygroundClient({
|
||||
});
|
||||
|
||||
const testQuery = useQuery(async (input: FormValues) => {
|
||||
const targetServer = serverRef.current;
|
||||
const fetcher = await import('./fetcher').then((mod) =>
|
||||
mod.createBrowserFetcher(mediaAdapters, requestTimeout),
|
||||
);
|
||||
@@ -194,7 +193,7 @@ export default function PlaygroundClient({
|
||||
return fetcher.fetch(
|
||||
joinURL(
|
||||
withBase(
|
||||
targetServer ? resolveServerUrl(targetServer.url, targetServer.variables) : '/',
|
||||
server ? resolveServerUrl(server.url, server.variables) : '/',
|
||||
window.location.origin,
|
||||
),
|
||||
resolveRequestData(route, encoded),
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { Input, labelVariants } from '@/ui/components/input';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { FormatFlags } from '@/utils/schema-to-string';
|
||||
import { FormatFlags } from '@/utils/schema/to-string';
|
||||
import {
|
||||
anyFields,
|
||||
useFieldInfo,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import { useServerContext, useServerSelectContext } from '@/ui/contexts/api';
|
||||
import { useServerContext } from '@/ui/contexts/api';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -26,8 +26,7 @@ import { EditIcon } from 'lucide-react';
|
||||
import { useTranslations } from '@/ui/client/i18n';
|
||||
|
||||
export default function ServerSelect(props: ComponentProps<typeof DialogTrigger>) {
|
||||
const { servers } = useServerContext();
|
||||
const { server, setServer, setServerVariables } = useServerSelectContext();
|
||||
const { servers, server, setServer, setServerVariables } = useServerContext();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const t = useTranslations();
|
||||
@@ -36,7 +35,7 @@ export default function ServerSelect(props: ComponentProps<typeof DialogTrigger>
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
if (servers.length <= 0) return;
|
||||
if (!servers || servers.length <= 0) return;
|
||||
const serverSchema = server ? servers.find((obj) => obj.url === server.url) : null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -26,7 +26,7 @@ export type SecurityEntry = SecuritySchemeObject & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export async function APIPlayground({ path, method, ctx }: APIPlaygroundProps) {
|
||||
export function APIPlayground({ path, method, ctx }: APIPlaygroundProps) {
|
||||
if (ctx.playground?.render) {
|
||||
return ctx.playground.render({ path, method, ctx });
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import { mergeAllOf } from '@/utils/merge-schema';
|
||||
import { FieldKey, useDataEngine, useFieldValue, useNamespace } from '@fumari/stf';
|
||||
import { stringifyFieldKey } from '@fumari/stf/lib/utils';
|
||||
import { sample } from 'openapi-sampler';
|
||||
import { FormatFlags, schemaToString } from '@/utils/schema-to-string';
|
||||
import { FormatFlags, schemaToString } from '@/utils/schema/to-string';
|
||||
import { resolveRefSync } from '@/utils/schema/resolve-ref';
|
||||
|
||||
interface SchemaContextType extends SchemaScope {
|
||||
references: Record<string, ParsedSchema>;
|
||||
@@ -201,13 +202,8 @@ export function useResolvedSchema(raw: ParsedSchema): Exclude<ParsedSchema, bool
|
||||
|
||||
function dereference(schema: ParsedSchema, references: Record<string, ParsedSchema>): ParsedSchema {
|
||||
if (typeof schema === 'boolean') return schema;
|
||||
|
||||
let ref = schema.$ref;
|
||||
if (ref) {
|
||||
// use swallow resolution as it is already preprocessed in `playground/index.tsx`
|
||||
const prefix = '#/';
|
||||
if (ref.startsWith(prefix)) ref = ref.slice(prefix.length);
|
||||
if (ref in references) return references[ref];
|
||||
if (schema.$ref) {
|
||||
return resolveRefSync(schema.$ref, references) as ParsedSchema;
|
||||
}
|
||||
|
||||
return schema;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { CodeUsageGenerator } from '@/requests/generators';
|
||||
import { generatePythonObject } from '@/requests/to-python-object';
|
||||
import { resolveMediaAdapter } from '@/requests/media/adapter';
|
||||
|
||||
export const python: CodeUsageGenerator = {
|
||||
@@ -56,3 +55,28 @@ response = requests.request(${params.join(', ')})
|
||||
print(response.text)`;
|
||||
},
|
||||
};
|
||||
|
||||
export function generatePythonObject(v: unknown, imports: Set<string>): string {
|
||||
if (v === null) {
|
||||
return 'None';
|
||||
} else if (typeof v === 'boolean') {
|
||||
return v ? 'True' : 'False';
|
||||
} else if (typeof v === 'string') {
|
||||
return JSON.stringify(v);
|
||||
} else if (typeof v === 'number') {
|
||||
return v.toString();
|
||||
} else if (Array.isArray(v)) {
|
||||
const items = v.map((item) => generatePythonObject(item, imports));
|
||||
return `[${items.join(', ')}]`;
|
||||
} else if (v instanceof Date) {
|
||||
imports.add('datetime');
|
||||
return `datetime.datetime(${v.getFullYear()}, ${v.getMonth() + 1}, ${v.getDate()}, ${v.getHours()}, ${v.getMinutes()}, ${v.getSeconds()}, ${v.getMilliseconds()})`;
|
||||
} else if (typeof v === 'object') {
|
||||
const entries = Object.entries(v).map(
|
||||
([key, value]) => ` ${JSON.stringify(key)}: ${generatePythonObject(value, imports)}`,
|
||||
);
|
||||
return `{\n${entries.join(', \n')}\n}`;
|
||||
} else {
|
||||
throw new Error(`Unsupported type: ${typeof v}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
export function generatePythonObject(v: unknown, imports = new Set()): string {
|
||||
if (v === null) {
|
||||
return 'None';
|
||||
} else if (typeof v === 'boolean') {
|
||||
return v ? 'True' : 'False';
|
||||
} else if (typeof v === 'string') {
|
||||
return JSON.stringify(v);
|
||||
} else if (typeof v === 'number') {
|
||||
return v.toString();
|
||||
} else if (Array.isArray(v)) {
|
||||
const items = v.map((item) => generatePythonObject(item, imports));
|
||||
return `[${items.join(', ')}]`;
|
||||
} else if (v instanceof Date) {
|
||||
imports.add('datetime');
|
||||
return `datetime.datetime(${v.getFullYear()}, ${v.getMonth() + 1}, ${v.getDate()}, ${v.getHours()}, ${v.getMinutes()}, ${v.getSeconds()}, ${v.getMilliseconds()})`;
|
||||
} else if (typeof v === 'object') {
|
||||
const entries = Object.entries(v).map(
|
||||
([key, value]) => ` ${JSON.stringify(key)}: ${generatePythonObject(value, imports)}`,
|
||||
);
|
||||
return `{\n${entries.join(', \n')}\n}`;
|
||||
} else {
|
||||
throw new Error(`Unsupported type: ${typeof v}`);
|
||||
}
|
||||
}
|
||||
@@ -37,24 +37,18 @@ export function createOpenAPI(options: OpenAPIOptions = {}): OpenAPIServer {
|
||||
const { input = [], disableCache = false } = options;
|
||||
let schemas: Promise<ProcessedSchemaMap> | undefined;
|
||||
|
||||
async function getSchemas() {
|
||||
const out: ProcessedSchemaMap = {};
|
||||
|
||||
async function getSchemas(): Promise<ProcessedSchemaMap> {
|
||||
if (Array.isArray(input)) {
|
||||
await Promise.all(
|
||||
input.map(async (item) => {
|
||||
out[item] = await processDocument(item);
|
||||
}),
|
||||
const entries = await Promise.all(
|
||||
input.map(async (item) => [item, await processDocument(item)]),
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
} else {
|
||||
await Promise.all(
|
||||
Object.entries(await input()).map(async ([k, v]) => {
|
||||
out[k] = await processDocument(v);
|
||||
}),
|
||||
const entries = await Promise.all(
|
||||
Object.entries(await input()).map(async ([k, v]) => [k, await processDocument(v)]),
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Operation } from '@/ui/operation';
|
||||
import type { HttpMethods, RenderContext } from '@/types';
|
||||
import type { HttpMethods, RenderContext, ServerObject } from '@/types';
|
||||
import { createMethod } from '@/utils/schema';
|
||||
import type { ProcessedDocument } from '@/utils/process-document';
|
||||
import { ApiProviderLazy, ServerProviderLazy } from './contexts/api.lazy';
|
||||
@@ -36,7 +36,7 @@ export interface OperationItem {
|
||||
method: HttpMethods;
|
||||
}
|
||||
|
||||
export async function APIPage({
|
||||
export function APIPage({
|
||||
showTitle: hasHead = false,
|
||||
showDescription,
|
||||
operations,
|
||||
@@ -54,7 +54,7 @@ export async function APIPage({
|
||||
</div>
|
||||
);
|
||||
|
||||
const content = await renderPageLayout(
|
||||
const content = renderPageLayout(
|
||||
{
|
||||
operations: operations?.map((item) => {
|
||||
const pathItem = dereferenced.paths?.[item.path];
|
||||
@@ -112,12 +112,12 @@ export async function APIPage({
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
let servers = ctx.schema.dereferenced.servers;
|
||||
if (!servers || servers.length === 0) servers = [{ url: '/' }];
|
||||
|
||||
return (
|
||||
<ApiProviderLazy shikiOptions={ctx.shikiOptions} client={ctx.client ?? {}}>
|
||||
<ServerProviderLazy servers={servers}>{content}</ServerProviderLazy>
|
||||
<ServerProviderLazy servers={dereferenced.servers as ServerObject[]}>
|
||||
{content}
|
||||
</ServerProviderLazy>
|
||||
</ApiProviderLazy>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ import * as JsxRuntime from 'react/jsx-runtime';
|
||||
import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock';
|
||||
import type { SchemaUIOptions } from './schema';
|
||||
import type { ResponseTab } from './operation/response-tabs';
|
||||
import type { ExampleRequestItem } from './operation/request-tabs';
|
||||
import { APIPage, type ApiPageProps, type OperationItem, type WebhookItem } from './api-page';
|
||||
import type { CodeUsageGeneratorRegistry, InlineCodeUsageGenerator } from '@/requests/generators';
|
||||
import type { JSONSchema } from 'json-schema-typed';
|
||||
import type { BundledTheme, CodeOptionsThemes, CodeToHastOptionsCommon } from 'shiki';
|
||||
import { highlightHast, type ShikiFactory } from 'fumadocs-core/highlight/shiki';
|
||||
import type { ExampleRequestItem } from './operation/get-example-requests';
|
||||
|
||||
export interface GenerateTypeScriptDefinitionsContext extends RenderContext {
|
||||
operation: NoReference<MethodInformation>;
|
||||
@@ -95,7 +95,7 @@ export interface CreateAPIPageOptions {
|
||||
* Customise page content
|
||||
*/
|
||||
content?: {
|
||||
renderResponseTabs?: (tabs: ResponseTab[], ctx: RenderContext) => Awaitable<ReactNode>;
|
||||
renderResponseTabs?: (tabs: ResponseTab[], ctx: RenderContext) => ReactNode;
|
||||
|
||||
renderRequestTabs?: (
|
||||
items: ExampleRequestItem[],
|
||||
@@ -103,7 +103,7 @@ export interface CreateAPIPageOptions {
|
||||
route: string;
|
||||
operation: NoReference<MethodInformation>;
|
||||
},
|
||||
) => Awaitable<ReactNode>;
|
||||
) => ReactNode;
|
||||
|
||||
renderAPIExampleLayout?: (
|
||||
slots: {
|
||||
@@ -112,7 +112,7 @@ export interface CreateAPIPageOptions {
|
||||
responseTabs: ReactNode;
|
||||
},
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<ReactNode>;
|
||||
) => ReactNode;
|
||||
|
||||
/**
|
||||
* @param generators - codegens for API example usages
|
||||
@@ -120,7 +120,7 @@ export interface CreateAPIPageOptions {
|
||||
renderAPIExampleUsageTabs?: (
|
||||
generators: CodeUsageGeneratorRegistry,
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<ReactNode>;
|
||||
) => ReactNode;
|
||||
|
||||
/**
|
||||
* renderer of the entire page's layout (containing all operations & webhooks UI)
|
||||
@@ -137,7 +137,7 @@ export interface CreateAPIPageOptions {
|
||||
}[];
|
||||
},
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<ReactNode>;
|
||||
) => ReactNode;
|
||||
|
||||
renderOperationLayout?: (
|
||||
slots: {
|
||||
@@ -154,7 +154,7 @@ export interface CreateAPIPageOptions {
|
||||
},
|
||||
ctx: RenderContext,
|
||||
method: NoReference<MethodInformation>,
|
||||
) => Awaitable<ReactNode>;
|
||||
) => ReactNode;
|
||||
|
||||
renderWebhookLayout?: (slots: {
|
||||
header: ReactNode;
|
||||
@@ -165,7 +165,7 @@ export interface CreateAPIPageOptions {
|
||||
requests: ReactNode;
|
||||
responses: ReactNode;
|
||||
callbacks: ReactNode;
|
||||
}) => Awaitable<ReactNode>;
|
||||
}) => ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -200,10 +200,7 @@ export interface CreateAPIPageOptions {
|
||||
}) => Awaitable<ReactNode>;
|
||||
};
|
||||
|
||||
renderHeading?: (
|
||||
props: HTMLAttributes<HTMLHeadingElement>,
|
||||
depth: number,
|
||||
) => Awaitable<ReactNode>;
|
||||
renderHeading?: (props: HTMLAttributes<HTMLHeadingElement>, depth: number) => ReactNode;
|
||||
renderCodeBlock?: (props: { lang: string; code: string }) => Awaitable<ReactNode>;
|
||||
|
||||
client?: APIPageClientOptions;
|
||||
@@ -258,7 +255,7 @@ export function createAPIPage(
|
||||
...options.mediaAdapters,
|
||||
},
|
||||
slugger,
|
||||
async renderHeading(depth, text, props) {
|
||||
renderHeading(depth, text, props) {
|
||||
const id = typeof text === 'string' ? slugger.slug(text) : props?.id;
|
||||
if (!id) throw new Error("missing 'id' for non-string children");
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import type { PlaygroundClientOptions } from '@/playground/client';
|
||||
import type { MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { FC } from 'react';
|
||||
import type { ExampleRequestItem } from '../operation/request-tabs';
|
||||
import type { CodeUsageGeneratorRegistry } from '@/requests/generators';
|
||||
import type { ExampleRequestItem } from '../operation/get-example-requests';
|
||||
|
||||
export interface APIPageClientOptions {
|
||||
playground?: PlaygroundClientOptions;
|
||||
|
||||
@@ -17,6 +17,6 @@ export function useStorageKey() {
|
||||
);
|
||||
}
|
||||
|
||||
export function getStorageKey(prefix = 'fumadocs-openapi-', name: KeyName) {
|
||||
function getStorageKey(prefix = 'fumadocs-openapi-', name: KeyName) {
|
||||
return prefix + name;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
import { SelectTrigger, Select, SelectValue, SelectContent, SelectItem } from './select';
|
||||
import { type ReactNode, useState, useMemo, type ComponentProps, createContext, use } from 'react';
|
||||
|
||||
const Context = createContext<{
|
||||
type: string | null;
|
||||
setType: (type: string) => void;
|
||||
} | null>(null);
|
||||
|
||||
export function SelectTabs({
|
||||
defaultValue,
|
||||
children,
|
||||
}: {
|
||||
defaultValue?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [type, setType] = useState<string | null>(defaultValue ?? null);
|
||||
|
||||
return <Context value={useMemo(() => ({ type, setType }), [type])}>{children}</Context>;
|
||||
}
|
||||
|
||||
export function SelectTab({
|
||||
value,
|
||||
...props
|
||||
}: ComponentProps<'div'> & {
|
||||
value: string;
|
||||
}) {
|
||||
const ctx = use(Context);
|
||||
if (value !== ctx?.type) return;
|
||||
|
||||
return <div {...props}>{props.children}</div>;
|
||||
}
|
||||
|
||||
export function SelectTabTrigger({
|
||||
items,
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof SelectTrigger> & {
|
||||
items: {
|
||||
label: ReactNode;
|
||||
value: string;
|
||||
}[];
|
||||
}) {
|
||||
const { type, setType } = use(Context)!;
|
||||
|
||||
return (
|
||||
<Select value={type ?? ''} onValueChange={setType}>
|
||||
<SelectTrigger className={cn('not-prose w-fit min-w-0 *:min-w-0', className)} {...props}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{items.map(({ label, value }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,7 @@
|
||||
'use client';
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
use,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createContext, type ReactNode, use, useEffect, useMemo, useState } from 'react';
|
||||
import type { RenderContext, ServerObject } from '@/types';
|
||||
import { defaultAdapters, type MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { NoReference } from '@/utils/schema';
|
||||
import { useStorageKey } from '../client/storage-key';
|
||||
import type { APIPageClientOptions } from '../client';
|
||||
import {
|
||||
@@ -24,24 +14,13 @@ interface InheritFromContext extends Pick<RenderContext, 'shikiOptions'> {
|
||||
client: APIPageClientOptions;
|
||||
}
|
||||
|
||||
export interface ServerProviderProps {
|
||||
/**
|
||||
* Base URL for API requests
|
||||
*/
|
||||
defaultBaseUrl?: string;
|
||||
|
||||
servers: NoReference<ServerObject>[];
|
||||
interface ServerContextType {
|
||||
servers?: ServerObject[];
|
||||
server: SelectedServer | null;
|
||||
setServer: (value: string) => void;
|
||||
setServerVariables: (value: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
interface ServerContextType extends ServerProviderProps {
|
||||
/**
|
||||
* ref to selected API server (to query)
|
||||
*/
|
||||
serverRef: RefObject<SelectedServer | null>;
|
||||
}
|
||||
|
||||
const ServerContext = createContext<ServerContextType | null>(null);
|
||||
|
||||
export type ApiProviderProps = InheritFromContext;
|
||||
|
||||
export interface SelectedServer {
|
||||
@@ -55,14 +34,8 @@ interface ApiContextType extends InheritFromContext {
|
||||
codeUsages: CodeUsageGeneratorRegistry;
|
||||
}
|
||||
|
||||
interface ServerSelectType {
|
||||
server: SelectedServer | null;
|
||||
setServer: (value: string) => void;
|
||||
setServerVariables: (value: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
const ApiContext = createContext<ApiContextType | null>(null);
|
||||
const ServerSelectContext = createContext<ServerSelectType | null>(null);
|
||||
const ServerContext = createContext<ServerContextType | null>(null);
|
||||
|
||||
export function useApiContext(): ApiContextType {
|
||||
const ctx = use(ApiContext);
|
||||
@@ -72,11 +45,7 @@ export function useApiContext(): ApiContextType {
|
||||
}
|
||||
|
||||
export function useServerContext() {
|
||||
return use(ServerContext)!;
|
||||
}
|
||||
|
||||
export function useServerSelectContext(): ServerSelectType {
|
||||
const ctx = use(ServerSelectContext);
|
||||
const ctx = use(ServerContext);
|
||||
if (!ctx) throw new Error('Component must be used under <ApiProvider />');
|
||||
|
||||
return ctx;
|
||||
@@ -116,43 +85,25 @@ export function ApiProvider({
|
||||
|
||||
export function ServerProvider({
|
||||
servers,
|
||||
defaultBaseUrl,
|
||||
children,
|
||||
}: ServerProviderProps & { children: ReactNode }) {
|
||||
const serverRef = useRef<SelectedServer | null>(null);
|
||||
|
||||
return (
|
||||
<ServerContext value={useMemo(() => ({ servers, serverRef }), [servers])}>
|
||||
<ServerSelectProvider defaultBaseUrl={defaultBaseUrl}>{children}</ServerSelectProvider>
|
||||
</ServerContext>
|
||||
);
|
||||
}
|
||||
|
||||
function ServerSelectProvider({
|
||||
defaultBaseUrl,
|
||||
children,
|
||||
}: Pick<ServerProviderProps, 'defaultBaseUrl'> & {
|
||||
}: {
|
||||
servers?: ServerObject[];
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { servers, serverRef } = use(ServerContext)!;
|
||||
const storageKeys = useStorageKey();
|
||||
const storageKey = useStorageKey().of('server-url');
|
||||
const [server, setServer] = useState<SelectedServer | null>(() => {
|
||||
const defaultItem = defaultBaseUrl
|
||||
? servers.find((item) => item.url === defaultBaseUrl)
|
||||
: servers[0];
|
||||
if (!servers || servers.length === 0) return null;
|
||||
const defaultItem = servers[0];
|
||||
|
||||
return defaultItem
|
||||
? {
|
||||
name: defaultItem.name,
|
||||
url: defaultItem.url!,
|
||||
variables: getDefaultValues(defaultItem),
|
||||
}
|
||||
: null;
|
||||
return {
|
||||
name: defaultItem.name,
|
||||
url: defaultItem.url!,
|
||||
variables: getDefaultValues(defaultItem),
|
||||
};
|
||||
});
|
||||
serverRef.current = server;
|
||||
|
||||
useEffect(() => {
|
||||
const cached = localStorage.getItem(storageKeys.of('server-url'));
|
||||
const cached = localStorage.getItem(storageKey);
|
||||
if (!cached) return;
|
||||
|
||||
try {
|
||||
@@ -171,24 +122,25 @@ function ServerSelectProvider({
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [storageKeys]);
|
||||
}, [storageKey]);
|
||||
|
||||
return (
|
||||
<ServerSelectContext
|
||||
<ServerContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
servers,
|
||||
server,
|
||||
setServerVariables(variables) {
|
||||
setServer((prev) => {
|
||||
if (!prev) return null;
|
||||
|
||||
const updated = { ...prev, variables };
|
||||
localStorage.setItem(storageKeys.of('server-url'), JSON.stringify(updated));
|
||||
localStorage.setItem(storageKey, JSON.stringify(updated));
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
setServer(value) {
|
||||
const obj = servers.find((item) => item.url === value);
|
||||
const obj = servers?.find((item) => item.url === value);
|
||||
if (!obj) return;
|
||||
|
||||
const result: SelectedServer = {
|
||||
@@ -197,19 +149,19 @@ function ServerSelectProvider({
|
||||
variables: getDefaultValues(obj),
|
||||
};
|
||||
|
||||
localStorage.setItem(storageKeys.of('server-url'), JSON.stringify(result));
|
||||
localStorage.setItem(storageKey, JSON.stringify(result));
|
||||
setServer(result);
|
||||
},
|
||||
}),
|
||||
[server, servers, storageKeys],
|
||||
[server, servers, storageKey],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ServerSelectContext>
|
||||
</ServerContext>
|
||||
);
|
||||
}
|
||||
|
||||
function getDefaultValues(server: NoReference<ServerObject>): Record<string, string> {
|
||||
function getDefaultValues(server: ServerObject): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
if (!server.variables) return out;
|
||||
|
||||
|
||||
@@ -4,15 +4,91 @@ import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { type ComponentProps, createContext, type ReactNode, use, useMemo, useState } from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/ui/components/select';
|
||||
import { createContext, type ReactNode, use, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslations } from '@/ui/client/i18n';
|
||||
import type { ExampleRequestItem } from './get-example-requests';
|
||||
import type { RawRequestData, RequestData } from '@/requests/types';
|
||||
|
||||
export type ExampleUpdateListener = (data: RawRequestData, encoded: RequestData) => void;
|
||||
|
||||
const OperationContext = createContext<{
|
||||
route: string;
|
||||
examples: ExampleRequestItem[];
|
||||
example: string | undefined;
|
||||
setExample: (id: string) => void;
|
||||
setExampleData: (data: RawRequestData, encoded: RequestData) => void;
|
||||
|
||||
addListener: (listener: ExampleUpdateListener) => void;
|
||||
removeListener: (listener: ExampleUpdateListener) => void;
|
||||
} | null>(null);
|
||||
|
||||
export function OperationProvider({
|
||||
route,
|
||||
examples,
|
||||
defaultExampleId,
|
||||
children,
|
||||
}: {
|
||||
route: string;
|
||||
examples: ExampleRequestItem[];
|
||||
defaultExampleId?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [example, setExample] = useState(() => defaultExampleId ?? examples.at(0)?.id);
|
||||
const listeners = useRef<ExampleUpdateListener[]>([]);
|
||||
|
||||
return (
|
||||
<OperationContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
example,
|
||||
route,
|
||||
setExample(newKey: string) {
|
||||
const example = examples.find((example) => example.id === newKey);
|
||||
if (!example) return;
|
||||
|
||||
setExample(newKey);
|
||||
for (const listener of listeners.current) {
|
||||
listener(example.data, example.encoded);
|
||||
}
|
||||
},
|
||||
examples,
|
||||
setExampleData(data, encoded) {
|
||||
for (const item of examples) {
|
||||
if (item.id === example) {
|
||||
// persistent changes
|
||||
item.data = data;
|
||||
item.encoded = encoded;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const listener of listeners.current) {
|
||||
listener(data, encoded);
|
||||
}
|
||||
},
|
||||
removeListener(listener) {
|
||||
listeners.current = listeners.current.filter((item) => item !== listener);
|
||||
},
|
||||
addListener(listener) {
|
||||
// initial call to listeners to ensure their data is the latest
|
||||
// this is necessary to avoid race conditions between `useEffect()`
|
||||
const active = examples.find((item) => item.id === example)!;
|
||||
|
||||
listener(active.data, active.encoded);
|
||||
listeners.current.push(listener);
|
||||
},
|
||||
}),
|
||||
[example, route, examples],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</OperationContext>
|
||||
);
|
||||
}
|
||||
|
||||
export function useOperationContext() {
|
||||
return use(OperationContext)!;
|
||||
}
|
||||
|
||||
export function CopyTypeScriptPanel({
|
||||
name,
|
||||
@@ -56,60 +132,3 @@ export function CopyTypeScriptPanel({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Context = createContext<{
|
||||
type: string | null;
|
||||
setType: (type: string) => void;
|
||||
} | null>(null);
|
||||
|
||||
export function SelectTabs({
|
||||
defaultValue,
|
||||
children,
|
||||
}: {
|
||||
defaultValue?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [type, setType] = useState<string | null>(defaultValue ?? null);
|
||||
|
||||
return <Context value={useMemo(() => ({ type, setType }), [type])}>{children}</Context>;
|
||||
}
|
||||
|
||||
export function SelectTab({
|
||||
value,
|
||||
...props
|
||||
}: ComponentProps<'div'> & {
|
||||
value: string;
|
||||
}) {
|
||||
const ctx = use(Context);
|
||||
if (value !== ctx?.type) return;
|
||||
|
||||
return <div {...props}>{props.children}</div>;
|
||||
}
|
||||
|
||||
export function SelectTabTrigger({
|
||||
items,
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof SelectTrigger> & {
|
||||
items: {
|
||||
label: ReactNode;
|
||||
value: string;
|
||||
}[];
|
||||
}) {
|
||||
const { type, setType } = use(Context)!;
|
||||
|
||||
return (
|
||||
<Select value={type ?? ''} onValueChange={setType}>
|
||||
<SelectTrigger className={cn('not-prose w-fit min-w-0 *:min-w-0', className)} {...props}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{items.map(({ label, value }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { encodeRequestData } from '@/requests/media/encode';
|
||||
import type { RawRequestData, RequestData } from '@/requests/types';
|
||||
import type { MethodInformation, RenderContext } from '@/types';
|
||||
import { type NoReference, getPreferredType, pickExample } from '@/utils/schema';
|
||||
import { sample } from 'openapi-sampler';
|
||||
|
||||
export interface ExampleRequestItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
data: RawRequestData;
|
||||
encoded: RequestData;
|
||||
}
|
||||
|
||||
export function getExampleRequests(
|
||||
path: string,
|
||||
operation: NoReference<MethodInformation>,
|
||||
ctx: RenderContext,
|
||||
): ExampleRequestItem[] {
|
||||
const requestBody = operation.requestBody;
|
||||
const media = requestBody?.content ? getPreferredType(requestBody.content) : null;
|
||||
const bodyOfType = media ? requestBody!.content![media] : null;
|
||||
|
||||
if (bodyOfType?.examples) {
|
||||
const result: ExampleRequestItem[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(bodyOfType.examples)) {
|
||||
const data = getRequestData(path, operation, key, ctx);
|
||||
|
||||
result.push({
|
||||
id: key,
|
||||
name: value.summary || key,
|
||||
description: value.description,
|
||||
data,
|
||||
encoded: encodeRequestData(data, ctx.mediaAdapters, operation.parameters ?? []),
|
||||
});
|
||||
}
|
||||
|
||||
if (result.length > 0) return result;
|
||||
}
|
||||
|
||||
const data = getRequestData(path, operation, null, ctx);
|
||||
return [
|
||||
{
|
||||
id: '_default',
|
||||
name: 'Default',
|
||||
description:
|
||||
typeof bodyOfType?.schema === 'object' ? bodyOfType.schema.description : undefined,
|
||||
data,
|
||||
encoded: encodeRequestData(data, ctx.mediaAdapters, operation.parameters ?? []),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getRequestData(
|
||||
path: string,
|
||||
method: NoReference<MethodInformation>,
|
||||
sampleKey: string | null,
|
||||
_ctx: RenderContext,
|
||||
): RawRequestData {
|
||||
const result: RawRequestData = {
|
||||
path: {},
|
||||
cookie: {},
|
||||
header: {},
|
||||
query: {},
|
||||
method: method.method,
|
||||
};
|
||||
|
||||
for (const param of method.parameters ?? []) {
|
||||
let value = pickExample(param as never);
|
||||
|
||||
if (value === undefined && param.required) {
|
||||
if (param.schema) {
|
||||
value = sample(param.schema as object);
|
||||
} else if (param.content) {
|
||||
const type = getPreferredType(param.content);
|
||||
const content = type ? param.content[type] : undefined;
|
||||
if (!content || !content.schema)
|
||||
throw new Error(
|
||||
`Cannot find "${param.name}" parameter info for media type "${type}" in ${path} ${method.method}`,
|
||||
);
|
||||
|
||||
value = sample(content.schema as object);
|
||||
}
|
||||
}
|
||||
|
||||
switch (param.in) {
|
||||
case 'cookie':
|
||||
result.cookie[param.name!] = value;
|
||||
break;
|
||||
case 'header':
|
||||
result.header[param.name!] = value;
|
||||
break;
|
||||
case 'query':
|
||||
result.query[param.name!] = value;
|
||||
break;
|
||||
default:
|
||||
result.path[param.name!] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (method.requestBody?.content) {
|
||||
const body = method.requestBody.content;
|
||||
const type = getPreferredType(body);
|
||||
if (!type)
|
||||
throw new Error(`Cannot find body schema for ${path} ${method.method}: missing media type`);
|
||||
result.bodyMediaType = type as RawRequestData['bodyMediaType'];
|
||||
const bodyOfType = body[type];
|
||||
|
||||
if (bodyOfType.examples && sampleKey) {
|
||||
result.body = bodyOfType.examples[sampleKey].value;
|
||||
} else if (bodyOfType.example) {
|
||||
result.body = bodyOfType.example;
|
||||
} else {
|
||||
result.body = sample((bodyOfType?.schema ?? {}) as object, {
|
||||
skipReadOnly: method.method !== 'GET',
|
||||
skipWriteOnly: method.method === 'GET',
|
||||
skipNonRequired: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -4,13 +4,14 @@ import type {
|
||||
MethodInformation,
|
||||
RenderContext,
|
||||
SecuritySchemeObject,
|
||||
ServerObject,
|
||||
} from '@/types';
|
||||
import { createMethod, methodKeys, type NoReference, type ResolvedSchema } from '@/utils/schema';
|
||||
import { idToTitle } from '@/utils/id-to-title';
|
||||
import { Schema } from '../schema';
|
||||
import { UsageTabs } from '@/ui/operation/usage-tabs';
|
||||
import { Badge, MethodLabel } from '@/ui/components/method-label';
|
||||
import { CopyTypeScriptPanel, SelectTab, SelectTabs, SelectTabTrigger } from './client';
|
||||
import { CopyTypeScriptPanel, OperationProvider } from './client';
|
||||
import { I18nLabel } from '@/ui/client/i18n';
|
||||
import {
|
||||
AccordionContent,
|
||||
@@ -21,14 +22,15 @@ import {
|
||||
} from '@/ui/components/accordion';
|
||||
import { isMediaTypeSupported } from '@/requests/media/adapter';
|
||||
import { APIPlayground } from '@/playground';
|
||||
import { getExampleRequests, RequestTabs } from './request-tabs';
|
||||
import { UsageTabsProviderLazy } from './usage-tabs/lazy';
|
||||
import { RequestTabs } from './request-tabs';
|
||||
import { ServerProviderLazy } from '../contexts/api.lazy';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getExampleRequests } from './get-example-requests';
|
||||
import { SelectTabs, SelectTabTrigger, SelectTab } from '../components/server-tab';
|
||||
|
||||
const paramTypeKeys = ['path', 'query', 'header', 'cookie'] as const;
|
||||
|
||||
export async function Operation({
|
||||
export function Operation({
|
||||
type = 'operation',
|
||||
path,
|
||||
method,
|
||||
@@ -261,6 +263,8 @@ export async function Operation({
|
||||
}
|
||||
|
||||
let { renderOperationLayout, renderWebhookLayout } = ctx.content ?? {};
|
||||
const exampleRequests = getExampleRequests(path, method, ctx);
|
||||
|
||||
if (type === 'operation') {
|
||||
renderOperationLayout ??= (slots) => {
|
||||
return (
|
||||
@@ -283,7 +287,7 @@ export async function Operation({
|
||||
};
|
||||
|
||||
const playgroundEnabled = ctx.playground?.enabled ?? true;
|
||||
let content = await renderOperationLayout(
|
||||
let content = renderOperationLayout(
|
||||
{
|
||||
header: headNode,
|
||||
description: descriptionNode,
|
||||
@@ -309,16 +313,20 @@ export async function Operation({
|
||||
);
|
||||
|
||||
content = (
|
||||
<UsageTabsProviderLazy
|
||||
<OperationProvider
|
||||
defaultExampleId={method['x-exclusiveCodeSample'] ?? method['x-selectedCodeSample']}
|
||||
route={path}
|
||||
examples={getExampleRequests(path, method, ctx)}
|
||||
examples={exampleRequests}
|
||||
>
|
||||
{content}
|
||||
</UsageTabsProviderLazy>
|
||||
</OperationProvider>
|
||||
);
|
||||
if (method.servers) {
|
||||
content = <ServerProviderLazy servers={method.servers}>{content}</ServerProviderLazy>;
|
||||
content = (
|
||||
<ServerProviderLazy servers={method.servers as ServerObject[]}>
|
||||
{content}
|
||||
</ServerProviderLazy>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
@@ -347,12 +355,12 @@ export async function Operation({
|
||||
callbacks: callbacksNode,
|
||||
parameters: parameterNode,
|
||||
responses: responseNode,
|
||||
requests: <RequestTabs path={path} operation={method} ctx={ctx} />,
|
||||
requests: <RequestTabs examples={exampleRequests} path={path} operation={method} ctx={ctx} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function ResponseAccordion({
|
||||
function ResponseAccordion({
|
||||
status,
|
||||
operation,
|
||||
ctx,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MethodInformation, RenderContext } from '@/types';
|
||||
import { getPreferredType, type NoReference, pickExample } from '@/utils/schema';
|
||||
import type { NoReference } from '@/utils/schema';
|
||||
import { I18nLabel } from '@/ui/client/i18n';
|
||||
import {
|
||||
AccordionContent,
|
||||
@@ -8,146 +8,28 @@ import {
|
||||
Accordions,
|
||||
AccordionTrigger,
|
||||
} from '@/ui/components/accordion';
|
||||
import { sample } from 'openapi-sampler';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { RawRequestData, RequestData } from '@/requests/types';
|
||||
import { encodeRequestData } from '@/requests/media/encode';
|
||||
import type { RawRequestData } from '@/requests/types';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'fumadocs-ui/components/tabs';
|
||||
import { resolveRequestData } from '@/utils/url';
|
||||
import { MethodLabel } from '../components/method-label';
|
||||
import type { ExampleRequestItem } from './get-example-requests';
|
||||
|
||||
export interface ExampleRequestItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
data: RawRequestData;
|
||||
encoded: RequestData;
|
||||
}
|
||||
|
||||
export function getExampleRequests(
|
||||
path: string,
|
||||
operation: NoReference<MethodInformation>,
|
||||
ctx: RenderContext,
|
||||
): ExampleRequestItem[] {
|
||||
const requestBody = operation.requestBody;
|
||||
const media = requestBody?.content ? getPreferredType(requestBody.content) : null;
|
||||
const bodyOfType = media ? requestBody!.content![media] : null;
|
||||
|
||||
if (bodyOfType?.examples) {
|
||||
const result: ExampleRequestItem[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(bodyOfType.examples)) {
|
||||
const data = getRequestData(path, operation, key, ctx);
|
||||
|
||||
result.push({
|
||||
id: key,
|
||||
name: value.summary || key,
|
||||
description: value.description,
|
||||
data,
|
||||
encoded: encodeRequestData(data, ctx.mediaAdapters, operation.parameters ?? []),
|
||||
});
|
||||
}
|
||||
|
||||
if (result.length > 0) return result;
|
||||
}
|
||||
|
||||
const data = getRequestData(path, operation, null, ctx);
|
||||
return [
|
||||
{
|
||||
id: '_default',
|
||||
name: 'Default',
|
||||
description:
|
||||
typeof bodyOfType?.schema === 'object' ? bodyOfType.schema.description : undefined,
|
||||
data,
|
||||
encoded: encodeRequestData(data, ctx.mediaAdapters, operation.parameters ?? []),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getRequestData(
|
||||
path: string,
|
||||
method: NoReference<MethodInformation>,
|
||||
sampleKey: string | null,
|
||||
_ctx: RenderContext,
|
||||
): RawRequestData {
|
||||
const result: RawRequestData = {
|
||||
path: {},
|
||||
cookie: {},
|
||||
header: {},
|
||||
query: {},
|
||||
method: method.method,
|
||||
};
|
||||
|
||||
for (const param of method.parameters ?? []) {
|
||||
let value = pickExample(param as never);
|
||||
|
||||
if (value === undefined && param.required) {
|
||||
if (param.schema) {
|
||||
value = sample(param.schema as object);
|
||||
} else if (param.content) {
|
||||
const type = getPreferredType(param.content);
|
||||
const content = type ? param.content[type] : undefined;
|
||||
if (!content || !content.schema)
|
||||
throw new Error(
|
||||
`Cannot find "${param.name}" parameter info for media type "${type}" in ${path} ${method.method}`,
|
||||
);
|
||||
|
||||
value = sample(content.schema as object);
|
||||
}
|
||||
}
|
||||
|
||||
switch (param.in) {
|
||||
case 'cookie':
|
||||
result.cookie[param.name!] = value;
|
||||
break;
|
||||
case 'header':
|
||||
result.header[param.name!] = value;
|
||||
break;
|
||||
case 'query':
|
||||
result.query[param.name!] = value;
|
||||
break;
|
||||
default:
|
||||
result.path[param.name!] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (method.requestBody?.content) {
|
||||
const body = method.requestBody.content;
|
||||
const type = getPreferredType(body);
|
||||
if (!type)
|
||||
throw new Error(`Cannot find body schema for ${path} ${method.method}: missing media type`);
|
||||
result.bodyMediaType = type as RawRequestData['bodyMediaType'];
|
||||
const bodyOfType = body[type];
|
||||
|
||||
if (bodyOfType.examples && sampleKey) {
|
||||
result.body = bodyOfType.examples[sampleKey].value;
|
||||
} else if (bodyOfType.example) {
|
||||
result.body = bodyOfType.example;
|
||||
} else {
|
||||
result.body = sample((bodyOfType?.schema ?? {}) as object, {
|
||||
skipReadOnly: method.method !== 'GET',
|
||||
skipWriteOnly: method.method === 'GET',
|
||||
skipNonRequired: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function RequestTabs({
|
||||
export function RequestTabs({
|
||||
path,
|
||||
operation,
|
||||
examples,
|
||||
ctx,
|
||||
}: {
|
||||
path: string;
|
||||
examples: ExampleRequestItem[];
|
||||
operation: NoReference<MethodInformation>;
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
if (!operation.requestBody) return null;
|
||||
const { renderRequestTabs = renderRequestTabsDefault } = ctx.content ?? {};
|
||||
|
||||
return renderRequestTabs(getExampleRequests(path, operation, ctx), {
|
||||
return renderRequestTabs(examples, {
|
||||
...ctx,
|
||||
route: path,
|
||||
operation,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import { useApiContext, useServerSelectContext } from '@/ui/contexts/api';
|
||||
import { useApiContext, useServerContext } from '@/ui/contexts/api';
|
||||
import { joinURL, withBase, resolveServerUrl, resolveRequestData } from '@/utils/url';
|
||||
import {
|
||||
Select,
|
||||
@@ -8,95 +8,14 @@ import {
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from '@/ui/components/select';
|
||||
import { useState, useEffect, useMemo, createContext, ReactNode, useRef, use } from 'react';
|
||||
import type { ExampleRequestItem } from '../request-tabs';
|
||||
import type { RawRequestData, RequestData } from '@/requests/types';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import type { CodeUsageGenerator } from '@/requests/generators';
|
||||
import { ClientCodeBlock } from '@/ui/components/codeblock';
|
||||
|
||||
export type ExampleUpdateListener = (data: RawRequestData, encoded: RequestData) => void;
|
||||
|
||||
const Context = createContext<{
|
||||
route: string;
|
||||
examples: ExampleRequestItem[];
|
||||
example: string | undefined;
|
||||
setExample: (id: string) => void;
|
||||
setExampleData: (data: RawRequestData, encoded: RequestData) => void;
|
||||
|
||||
addListener: (listener: ExampleUpdateListener) => void;
|
||||
removeListener: (listener: ExampleUpdateListener) => void;
|
||||
} | null>(null);
|
||||
|
||||
export function UsageTabsProvider({
|
||||
route,
|
||||
examples,
|
||||
defaultExampleId,
|
||||
children,
|
||||
}: {
|
||||
route: string;
|
||||
examples: ExampleRequestItem[];
|
||||
defaultExampleId?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [example, setExample] = useState(() => defaultExampleId ?? examples.at(0)?.id);
|
||||
const listeners = useRef<ExampleUpdateListener[]>([]);
|
||||
|
||||
return (
|
||||
<Context
|
||||
value={useMemo(
|
||||
() => ({
|
||||
example,
|
||||
route,
|
||||
setExample(newKey: string) {
|
||||
const example = examples.find((example) => example.id === newKey);
|
||||
if (!example) return;
|
||||
|
||||
setExample(newKey);
|
||||
for (const listener of listeners.current) {
|
||||
listener(example.data, example.encoded);
|
||||
}
|
||||
},
|
||||
examples,
|
||||
setExampleData(data, encoded) {
|
||||
for (const item of examples) {
|
||||
if (item.id === example) {
|
||||
// persistent changes
|
||||
item.data = data;
|
||||
item.encoded = encoded;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const listener of listeners.current) {
|
||||
listener(data, encoded);
|
||||
}
|
||||
},
|
||||
removeListener(listener) {
|
||||
listeners.current = listeners.current.filter((item) => item !== listener);
|
||||
},
|
||||
addListener(listener) {
|
||||
// initial call to listeners to ensure their data is the latest
|
||||
// this is necessary to avoid race conditions between `useEffect()`
|
||||
const active = examples.find((item) => item.id === example)!;
|
||||
|
||||
listener(active.data, active.encoded);
|
||||
listeners.current.push(listener);
|
||||
},
|
||||
}),
|
||||
[example, route, examples],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Context>
|
||||
);
|
||||
}
|
||||
|
||||
export function useExampleRequests() {
|
||||
return use(Context)!;
|
||||
}
|
||||
import { type ExampleUpdateListener, useOperationContext } from '../client';
|
||||
import type { ExampleRequestItem } from '../get-example-requests';
|
||||
|
||||
export function UsageTabsSelector() {
|
||||
const { example: key, setExample: setKey, examples } = useExampleRequests();
|
||||
const { example: key, setExample: setKey, examples } = useOperationContext();
|
||||
const { APIExampleSelector: Override } = useApiContext().client.operation ?? {};
|
||||
|
||||
if (Override) {
|
||||
@@ -142,8 +61,8 @@ export function UsageTab({
|
||||
route,
|
||||
addListener,
|
||||
removeListener,
|
||||
} = useExampleRequests();
|
||||
const { server } = useServerSelectContext();
|
||||
} = useOperationContext();
|
||||
const { server } = useServerContext();
|
||||
const codegen = codeUsages.get(id);
|
||||
const [data, setData] = useState(
|
||||
() => examples.find((example) => example.id === selectedExampleId)?.encoded,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { MethodInformation, RenderContext } from '@/types';
|
||||
import type { CodeUsageGeneratorFn } from '@/requests/generators';
|
||||
import {
|
||||
type CodeUsageGeneratorRegistry,
|
||||
createCodeUsageGeneratorRegistry,
|
||||
@@ -14,27 +13,6 @@ import { UsageTabsSelectorLazy, UsageTabLazy } from './lazy';
|
||||
import { ResponseTabs } from '../response-tabs';
|
||||
import { registerDefault } from '@/requests/generators/all';
|
||||
|
||||
/**
|
||||
* Generate code example for given programming language
|
||||
*/
|
||||
export interface CodeUsageGenerator<T = unknown> {
|
||||
id: string;
|
||||
lang: string;
|
||||
label?: string;
|
||||
/**
|
||||
* either:
|
||||
* - code
|
||||
* - a function imported from a file with "use client" directive
|
||||
* - false (disabled)
|
||||
*/
|
||||
source?: string | CodeUsageGeneratorFn<T> | false;
|
||||
|
||||
/**
|
||||
* Pass extra context to client-side source generator
|
||||
*/
|
||||
serverContext?: T;
|
||||
}
|
||||
|
||||
export async function UsageTabs({
|
||||
method,
|
||||
ctx,
|
||||
@@ -97,7 +75,7 @@ export async function UsageTabs({
|
||||
return renderAPIExampleLayout(
|
||||
{
|
||||
selector: method['x-exclusiveCodeSample'] ? null : <UsageTabsSelectorLazy />,
|
||||
usageTabs: await renderAPIExampleUsageTabs(registry, ctx),
|
||||
usageTabs: renderAPIExampleUsageTabs(registry, ctx),
|
||||
responseTabs: <ResponseTabs operation={method} ctx={ctx} />,
|
||||
},
|
||||
ctx,
|
||||
|
||||
@@ -8,7 +8,3 @@ export const UsageTabsSelectorLazy = wrapLazy(() =>
|
||||
export const UsageTabLazy = wrapLazy(() =>
|
||||
import('./client').then((mod) => ({ default: mod.UsageTab })),
|
||||
);
|
||||
|
||||
export const UsageTabsProviderLazy = wrapLazy(() =>
|
||||
import('./client').then((mod) => ({ default: mod.UsageTabsProvider })),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ResolvedSchema } from '@/utils/schema';
|
||||
import type { RenderContext } from '@/types';
|
||||
import { FormatFlags, schemaToString } from '@/utils/schema-to-string';
|
||||
import { FormatFlags, schemaToString } from '@/utils/schema/to-string';
|
||||
import { mergeAllOf } from '@/utils/merge-schema';
|
||||
import type { SchemaUIProps } from '@/ui/schema/client';
|
||||
import { SchemaUILazy } from '@/ui/schema/lazy';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { bundle } from '@scalar/json-magic/bundle';
|
||||
import { upgrade } from '@scalar/openapi-upgrader';
|
||||
import { fetchUrls, readFiles } from '@scalar/json-magic/bundle/plugins/node';
|
||||
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
||||
import dereference from 'dereference-json-schema';
|
||||
import { dereferenceSync } from './schema/dereference';
|
||||
|
||||
export interface ProcessedDocument {
|
||||
/**
|
||||
@@ -55,60 +55,3 @@ export async function processDocument(input: string | Document): Promise<Process
|
||||
bundled,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves all $ref pointers in a schema and returns a new schema without any $ref pointers.
|
||||
*/
|
||||
function dereferenceSync(
|
||||
schema: JSONSchema,
|
||||
setOriginalRef: (schema: JSONSchema, ref: string) => void,
|
||||
): JSONSchema {
|
||||
if (typeof schema === 'boolean') return schema;
|
||||
const visitedNodes = new Set<unknown>();
|
||||
const cloned = structuredClone(schema);
|
||||
|
||||
function resolve(current: unknown): JSONSchema {
|
||||
if (typeof current === 'object' && current !== null) {
|
||||
// make sure we don't visit the same node twice
|
||||
if (visitedNodes.has(current)) {
|
||||
return current;
|
||||
}
|
||||
visitedNodes.add(current);
|
||||
|
||||
if (Array.isArray(current)) {
|
||||
// array
|
||||
for (let index = 0; index < current.length; index++) {
|
||||
current[index] = resolve(current[index]);
|
||||
}
|
||||
|
||||
return current as JSONSchema;
|
||||
}
|
||||
|
||||
const obj = current as Record<string, unknown>;
|
||||
|
||||
// object
|
||||
if ('$ref' in current && typeof current['$ref'] === 'string') {
|
||||
const ref = current['$ref'];
|
||||
delete current['$ref'];
|
||||
const resolved = resolve(dereference.resolveRefSync(cloned as never, ref) as JSONSchema);
|
||||
setOriginalRef(resolved, ref);
|
||||
setOriginalRef(current as JSONSchema, ref);
|
||||
|
||||
if (typeof resolved === 'boolean') throw new Error('invalid schema');
|
||||
for (const k in resolved) {
|
||||
if (!(k in current)) {
|
||||
obj[k] = resolved[k as never];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in current) {
|
||||
obj[key] = resolve(obj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return current as JSONSchema;
|
||||
}
|
||||
|
||||
return resolve(cloned);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
||||
import { resolveRefSync } from './resolve-ref';
|
||||
|
||||
/**
|
||||
* Resolves all $ref pointers in a schema and returns a new schema without any $ref pointers.
|
||||
*/
|
||||
export function dereferenceSync(
|
||||
schema: JSONSchema,
|
||||
setOriginalRef: (schema: JSONSchema, ref: string) => void,
|
||||
): JSONSchema {
|
||||
if (typeof schema === 'boolean') return schema;
|
||||
const visitedNodes = new Set<unknown>();
|
||||
const cloned = structuredClone(schema);
|
||||
|
||||
function resolve(current: unknown): JSONSchema {
|
||||
if (typeof current === 'object' && current !== null) {
|
||||
// make sure we don't visit the same node twice
|
||||
if (visitedNodes.has(current)) {
|
||||
return current;
|
||||
}
|
||||
visitedNodes.add(current);
|
||||
|
||||
if (Array.isArray(current)) {
|
||||
// array
|
||||
for (let index = 0; index < current.length; index++) {
|
||||
current[index] = resolve(current[index]);
|
||||
}
|
||||
|
||||
return current as JSONSchema;
|
||||
}
|
||||
|
||||
const obj = current as Record<string, unknown>;
|
||||
|
||||
// object
|
||||
if ('$ref' in current && typeof current['$ref'] === 'string') {
|
||||
const ref = current['$ref'];
|
||||
delete current['$ref'];
|
||||
const resolved = resolve(resolveRefSync(ref, cloned) as JSONSchema);
|
||||
setOriginalRef(resolved, ref);
|
||||
setOriginalRef(current as JSONSchema, ref);
|
||||
|
||||
if (typeof resolved === 'boolean') throw new Error('invalid schema');
|
||||
for (const k in resolved) {
|
||||
if (!(k in current)) {
|
||||
obj[k] = resolved[k as never];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in current) {
|
||||
obj[key] = resolve(obj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return current as JSONSchema;
|
||||
}
|
||||
|
||||
return resolve(cloned);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
||||
|
||||
export function resolveRefSync(ref: string, schema: JSONSchema): unknown | undefined {
|
||||
if (!ref.startsWith('#')) return;
|
||||
const segments = ref.slice(1).split('/');
|
||||
let current: unknown = schema;
|
||||
|
||||
for (const seg of segments) {
|
||||
if (seg.length === 0) continue;
|
||||
|
||||
if (isPlainObject(current)) current = current[seg];
|
||||
else return;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return (
|
||||
prototype === null ||
|
||||
prototype === Object.prototype ||
|
||||
Object.getPrototypeOf(prototype) === null
|
||||
);
|
||||
}
|
||||
Generated
-9
@@ -2445,9 +2445,6 @@ importers:
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
dereference-json-schema:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
github-slugger:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
@@ -7562,7 +7559,6 @@ packages:
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-AvXWYFO6j/ZQ7bhGm4X6eilq2JHsDVC90ZM32k2B7/srhC2gs3Sdki1QTbwrdRCo8o7eT+167vcB1yzOvPdbjA==}
|
||||
cpu: [arm64, x64]
|
||||
os: [darwin, linux, win32]
|
||||
hasBin: true
|
||||
|
||||
@@ -8230,9 +8226,6 @@ packages:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-w8dUsJyrzH4Zsj8W/tKcjLsmcTKXfdNf+n3BBm1SAfnqpaCodgEUWqQGJ+pNb9NOqPwYMGvUnZZ8nQfeFjJlbQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
||||
|
||||
@@ -18105,8 +18098,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
Reference in New Issue
Block a user