mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 05:23:40 +00:00
OpenAPI: Support code usage generator registry
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Support code usage generator registry
|
||||
@@ -29,40 +29,58 @@ export default defineClientConfig({
|
||||
});
|
||||
```
|
||||
|
||||
Installing the types packages is highly recommended for advanced customisations:
|
||||
### Generate Code Usages
|
||||
|
||||
```package-install
|
||||
openapi-types json-schema-typed -D
|
||||
Generate code usage examples for each API endpoint.
|
||||
|
||||
```ts title="lib/openapi/code-usage.ts" twoslash
|
||||
import {
|
||||
createCodeUsageGeneratorRegistry,
|
||||
type CodeUsageGenerator,
|
||||
} from 'fumadocs-openapi/requests/generators';
|
||||
import { registerDefault } from 'fumadocs-openapi/requests/generators/all';
|
||||
|
||||
export const codeUsages = createCodeUsageGeneratorRegistry();
|
||||
|
||||
// include defaults
|
||||
registerDefault(codeUsages);
|
||||
|
||||
// add custom generators
|
||||
codeUsages.add('custom-id', {
|
||||
label: 'My Example',
|
||||
lang: 'js',
|
||||
generate(url, data, { mediaAdapters }) {
|
||||
// request data
|
||||
console.log(url, data);
|
||||
|
||||
return 'const response = "hello world";';
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
It gives you a higher level of type-safety.
|
||||
Pass the registry to both client & server configs:
|
||||
|
||||
### Generate Code Samples
|
||||
|
||||
Generate custom code samples for each API endpoint.
|
||||
|
||||
```ts
|
||||
```tsx tab="components/api-page.tsx"
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { createAPIPage } from 'fumadocs-openapi/ui';
|
||||
import { codeUsages } from '@/lib/openapi/code-usage';
|
||||
import client from './api-page.client';
|
||||
|
||||
export const APIPage = createAPIPage(openapi, {
|
||||
generateCodeSamples(endpoint) {
|
||||
return [
|
||||
{
|
||||
id: 'js',
|
||||
lang: 'js',
|
||||
label: 'JavaScript SDK',
|
||||
source: "console.log('hello')",
|
||||
},
|
||||
// or to disable the default code samples
|
||||
// set `source: false`
|
||||
{
|
||||
id: 'curl',
|
||||
lang: 'bash',
|
||||
source: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
client,
|
||||
// [!code ++]
|
||||
codeUsages,
|
||||
});
|
||||
```
|
||||
|
||||
```tsx tab="components/api-page.client.tsx"
|
||||
'use client';
|
||||
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
|
||||
import { codeUsages } from '@/lib/openapi/code-usage';
|
||||
|
||||
export default defineClientConfig({
|
||||
// [!code ++]
|
||||
codeUsages,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -89,7 +107,7 @@ You can create a media adapter to support other media types in API pages, a medi
|
||||
|
||||
Put your media adapters in a separate file.
|
||||
|
||||
```ts title="lib/media-adapters.ts" twoslash
|
||||
```ts title="lib/openapi/media.ts" twoslash
|
||||
import type { MediaAdapter } from 'fumadocs-openapi';
|
||||
|
||||
export const mediaAdapters: Record<string, MediaAdapter> = {
|
||||
@@ -123,7 +141,7 @@ Pass the adapter to both client & server configs.
|
||||
```tsx tab="components/api-page.tsx"
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { createAPIPage } from 'fumadocs-openapi/ui';
|
||||
import { mediaAdapters } from '@/lib/media-adapters';
|
||||
import { mediaAdapters } from '@/lib/openapi/media';
|
||||
import client from './api-page.client';
|
||||
|
||||
export const APIPage = createAPIPage(openapi, {
|
||||
@@ -136,7 +154,7 @@ export const APIPage = createAPIPage(openapi, {
|
||||
```tsx tab="components/api-page.client.tsx"
|
||||
'use client';
|
||||
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
|
||||
import { mediaAdapters } from '@/lib/media-adapters';
|
||||
import { mediaAdapters } from '@/lib/openapi/media';
|
||||
|
||||
export default defineClientConfig({
|
||||
// [!code ++]
|
||||
|
||||
@@ -16,41 +16,27 @@
|
||||
],
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./css/*": "./css/*",
|
||||
"./ui": {
|
||||
"types": "./dist/ui/index.d.ts",
|
||||
"import": "./dist/ui/index.js"
|
||||
},
|
||||
"./ui/base": {
|
||||
"types": "./dist/ui/base.d.ts",
|
||||
"import": "./dist/ui/base.js"
|
||||
},
|
||||
"./ui/client": {
|
||||
"types": "./dist/ui/client/index.d.ts",
|
||||
"import": "./dist/ui/client/index.js"
|
||||
},
|
||||
"./playground": {
|
||||
"types": "./dist/playground/index.d.ts",
|
||||
"import": "./dist/playground/index.js"
|
||||
},
|
||||
"./playground/client": {
|
||||
"types": "./dist/playground/client.d.ts",
|
||||
"import": "./dist/playground/client.js"
|
||||
},
|
||||
"./scalar": {
|
||||
"types": "./dist/scalar/index.d.ts",
|
||||
"import": "./dist/scalar/index.js"
|
||||
},
|
||||
"./server": {
|
||||
"types": "./dist/server/index.d.ts",
|
||||
"import": "./dist/server/index.js"
|
||||
}
|
||||
".": "./dist/index.js",
|
||||
"./playground": "./dist/playground/index.js",
|
||||
"./playground/client": "./dist/playground/client.js",
|
||||
"./requests/generators": "./dist/requests/generators/index.js",
|
||||
"./requests/generators/all": "./dist/requests/generators/all.js",
|
||||
"./requests/generators/csharp": "./dist/requests/generators/csharp.js",
|
||||
"./requests/generators/curl": "./dist/requests/generators/curl.js",
|
||||
"./requests/generators/go": "./dist/requests/generators/go.js",
|
||||
"./requests/generators/java": "./dist/requests/generators/java.js",
|
||||
"./requests/generators/javascript": "./dist/requests/generators/javascript.js",
|
||||
"./requests/generators/python": "./dist/requests/generators/python.js",
|
||||
"./scalar": "./dist/scalar/index.js",
|
||||
"./server": "./dist/server/index.js",
|
||||
"./ui": "./dist/ui/index.js",
|
||||
"./ui/base": "./dist/ui/base.js",
|
||||
"./ui/client": "./dist/ui/client/index.js",
|
||||
"./package.json": "./package.json",
|
||||
"./css/*": "./css/*"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { CodeUsageGeneratorRegistry } from '.';
|
||||
import { csharp } from './csharp';
|
||||
import { curl } from './curl';
|
||||
import { go } from './go';
|
||||
import { java } from './java';
|
||||
import { javascript } from './javascript';
|
||||
import { python } from './python';
|
||||
|
||||
export function registerDefault(registry: CodeUsageGeneratorRegistry) {
|
||||
registry.add('curl', curl);
|
||||
registry.add('js', javascript);
|
||||
registry.add('go', go);
|
||||
registry.add('python', python);
|
||||
registry.add('java', java);
|
||||
registry.add('csharp', csharp);
|
||||
}
|
||||
@@ -1,66 +1,69 @@
|
||||
'use client';
|
||||
import type { SampleGenerator } from '@/requests/types';
|
||||
import type { CodeUsageGenerator } from '@/requests/generators';
|
||||
import { resolveMediaAdapter } from '@/requests/media/adapter';
|
||||
|
||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
const s: string[] = [];
|
||||
const imports = new Set<string>(['System', 'System.Net.Http', 'System.Text']);
|
||||
const headers = { ...data.header };
|
||||
export const csharp: CodeUsageGenerator = {
|
||||
label: 'C#',
|
||||
lang: 'csharp',
|
||||
generate(url, data, { mediaAdapters }) {
|
||||
const s: string[] = [];
|
||||
const imports = new Set<string>(['System', 'System.Net.Http', 'System.Text']);
|
||||
const headers = { ...data.header };
|
||||
|
||||
// Handle request body
|
||||
let body: string | undefined;
|
||||
if (data.body && data.bodyMediaType) {
|
||||
const adapter = resolveMediaAdapter(data.bodyMediaType, mediaAdapters);
|
||||
body = adapter?.generateExample(data as { body: unknown }, {
|
||||
lang: 'csharp',
|
||||
addImport(from) {
|
||||
imports.add(from);
|
||||
},
|
||||
});
|
||||
}
|
||||
// Handle request body
|
||||
let body: string | undefined;
|
||||
if (data.body && data.bodyMediaType) {
|
||||
const adapter = resolveMediaAdapter(data.bodyMediaType, mediaAdapters);
|
||||
body = adapter?.generateExample(data as { body: unknown }, {
|
||||
lang: 'csharp',
|
||||
addImport(from) {
|
||||
imports.add(from);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const specifier of imports) {
|
||||
s.push(`using ${specifier};`);
|
||||
}
|
||||
for (const specifier of imports) {
|
||||
s.push(`using ${specifier};`);
|
||||
}
|
||||
|
||||
s.push('');
|
||||
s.push('');
|
||||
|
||||
if (body) {
|
||||
s.push(body, '');
|
||||
}
|
||||
if (body) {
|
||||
s.push(body, '');
|
||||
}
|
||||
|
||||
s.push('var client = new HttpClient();');
|
||||
const headerLines: string[] = [];
|
||||
s.push('var client = new HttpClient();');
|
||||
const headerLines: string[] = [];
|
||||
|
||||
function addHeader(key: string, value: string) {
|
||||
headerLines.push(`client.DefaultRequestHeaders.Add("${key}", ${JSON.stringify(value)});`);
|
||||
}
|
||||
for (const k in headers) {
|
||||
addHeader(k, headers[k].value);
|
||||
}
|
||||
function addHeader(key: string, value: string) {
|
||||
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) {
|
||||
const cookie = Object.entries(data.cookie)
|
||||
.map(([key, param]) => `${key}=${param.value}`)
|
||||
.join('; ');
|
||||
// Add cookie header if cookies are present
|
||||
if (Object.keys(data.cookie).length > 0) {
|
||||
const cookie = Object.entries(data.cookie)
|
||||
.map(([key, param]) => `${key}=${param.value}`)
|
||||
.join('; ');
|
||||
|
||||
addHeader('cookie', cookie);
|
||||
}
|
||||
addHeader('cookie', cookie);
|
||||
}
|
||||
|
||||
s.push(...headerLines);
|
||||
s.push(...headerLines);
|
||||
|
||||
// Build the request
|
||||
const method = data.method[0].toUpperCase() + data.method.slice(1).toLowerCase() + 'Async';
|
||||
// Build the request
|
||||
const method = data.method[0].toUpperCase() + data.method.slice(1).toLowerCase() + 'Async';
|
||||
|
||||
if (body) {
|
||||
s.push(`var response = await client.${method}("${url}", body);`);
|
||||
} else {
|
||||
s.push(`var response = await client.${method}("${url}");`);
|
||||
}
|
||||
if (body) {
|
||||
s.push(`var response = await client.${method}("${url}", body);`);
|
||||
} else {
|
||||
s.push(`var response = await client.${method}("${url}");`);
|
||||
}
|
||||
|
||||
// Add response handling
|
||||
s.push('var responseBody = await response.Content.ReadAsStringAsync();');
|
||||
// Add response handling
|
||||
s.push('var responseBody = await response.Content.ReadAsStringAsync();');
|
||||
|
||||
return s.join('\n');
|
||||
return s.join('\n');
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,42 +1,45 @@
|
||||
'use client';
|
||||
import { escapeString, ident, inputToString } from '@/requests/string-utils';
|
||||
import type { SampleGenerator } from '@/requests/types';
|
||||
import type { CodeUsageGenerator } from '@/requests/generators';
|
||||
|
||||
export const generator: SampleGenerator = (url, data) => {
|
||||
const s: string[] = [];
|
||||
s.push(`curl -X ${data.method} "${url}"`);
|
||||
export const curl: CodeUsageGenerator = {
|
||||
label: 'cURL',
|
||||
lang: 'bash',
|
||||
generate(url, data) {
|
||||
const s: string[] = [];
|
||||
s.push(`curl -X ${data.method} "${url}"`);
|
||||
|
||||
for (const header in data.header) {
|
||||
const value = `${header}: ${data.header[header].value}`;
|
||||
for (const header in data.header) {
|
||||
const value = `${header}: ${data.header[header].value}`;
|
||||
|
||||
s.push(`-H "${value}"`);
|
||||
}
|
||||
|
||||
for (const k in data.cookie) {
|
||||
const param = data.cookie[k];
|
||||
|
||||
s.push(`--cookie ${JSON.stringify(`${k}=${param.value}`)}`);
|
||||
}
|
||||
|
||||
if (data.body && data.bodyMediaType === 'multipart/form-data') {
|
||||
if (typeof data.body !== 'object') throw new Error('[CURL] request body must be an object.');
|
||||
|
||||
for (const [key, value] of Object.entries(data.body)) {
|
||||
s.push(`-F ${key}=${JSON.stringify(inputToString(value))}`);
|
||||
s.push(`-H "${value}"`);
|
||||
}
|
||||
} else if (data.body && data.bodyMediaType) {
|
||||
const escaped = escapeString(
|
||||
inputToString(
|
||||
data.body,
|
||||
// @ts-expect-error -- assume the body media type is supported
|
||||
data.bodyMediaType,
|
||||
),
|
||||
"'",
|
||||
);
|
||||
|
||||
s.push(`-H "Content-Type: ${data.bodyMediaType}"`);
|
||||
s.push(`-d ${escaped}`);
|
||||
}
|
||||
for (const k in data.cookie) {
|
||||
const param = data.cookie[k];
|
||||
|
||||
return s.flatMap((v, i) => ident(v, i > 0 ? 1 : 0)).join(' \\\n');
|
||||
s.push(`--cookie ${JSON.stringify(`${k}=${param.value}`)}`);
|
||||
}
|
||||
|
||||
if (data.body && data.bodyMediaType === 'multipart/form-data') {
|
||||
if (typeof data.body !== 'object') throw new Error('[CURL] request body must be an object.');
|
||||
|
||||
for (const [key, value] of Object.entries(data.body)) {
|
||||
s.push(`-F ${key}=${JSON.stringify(inputToString(value))}`);
|
||||
}
|
||||
} else if (data.body && data.bodyMediaType) {
|
||||
const escaped = escapeString(
|
||||
inputToString(
|
||||
data.body,
|
||||
// @ts-expect-error -- assume the body media type is supported
|
||||
data.bodyMediaType,
|
||||
),
|
||||
"'",
|
||||
);
|
||||
|
||||
s.push(`-H "Content-Type: ${data.bodyMediaType}"`);
|
||||
s.push(`-d ${escaped}`);
|
||||
}
|
||||
|
||||
return s.flatMap((v, i) => ident(v, i > 0 ? 1 : 0)).join(' \\\n');
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
'use client';
|
||||
import { ident } from '@/requests/string-utils';
|
||||
import type { SampleGenerator } from '@/requests/types';
|
||||
import type { CodeUsageGenerator } from '@/requests/generators';
|
||||
import { resolveMediaAdapter } from '@/requests/media/adapter';
|
||||
|
||||
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(url));
|
||||
export const go: CodeUsageGenerator = {
|
||||
label: 'Go',
|
||||
lang: 'go',
|
||||
generate(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(url));
|
||||
|
||||
for (const header in data.header) {
|
||||
headers.set(header, JSON.stringify(data.header[header].value));
|
||||
}
|
||||
for (const header in data.header) {
|
||||
headers.set(header, JSON.stringify(data.header[header].value));
|
||||
}
|
||||
|
||||
const cookies = Object.entries(data.cookie);
|
||||
if (cookies.length > 0) {
|
||||
headers.set(
|
||||
'Cookie',
|
||||
JSON.stringify(cookies.map(([k, param]) => `${k}=${param.value}`).join('; ')),
|
||||
);
|
||||
}
|
||||
const cookies = Object.entries(data.cookie);
|
||||
if (cookies.length > 0) {
|
||||
headers.set(
|
||||
'Cookie',
|
||||
JSON.stringify(cookies.map(([k, param]) => `${k}=${param.value}`).join('; ')),
|
||||
);
|
||||
}
|
||||
|
||||
let body: string | undefined;
|
||||
let body: string | undefined;
|
||||
|
||||
if (data.body && data.bodyMediaType) {
|
||||
const adapter = resolveMediaAdapter(data.bodyMediaType, mediaAdapters);
|
||||
headers.set('Content-Type', `"${data.bodyMediaType}"`);
|
||||
body = adapter?.generateExample(data as { body: unknown }, {
|
||||
lang: 'go',
|
||||
addImport(from) {
|
||||
imports.push(from);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (data.body && data.bodyMediaType) {
|
||||
const adapter = resolveMediaAdapter(data.bodyMediaType, mediaAdapters);
|
||||
headers.set('Content-Type', `"${data.bodyMediaType}"`);
|
||||
body = adapter?.generateExample(data as { body: unknown }, {
|
||||
lang: 'go',
|
||||
addImport(from) {
|
||||
imports.push(from);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return `package main
|
||||
return `package main
|
||||
|
||||
import (
|
||||
${ident(imports.map((v) => `"${v}"`).join('\n'))}
|
||||
@@ -58,4 +60,5 @@ ${ident(
|
||||
fmt.Println(res)
|
||||
fmt.Println(string(body))
|
||||
}`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,46 +1,101 @@
|
||||
import * as CURL from './curl';
|
||||
import * as JS from './javascript';
|
||||
import * as Go from './go';
|
||||
import * as Python from './python';
|
||||
import * as Java from './java';
|
||||
import * as CSharp from './csharp';
|
||||
import type { CodeUsageGenerator } from '@/ui/operation/usage-tabs';
|
||||
import type { MediaAdapter } from '../media/adapter';
|
||||
import type { RequestData } from '../types';
|
||||
|
||||
export const defaultSamples: CodeUsageGenerator[] = [
|
||||
{
|
||||
id: 'curl',
|
||||
label: 'cURL',
|
||||
source: CURL.generator,
|
||||
lang: 'bash',
|
||||
export interface CodeUsageGeneratorRegistry {
|
||||
add: (id: string, generator: CodeUsageGenerator) => void;
|
||||
get: (id: string) => CodeUsageGenerator | undefined;
|
||||
addInline: (generator: InlineCodeUsageGenerator) => void;
|
||||
/**
|
||||
* @returns if the generator is removed
|
||||
*/
|
||||
remove: (id: string) => boolean;
|
||||
map: () => Map<string, CodeUsageGenerator>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code example for given programming language
|
||||
*/
|
||||
export interface CodeUsageGenerator {
|
||||
generate: CodeUsageGeneratorFn;
|
||||
lang: string;
|
||||
label?: string;
|
||||
|
||||
/**
|
||||
* for inline generators passed from server, this stores info available for client (e.g. forwarded from "use client").
|
||||
*/
|
||||
_client?: {
|
||||
generate: string | CodeUsageGeneratorFn;
|
||||
serverContext?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code example for given programming language
|
||||
*/
|
||||
export interface InlineCodeUsageGenerator<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 function createCodeUsageGeneratorRegistry(
|
||||
ihherit?: CodeUsageGeneratorRegistry,
|
||||
): CodeUsageGeneratorRegistry {
|
||||
const registry = new Map<string, CodeUsageGenerator>(ihherit?.map());
|
||||
|
||||
return {
|
||||
add(id, generator) {
|
||||
registry.set(id, generator);
|
||||
},
|
||||
get(id) {
|
||||
return registry.get(id);
|
||||
},
|
||||
addInline(generator) {
|
||||
const source = generator.source;
|
||||
const id = generator.id ?? generator.lang;
|
||||
if (!source) {
|
||||
this.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
registry.set(id, {
|
||||
lang: generator.lang,
|
||||
label: generator.label,
|
||||
generate() {
|
||||
return typeof source === 'string' ? source : '';
|
||||
},
|
||||
_client: {
|
||||
generate: source,
|
||||
serverContext: generator.serverContext,
|
||||
},
|
||||
});
|
||||
},
|
||||
remove(id) {
|
||||
return registry.delete(id);
|
||||
},
|
||||
map() {
|
||||
return registry;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type CodeUsageGeneratorFn<ServerContext = unknown> = (
|
||||
url: string,
|
||||
data: RequestData,
|
||||
context: {
|
||||
mediaAdapters: Record<string, MediaAdapter>;
|
||||
server: ServerContext;
|
||||
},
|
||||
{
|
||||
id: 'js',
|
||||
label: 'JavaScript',
|
||||
source: JS.generator,
|
||||
lang: 'js',
|
||||
},
|
||||
{
|
||||
id: 'go',
|
||||
label: 'Go',
|
||||
source: Go.generator,
|
||||
lang: 'go',
|
||||
},
|
||||
{
|
||||
id: 'python',
|
||||
label: 'Python',
|
||||
source: Python.generator,
|
||||
lang: 'python',
|
||||
},
|
||||
{
|
||||
id: 'java',
|
||||
label: 'Java',
|
||||
source: Java.generator,
|
||||
lang: 'java',
|
||||
},
|
||||
{
|
||||
id: 'csharp',
|
||||
label: 'C#',
|
||||
source: CSharp.generator,
|
||||
lang: 'csharp',
|
||||
},
|
||||
];
|
||||
) => string;
|
||||
|
||||
@@ -1,86 +1,89 @@
|
||||
'use client';
|
||||
import { ident } from '@/requests/string-utils';
|
||||
import type { SampleGenerator } from '@/requests/types';
|
||||
import type { CodeUsageGenerator } from '@/requests/generators';
|
||||
import { resolveMediaAdapter } from '@/requests/media/adapter';
|
||||
|
||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
const s: string[] = [];
|
||||
const headers = { ...data.header };
|
||||
const imports = new Set<string>([
|
||||
'java.net.URI',
|
||||
'java.net.http.HttpClient',
|
||||
'java.net.http.HttpRequest',
|
||||
'java.net.http.HttpResponse',
|
||||
'java.net.http.HttpResponse.BodyHandlers',
|
||||
'java.time.Duration',
|
||||
]);
|
||||
export const java: CodeUsageGenerator = {
|
||||
label: 'Java',
|
||||
lang: 'java',
|
||||
generate(url, data, { mediaAdapters }) {
|
||||
const s: string[] = [];
|
||||
const headers = { ...data.header };
|
||||
const imports = new Set<string>([
|
||||
'java.net.URI',
|
||||
'java.net.http.HttpClient',
|
||||
'java.net.http.HttpRequest',
|
||||
'java.net.http.HttpResponse',
|
||||
'java.net.http.HttpResponse.BodyHandlers',
|
||||
'java.time.Duration',
|
||||
]);
|
||||
|
||||
// Handle body if present
|
||||
let body: string | undefined;
|
||||
if (data.body && data.bodyMediaType) {
|
||||
const adapter = resolveMediaAdapter(data.bodyMediaType, mediaAdapters);
|
||||
body = adapter?.generateExample(data as { body: unknown }, {
|
||||
lang: 'java',
|
||||
addImport(specifier) {
|
||||
imports.add(specifier);
|
||||
},
|
||||
});
|
||||
}
|
||||
// Handle body if present
|
||||
let body: string | undefined;
|
||||
if (data.body && data.bodyMediaType) {
|
||||
const adapter = resolveMediaAdapter(data.bodyMediaType, mediaAdapters);
|
||||
body = adapter?.generateExample(data as { body: unknown }, {
|
||||
lang: 'java',
|
||||
addImport(specifier) {
|
||||
imports.add(specifier);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const value of imports.values()) {
|
||||
s.push(`import ${value};`);
|
||||
}
|
||||
s.push('');
|
||||
for (const value of imports.values()) {
|
||||
s.push(`import ${value};`);
|
||||
}
|
||||
s.push('');
|
||||
|
||||
if (body) {
|
||||
s.push(body);
|
||||
}
|
||||
if (body) {
|
||||
s.push(body);
|
||||
}
|
||||
|
||||
// Create HttpClient
|
||||
s.push('HttpClient client = HttpClient.newBuilder()');
|
||||
s.push(ident('.connectTimeout(Duration.ofSeconds(10))'));
|
||||
s.push(ident('.build();'));
|
||||
s.push('');
|
||||
// Create HttpClient
|
||||
s.push('HttpClient client = HttpClient.newBuilder()');
|
||||
s.push(ident('.connectTimeout(Duration.ofSeconds(10))'));
|
||||
s.push(ident('.build();'));
|
||||
s.push('');
|
||||
|
||||
// Build request
|
||||
s.push('HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()');
|
||||
s.push(ident(`.uri(URI.create(${JSON.stringify(url)}))`));
|
||||
// Build request
|
||||
s.push('HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()');
|
||||
s.push(ident(`.uri(URI.create(${JSON.stringify(url)}))`));
|
||||
|
||||
// Add headers
|
||||
for (const [key, param] of Object.entries(headers)) {
|
||||
s.push(ident(`.header(${JSON.stringify(key)}, ${JSON.stringify(param.value)})`));
|
||||
}
|
||||
// Add headers
|
||||
for (const [key, param] of Object.entries(headers)) {
|
||||
s.push(ident(`.header(${JSON.stringify(key)}, ${JSON.stringify(param.value)})`));
|
||||
}
|
||||
|
||||
if (data.bodyMediaType) {
|
||||
s.push(ident(`.header("Content-Type", "${data.bodyMediaType}")`));
|
||||
}
|
||||
if (data.bodyMediaType) {
|
||||
s.push(ident(`.header("Content-Type", "${data.bodyMediaType}")`));
|
||||
}
|
||||
|
||||
// Add cookies if present
|
||||
const cookies = Object.entries(data.cookie);
|
||||
// Add cookies if present
|
||||
const cookies = Object.entries(data.cookie);
|
||||
|
||||
if (cookies.length > 0) {
|
||||
const cookieString = cookies.map(([key, param]) => `${key}=${param.value}`).join('; ');
|
||||
if (cookies.length > 0) {
|
||||
const cookieString = cookies.map(([key, param]) => `${key}=${param.value}`).join('; ');
|
||||
|
||||
s.push(ident(`.header("Cookie", ${JSON.stringify(cookieString)})`));
|
||||
}
|
||||
s.push(ident(`.header("Cookie", ${JSON.stringify(cookieString)})`));
|
||||
}
|
||||
|
||||
const arg = body ? 'body' : '';
|
||||
s.push(ident(`.${data.method.toUpperCase()}(${arg})`));
|
||||
s.push(ident('.build();'));
|
||||
s.push('');
|
||||
const arg = body ? 'body' : '';
|
||||
s.push(ident(`.${data.method.toUpperCase()}(${arg})`));
|
||||
s.push(ident('.build();'));
|
||||
s.push('');
|
||||
|
||||
// Add response handling
|
||||
s.push('try {');
|
||||
s.push(
|
||||
ident(
|
||||
'HttpResponse<String> response = client.send(requestBuilder.build(), BodyHandlers.ofString());',
|
||||
),
|
||||
);
|
||||
s.push(ident('System.out.println("Status code: " + response.statusCode());'));
|
||||
s.push(ident('System.out.println("Response body: " + response.body());'));
|
||||
s.push('} catch (Exception e) {');
|
||||
s.push(ident('e.printStackTrace();'));
|
||||
s.push('}');
|
||||
// Add response handling
|
||||
s.push('try {');
|
||||
s.push(
|
||||
ident(
|
||||
'HttpResponse<String> response = client.send(requestBuilder.build(), BodyHandlers.ofString());',
|
||||
),
|
||||
);
|
||||
s.push(ident('System.out.println("Status code: " + response.statusCode());'));
|
||||
s.push(ident('System.out.println("Response body: " + response.body());'));
|
||||
s.push('} catch (Exception e) {');
|
||||
s.push(ident('e.printStackTrace();'));
|
||||
s.push('}');
|
||||
|
||||
return s.join('\n');
|
||||
return s.join('\n');
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,57 +1,60 @@
|
||||
'use client';
|
||||
import { ident } from '@/requests/string-utils';
|
||||
import type { SampleGenerator } from '@/requests/types';
|
||||
import type { CodeUsageGenerator } from '@/requests/generators';
|
||||
import { resolveMediaAdapter } from '@/requests/media/adapter';
|
||||
|
||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
const s: string[] = [];
|
||||
const options = new Map<string, string>();
|
||||
const headers: Record<string, string> = {};
|
||||
export const javascript: CodeUsageGenerator = {
|
||||
label: 'JavaScript',
|
||||
lang: 'js',
|
||||
generate(url, data, { mediaAdapters }) {
|
||||
const s: string[] = [];
|
||||
const options = new Map<string, string>();
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
options.set('method', JSON.stringify(data.method));
|
||||
if (data.bodyMediaType) {
|
||||
headers['Content-Type'] = data.bodyMediaType;
|
||||
}
|
||||
options.set('method', JSON.stringify(data.method));
|
||||
if (data.bodyMediaType) {
|
||||
headers['Content-Type'] = data.bodyMediaType;
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(data.header)) {
|
||||
headers[k] = v.value;
|
||||
}
|
||||
for (const [k, v] of Object.entries(data.header)) {
|
||||
headers[k] = v.value;
|
||||
}
|
||||
|
||||
const cookies = Object.entries(data.cookie);
|
||||
if (cookies.length > 0) {
|
||||
headers['cookie'] = cookies.map(([key, param]) => `${key}=${param.value}`).join('; ');
|
||||
}
|
||||
const cookies = Object.entries(data.cookie);
|
||||
if (cookies.length > 0) {
|
||||
headers['cookie'] = cookies.map(([key, param]) => `${key}=${param.value}`).join('; ');
|
||||
}
|
||||
|
||||
if (Object.keys(headers).length > 0) {
|
||||
options.set('headers', JSON.stringify(headers, null, 2));
|
||||
}
|
||||
if (Object.keys(headers).length > 0) {
|
||||
options.set('headers', JSON.stringify(headers, null, 2));
|
||||
}
|
||||
|
||||
let body: string | undefined;
|
||||
if (data.body && data.bodyMediaType) {
|
||||
const adapter = resolveMediaAdapter(data.bodyMediaType, mediaAdapters);
|
||||
body = adapter?.generateExample(data as { body: unknown }, {
|
||||
lang: 'js',
|
||||
addImport(from, name) {
|
||||
s.unshift(`import { ${name} } from "${from}"`);
|
||||
},
|
||||
});
|
||||
}
|
||||
let body: string | undefined;
|
||||
if (data.body && data.bodyMediaType) {
|
||||
const adapter = resolveMediaAdapter(data.bodyMediaType, mediaAdapters);
|
||||
body = adapter?.generateExample(data as { body: unknown }, {
|
||||
lang: 'js',
|
||||
addImport(from, name) {
|
||||
s.unshift(`import { ${name} } from "${from}"`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (body) {
|
||||
s.push(body);
|
||||
options.set('body', 'body');
|
||||
}
|
||||
if (body) {
|
||||
s.push(body);
|
||||
options.set('body', 'body');
|
||||
}
|
||||
|
||||
const params = [JSON.stringify(url)];
|
||||
if (options.size > 0) {
|
||||
const str = Array.from(options.entries())
|
||||
.map(([k, v]) => ident(k === v ? k : `${k}: ${v}`))
|
||||
.join(',\n');
|
||||
const params = [JSON.stringify(url)];
|
||||
if (options.size > 0) {
|
||||
const str = Array.from(options.entries())
|
||||
.map(([k, v]) => ident(k === v ? k : `${k}: ${v}`))
|
||||
.join(',\n');
|
||||
|
||||
params.push(`{\n${str}\n}`);
|
||||
}
|
||||
params.push(`{\n${str}\n}`);
|
||||
}
|
||||
|
||||
s.push(`fetch(${params.join(', ')})`);
|
||||
s.push(`fetch(${params.join(', ')})`);
|
||||
|
||||
return s.join('\n\n');
|
||||
return s.join('\n\n');
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,55 +1,58 @@
|
||||
'use client';
|
||||
import type { SampleGenerator } from '@/requests/types';
|
||||
import type { CodeUsageGenerator } from '@/requests/generators';
|
||||
import { generatePythonObject } from '@/requests/to-python-object';
|
||||
import { resolveMediaAdapter } from '@/requests/media/adapter';
|
||||
|
||||
export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
const headers: Record<string, string> = {};
|
||||
const imports = new Set<string>();
|
||||
const params = [`"${data.method}"`, 'url'];
|
||||
let body: string | undefined;
|
||||
export const python: CodeUsageGenerator = {
|
||||
label: 'Python',
|
||||
lang: 'python',
|
||||
generate(url, data, { mediaAdapters }) {
|
||||
const headers: Record<string, string> = {};
|
||||
const imports = new Set<string>();
|
||||
const params = [`"${data.method}"`, 'url'];
|
||||
let body: string | undefined;
|
||||
|
||||
imports.add('requests');
|
||||
imports.add('requests');
|
||||
|
||||
if (data.body && data.bodyMediaType) {
|
||||
const adapter = resolveMediaAdapter(data.bodyMediaType, mediaAdapters);
|
||||
headers['Content-Type'] = data.bodyMediaType;
|
||||
if (data.body && data.bodyMediaType) {
|
||||
const adapter = resolveMediaAdapter(data.bodyMediaType, mediaAdapters);
|
||||
headers['Content-Type'] = data.bodyMediaType;
|
||||
|
||||
body = adapter?.generateExample(data as { body: unknown }, {
|
||||
lang: 'python',
|
||||
});
|
||||
body = adapter?.generateExample(data as { body: unknown }, {
|
||||
lang: 'python',
|
||||
});
|
||||
|
||||
if (body) {
|
||||
params.push('data = body');
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(data.header)) {
|
||||
headers[k] = v.value;
|
||||
}
|
||||
|
||||
if (Object.keys(headers).length > 0) {
|
||||
params.push(`headers = ${generatePythonObject(headers, imports)}`);
|
||||
}
|
||||
|
||||
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;
|
||||
if (body) {
|
||||
params.push('data = body');
|
||||
}
|
||||
}
|
||||
|
||||
params.push(`cookies = ${generatePythonObject(cookies, imports)}`);
|
||||
}
|
||||
for (const [k, v] of Object.entries(data.header)) {
|
||||
headers[k] = v.value;
|
||||
}
|
||||
|
||||
return `${Array.from(imports)
|
||||
.map((name) => 'import ' + name)
|
||||
.join('\n')}
|
||||
if (Object.keys(headers).length > 0) {
|
||||
params.push(`headers = ${generatePythonObject(headers, imports)}`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
params.push(`cookies = ${generatePythonObject(cookies, imports)}`);
|
||||
}
|
||||
|
||||
return `${Array.from(imports)
|
||||
.map((name) => 'import ' + name)
|
||||
.join('\n')}
|
||||
|
||||
url = ${JSON.stringify(url)}
|
||||
${body ?? ''}
|
||||
response = requests.request(${params.join(', ')})
|
||||
|
||||
print(response.text)`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import type { MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { EncodedParameter, EncodedParameterMultiple } from '@/requests/media/encode';
|
||||
|
||||
export type SampleGenerator<ServerContext = unknown> = (
|
||||
url: string,
|
||||
data: RequestData,
|
||||
context: {
|
||||
mediaAdapters: Record<string, MediaAdapter>;
|
||||
server: ServerContext;
|
||||
},
|
||||
) => string;
|
||||
|
||||
export interface RawRequestData {
|
||||
method: string;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createProxy } from '@/server/proxy';
|
||||
import { processDocument, type ProcessedDocument } from '@/utils/process-document';
|
||||
import type { CodeUsageGenerator } from '@/ui/operation/usage-tabs';
|
||||
import type { Document } from '@/types';
|
||||
import type { InlineCodeUsageGenerator } from '@/requests/generators';
|
||||
|
||||
/**
|
||||
* schema id -> file path, URL, or downloaded schema object
|
||||
@@ -78,8 +78,11 @@ export function createOpenAPI(options: OpenAPIOptions = {}): OpenAPIServer {
|
||||
};
|
||||
}
|
||||
|
||||
export function createCodeSample<T>(options: Partial<CodeUsageGenerator<T>>): CodeUsageGenerator {
|
||||
const { lang = 'unknown', id = lang, ...rest } = options as Partial<CodeUsageGenerator>;
|
||||
|
||||
return { id, lang, ...rest };
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function createCodeSample<T>(
|
||||
options: InlineCodeUsageGenerator<T>,
|
||||
): InlineCodeUsageGenerator<T> {
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { ProcessedDocument } from '@/utils/process-document';
|
||||
import type { MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { OpenAPIOptions } from '@/server';
|
||||
import type { CreateAPIPageOptions } from './ui/base';
|
||||
import type { CodeUsageGenerator } from './ui/operation/usage-tabs';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import type { InlineCodeUsageGenerator } from './requests/generators';
|
||||
|
||||
export type Document = OpenAPIV3_2.Document;
|
||||
export type OperationObject = OpenAPIV3_2.OperationObject;
|
||||
@@ -27,7 +27,7 @@ export type RequestBodyObject = OpenAPIV3_2.RequestBodyObject;
|
||||
|
||||
export type MethodInformation = NoReference<OperationObject> & {
|
||||
method: string;
|
||||
'x-codeSamples'?: Omit<CodeUsageGenerator, 'id'>[];
|
||||
'x-codeSamples'?: InlineCodeUsageGenerator[];
|
||||
'x-selectedCodeSample'?: string;
|
||||
'x-exclusiveCodeSample'?: string;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { FC, ReactNode } from 'react';
|
||||
import { highlight, type CoreHighlightOptions } from 'fumadocs-core/highlight/core';
|
||||
import type { OpenAPIServer } from '@/server';
|
||||
import type { APIPageClientOptions } from './client';
|
||||
import type { CodeUsageGenerator } from './operation/usage-tabs';
|
||||
import { Heading } from 'fumadocs-ui/components/heading';
|
||||
import { createRehypeCode } from 'fumadocs-core/mdx-plugins/rehype-code.core';
|
||||
import { remarkGfm } from 'fumadocs-core/mdx-plugins/remark-gfm';
|
||||
@@ -23,6 +22,7 @@ import type { ResponseTab } from './operation/response-tabs';
|
||||
import type { ExampleRequestItem } from './operation/request-tabs';
|
||||
import type { ResolvedShikiConfig } from 'fumadocs-core/highlight/config';
|
||||
import { APIPage, type ApiPageProps, type OperationItem, type WebhookItem } from './api-page';
|
||||
import type { CodeUsageGeneratorRegistry, InlineCodeUsageGenerator } from '@/requests/generators';
|
||||
|
||||
export interface CreateAPIPageOptions {
|
||||
/**
|
||||
@@ -45,7 +45,12 @@ export interface CreateAPIPageOptions {
|
||||
/**
|
||||
* Generate example code usage for endpoints.
|
||||
*/
|
||||
generateCodeSamples?: (method: MethodInformation) => Awaitable<CodeUsageGenerator[]>;
|
||||
codeUsages?: CodeUsageGeneratorRegistry;
|
||||
|
||||
/**
|
||||
* Generate example code usage for endpoints.
|
||||
*/
|
||||
generateCodeSamples?: (method: MethodInformation) => Awaitable<InlineCodeUsageGenerator[]>;
|
||||
|
||||
shiki: ResolvedShikiConfig;
|
||||
renderMarkdown?: (md: string) => Awaitable<ReactNode>;
|
||||
@@ -90,7 +95,7 @@ export interface CreateAPIPageOptions {
|
||||
* @param generators - codegens for API example usages
|
||||
*/
|
||||
renderAPIExampleUsageTabs?: (
|
||||
generators: CodeUsageGenerator[],
|
||||
generators: CodeUsageGeneratorRegistry,
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<ReactNode>;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
|
||||
export interface APIPageClientOptions {
|
||||
playground?: PlaygroundClientOptions;
|
||||
@@ -21,6 +22,11 @@ export interface APIPageClientOptions {
|
||||
* Support other media types (for client-side serialization)
|
||||
*/
|
||||
mediaAdapters?: Record<string, MediaAdapter>;
|
||||
|
||||
/**
|
||||
* generate code usage examples
|
||||
*/
|
||||
codeUsages?: CodeUsageGeneratorRegistry;
|
||||
}
|
||||
|
||||
export interface OperationClientOptions {
|
||||
|
||||
@@ -14,6 +14,11 @@ 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 {
|
||||
type CodeUsageGeneratorRegistry,
|
||||
createCodeUsageGeneratorRegistry,
|
||||
} from '@/requests/generators';
|
||||
import { registerDefault } from '@/requests/generators/all';
|
||||
|
||||
interface InheritFromContext extends Pick<RenderContext, 'shikiOptions'> {
|
||||
client: APIPageClientOptions;
|
||||
@@ -47,6 +52,7 @@ export interface SelectedServer {
|
||||
|
||||
interface ApiContextType extends InheritFromContext {
|
||||
mediaAdapters: Record<string, MediaAdapter>;
|
||||
codeUsages: CodeUsageGeneratorRegistry;
|
||||
}
|
||||
|
||||
interface ServerSelectType {
|
||||
@@ -83,17 +89,25 @@ export function ApiProvider({
|
||||
}: ApiProviderProps & { children: ReactNode }) {
|
||||
return (
|
||||
<ApiContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
value={useMemo(() => {
|
||||
let codeUsages: CodeUsageGeneratorRegistry;
|
||||
if (client.codeUsages) {
|
||||
codeUsages = createCodeUsageGeneratorRegistry(client.codeUsages);
|
||||
} else {
|
||||
codeUsages = createCodeUsageGeneratorRegistry();
|
||||
registerDefault(codeUsages);
|
||||
}
|
||||
|
||||
return {
|
||||
shikiOptions,
|
||||
client,
|
||||
codeUsages,
|
||||
mediaAdapters: {
|
||||
...defaultAdapters,
|
||||
...client.mediaAdapters,
|
||||
},
|
||||
}),
|
||||
[client, shikiOptions],
|
||||
)}
|
||||
};
|
||||
}, [client, shikiOptions])}
|
||||
>
|
||||
{children}
|
||||
</ApiContext>
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
} from '@/ui/components/select';
|
||||
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock.core';
|
||||
import { useState, useEffect, useMemo, createContext, ReactNode, useRef, use } from 'react';
|
||||
import type { CodeUsageGenerator } from '.';
|
||||
import type { ExampleRequestItem } from '../request-tabs';
|
||||
import type { RawRequestData, RequestData } from '@/requests/types';
|
||||
import type { CodeUsageGenerator } from '@/requests/generators';
|
||||
|
||||
export type ExampleUpdateListener = (data: RawRequestData, encoded: RequestData) => void;
|
||||
|
||||
@@ -130,8 +130,12 @@ export function UsageTabsSelector() {
|
||||
);
|
||||
}
|
||||
|
||||
export function UsageTab(sample: CodeUsageGenerator) {
|
||||
const { shikiOptions, mediaAdapters } = useApiContext();
|
||||
export function UsageTab({
|
||||
id,
|
||||
lang,
|
||||
_client,
|
||||
}: Pick<CodeUsageGenerator, 'lang' | '_client'> & { id: string }) {
|
||||
const { shikiOptions, mediaAdapters, codeUsages } = useApiContext();
|
||||
const {
|
||||
examples,
|
||||
example: selectedExampleId,
|
||||
@@ -154,26 +158,33 @@ export function UsageTab(sample: CodeUsageGenerator) {
|
||||
}, [addListener, removeListener]);
|
||||
|
||||
const code = useMemo(() => {
|
||||
if (!sample.source || !data) return;
|
||||
if (typeof sample.source === 'string') return sample.source;
|
||||
|
||||
return sample.source(
|
||||
joinURL(
|
||||
withBase(
|
||||
server ? resolveServerUrl(server.url, server.variables) : '/',
|
||||
typeof window !== 'undefined' ? window.location.origin : 'https://loading',
|
||||
),
|
||||
resolveRequestData(route, data),
|
||||
if (!data) return;
|
||||
const url = joinURL(
|
||||
withBase(
|
||||
server ? resolveServerUrl(server.url, server.variables) : '/',
|
||||
typeof window !== 'undefined' ? window.location.origin : 'https://loading',
|
||||
),
|
||||
data,
|
||||
{
|
||||
server: sample.serverContext,
|
||||
mediaAdapters,
|
||||
},
|
||||
resolveRequestData(route, data),
|
||||
);
|
||||
}, [mediaAdapters, sample, server, route, data]);
|
||||
|
||||
if (!code || !sample) return null;
|
||||
if (_client) {
|
||||
const { generate, serverContext } = _client;
|
||||
if (typeof generate === 'string') return generate;
|
||||
return generate(url, data, {
|
||||
mediaAdapters,
|
||||
server: serverContext,
|
||||
});
|
||||
}
|
||||
|
||||
return <DynamicCodeBlock lang={sample.lang} code={code} options={shikiOptions} />;
|
||||
const codegen = codeUsages.get(id);
|
||||
if (!codegen) return;
|
||||
return codegen.generate(url, data, {
|
||||
mediaAdapters,
|
||||
server: null,
|
||||
});
|
||||
}, [data, server, route, _client, codeUsages, id, mediaAdapters]);
|
||||
|
||||
if (!code) return null;
|
||||
|
||||
return <DynamicCodeBlock lang={lang} code={code} options={shikiOptions} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { MethodInformation, RenderContext } from '@/types';
|
||||
import type { SampleGenerator } from '@/requests/types';
|
||||
import { defaultSamples } from '@/requests/generators';
|
||||
import type { CodeUsageGeneratorFn } from '@/requests/generators';
|
||||
import {
|
||||
type CodeUsageGeneratorRegistry,
|
||||
createCodeUsageGeneratorRegistry,
|
||||
} from '@/requests/generators';
|
||||
import {
|
||||
CodeBlockTab,
|
||||
CodeBlockTabs,
|
||||
@@ -9,6 +12,7 @@ import {
|
||||
} from 'fumadocs-ui/components/codeblock';
|
||||
import { UsageTabsSelectorLazy, UsageTabLazy } from './lazy';
|
||||
import { ResponseTabs } from '../response-tabs';
|
||||
import { registerDefault } from '@/requests/generators/all';
|
||||
|
||||
/**
|
||||
* Generate code example for given programming language
|
||||
@@ -23,7 +27,7 @@ export interface CodeUsageGenerator<T = unknown> {
|
||||
* - a function imported from a file with "use client" directive
|
||||
* - false (disabled)
|
||||
*/
|
||||
source?: string | SampleGenerator<T> | false;
|
||||
source?: string | CodeUsageGeneratorFn<T> | false;
|
||||
|
||||
/**
|
||||
* Pass extra context to client-side source generator
|
||||
@@ -50,70 +54,52 @@ export async function UsageTabs({
|
||||
);
|
||||
};
|
||||
|
||||
renderAPIExampleUsageTabs ??= (generators) => {
|
||||
if (generators.length === 0) return null;
|
||||
renderAPIExampleUsageTabs ??= (registry) => {
|
||||
const map = Array.from(registry.map().entries());
|
||||
if (map.length === 0) return null;
|
||||
|
||||
return (
|
||||
<CodeBlockTabs groupId="fumadocs_openapi_requests" defaultValue={generators[0].id}>
|
||||
<CodeBlockTabs groupId="fumadocs_openapi_requests" defaultValue={map[0][0]}>
|
||||
<CodeBlockTabsList>
|
||||
{generators.map((item) => (
|
||||
<CodeBlockTabsTrigger key={item.id} value={item.id}>
|
||||
{map.map(([id, item]) => (
|
||||
<CodeBlockTabsTrigger key={id} value={id}>
|
||||
{item.label ?? item.lang}
|
||||
</CodeBlockTabsTrigger>
|
||||
))}
|
||||
</CodeBlockTabsList>
|
||||
{generators.map((item) => (
|
||||
<CodeBlockTab key={item.id} value={item.id}>
|
||||
<UsageTabLazy {...item} />
|
||||
{map.map(([id, item]) => (
|
||||
<CodeBlockTab key={id} value={id}>
|
||||
<UsageTabLazy id={id} lang={item.lang} _client={item._client} />
|
||||
</CodeBlockTab>
|
||||
))}
|
||||
</CodeBlockTabs>
|
||||
);
|
||||
};
|
||||
|
||||
let generators: CodeUsageGenerator[] = [...defaultSamples];
|
||||
if (ctx.generateCodeSamples) {
|
||||
generators.push(...(await ctx.generateCodeSamples(method)));
|
||||
let registry: CodeUsageGeneratorRegistry;
|
||||
if (ctx.codeUsages) {
|
||||
registry = createCodeUsageGeneratorRegistry(ctx.codeUsages);
|
||||
} else {
|
||||
registry = createCodeUsageGeneratorRegistry();
|
||||
registerDefault(registry);
|
||||
}
|
||||
|
||||
for (const gen of (await ctx.generateCodeSamples?.(method)) ?? []) {
|
||||
registry.addInline(gen);
|
||||
}
|
||||
|
||||
if (method['x-codeSamples']) {
|
||||
for (const sample of method['x-codeSamples']) {
|
||||
generators.push(
|
||||
'id' in sample && typeof sample.id === 'string'
|
||||
? (sample as CodeUsageGenerator)
|
||||
: {
|
||||
id: sample.lang,
|
||||
...sample,
|
||||
},
|
||||
);
|
||||
registry.addInline(sample);
|
||||
}
|
||||
}
|
||||
|
||||
generators = dedupe(generators);
|
||||
|
||||
return renderAPIExampleLayout(
|
||||
{
|
||||
selector: method['x-exclusiveCodeSample'] ? null : <UsageTabsSelectorLazy />,
|
||||
usageTabs: await renderAPIExampleUsageTabs(generators, ctx),
|
||||
usageTabs: await renderAPIExampleUsageTabs(registry, ctx),
|
||||
responseTabs: <ResponseTabs operation={method} ctx={ctx} />,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicated ids
|
||||
*/
|
||||
function dedupe(samples: CodeUsageGenerator[]): CodeUsageGenerator[] {
|
||||
const set = new Set<string>();
|
||||
const out: CodeUsageGenerator[] = [];
|
||||
|
||||
for (let i = samples.length - 1; i >= 0; i--) {
|
||||
const item = samples[i];
|
||||
if (set.has(item.id)) continue;
|
||||
set.add(item.id);
|
||||
if (item.source !== false) out.unshift(item);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import * as Python from '@/requests/generators/python';
|
||||
import * as Go from '@/requests/generators/go';
|
||||
import * as Curl from '@/requests/generators/curl';
|
||||
import * as JS from '@/requests/generators/javascript';
|
||||
import type { RequestData } from '@/requests/types';
|
||||
import { defaultAdapters } from '@/requests/media/adapter';
|
||||
import { resolveRequestData } from '@/utils/url';
|
||||
import { go } from '@/requests/generators/go';
|
||||
import { curl } from '@/requests/generators/curl';
|
||||
import { python } from '@/requests/generators/python';
|
||||
import { javascript } from '@/requests/generators/javascript';
|
||||
import { csharp } from '@/requests/generators/csharp';
|
||||
|
||||
describe('Code Sample Generators', () => {
|
||||
const data: RequestData = {
|
||||
@@ -35,18 +36,22 @@ describe('Code Sample Generators', () => {
|
||||
};
|
||||
|
||||
test(`Go`, async () => {
|
||||
await expect(Go.generator(url, data, context)).toMatchFileSnapshot(`./out/samples/1.go`);
|
||||
await expect(go.generate(url, data, context)).toMatchFileSnapshot(`./out/samples/1.go`);
|
||||
});
|
||||
|
||||
test(`Curl`, async () => {
|
||||
await expect(Curl.generator(url, data, context)).toMatchFileSnapshot(`./out/samples/1.bash`);
|
||||
await expect(curl.generate(url, data, context)).toMatchFileSnapshot(`./out/samples/1.bash`);
|
||||
});
|
||||
|
||||
test(`Python`, async () => {
|
||||
await expect(Python.generator(url, data, context)).toMatchFileSnapshot(`./out/samples/1.py`);
|
||||
await expect(python.generate(url, data, context)).toMatchFileSnapshot(`./out/samples/1.py`);
|
||||
});
|
||||
|
||||
test(`JavaScript`, async () => {
|
||||
await expect(JS.generator(url, data, context)).toMatchFileSnapshot(`./out/samples/1.js`);
|
||||
await expect(javascript.generate(url, data, context)).toMatchFileSnapshot(`./out/samples/1.js`);
|
||||
});
|
||||
|
||||
test(`C#`, async () => {
|
||||
await expect(csharp.generate(url, data, context)).toMatchFileSnapshot(`./out/samples/1.cs`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
var body = new StringContent("""
|
||||
{
|
||||
"id": "id"
|
||||
}
|
||||
""", Encoding.UTF8, "application/json");
|
||||
|
||||
var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Add("authorization", "Bearer");
|
||||
client.DefaultRequestHeaders.Add("cookie", "mode=light");
|
||||
var response = await client.GetAsync("http://localhost:8080/hello_world?search=ai", body);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
@@ -12,6 +12,7 @@ export default defineConfig({
|
||||
'./src/playground/{index,client}.tsx',
|
||||
'./src/scalar/index.tsx',
|
||||
'./src/server/index.ts',
|
||||
'./src/requests/generators/*.ts',
|
||||
],
|
||||
fixedExtension: false,
|
||||
unbundle: true,
|
||||
@@ -22,4 +23,12 @@ export default defineConfig({
|
||||
await compileInline();
|
||||
},
|
||||
inlineOnly: [],
|
||||
exports: {
|
||||
enabled: true,
|
||||
customExports(v) {
|
||||
v['./css/*'] = './css/*';
|
||||
return v;
|
||||
},
|
||||
legacy: true,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user