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,381 @@
#!/usr/bin/env python3
"""Generate GDK API reference data from the UniFFI surface in goose-sdk.
`crates/goose-sdk/src/bindings.rs` is the single source of truth for the Rust,
Python, and Kotlin GDK APIs, so the docs are derived from it instead of being
written by hand. Output is `documentation/src/data/gdk-api.json`, holding one
entry per GDK release series, consumed by the GdkApiReference component.
Usage:
python3 documentation/automation/gdk-api/generate.py [--check]
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
BINDINGS = REPO_ROOT / "crates/goose-sdk/src/bindings.rs"
CARGO_TOML = REPO_ROOT / "crates/goose-sdk/Cargo.toml"
OUT_FILE = REPO_ROOT / "documentation/src/data/gdk-api.json"
def crate_version() -> str:
match = re.search(r'^version\s*=\s*"([^"]+)"', CARGO_TOML.read_text(), re.MULTILINE)
if not match:
sys.exit(f"could not read version from {CARGO_TOML}")
return match.group(1)
def doc_version(version: str) -> str:
"""Docs are versioned per release series, e.g. 0.1.0-alpha.6 -> 0.1."""
major, minor = version.split(".")[:2]
return f"{major}.{minor}"
@dataclass
class Param:
name: str
type: str
default: str | None = None
docs: str = ""
@dataclass
class Func:
name: str
docs: str = ""
params: list[Param] = field(default_factory=list)
returns: str | None = None
throws: str | None = None
is_async: bool = False
@dataclass
class Item:
name: str
kind: str
docs: str = ""
fields: list[Param] = field(default_factory=list)
variants: list[dict] = field(default_factory=list)
methods: list[Func] = field(default_factory=list)
def split_top_level(text: str, sep: str = ",") -> list[str]:
parts, depth, current = [], 0, ""
for char in text:
if char in "<([{":
depth += 1
elif char in ">)]}":
depth -= 1
if char == sep and depth == 0:
parts.append(current)
current = ""
else:
current += char
if current.strip():
parts.append(current)
return [part.strip() for part in parts if part.strip()]
def unwrap(type_text: str, wrapper: str) -> str | None:
match = re.fullmatch(rf"{wrapper}\s*<(.+)>", type_text.strip(), re.DOTALL)
return match.group(1).strip() if match else None
def clean_type(type_text: str) -> str:
type_text = re.sub(r"\s+", " ", type_text).strip()
while True:
inner = unwrap(type_text, r"(?:std::sync::)?Arc") or unwrap(type_text, r"Box<\s*dyn")
if inner is None:
inner = unwrap(type_text, "Box")
if inner is None:
break
type_text = re.sub(r"^dyn\s+", "", inner)
return type_text
class Scanner:
"""Line scanner that pairs doc comments and attributes with the next item."""
def __init__(self, source: str) -> None:
self.lines = source.splitlines()
self.index = 0
self.docs: list[str] = []
self.attrs: list[str] = []
def take_docs(self) -> str:
docs = "\n".join(self.docs).strip()
self.docs = []
return docs
def block(self) -> str:
"""Consume from the current line through its balanced brace block."""
text, depth, started = "", 0, False
while self.index < len(self.lines):
line = self.lines[self.index]
self.index += 1
text += line + "\n"
depth += line.count("{") - line.count("}")
started = started or "{" in line
if started and depth <= 0:
break
if not started and line.rstrip().endswith(";"):
break
return text
def parse_fields(block: str) -> list[Param]:
fields: list[Param] = []
docs: list[str] = []
default: str | None = None
for line in block.splitlines():
stripped = line.strip()
if stripped.startswith("///"):
docs.append(stripped[3:].strip())
continue
match = re.match(r"#\[uniffi\(default\s*=\s*(.+?)\)\]", stripped)
if match:
default = match.group(1).strip()
continue
match = re.match(r"pub\s+([a-z_0-9]+)\s*:\s*(.+?),?$", stripped)
if match:
fields.append(
Param(
match.group(1),
clean_type(match.group(2)),
default,
" ".join(docs).strip(),
)
)
docs, default = [], None
return fields
def parse_variants(block: str) -> list[dict]:
body = block[block.index("{") + 1 : block.rindex("}")]
variants: list[dict] = []
for chunk in split_top_level(re.sub(r"#\[[^\]]*\]", "", body)):
chunk = chunk.strip()
match = re.match(r"^([A-Z]\w*)\s*\{(.*)\}$", chunk, re.DOTALL)
if match:
fields = [
Param(name.strip(), clean_type(type_text))
for name, _, type_text in (
part.partition(":") for part in split_top_level(match.group(2))
)
if name.strip() and not name.strip().startswith("#")
]
variants.append({"name": match.group(1), "fields": [vars(f) for f in fields]})
elif re.fullmatch(r"[A-Z]\w*", chunk):
variants.append({"name": chunk, "fields": []})
return variants
def parse_signature(signature: str, docs: str) -> Func:
signature = re.sub(r"\s+", " ", signature).strip().rstrip("{;").strip()
is_async = " async fn " in f" {signature} "
match = re.search(r"fn\s+(\w+)\s*\((.*)\)\s*(?:->\s*(.+))?$", signature, re.DOTALL)
if not match:
return Func(name=signature, docs=docs)
name, raw_params, raw_return = match.group(1), match.group(2), match.group(3)
params = []
for part in split_top_level(raw_params):
if re.fullmatch(r"&?\s*(mut\s+)?self", part):
continue
param_name, _, type_text = part.partition(":")
if type_text:
params.append(Param(param_name.strip(), clean_type(type_text)))
returns, throws = None, None
if raw_return:
result = clean_type(raw_return)
inner = unwrap(result, "Result")
if inner:
parts = split_top_level(inner)
returns = clean_type(parts[0])
throws = clean_type(parts[1]) if len(parts) > 1 else "GooseError"
else:
returns = result
if returns in ("()", ""):
returns = None
return Func(name=name, docs=docs, params=params, returns=returns, throws=throws, is_async=is_async)
def parse_bindings(source: str) -> dict[str, list[Item] | list[Func]]:
source = source.split("#[cfg(test)]")[0]
scanner = Scanner(source)
items: list[Item] = []
functions: list[Func] = []
while scanner.index < len(scanner.lines):
line = scanner.lines[scanner.index]
stripped = line.strip()
if stripped.startswith("///"):
scanner.docs.append(stripped[3:].strip())
scanner.index += 1
continue
if stripped.startswith("#["):
scanner.attrs.append(stripped)
scanner.index += 1
continue
if not stripped or stripped.startswith("//"):
scanner.index += 1
scanner.docs = []
continue
attrs = " ".join(scanner.attrs)
scanner.attrs = []
docs = scanner.take_docs()
exported = "uniffi::export" in attrs
if "uniffi::Record" in attrs and stripped.startswith("pub struct"):
block = scanner.block()
name = re.search(r"pub struct\s+(\w+)", block).group(1)
items.append(Item(name, "record", docs, fields=parse_fields(block)))
continue
if "uniffi::Object" in attrs and stripped.startswith("pub struct"):
block = scanner.block()
name = re.search(r"pub struct\s+(\w+)", block).group(1)
items.append(Item(name, "object", docs))
continue
if ("uniffi::Enum" in attrs or "uniffi::Error" in attrs) and stripped.startswith("pub enum"):
block = scanner.block()
name = re.search(r"pub enum\s+(\w+)", block).group(1)
kind = "error" if "uniffi::Error" in attrs else "enum"
items.append(Item(name, kind, docs, variants=parse_variants(block)))
continue
if exported and stripped.startswith("pub trait"):
block = scanner.block()
name = re.search(r"pub trait\s+(\w+)", block).group(1)
methods = [
parse_signature(match, "")
for match in re.findall(r"fn\s+\w+\s*\([^;]*?\)\s*(?:->[^;]+)?;", block)
]
items.append(Item(name, "callback", docs, methods=methods))
continue
if exported and stripped.startswith("impl "):
block = scanner.block()
target = re.search(r"impl\s+(\w+)", block).group(1)
owner = next((item for item in items if item.name == target), None)
if owner:
owner.methods.extend(parse_impl_methods(block))
continue
if exported and re.match(r"pub\s+(async\s+)?fn", stripped):
block = scanner.block()
functions.append(parse_signature(block.split("{")[0], docs))
continue
scanner.index += 1
return {"items": items, "functions": functions}
def parse_impl_methods(block: str) -> list[Func]:
methods: list[Func] = []
lines = block.splitlines()
docs: list[str] = []
index = 0
while index < len(lines):
stripped = lines[index].strip()
if stripped.startswith("///"):
docs.append(stripped[3:].strip())
index += 1
continue
if re.match(r"pub\s+(async\s+)?fn", stripped):
signature, depth = "", 0
while index < len(lines):
signature += lines[index] + "\n"
depth += lines[index].count("(") - lines[index].count(")")
if depth <= 0 and ("{" in lines[index] or ";" in lines[index]):
break
index += 1
methods.append(parse_signature(signature.split("{")[0], "\n".join(docs).strip()))
docs = []
elif stripped and not stripped.startswith("#"):
docs = []
index += 1
return methods
def build(version: str) -> dict:
parsed = parse_bindings(BINDINGS.read_text())
items: list[Item] = parsed["items"]
functions: list[Func] = parsed["functions"]
if not items or not functions:
sys.exit("parsed no API items; the bindings layout likely changed")
def serialize_func(func: Func) -> dict:
return {
"name": func.name,
"docs": func.docs,
"params": [vars(param) for param in func.params],
"returns": func.returns,
"throws": func.throws,
"isAsync": func.is_async,
}
def serialize_item(item: Item) -> dict:
return {
"name": item.name,
"kind": item.kind,
"docs": item.docs,
"fields": [vars(field_) for field_ in item.fields],
"variants": item.variants,
"methods": [serialize_func(method) for method in item.methods],
}
return {
"version": version,
"docVersion": doc_version(version),
"source": "crates/goose-sdk/src/bindings.rs",
"functions": [serialize_func(func) for func in sorted(functions, key=lambda f: f.name)],
"items": [serialize_item(item) for item in items],
}
def merge(current: dict) -> dict:
"""Upserts the current release series, keeping older series newest-first."""
existing = json.loads(OUT_FILE.read_text())["versions"] if OUT_FILE.exists() else []
versions = [
entry for entry in existing if entry["docVersion"] != current["docVersion"]
] + [current]
versions.sort(
key=lambda entry: [int(part) for part in entry["docVersion"].split(".")],
reverse=True,
)
return {"versions": versions}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="fail if output is stale")
args = parser.parse_args()
version = crate_version()
payload = json.dumps(merge(build(version)), indent=2) + "\n"
relative = OUT_FILE.relative_to(REPO_ROOT)
if args.check:
if not OUT_FILE.exists() or OUT_FILE.read_text() != payload:
print(f"{relative} is out of date; run {Path(__file__).name}")
return 1
print(f"{relative} is up to date")
return 0
OUT_FILE.parent.mkdir(parents=True, exist_ok=True)
OUT_FILE.write_text(payload)
print(f"wrote {relative} for goose-sdk {version}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,6 +1,6 @@
{
"label": "Experimental",
"position": 7,
"position": 8,
"link": {
"type": "doc",
"id": "experimental/index"
+8
View File
@@ -0,0 +1,8 @@
{
"label": "GDK",
"position": 3,
"link": {
"type": "doc",
"id": "gdk/index"
}
}
+18
View File
@@ -0,0 +1,18 @@
---
sidebar_position: 2
title: API Reference
sidebar_label: API Reference
description: Complete goose GDK API reference for Rust, Python, and Kotlin.
---
import GdkApiReference from '@site/src/components/GdkApiReference';
# API Reference
The complete GDK surface. Use the toggles to switch language and GDK
version — names and types are shown using each language's own conventions.
New to the GDK? Start with the [GDK overview](/docs/gdk) for installation and
runnable examples.
<GdkApiReference />
+239
View File
@@ -0,0 +1,239 @@
---
sidebar_position: 1
title: GDK Overview
sidebar_label: Overview
description: Build with goose providers in Rust, Python, and Kotlin.
---
# GDK
The goose Development Kit (GDK) exposes goose's provider layer as a library so you
can call models, stream completions, and compact conversations from your own
application.
One Rust crate, `goose-sdk`, is the source of every language binding. Python and
Kotlin are generated from it with [UniFFI](https://github.com/mozilla/uniffi-rs),
so all three languages share the same types, behavior, and version number.
See the [API Reference](/docs/gdk/api-reference) for the complete surface in your
language of choice.
:::info Alpha
The GDK is in alpha. The surface may change between `0.x` releases. Pin an exact
version and check the API reference version selector when upgrading.
:::
## What you can do
- Construct providers for OpenAI, Anthropic, Groq, Databricks, or any
[declarative provider](#declarative-providers) defined in JSON
- Stream a completion chunk by chunk, including tool calls and reasoning output
- Request a single non-streaming completion
- Compact a long conversation into a summary so it can continue past the
model's context window
- Capture provider request logs as JSONL
## Install
<!-- prettier-ignore-start -->
### Rust
```bash
cargo add goose-sdk
```
By default the crate re-exports the Agent Client Protocol (ACP) wire types for
talking to `goose acp` over stdio. Enable the `uniffi` feature for the
in-process provider API documented in the reference:
```bash
cargo add goose-sdk --features uniffi
```
### Python
```bash
pip install goose-sdk
```
The package installs as `goose-sdk` and imports as `goose`. Wheels bundle the
native library, so there is nothing else to build. Requires Python 3.9+.
```python
import goose
```
### Kotlin / JVM
```kotlin
dependencies {
implementation("io.github.aaif-goose:gdk:<version>")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}
```
The artifact version matches the Rust crate version. Classes live in the
`io.github.aaif_goose` package. The jar bundles native libraries for
macOS (arm64, x86-64), Linux (arm64, x86-64), and Windows (x86-64).
On JDK 24+, add `--enable-native-access=ALL-UNNAMED` because the GDK loads its
native library through JNA.
<!-- prettier-ignore-end -->
## Quickstart
Each example builds a provider, sends one message, and prints the streamed
response.
### Python
```python
import asyncio
from goose import (
MessageContent,
MessageRole,
ProviderMessage,
ProviderModelConfig,
StreamChunk,
openai_default_model,
openai_provider,
)
async def main() -> None:
provider = openai_provider(api_key="...")
model = ProviderModelConfig(model_name=openai_default_model())
messages = [
ProviderMessage(
role=MessageRole.USER,
content=[MessageContent.Text(text="What is the capital of France?")],
)
]
stream = await provider.stream(model, "You are a geography expert.", messages, [])
while chunk := await stream.next_chunk():
if isinstance(chunk, StreamChunk.TextChunk):
print(chunk.text, end="")
asyncio.run(main())
```
### Kotlin
```kotlin
import io.github.aaif_goose.MessageContent
import io.github.aaif_goose.MessageRole
import io.github.aaif_goose.ProviderMessage
import io.github.aaif_goose.ProviderModelConfig
import io.github.aaif_goose.StreamChunk
import io.github.aaif_goose.streamFlow
import io.github.aaif_goose.providers.openai.defaultModel
import io.github.aaif_goose.providers.openai.provider as openAiProvider
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val provider = openAiProvider(System.getenv("OPENAI_API_KEY"))
val model = ProviderModelConfig(modelName = defaultModel())
val messages = listOf(
ProviderMessage(
role = MessageRole.USER,
content = listOf(MessageContent.Text(text = "What is the capital of France?")),
),
)
provider.streamFlow(model, "You are a geography expert.", messages)
.collect { chunk ->
if (chunk is StreamChunk.TextChunk) print(chunk.text)
}
}
```
### Rust
```rust
use goose_sdk::bindings::{
openai_default_model, openai_provider, MessageContent, MessageRole, ProviderMessage,
ProviderModelConfig, StreamChunk,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let provider = openai_provider(std::env::var("OPENAI_API_KEY")?)?;
let model = ProviderModelConfig {
model_name: openai_default_model(),
..Default::default()
};
let messages = vec![ProviderMessage {
role: MessageRole::User,
content: vec![MessageContent::Text {
text: "What is the capital of France?".to_string(),
}],
}];
let stream = provider
.stream(model, "You are a geography expert.".to_string(), messages, vec![])
.await?;
while let Some(chunk) = stream.next_chunk().await? {
if let StreamChunk::TextChunk { text } = chunk {
print!("{text}");
}
}
Ok(())
}
```
## Kotlin idioms
The Kotlin package adds a few conveniences on top of the generated bindings:
| Kotlin API | Equivalent generated call |
| --- | --- |
| `provider.streamFlow(model, system, messages, tools)` | `stream(...)` plus a `nextChunk()` loop, as a `Flow<StreamChunk>` |
| `providers.openai.provider(apiKey)` | `openaiProvider(apiKey)` |
| `providers.openai.defaultModel()` | `openaiDefaultModel()` |
| `providers.anthropic.provider(apiKey, baseUrl, betaHeaders)` | `anthropicProvider(...)` |
| `providers.groq.provider(apiKey)` | `groqProvider(apiKey)` |
| `providers.databricks.provider(host, token)` | `databricksProvider(host, token)` |
`tools` defaults to an empty list in the Kotlin helpers, and suspending
functions map to Kotlin coroutines. Errors surface as `GooseException`
subclasses.
## Declarative providers
Any provider that speaks an OpenAI- or Anthropic-compatible API can be defined
in JSON and loaded without new Rust code:
```python
provider = goose.declarative_provider_from_json(open("deepseek.json").read())
```
Environment variable placeholders such as `${DEEPSEEK_API_KEY}` in the JSON are
resolved when the provider is constructed.
## Streaming model
`stream()` returns a `ProviderStream`. Call `next_chunk()` until it returns
`None` to consume the response:
| Chunk | Meaning |
| --- | --- |
| `TextChunk` | Assistant text |
| `ToolChunk` | A tool call request with JSON arguments |
| `ThinkingChunk` / `RedactedThinkingChunk` | Reasoning output |
| `EndChunk` | Stream finished, carries final token `Usage` |
| `ErrorChunk` | Mid-stream failure, carries a `GooseStreamError` |
Errors raised before the stream starts are thrown as `GooseError`
(`GooseException` in Kotlin). Errors that occur mid-stream arrive as an
`ErrorChunk` instead.
## Next steps
- [API Reference](/docs/gdk/api-reference) — every function, type, and error
- [goose in ACP clients](/docs/guides/acp-clients) — drive the full goose agent
over the Agent Client Protocol
@@ -1,6 +1,6 @@
{
"label": "Architecture Overview",
"position": 6,
"position": 7,
"link": {
"type": "generated-index",
"description": "Extend goose functionalities with extensions and custom configurations"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Guides",
"position": 3,
"position": 4,
"link": {
"type": "generated-index",
"description": "Learn essential tips and recommendations for using goose"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "MCP Servers",
"position": 5,
"position": 6,
"link": {
"type": "generated-index",
"description": "How to integrate and use MCP servers as goose extensions"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Tutorials",
"position": 4,
"position": 5,
"link": {
"type": "generated-index",
"description": "How to use goose in various ways"
@@ -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