OpenAPI: Support Python code sample

This commit is contained in:
fuma-nama
2024-08-19 23:23:44 +08:00
parent 10f6f39372
commit 3fa2436df1
19 changed files with 257 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': minor
---
Support Python code sample
@@ -1,6 +1,7 @@
import { type HTMLAttributes, useMemo } from 'react';
import * as Base from 'fumadocs-ui/components/codeblock';
import { createHighlighter, bundledLanguages, bundledThemes } from 'shiki';
import { sharedTransformers } from '@/utils/shiki';
export type CodeBlockProps = HTMLAttributes<HTMLPreElement> & {
code: string;
@@ -22,6 +23,7 @@ export function CodeBlock({
lang,
defaultColor: false,
themes: { light: 'github-light', dark: 'github-dark' },
transformers: sharedTransformers,
});
}, [code, lang]);
@@ -4,6 +4,7 @@ import { generateSample, type EndpointSample } from '@/schema/sample';
import * as CURL from '@/requests/curl';
import * as JS from '@/requests/javascript';
import * as Go from '@/requests/go';
import * as Python from '@/requests/python';
import { type MethodInformation, type RenderContext } from '@/types';
import { noRef, getPreferredType } from '@/utils/schema';
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
@@ -176,6 +177,11 @@ async function APIExample({
source: Go.getSampleRequest(endpoint),
lang: 'go',
},
{
label: 'Python',
source: Python.getSampleRequest(endpoint),
lang: 'python',
},
...(ctx.generateCodeSamples ? await ctx.generateCodeSamples(endpoint) : []),
...((method as CustomProperty)['x-codeSamples'] ?? []),
]).filter((item) => item.source !== undefined) as Required<CodeSample>[];
+49
View File
@@ -0,0 +1,49 @@
import { type EndpointSample } from '@/schema/sample';
export function getSampleRequest(endpoint: EndpointSample): string {
const headers = new Map<string, unknown>();
const cookies = new Map<string, unknown>();
const variables = new Map<string, string>();
for (const param of endpoint.parameters) {
if (param.in === 'header') headers.set(param.name, param.sample);
if (param.in === 'cookie') cookies.set(param.name, param.sample);
}
if (headers.size > 0) {
variables.set(
'headers',
JSON.stringify(Object.fromEntries(headers.entries()), null, 2),
);
}
if (cookies.size > 0) {
variables.set(
'cookies',
JSON.stringify(Object.fromEntries(cookies.entries()), null, 2),
);
}
if (endpoint.body) {
variables.set(
endpoint.body.mediaType === 'multipart/form-data' ? 'data' : 'json',
JSON.stringify(endpoint.body.sample, null, 2),
);
}
return `import requests
url = ${JSON.stringify(endpoint.url)}
${Array.from(variables.entries())
.map(([k, v]) => `${k} = ${v}`)
.join('\n')}
response = requests.request("${endpoint.method}", url${
variables.size > 0
? `, ${Array.from(variables.keys())
.map((k) => `${k}=${k}`)
.join(', ')}`
: ''
})
print(response.text)`;
}
@@ -1,6 +1,7 @@
import { type HTMLAttributes, useLayoutEffect, useState } from 'react';
import * as Base from 'fumadocs-ui/components/codeblock';
import { useApiContext } from '@/ui/contexts/api';
import { sharedTransformers } from '@/utils/shiki';
export type CodeBlockProps = HTMLAttributes<HTMLPreElement> & {
code: string;
@@ -22,6 +23,7 @@ export function CodeBlock({
lang,
defaultColor: false,
themes: { light: 'github-light', dark: 'github-dark' },
transformers: sharedTransformers,
});
setHtml(themedHtml);
+16
View File
@@ -0,0 +1,16 @@
import { type ShikiTransformer } from 'shiki';
export const sharedTransformers: ShikiTransformer[] = [
{
name: 'fumadocs:pre-process',
line(hast) {
if (hast.children.length === 0) {
// Keep the empty lines when using grid layout
hast.children.push({
type: 'text',
value: ' ',
});
}
},
},
];
+64
View File
@@ -0,0 +1,64 @@
import { fileURLToPath } from 'node:url';
import { describe, expect, test } from 'vitest';
import Parser from '@apidevtools/json-schema-ref-parser';
import { type OpenAPIV3 as OpenAPI } from 'openapi-types';
import Slugger from 'github-slugger';
import * as Python from '@/requests/python';
import * as Go from '@/requests/go';
import * as Curl from '@/requests/curl';
import * as JS from '@/requests/javascript';
import { generateSample } from '@/schema/sample';
import { createMethod } from '@/schema/method';
import { methodKeys } from '@/build-routes';
import { defaultRenderer } from '@/render/renderer';
import { type RenderContext } from '@/types';
const example = fileURLToPath(
new URL('./fixtures/petstore.yaml', import.meta.url),
);
const document = await Parser.dereference<OpenAPI.Document>(example);
const ctx: RenderContext = {
baseUrl: 'http://localhost:8080',
document,
renderer: defaultRenderer,
slugger: new Slugger(),
};
describe('Code Sample Generators', () => {
for (const [path, item] of Object.entries(document.paths)) {
if (!item) continue;
for (const method of methodKeys) {
const operation = item[method];
if (!operation) continue;
const info = createMethod(method, item, operation);
const endpoint = generateSample(path, info, ctx);
test(`Go: ${method} ${path}`, async () => {
await expect(Go.getSampleRequest(endpoint)).toMatchFileSnapshot(
`./out/samples/${path}/${method}.go`,
);
});
test(`Curl: ${method} ${path}`, async () => {
await expect(Curl.getSampleRequest(endpoint)).toMatchFileSnapshot(
`./out/samples/${path}/${method}.bash`,
);
});
test(`Python: ${method} ${path}`, async () => {
await expect(Python.getSampleRequest(endpoint)).toMatchFileSnapshot(
`./out/samples/${path}/${method}.py`,
);
});
test(`JavaScript: ${method} ${path}`, async () => {
await expect(JS.getSampleRequest(endpoint)).toMatchFileSnapshot(
`./out/samples/${path}/${method}.js`,
);
});
}
}
});
@@ -0,0 +1 @@
curl -X GET "http://localhost:8080/pets?limit=100"
@@ -0,0 +1,20 @@
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "http://localhost:8080/pets?limit=100"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(res)
fmt.Println(string(body))
}
@@ -0,0 +1,3 @@
fetch("http://localhost:8080/pets?limit=100", {
});
@@ -0,0 +1,7 @@
import requests
url = "http://localhost:8080/pets?limit=100"
response = requests.request("GET", url)
print(response.text)
@@ -0,0 +1,7 @@
curl -X POST "http://localhost:8080/pets" \
-H "Content-Type: application/json" \
-d '{
"id": 0,
"name": "string",
"tag": "string"
}'
@@ -0,0 +1,26 @@
package main
import (
"fmt"
"net/http"
"io/ioutil"
"strings"
)
func main() {
url := "http://localhost:8080/pets"
payload := strings.NewReader(`{
"id": 0,
"name": "string",
"tag": "string"
}`)
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(res)
fmt.Println(string(body))
}
@@ -0,0 +1,7 @@
fetch("http://localhost:8080/pets", {
body: JSON.stringify({
"id": 0,
"name": "string",
"tag": "string"
})
});
@@ -0,0 +1,11 @@
import requests
url = "http://localhost:8080/pets"
json = {
"id": 0,
"name": "string",
"tag": "string"
}
response = requests.request("POST", url, json=json)
print(response.text)
@@ -0,0 +1 @@
curl -X GET "http://localhost:8080/pets/string"
@@ -0,0 +1,20 @@
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "http://localhost:8080/pets/string"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(res)
fmt.Println(string(body))
}
@@ -0,0 +1,3 @@
fetch("http://localhost:8080/pets/string", {
});
@@ -0,0 +1,7 @@
import requests
url = "http://localhost:8080/pets/string"
response = requests.request("GET", url)
print(response.text)