docs: add SDK API reference for Rust, Python, and Kotlin (#11251)

This commit is contained in:
Alex Hancock
2026-08-24 20:07:29 +00:00
committed by GitHub
parent 4db9e21b98
commit f9ac24cbfc
33 changed files with 2606 additions and 23 deletions
@@ -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