docs: add SDK API reference for Rust, Python, and Kotlin (#11251)
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
import apiData from "@site/src/data/gdk-api.json";
|
||||
import { LANGUAGES, Language, LanguageId } from "./languages";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
type GdkParam = {
|
||||
name: string;
|
||||
type: string;
|
||||
default: string | null;
|
||||
docs: string;
|
||||
};
|
||||
|
||||
type GdkFunc = {
|
||||
name: string;
|
||||
docs: string;
|
||||
params: GdkParam[];
|
||||
returns: string | null;
|
||||
throws: string | null;
|
||||
isAsync: boolean;
|
||||
};
|
||||
|
||||
type GdkItem = {
|
||||
name: string;
|
||||
kind: "object" | "callback" | "record" | "enum" | "error";
|
||||
docs: string;
|
||||
fields: GdkParam[];
|
||||
variants: { name: string; fields: GdkParam[] }[];
|
||||
methods: GdkFunc[];
|
||||
};
|
||||
|
||||
type GdkApiDoc = {
|
||||
version: string;
|
||||
docVersion: string;
|
||||
source: string;
|
||||
functions: GdkFunc[];
|
||||
items: GdkItem[];
|
||||
};
|
||||
|
||||
const VERSIONS = (apiData as { versions: GdkApiDoc[] }).versions;
|
||||
|
||||
const KIND_LABELS: Record<GdkItem["kind"], string> = {
|
||||
object: "Class",
|
||||
callback: "Interface",
|
||||
record: "Data type",
|
||||
enum: "Enum",
|
||||
error: "Error",
|
||||
};
|
||||
|
||||
const KIND_HEADINGS: Record<GdkItem["kind"], string> = {
|
||||
object: "Classes",
|
||||
callback: "Interfaces",
|
||||
record: "Data types",
|
||||
enum: "Enums",
|
||||
error: "Errors",
|
||||
};
|
||||
|
||||
const slug = (...parts: string[]) =>
|
||||
parts.join("-").replace(/[^a-zA-Z0-9]+/g, "-").toLowerCase();
|
||||
|
||||
function signature(func: GdkFunc, language: Language, owner?: string): string {
|
||||
const params = func.params
|
||||
.map((param) => {
|
||||
const type = language.type(param.type);
|
||||
const suffix = param.default ? ` = ${language.default(param.default)}` : "";
|
||||
switch (language.id) {
|
||||
case "rust":
|
||||
return `${param.name}: ${type}${suffix}`;
|
||||
case "python":
|
||||
return `${language.field(param.name)}: ${type}${suffix}`;
|
||||
default:
|
||||
return `${language.field(param.name)}: ${type}${suffix}`;
|
||||
}
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
const name = language.func(func.name);
|
||||
const returns = func.returns ? language.type(func.returns) : null;
|
||||
const prefix = owner ? `${owner}.` : "";
|
||||
|
||||
if (language.id === "rust") {
|
||||
const asyncKeyword = func.isAsync ? "async " : "";
|
||||
const result = func.throws
|
||||
? `Result<${returns ?? "()"}, ${func.throws}>`
|
||||
: returns;
|
||||
return `${asyncKeyword}fn ${prefix}${name}(${params})${result ? ` -> ${result}` : ""}`;
|
||||
}
|
||||
|
||||
if (language.id === "python") {
|
||||
const asyncKeyword = func.isAsync ? "async " : "";
|
||||
return `${asyncKeyword}def ${prefix}${name}(${params})${returns ? ` -> ${returns}` : ""}`;
|
||||
}
|
||||
|
||||
const suspend = func.isAsync ? "suspend " : "";
|
||||
const throwsAnnotation = func.throws
|
||||
? `@Throws(${language.errorType(func.throws)}::class)\n`
|
||||
: "";
|
||||
return `${throwsAnnotation}${suspend}fun ${prefix}${name}(${params})${returns ? `: ${returns}` : ""}`;
|
||||
}
|
||||
|
||||
function ParamTable({
|
||||
rows,
|
||||
language,
|
||||
caption,
|
||||
}: {
|
||||
rows: GdkParam[];
|
||||
language: Language;
|
||||
caption: string;
|
||||
}) {
|
||||
if (rows.length === 0) return null;
|
||||
const hasDefaults = rows.some((row) => row.default);
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{caption}</th>
|
||||
<th>Type</th>
|
||||
{hasDefaults && <th>Default</th>}
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => (
|
||||
<tr key={row.name}>
|
||||
<td>
|
||||
<code>{language.field(row.name)}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{language.type(row.type)}</code>
|
||||
</td>
|
||||
{hasDefaults && (
|
||||
<td>{row.default ? <code>{language.default(row.default)}</code> : "—"}</td>
|
||||
)}
|
||||
<td>{row.docs || "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function FuncEntry({
|
||||
func,
|
||||
language,
|
||||
owner,
|
||||
}: {
|
||||
func: GdkFunc;
|
||||
language: Language;
|
||||
owner?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.entry} id={slug(owner ?? "fn", func.name)}>
|
||||
<h4 className={styles.entryTitle}>
|
||||
<code>{language.func(func.name)}</code>
|
||||
</h4>
|
||||
{func.docs && <p>{func.docs}</p>}
|
||||
<CodeBlock language={language.prism}>{signature(func, language, owner)}</CodeBlock>
|
||||
<ParamTable rows={func.params} language={language} caption="Parameter" />
|
||||
{func.throws && (
|
||||
<p className={styles.meta}>
|
||||
Raises <code>{language.errorType(func.throws)}</code>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemEntry({ item, language }: { item: GdkItem; language: Language }) {
|
||||
const dataCarrying = item.variants.some((variant) => variant.fields.length > 0);
|
||||
return (
|
||||
<section className={styles.item} id={slug(item.name)}>
|
||||
<h3 className={styles.itemTitle}>
|
||||
<code>{item.kind === "error" ? language.errorType(item.name) : item.name}</code>
|
||||
<span className={styles.badge}>{KIND_LABELS[item.kind]}</span>
|
||||
</h3>
|
||||
{item.docs && <p>{item.docs}</p>}
|
||||
|
||||
<ParamTable rows={item.fields} language={language} caption="Field" />
|
||||
|
||||
{item.variants.length > 0 && (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{item.kind === "error" ? "Variant" : "Case"}</th>
|
||||
<th>Associated data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{item.variants.map((variant) => (
|
||||
<tr key={variant.name}>
|
||||
<td>
|
||||
<code>
|
||||
{item.kind === "error" && language.id === "kotlin"
|
||||
? `${language.errorType(item.name)}.${variant.name}`
|
||||
: language.variant(variant.name, dataCarrying)}
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
{variant.fields.length === 0
|
||||
? "—"
|
||||
: variant.fields.map((field) => (
|
||||
<div key={field.name}>
|
||||
<code>
|
||||
{language.field(field.name)}: {language.type(field.type)}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{item.methods.map((method) => (
|
||||
<FuncEntry key={method.name} func={method} language={language} owner={item.name} />
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GdkApiReference() {
|
||||
const [languageId, setLanguageId] = useState<LanguageId>("rust");
|
||||
const [docVersion, setDocVersion] = useState(VERSIONS[0].docVersion);
|
||||
|
||||
const language = LANGUAGES.find((entry) => entry.id === languageId)!;
|
||||
const doc = useMemo(
|
||||
() => VERSIONS.find((entry) => entry.docVersion === docVersion) ?? VERSIONS[0],
|
||||
[docVersion],
|
||||
);
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
const order: GdkItem["kind"][] = ["object", "callback", "record", "enum", "error"];
|
||||
return order
|
||||
.map((kind) => ({ kind, items: doc.items.filter((item) => item.kind === kind) }))
|
||||
.filter((group) => group.items.length > 0);
|
||||
}, [doc]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.tabs} role="tablist" aria-label="GDK language">
|
||||
{LANGUAGES.map((entry) => (
|
||||
<button
|
||||
key={entry.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={entry.id === languageId}
|
||||
className={entry.id === languageId ? styles.tabActive : styles.tab}
|
||||
onClick={() => setLanguageId(entry.id)}
|
||||
>
|
||||
{entry.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label className={styles.version}>
|
||||
Version
|
||||
<select
|
||||
value={docVersion}
|
||||
onChange={(event) => setDocVersion(event.target.value)}
|
||||
aria-label="GDK version"
|
||||
>
|
||||
{VERSIONS.map((entry) => (
|
||||
<option key={entry.docVersion} value={entry.docVersion}>
|
||||
{entry.docVersion}.x
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p className={styles.meta}>
|
||||
Generated from <code>{doc.source}</code> at <code>goose-sdk {doc.version}</code>.
|
||||
</p>
|
||||
|
||||
<h2 id="functions">Functions</h2>
|
||||
{doc.functions.map((func) => (
|
||||
<FuncEntry key={func.name} func={func} language={language} />
|
||||
))}
|
||||
|
||||
{grouped.map((group) => (
|
||||
<React.Fragment key={group.kind}>
|
||||
<h2 id={slug(group.kind, "types")}>{KIND_HEADINGS[group.kind]}</h2>
|
||||
{group.items.map((item) => (
|
||||
<ItemEntry key={item.name} item={item} language={language} />
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Renders the Rust API surface in each target language's idioms. The rules
|
||||
// mirror the uniffi 0.32 code generators, which are the actual source of the
|
||||
// Python and Kotlin bindings.
|
||||
|
||||
export type LanguageId = "rust" | "python" | "kotlin";
|
||||
|
||||
const toSnake = (name: string) => name;
|
||||
const toCamel = (name: string) =>
|
||||
name.replace(/_([a-z0-9])/g, (_, char: string) => char.toUpperCase());
|
||||
const toShoutySnake = (name: string) =>
|
||||
name
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
||||
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
||||
.toUpperCase();
|
||||
|
||||
type Scalars = Record<string, string>;
|
||||
|
||||
const PYTHON_SCALARS: Scalars = {
|
||||
String: "str",
|
||||
bool: "bool",
|
||||
i8: "int",
|
||||
i16: "int",
|
||||
i32: "int",
|
||||
i64: "int",
|
||||
u8: "int",
|
||||
u16: "int",
|
||||
u32: "int",
|
||||
u64: "int",
|
||||
f32: "float",
|
||||
f64: "float",
|
||||
"()": "None",
|
||||
};
|
||||
|
||||
const KOTLIN_SCALARS: Scalars = {
|
||||
String: "String",
|
||||
bool: "Boolean",
|
||||
i8: "Byte",
|
||||
i16: "Short",
|
||||
i32: "Int",
|
||||
i64: "Long",
|
||||
u8: "UByte",
|
||||
u16: "UShort",
|
||||
u32: "UInt",
|
||||
u64: "ULong",
|
||||
f32: "Float",
|
||||
f64: "Double",
|
||||
"()": "Unit",
|
||||
};
|
||||
|
||||
const generic = (type: string, name: string): string[] | null => {
|
||||
const match = new RegExp(`^${name}\\s*<(.+)>$`, "s").exec(type.trim());
|
||||
if (!match) return null;
|
||||
const args: string[] = [];
|
||||
let depth = 0;
|
||||
let current = "";
|
||||
for (const char of match[1]) {
|
||||
if (char === "<") depth += 1;
|
||||
if (char === ">") depth -= 1;
|
||||
if (char === "," && depth === 0) {
|
||||
args.push(current.trim());
|
||||
current = "";
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
if (current.trim()) args.push(current.trim());
|
||||
return args;
|
||||
};
|
||||
|
||||
const mapType = (type: string, language: LanguageId): string => {
|
||||
const trimmed = type.trim();
|
||||
if (language === "rust") return trimmed;
|
||||
|
||||
const scalars = language === "python" ? PYTHON_SCALARS : KOTLIN_SCALARS;
|
||||
if (scalars[trimmed]) return scalars[trimmed];
|
||||
|
||||
const option = generic(trimmed, "Option");
|
||||
if (option) {
|
||||
const inner = mapType(option[0], language);
|
||||
return language === "python" ? `${inner} | None` : `${inner}?`;
|
||||
}
|
||||
|
||||
const bytes = generic(trimmed, "Vec");
|
||||
if (bytes && bytes[0].trim() === "u8") {
|
||||
return language === "python" ? "bytes" : "ByteArray";
|
||||
}
|
||||
if (bytes) {
|
||||
const inner = mapType(bytes[0], language);
|
||||
return language === "python" ? `list[${inner}]` : `List<${inner}>`;
|
||||
}
|
||||
|
||||
const map = generic(trimmed, "HashMap");
|
||||
if (map) {
|
||||
const [key, value] = map.map((arg) => mapType(arg, language));
|
||||
return language === "python" ? `dict[${key}, ${value}]` : `Map<${key}, ${value}>`;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const mapDefault = (value: string, language: LanguageId): string => {
|
||||
if (language === "rust") return value;
|
||||
if (value === "None") return language === "python" ? "None" : "null";
|
||||
if (value === "true" || value === "false") {
|
||||
return language === "python" ? (value === "true" ? "True" : "False") : value;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export type Language = {
|
||||
id: LanguageId;
|
||||
label: string;
|
||||
/** Prism language for syntax highlighting. */
|
||||
prism: string;
|
||||
func: (name: string) => string;
|
||||
field: (name: string) => string;
|
||||
variant: (name: string, isDataCarrying: boolean) => string;
|
||||
type: (type: string) => string;
|
||||
default: (value: string) => string;
|
||||
errorType: (name: string) => string;
|
||||
};
|
||||
|
||||
export const LANGUAGES: Language[] = [
|
||||
{
|
||||
id: "rust",
|
||||
label: "Rust",
|
||||
prism: "rust",
|
||||
func: toSnake,
|
||||
field: toSnake,
|
||||
variant: (name) => name,
|
||||
type: (type) => mapType(type, "rust"),
|
||||
default: (value) => mapDefault(value, "rust"),
|
||||
errorType: (name) => name,
|
||||
},
|
||||
{
|
||||
id: "python",
|
||||
label: "Python",
|
||||
prism: "python",
|
||||
func: toSnake,
|
||||
field: toSnake,
|
||||
// Flat enums become `enum.Enum` members; data-carrying variants become
|
||||
// nested dataclasses that keep their Rust casing.
|
||||
variant: (name, isDataCarrying) => (isDataCarrying ? name : toShoutySnake(name)),
|
||||
type: (type) => mapType(type, "python"),
|
||||
default: (value) => mapDefault(value, "python"),
|
||||
errorType: (name) => name,
|
||||
},
|
||||
{
|
||||
id: "kotlin",
|
||||
label: "Kotlin",
|
||||
prism: "kotlin",
|
||||
func: toCamel,
|
||||
field: toCamel,
|
||||
// Flat enums become `enum class` entries; data-carrying variants become
|
||||
// `sealed class` subclasses that keep their Rust casing.
|
||||
variant: (name, isDataCarrying) => (isDataCarrying ? name : toShoutySnake(name)),
|
||||
type: (type) => mapType(type, "kotlin"),
|
||||
default: (value) => mapDefault(value, "kotlin"),
|
||||
errorType: (name) => name.replace(/Error$/, "Exception"),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,103 @@
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
border-radius: var(--ifm-global-radius);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.tab,
|
||||
.tabActive {
|
||||
border: 0;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
padding: 0.35rem 0.9rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
color: var(--ifm-color-emphasis-700);
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
.tabActive {
|
||||
background: var(--ifm-color-primary);
|
||||
color: var(--ifm-color-primary-contrast-background);
|
||||
}
|
||||
|
||||
.version {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.version select {
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
border-radius: var(--ifm-global-radius);
|
||||
background: var(--ifm-background-color);
|
||||
color: var(--ifm-font-color-base);
|
||||
padding: 0.35rem 0.5rem;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 0.9rem;
|
||||
color: var(--ifm-color-emphasis-700);
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-bottom: 2.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
.itemTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: var(--ifm-color-emphasis-200);
|
||||
color: var(--ifm-color-emphasis-800);
|
||||
}
|
||||
|
||||
.entry {
|
||||
margin: 1.25rem 0 1.75rem;
|
||||
}
|
||||
|
||||
.entryTitle {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user