feat(zip): support native Rust, C++, and Go self-documenting SDKs, MCPs, and @hanzo/docs

- Add App.RustSDK() for full ZAP-native self-documenting Rust SDK with rustdoc
- Add App.CppSDK() for modern C++20 self-documenting SDK with Doxygen
- Add App.RustMCP() and App.CppMCP() for native Model Context Protocol tools & servers
- Add App.DocsMarkdown() for @hanzo/docs MDX pages with Go/Rust/C++ tabbed snippets
- Add cmd/zipgen CLI tool for sdk, mcp, and docs generation
This commit is contained in:
2026-09-03 19:16:02 -07:00
parent 113e3df8a7
commit 4d74af5e3d
8 changed files with 1460 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
// Command zipgen generates native Go, Rust, and C++ SDKs, MCP servers, and @hanzo/docs.
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/zap-proto/zip"
)
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(1)
}
cmd := os.Args[1]
args := os.Args[2:]
switch cmd {
case "sdk":
runSDK(args)
case "mcp":
runMCP(args)
case "docs":
runDocs(args)
default:
usage()
os.Exit(1)
}
}
func usage() {
fmt.Fprintf(os.Stderr, `Usage: zipgen <command> [options]
Commands:
sdk Generate native Go, Rust, or C++ client SDK with full doc comments
mcp Generate native Go, Rust, or C++ MCP server and tools
docs Generate @hanzo/docs compatible MDX documentation pages
Run 'zipgen <command> -h' for command options.
`)
}
func runSDK(args []string) {
fs := flag.NewFlagSet("sdk", flag.ExitOnError)
lang := fs.String("lang", "rust", "target language: rust, cpp, or go")
pkg := fs.String("pkg", "client", "package/crate/namespace name")
out := fs.String("o", "", "output directory or file")
fs.Parse(args)
app := zip.New(zip.Config{AppName: *pkg})
switch strings.ToLower(*lang) {
case "rust", "rs":
res, err := app.RustSDK(*pkg)
if err != nil {
fmt.Fprintf(os.Stderr, "zipgen: %v\n", err)
os.Exit(1)
}
writeOutput(*out, "lib.rs", res.Source)
case "cpp", "c++":
res, err := app.CppSDK(*pkg)
if err != nil {
fmt.Fprintf(os.Stderr, "zipgen: %v\n", err)
os.Exit(1)
}
writeOutput(*out, "client.hpp", res.Header)
case "go":
res, err := app.SDK(*pkg)
if err != nil {
fmt.Fprintf(os.Stderr, "zipgen: %v\n", err)
os.Exit(1)
}
writeOutput(*out, "client.go", res.Source)
default:
fmt.Fprintf(os.Stderr, "unknown language: %s (choose rust, cpp, or go)\n", *lang)
os.Exit(1)
}
}
func runMCP(args []string) {
fs := flag.NewFlagSet("mcp", flag.ExitOnError)
lang := fs.String("lang", "rust", "target language: rust, cpp, or go")
pkg := fs.String("pkg", "mcp", "package/crate/namespace name")
out := fs.String("o", "", "output directory or file")
fs.Parse(args)
app := zip.New(zip.Config{AppName: *pkg})
switch strings.ToLower(*lang) {
case "rust", "rs":
res, err := app.RustMCP(*pkg)
if err != nil {
fmt.Fprintf(os.Stderr, "zipgen: %v\n", err)
os.Exit(1)
}
writeOutput(*out, "mcp.rs", res.Source)
case "cpp", "c++":
res, err := app.CppMCP(*pkg)
if err != nil {
fmt.Fprintf(os.Stderr, "zipgen: %v\n", err)
os.Exit(1)
}
writeOutput(*out, "mcp.hpp", res.Header)
default:
fmt.Fprintf(os.Stderr, "unknown language: %s (choose rust or cpp)\n", *lang)
os.Exit(1)
}
}
func runDocs(args []string) {
fs := flag.NewFlagSet("docs", flag.ExitOnError)
title := fs.String("title", "API Reference", "documentation title")
out := fs.String("o", "./docs", "output directory for MDX pages")
fs.Parse(args)
app := zip.New(zip.Config{AppName: *title})
bundle, err := app.DocsMarkdown(*title)
if err != nil {
fmt.Fprintf(os.Stderr, "zipgen: %v\n", err)
os.Exit(1)
}
if err := os.MkdirAll(*out, 0755); err != nil {
fmt.Fprintf(os.Stderr, "zipgen: %v\n", err)
os.Exit(1)
}
_ = os.WriteFile(filepath.Join(*out, "index.mdx"), []byte(bundle.Index), 0644)
for fname, content := range bundle.Pages {
_ = os.WriteFile(filepath.Join(*out, fname), []byte(content), 0644)
}
fmt.Printf("Generated %d documentation pages in %s\n", len(bundle.Pages)+1, *out)
}
func writeOutput(outPath, defaultName string, data []byte) {
if outPath == "" || outPath == "-" {
os.Stdout.Write(data)
return
}
stat, err := os.Stat(outPath)
target := outPath
if err == nil && stat.IsDir() {
target = filepath.Join(outPath, defaultName)
}
if err := os.WriteFile(target, data, 0644); err != nil {
fmt.Fprintf(os.Stderr, "zipgen: %v\n", err)
os.Exit(1)
}
fmt.Printf("Wrote %s (%d bytes)\n", target, len(data))
}
+105
View File
@@ -0,0 +1,105 @@
package zip
import (
"encoding/json"
"fmt"
"sort"
"strings"
)
// CppMCP is one generated C++20 Model Context Protocol (MCP) server header.
type CppMCP struct {
Namespace string
Header []byte
}
// CppMCP renders a native C++20 Model Context Protocol (MCP) tool surface and server dispatch.
// Every tool is self-documenting with Doxygen comments and JSON input schema.
func (a *App) CppMCP(namespace string) (*CppMCP, error) {
if namespace == "" {
namespace = "mcp"
}
tools := a.MCPTools()
var b strings.Builder
b.WriteString("// Code generated from the typed-op registry by zip. DO NOT EDIT.\n")
fmt.Fprintf(&b, "// Model Context Protocol (MCP) Server tools for %s.\n\n", namespace)
b.WriteString("#pragma once\n\n")
b.WriteString("#include <string>\n")
b.WriteString("#include <vector>\n")
b.WriteString("#include <memory>\n")
b.WriteString("#include <stdexcept>\n")
b.WriteString("#include <nlohmann/json.hpp>\n\n")
fmt.Fprintf(&b, "namespace %s {\n\n", namespace)
b.WriteString("/** Tool descriptor exposed to MCP clients and agents. */\n")
b.WriteString("struct ToolDescriptor {\n")
b.WriteString(" std::string name;\n")
b.WriteString(" std::string description;\n")
b.WriteString(" nlohmann::json input_schema;\n")
b.WriteString("};\n\n")
b.WriteString("/** Interface implemented by service handlers to process MCP tool calls. */\n")
b.WriteString("class McpHandler {\n")
b.WriteString("public:\n")
b.WriteString(" virtual ~McpHandler() = default;\n\n")
// Sort tools by name
sort.Slice(tools, func(i, j int) bool {
return tools[i]["name"].(string) < tools[j]["name"].(string)
})
for _, tool := range tools {
name := tool["name"].(string)
methodName := snakeCase(name)
desc := ""
if d, ok := tool["description"].(string); ok {
desc = d
}
if desc != "" {
cppDoxygen(&b, " ", desc)
} else {
fmt.Fprintf(&b, " /** Handler for tool `%s`. */\n", name)
}
fmt.Fprintf(&b, " virtual nlohmann::json %s(const nlohmann::json& args) = 0;\n", methodName)
}
b.WriteString("};\n\n")
b.WriteString("/** Returns the list of available MCP tools for tools/list queries. */\n")
b.WriteString("inline std::vector<ToolDescriptor> list_tools() {\n")
b.WriteString(" return {\n")
for _, tool := range tools {
name := tool["name"].(string)
desc := ""
if d, ok := tool["description"].(string); ok {
desc = d
}
schemaBytes, _ := json.Marshal(tool["inputSchema"])
b.WriteString(" ToolDescriptor{\n")
fmt.Fprintf(&b, " .name = %q,\n", name)
fmt.Fprintf(&b, " .description = %q,\n", desc)
fmt.Fprintf(&b, " .input_schema = nlohmann::json::parse(%q),\n", string(schemaBytes))
b.WriteString(" },\n")
}
b.WriteString(" };\n")
b.WriteString("}\n\n")
b.WriteString("/** Dispatches an MCP tools/call request to the corresponding handler method. */\n")
b.WriteString("inline nlohmann::json dispatch(McpHandler& handler, const std::string& name, const nlohmann::json& arguments) {\n")
for _, tool := range tools {
name := tool["name"].(string)
methodName := snakeCase(name)
fmt.Fprintf(&b, " if (name == %q) return handler.%s(arguments);\n", name, methodName)
}
b.WriteString(" throw std::runtime_error(\"unknown tool: \" + name);\n")
b.WriteString("}\n\n")
fmt.Fprintf(&b, "} // namespace %s\n", namespace)
return &CppMCP{
Namespace: namespace,
Header: []byte(b.String()),
}, nil
}
+330
View File
@@ -0,0 +1,330 @@
package zip
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/zap-proto/zip/internal/zapenc"
)
// CppSDK is one generated C++20 client SDK: the header content, and any gaps.
type CppSDK struct {
Namespace string
Header []byte
Gaps []Gap
}
// Ops reports how many operations the C++ SDK implements.
func (s *CppSDK) Ops() int {
return strings.Count(string(s.Header), "virtual ")
}
// CppSDK renders a native modern C++20 client SDK whose methods are this app's operations.
// Every method and struct is fully self-documenting using Doxygen doc comments.
func (a *App) CppSDK(namespace string) (*CppSDK, error) {
if namespace == "" {
namespace = "client"
}
g := &cppRender{
sdk: &CppSDK{Namespace: namespace},
named: map[reflect.Type]string{},
taken: map[string]bool{},
}
ops := append([]*registeredOp(nil), a.Registry()...)
sort.Slice(ops, func(i, j int) bool { return opName(ops[i]) < opName(ops[j]) })
for _, op := range ops {
g.method(op)
}
g.sdk.Header = g.render(a)
sort.Slice(g.sdk.Gaps, func(i, j int) bool {
if g.sdk.Gaps[i].Op != g.sdk.Gaps[j].Op {
return g.sdk.Gaps[i].Op < g.sdk.Gaps[j].Op
}
return g.sdk.Gaps[i].Field < g.sdk.Gaps[j].Field
})
return g.sdk, nil
}
type cppCall struct {
id string
method string
doc Doc
hasDoc bool
in string
out string
httpM string
path string
}
type cppRender struct {
sdk *CppSDK
named map[reflect.Type]string
taken map[string]bool
calls []cppCall
decls []string
}
func (g *cppRender) method(op *registeredOp) {
id := opName(op)
doc, hasDoc := docFor(op.Pkg, op.Method, op.Path)
mName := snakeCase(id)
if mName == "" {
g.gap(id, "", "", causeUnnamed)
return
}
c := cppCall{
id: id,
method: mName,
doc: doc,
hasDoc: hasDoc,
httpM: op.Method,
path: op.Path,
}
in, ok := g.declare(op.InType, id, docFields(hasDoc, doc))
if !ok {
return
}
out, ok := g.declare(op.OutType, id, docFields(hasDoc, doc))
if !ok {
return
}
c.in, c.out = in, out
g.calls = append(g.calls, c)
}
func (g *cppRender) declare(t reflect.Type, op string, fields map[string]string) (string, bool) {
t = deref(t)
if t == nil || t.Kind() != reflect.Struct || t.NumField() == 0 {
return "", true
}
if name, seen := g.named[t]; seen {
return name, name != ""
}
shape, err := zapenc.LayoutOf(t)
if err != nil {
g.named[t] = ""
g.gap(op, goName(t), t.String(), causeOf(err))
return "", false
}
for _, s := range shape.Slots {
if strings.HasPrefix(s.Type, "bytes_fixed[") || strings.HasPrefix(s.Elem, "bytes_fixed[") {
g.named[t] = ""
g.gap(op, goName(t)+"."+s.Name, s.Type, causeCodec)
return "", false
}
}
name := exportIdent(t.Name())
if name == "" || g.taken[name] {
name = exportIdent(goName(t))
}
name = g.claim(name)
g.named[t] = name
var b strings.Builder
if d := fields[t.Name()]; d != "" {
cppDoxygen(&b, "", d)
}
fmt.Fprintf(&b, "struct %s {\n", name)
var fieldNames []string
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.IsExported() {
continue
}
jsonName := jsonFieldName(f)
if d := fields[t.Name()+"."+jsonName]; d != "" {
cppDoxygen(&b, " ", d)
}
cppType := g.typeOf(f.Type, op, t.Name()+"."+f.Name, fields)
fieldName := snakeCase(f.Name)
if isCppKeyword(fieldName) {
fieldName += "_"
}
fieldNames = append(fieldNames, fieldName)
fmt.Fprintf(&b, " %s %s{};\n", cppType, fieldName)
}
b.WriteString("};\n")
g.decls = append(g.decls, b.String())
return name, true
}
func (g *cppRender) typeOf(t reflect.Type, op, at string, fields map[string]string) string {
switch t.Kind() {
case reflect.Pointer:
return "std::optional<" + g.typeOf(t.Elem(), op, at, fields) + ">"
case reflect.Slice:
if t.Elem().Kind() == reflect.Uint8 {
return "std::vector<std::uint8_t>"
}
return "std::vector<" + g.typeOf(t.Elem(), op, at, fields) + ">"
case reflect.Array:
return "std::array<" + g.typeOf(t.Elem(), op, at, fields) + ", " + strconv.Itoa(t.Len()) + ">"
case reflect.Map:
return "std::map<" + g.typeOf(t.Key(), op, at, fields) + ", " + g.typeOf(t.Elem(), op, at, fields) + ">"
case reflect.Struct:
if t.NumField() == 0 {
return "void"
}
name, ok := g.declare(t, op, fields)
if !ok {
return "void"
}
return name
case reflect.Interface:
g.gap(op, at, t.String(), CauseAny)
return "nlohmann::json"
case reflect.String:
return "std::string"
case reflect.Bool:
return "bool"
case reflect.Int8:
return "std::int8_t"
case reflect.Int16:
return "std::int16_t"
case reflect.Int32:
return "std::int32_t"
case reflect.Int, reflect.Int64:
return "std::int64_t"
case reflect.Uint8:
return "std::uint8_t"
case reflect.Uint16:
return "std::uint16_t"
case reflect.Uint32:
return "std::uint32_t"
case reflect.Uint, reflect.Uint64:
return "std::uint64_t"
case reflect.Float32:
return "float"
case reflect.Float64:
return "double"
}
return "nlohmann::json"
}
func (g *cppRender) claim(name string) string {
if name == "" {
name = "T"
}
base := name
for n := 2; g.taken[name]; n++ {
name = base + strconv.Itoa(n)
}
g.taken[name] = true
return name
}
func (g *cppRender) gap(op, field, goType, cause string) {
g.sdk.Gaps = append(g.sdk.Gaps, Gap{Op: op, Field: field, Go: goType, Cause: cause})
}
func (g *cppRender) render(a *App) []byte {
appName := a.cfg.AppName
if appName == "" {
appName = g.sdk.Namespace
}
var b strings.Builder
b.WriteString("// Code generated from the typed-op registry by zip. DO NOT EDIT.\n")
fmt.Fprintf(&b, "// Client SDK for %s operations over ZAP.\n\n", appName)
b.WriteString("#pragma once\n\n")
b.WriteString("#include <string>\n")
b.WriteString("#include <vector>\n")
b.WriteString("#include <optional>\n")
b.WriteString("#include <array>\n")
b.WriteString("#include <map>\n")
b.WriteString("#include <cstdint>\n")
b.WriteString("#include <memory>\n")
b.WriteString("#include <stdexcept>\n")
b.WriteString("#include <nlohmann/json.hpp>\n\n")
fmt.Fprintf(&b, "namespace %s {\n\n", g.sdk.Namespace)
for _, d := range g.decls {
b.WriteString(d)
b.WriteString("\n")
}
b.WriteString(cppClientPreamble)
for _, c := range g.calls {
if c.hasDoc && c.doc.Description != "" {
cppDoxygen(&b, " ", c.doc.Description)
if len(c.doc.Example) > 0 {
b.WriteString(" /**\n * Example payload:\n")
for _, line := range strings.Split(string(c.doc.Example), "\n") {
fmt.Fprintf(&b, " * %s\n", line)
}
b.WriteString(" */\n")
}
} else {
fmt.Fprintf(&b, " /** Calls operation `%s` (%s %s). */\n", c.id, c.httpM, c.path)
}
retType := "void"
if c.out != "" {
retType = c.out
}
inParam := ""
if c.in != "" {
inParam = "const " + c.in + "& in"
}
fmt.Fprintf(&b, " virtual %s %s(%s) = 0;\n\n", retType, c.method, inParam)
}
b.WriteString("};\n\n")
fmt.Fprintf(&b, "} // namespace %s\n", g.sdk.Namespace)
return []byte(b.String())
}
func cppDoxygen(b *strings.Builder, indent, s string) {
lines := strings.Split(strings.TrimSpace(s), "\n")
fmt.Fprintf(b, "%s/**\n", indent)
for _, l := range lines {
l = strings.TrimSpace(l)
if l == "" {
fmt.Fprintf(b, "%s *\n", indent)
} else {
fmt.Fprintf(b, "%s * %s\n", indent, l)
}
}
fmt.Fprintf(b, "%s */\n", indent)
}
func isCppKeyword(s string) bool {
switch s {
case "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel",
"atomic_commit", "atomic_noexcept", "auto", "bitand", "bitor",
"bool", "break", "case", "catch", "char", "char8_t", "char16_t",
"char32_t", "class", "compl", "concept", "const", "consteval",
"constexpr", "constinit", "const_cast", "continue", "co_await",
"co_return", "co_yield", "decltype", "default", "delete", "do",
"double", "dynamic_cast", "else", "enum", "explicit", "export",
"extern", "false", "float", "for", "friend", "goto", "if", "inline",
"int", "long", "mutable", "namespace", "new", "noexcept", "not",
"not_eq", "nullptr", "operator", "or", "or_eq", "private", "protected",
"public", "reflexpr", "register", "reinterpret_cast", "requires",
"return", "short", "signed", "sizeof", "static", "static_assert",
"static_cast", "struct", "switch", "synchronized", "template", "this",
"thread_local", "throw", "true", "try", "typedef", "typeid", "typename",
"union", "unsigned", "using", "virtual", "void", "volatile", "wchar_t",
"while", "xor", "xor_eq":
return true
}
return false
}
const cppClientPreamble = `/**
* \brief Client interface for invoking service operations over ZAP or HTTP.
*/
class Client {
public:
virtual ~Client() = default;
`
+171
View File
@@ -0,0 +1,171 @@
package zip_test
import (
"context"
"strings"
"testing"
"github.com/zap-proto/zip"
)
type AccountIn struct {
ID string `json:"id" validate:"required"`
}
type AccountOut struct {
ID string `json:"id"`
Balance uint64 `json:"balance"`
Active bool `json:"active"`
}
func getAccount(context.Context, *AccountIn) (*AccountOut, error) {
return &AccountOut{ID: "acc_1", Balance: 1000, Active: true}, nil
}
func crossApp() *zip.App {
app := zip.New(zip.Config{AppName: "ledger", DisableStartupMessage: true})
zip.Describe("POST /v1/account", zip.Doc{
Description: "GetAccount retrieves account details by ID.\n\nReturns the balance and active status.",
Fields: map[string]string{
"AccountIn.id": "Unique account identifier",
"AccountOut.id": "Account identifier",
"AccountOut.balance": "Current balance in cents",
"AccountOut.active": "Whether account is currently active",
},
Example: []byte(`{"id": "acc_1"}`),
Response: []byte(`{"id": "acc_1", "balance": 1000, "active": true}`),
})
zip.Post(app, "/v1/account", getAccount)
return app
}
func TestRustSDK_GeneratesSelfDocumentingSDK(t *testing.T) {
app := crossApp()
sdk, err := app.RustSDK("ledger")
if err != nil {
t.Fatalf("RustSDK error: %v", err)
}
src := string(sdk.Source)
t.Logf("Generated Rust SDK Source:\n%s", src)
// Check self-documenting rustdoc comments
if !strings.Contains(src, "/// GetAccount retrieves account details by ID.") {
t.Errorf("Rust SDK missing method rustdoc comment")
}
if !strings.Contains(src, "/// # Example") {
t.Errorf("Rust SDK missing example block")
}
if !strings.Contains(src, "pub struct AccountIn") {
t.Errorf("Rust SDK missing AccountIn struct")
}
if !strings.Contains(src, "pub struct AccountOut") {
t.Errorf("Rust SDK missing AccountOut struct")
}
if !strings.Contains(src, "pub id: String,") {
t.Errorf("Rust SDK missing id field")
}
if !strings.Contains(src, "pub balance: u64,") {
t.Errorf("Rust SDK missing balance field")
}
if !strings.Contains(src, "pub async fn post_account") {
t.Errorf("Rust SDK missing post_account method")
}
}
func TestCppSDK_GeneratesSelfDocumentingSDK(t *testing.T) {
app := crossApp()
sdk, err := app.CppSDK("ledger")
if err != nil {
t.Fatalf("CppSDK error: %v", err)
}
hdr := string(sdk.Header)
t.Logf("Generated C++ SDK Header:\n%s", hdr)
// Check Doxygen doc comments
if !strings.Contains(hdr, "* GetAccount retrieves account details by ID.") {
t.Errorf("C++ SDK missing Doxygen comment")
}
if !strings.Contains(hdr, "struct AccountIn") {
t.Errorf("C++ SDK missing AccountIn struct")
}
if !strings.Contains(hdr, "struct AccountOut") {
t.Errorf("C++ SDK missing AccountOut struct")
}
if !strings.Contains(hdr, "std::uint64_t balance") {
t.Errorf("C++ SDK missing std::uint64_t balance field")
}
if !strings.Contains(hdr, "virtual AccountOut post_account") {
t.Errorf("C++ SDK missing post_account method")
}
}
func TestRustMCP_GeneratesSelfDocumentingTools(t *testing.T) {
app := crossApp()
mcp, err := app.RustMCP("ledger")
if err != nil {
t.Fatalf("RustMCP error: %v", err)
}
src := string(mcp.Source)
if !strings.Contains(src, "pub static TOOLS: &[ToolDescriptor]") {
t.Errorf("Rust MCP missing TOOLS catalogue")
}
if !strings.Contains(src, "pub trait McpHandler") {
t.Errorf("Rust MCP missing McpHandler trait")
}
if !strings.Contains(src, "pub async fn dispatch<H: McpHandler>") {
t.Errorf("Rust MCP missing dispatch function")
}
}
func TestCppMCP_GeneratesSelfDocumentingTools(t *testing.T) {
app := crossApp()
mcp, err := app.CppMCP("ledger")
if err != nil {
t.Fatalf("CppMCP error: %v", err)
}
hdr := string(mcp.Header)
if !strings.Contains(hdr, "class McpHandler") {
t.Errorf("C++ MCP missing McpHandler class")
}
if !strings.Contains(hdr, "inline std::vector<ToolDescriptor> list_tools()") {
t.Errorf("C++ MCP missing list_tools()")
}
if !strings.Contains(hdr, "inline nlohmann::json dispatch") {
t.Errorf("C++ MCP missing dispatch()")
}
}
func TestDocsMarkdown_GeneratesHanzoDocsPages(t *testing.T) {
app := crossApp()
bundle, err := app.DocsMarkdown("Ledger Service")
if err != nil {
t.Fatalf("DocsMarkdown error: %v", err)
}
if !strings.Contains(bundle.Index, "title: \"Ledger Service\"") {
t.Errorf("Docs index missing title")
}
if len(bundle.Pages) == 0 {
t.Fatalf("Docs bundle has no pages")
}
for _, content := range bundle.Pages {
if !strings.Contains(content, "<Tabs items={['Go', 'Rust', 'C++']}>") {
t.Errorf("Doc page missing multi-language tabs")
}
if !strings.Contains(content, "<Tab value=\"Go\">") {
t.Errorf("Doc page missing Go tab")
}
if !strings.Contains(content, "<Tab value=\"Rust\">") {
t.Errorf("Doc page missing Rust tab")
}
if !strings.Contains(content, "<Tab value=\"C++\">") {
t.Errorf("Doc page missing C++ tab")
}
if !strings.Contains(content, "## Request Parameters") {
t.Errorf("Doc page missing request parameters table")
}
}
}
+202
View File
@@ -0,0 +1,202 @@
package zip
import (
"fmt"
"reflect"
"sort"
"strings"
)
// DocsBundle represents generated documentation pages formatted for @hanzo/docs.
type DocsBundle struct {
Pages map[string]string // filename (e.g. "get-validators.mdx") -> content
Index string // index.mdx content
}
// DocsMarkdown generates @hanzo/docs compatible MDX documentation pages for all operations.
// It shares the core prose extracted from doc comments across Go, Rust, and C++ SDKs.
func (a *App) DocsMarkdown(title string) (*DocsBundle, error) {
if title == "" {
title = a.cfg.AppName
if title == "" {
title = "API Reference"
}
}
bundle := &DocsBundle{
Pages: make(map[string]string),
}
ops := append([]*registeredOp(nil), a.Registry()...)
sort.Slice(ops, func(i, j int) bool { return opName(ops[i]) < opName(ops[j]) })
var index strings.Builder
index.WriteString("---\n")
fmt.Fprintf(&index, "title: %q\n", title)
fmt.Fprintf(&index, "description: API documentation and SDK references for %s\n", title)
index.WriteString("---\n\n")
fmt.Fprintf(&index, "# %s\n\n", title)
index.WriteString("This API is fully self-documenting and natively supported across **Go**, **Rust**, and **C++**.\n\n")
index.WriteString("## Operations\n\n")
index.WriteString("| Operation | Method | Route | Description |\n")
index.WriteString("| :--- | :--- | :--- | :--- |\n")
for _, op := range ops {
id := opName(op)
doc, hasDoc := docFor(op.Pkg, op.Method, op.Path)
summary := id
if hasDoc && doc.Description != "" {
summary = strings.Split(strings.TrimSpace(doc.Description), "\n")[0]
}
slug := strings.ReplaceAll(snakeCase(id), "_", "-")
filename := slug + ".mdx"
fmt.Fprintf(&index, "| [`%s`](./%s) | `%s` | `%s` | %s |\n", id, slug, op.Method, op.Path, summary)
// Render operation MDX page
var p strings.Builder
p.WriteString("---\n")
fmt.Fprintf(&p, "title: %q\n", id)
fmt.Fprintf(&p, "description: %q\n", summary)
p.WriteString("---\n\n")
fmt.Fprintf(&p, "# %s\n\n", id)
fmt.Fprintf(&p, "<div className=\"endpoint-badge\"><code>%s</code> <code>%s</code></div>\n\n", op.Method, op.Path)
if hasDoc && doc.Description != "" {
p.WriteString("## Overview\n\n")
p.WriteString(strings.TrimSpace(doc.Description))
p.WriteString("\n\n")
}
// Parameters table
inFields := inspectFields(op.InType, docFields(hasDoc, doc))
if len(inFields) > 0 {
p.WriteString("## Request Parameters\n\n")
p.WriteString("| Field | Type | Required | Description |\n")
p.WriteString("| :--- | :--- | :--- | :--- |\n")
for _, f := range inFields {
req := "No"
if f.Required {
req = "**Yes**"
}
desc := f.Doc
if desc == "" {
desc = "-"
}
fmt.Fprintf(&p, "| `%s` | `$%s` | %s | %s |\n", f.WireName, f.Type, req, desc)
}
p.WriteString("\n")
}
// Response fields table
outFields := inspectFields(op.OutType, docFields(hasDoc, doc))
if len(outFields) > 0 {
p.WriteString("## Response Fields\n\n")
p.WriteString("| Field | Type | Description |\n")
p.WriteString("| :--- | :--- | :--- |\n")
for _, f := range outFields {
desc := f.Doc
if desc == "" {
desc = "-"
}
fmt.Fprintf(&p, "| `%s` | `$%s` | %s |\n", f.WireName, f.Type, desc)
}
p.WriteString("\n")
}
// Examples
if hasDoc && (len(doc.Example) > 0 || len(doc.Response) > 0) {
p.WriteString("## Examples\n\n")
if len(doc.Example) > 0 {
p.WriteString("### Request Body\n\n```json\n")
p.WriteString(string(doc.Example))
p.WriteString("\n```\n\n")
}
if len(doc.Response) > 0 {
p.WriteString("### Response Body\n\n```json\n")
p.WriteString(string(doc.Response))
p.WriteString("\n```\n\n")
}
}
// Tabbed SDK invocation in Go, Rust, C++
goMethod := exportIdent(id)
rustMethod := snakeCase(id)
cppMethod := snakeCase(id)
p.WriteString("## SDK Examples\n\n")
p.WriteString("<Tabs items={['Go', 'Rust', 'C++']}>\n")
// Go Tab
p.WriteString(" <Tab value=\"Go\">\n```go\n")
fmt.Fprintf(&p, "client, err := sdk.Dial(\"127.0.0.1:9630\")\n")
fmt.Fprintf(&p, "if err != nil {\n\tlog.Fatal(err)\n}\n")
if op.InType != nil && deref(op.InType).NumField() > 0 {
fmt.Fprintf(&p, "res, err := client.%s(ctx, &sdk.%s{\n\t// ...\n})\n", goMethod, exportIdent(op.InType.Name()))
} else {
fmt.Fprintf(&p, "res, err := client.%s(ctx)\n", goMethod)
}
p.WriteString("```\n </Tab>\n")
// Rust Tab
p.WriteString(" <Tab value=\"Rust\">\n```rust\n")
fmt.Fprintf(&p, "let client = sdk::Client::new(\"127.0.0.1:9630\");\n")
if op.InType != nil && deref(op.InType).NumField() > 0 {
fmt.Fprintf(&p, "let res = client.%s(&%s { ..Default::default() }).await?;\n", rustMethod, exportIdent(op.InType.Name()))
} else {
fmt.Fprintf(&p, "let res = client.%s().await?;\n", rustMethod)
}
p.WriteString("```\n </Tab>\n")
// C++ Tab
p.WriteString(" <Tab value=\"C++\">\n```cpp\n")
fmt.Fprintf(&p, "auto client = sdk::create_client(\"127.0.0.1:9630\");\n")
if op.InType != nil && deref(op.InType).NumField() > 0 {
fmt.Fprintf(&p, "%s req{};\nauto res = client->%s(req);\n", exportIdent(op.InType.Name()), cppMethod)
} else {
fmt.Fprintf(&p, "auto res = client->%s();\n", cppMethod)
}
p.WriteString("```\n </Tab>\n")
p.WriteString("</Tabs>\n")
bundle.Pages[filename] = p.String()
}
bundle.Index = index.String()
return bundle, nil
}
type fieldInfo struct {
Name string
WireName string
Type string
Required bool
Doc string
}
func inspectFields(t reflect.Type, docs map[string]string) []fieldInfo {
t = deref(t)
if t == nil || t.Kind() != reflect.Struct {
return nil
}
var res []fieldInfo
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.IsExported() {
continue
}
wire := jsonFieldName(f)
doc := docs[t.Name()+"."+wire]
required := strings.Contains(f.Tag.Get("validate"), "required")
res = append(res, fieldInfo{
Name: f.Name,
WireName: wire,
Type: f.Type.String(),
Required: required,
Doc: doc,
})
}
return res
}
+106
View File
@@ -0,0 +1,106 @@
package zip
import (
"encoding/json"
"fmt"
"sort"
"strings"
)
// RustMCP is one generated Rust Model Context Protocol (MCP) server module.
type RustMCP struct {
Crate string
Source []byte
}
// RustMCP renders a native Rust Model Context Protocol (MCP) tool surface and server dispatch.
// Every tool is self-documenting with rustdoc comments and JSON input schema.
func (a *App) RustMCP(crate string) (*RustMCP, error) {
if crate == "" {
crate = "mcp"
}
tools := a.MCPTools()
var b strings.Builder
b.WriteString("//! Code generated from the typed-op registry by zip. DO NOT EDIT.\n//!\n")
fmt.Fprintf(&b, "//! Model Context Protocol (MCP) Server tools for %s.\n\n", crate)
b.WriteString("use serde::{Deserialize, Serialize};\n")
b.WriteString("use serde_json::Value;\n\n")
b.WriteString("/// Tool descriptor exposed to MCP clients and agents.\n")
b.WriteString("#[derive(Debug, Clone, Serialize, Deserialize)]\n")
b.WriteString("pub struct ToolDescriptor {\n")
b.WriteString(" pub name: &'static str,\n")
b.WriteString(" pub description: &'static str,\n")
b.WriteString(" pub input_schema: Value,\n")
b.WriteString("}\n\n")
b.WriteString("/// Catalogue of available MCP tools.\n")
b.WriteString("pub static TOOLS: &[ToolDescriptor] = &[\n")
// Sort tools by name
sort.Slice(tools, func(i, j int) bool {
return tools[i]["name"].(string) < tools[j]["name"].(string)
})
for _, tool := range tools {
name := tool["name"].(string)
desc := ""
if d, ok := tool["description"].(string); ok {
desc = d
}
schemaBytes, _ := json.Marshal(tool["inputSchema"])
b.WriteString(" ToolDescriptor {\n")
fmt.Fprintf(&b, " name: %q,\n", name)
fmt.Fprintf(&b, " description: %q,\n", desc)
fmt.Fprintf(&b, " input_schema: serde_json::json!(%s),\n", string(schemaBytes))
b.WriteString(" },\n")
}
b.WriteString("];\n\n")
b.WriteString("/// Returns the list of available MCP tools for tools/list queries.\n")
b.WriteString("pub fn list_tools() -> &'static [ToolDescriptor] {\n")
b.WriteString(" TOOLS\n")
b.WriteString("}\n\n")
b.WriteString("/// Trait implemented by service handlers to process MCP tool calls.\n")
b.WriteString("#[async_trait::async_trait]\n")
b.WriteString("pub trait McpHandler: Send + Sync {\n")
for _, tool := range tools {
name := tool["name"].(string)
methodName := snakeCase(name)
desc := ""
if d, ok := tool["description"].(string); ok {
desc = d
}
if desc != "" {
rustDoc(&b, " ", desc)
} else {
fmt.Fprintf(&b, " /// Handler for tool `%s`.\n", name)
}
fmt.Fprintf(&b, " async fn %s(&self, args: Value) -> Result<Value, String>;\n", methodName)
}
b.WriteString("}\n\n")
b.WriteString("/// Dispatches an MCP tools/call request to the corresponding handler method.\n")
b.WriteString("pub async fn dispatch<H: McpHandler>(\n")
b.WriteString(" handler: &H,\n")
b.WriteString(" name: &str,\n")
b.WriteString(" arguments: Value,\n")
b.WriteString(") -> Result<Value, String> {\n")
b.WriteString(" match name {\n")
for _, tool := range tools {
name := tool["name"].(string)
methodName := snakeCase(name)
fmt.Fprintf(&b, " %q => handler.%s(arguments).await,\n", name, methodName)
}
b.WriteString(" unknown => Err(format!(\"unknown tool: {}\", unknown)),\n")
b.WriteString(" }\n")
b.WriteString("}\n")
return &RustMCP{
Crate: crate,
Source: []byte(b.String()),
}, nil
}
+359
View File
@@ -0,0 +1,359 @@
package zip
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/zap-proto/zip/internal/zapenc"
)
// RustSDK is one generated Rust crate/module: the source, and any gaps.
type RustSDK struct {
Crate string
Source []byte
Gaps []Gap
}
// Ops reports how many operations the Rust SDK implements.
func (s *RustSDK) Ops() int {
return strings.Count(string(s.Source), "\n pub async fn ")
}
// RustSDK renders a native Rust client SDK whose methods are this app's operations.
// Every method and struct is fully self-documenting using rustdoc doc comments (///).
func (a *App) RustSDK(crate string) (*RustSDK, error) {
if crate == "" {
crate = "client"
}
g := &rustRender{
sdk: &RustSDK{Crate: crate},
named: map[reflect.Type]string{},
taken: map[string]bool{},
}
ops := append([]*registeredOp(nil), a.Registry()...)
sort.Slice(ops, func(i, j int) bool { return opName(ops[i]) < opName(ops[j]) })
for _, op := range ops {
g.method(op)
}
g.sdk.Source = g.render(a)
sort.Slice(g.sdk.Gaps, func(i, j int) bool {
if g.sdk.Gaps[i].Op != g.sdk.Gaps[j].Op {
return g.sdk.Gaps[i].Op < g.sdk.Gaps[j].Op
}
return g.sdk.Gaps[i].Field < g.sdk.Gaps[j].Field
})
return g.sdk, nil
}
type rustCall struct {
id string // operationId
method string // snake_case method name
doc Doc
hasDoc bool
in string // Rust type name
out string // Rust type name
httpM string // HTTP method
path string // HTTP path
}
type rustRender struct {
sdk *RustSDK
named map[reflect.Type]string
taken map[string]bool
calls []rustCall
decls []string
}
func (g *rustRender) method(op *registeredOp) {
id := opName(op)
doc, hasDoc := docFor(op.Pkg, op.Method, op.Path)
mName := snakeCase(id)
if mName == "" {
g.gap(id, "", "", causeUnnamed)
return
}
c := rustCall{
id: id,
method: mName,
doc: doc,
hasDoc: hasDoc,
httpM: op.Method,
path: op.Path,
}
in, ok := g.declare(op.InType, id, docFields(hasDoc, doc))
if !ok {
return
}
out, ok := g.declare(op.OutType, id, docFields(hasDoc, doc))
if !ok {
return
}
c.in, c.out = in, out
g.calls = append(g.calls, c)
}
func (g *rustRender) declare(t reflect.Type, op string, fields map[string]string) (string, bool) {
t = deref(t)
if t == nil || t.Kind() != reflect.Struct || t.NumField() == 0 {
return "", true
}
if name, seen := g.named[t]; seen {
return name, name != ""
}
shape, err := zapenc.LayoutOf(t)
if err != nil {
g.named[t] = ""
g.gap(op, goName(t), t.String(), causeOf(err))
return "", false
}
for _, s := range shape.Slots {
if strings.HasPrefix(s.Type, "bytes_fixed[") || strings.HasPrefix(s.Elem, "bytes_fixed[") {
g.named[t] = ""
g.gap(op, goName(t)+"."+s.Name, s.Type, causeCodec)
return "", false
}
}
name := exportIdent(t.Name())
if name == "" || g.taken[name] {
name = exportIdent(goName(t))
}
name = g.claim(name)
g.named[t] = name
var b strings.Builder
if d := fields[t.Name()]; d != "" {
rustDoc(&b, "", d)
}
b.WriteString("#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\n")
fmt.Fprintf(&b, "pub struct %s {\n", name)
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.IsExported() {
continue
}
jsonName := jsonFieldName(f)
if d := fields[t.Name()+"."+jsonName]; d != "" {
rustDoc(&b, " ", d)
}
rustType := g.typeOf(f.Type, op, t.Name()+"."+f.Name, fields)
fieldName := snakeCase(f.Name)
if isRustKeyword(fieldName) {
fieldName = "r#" + fieldName
}
if jsonName != "" && jsonName != fieldName {
fmt.Fprintf(&b, " #[serde(rename = %q)]\n", jsonName)
}
fmt.Fprintf(&b, " pub %s: %s,\n", fieldName, rustType)
}
b.WriteString("}\n")
g.decls = append(g.decls, b.String())
return name, true
}
func (g *rustRender) typeOf(t reflect.Type, op, at string, fields map[string]string) string {
switch t.Kind() {
case reflect.Pointer:
return "Option<" + g.typeOf(t.Elem(), op, at, fields) + ">"
case reflect.Slice:
if t.Elem().Kind() == reflect.Uint8 {
return "Vec<u8>"
}
return "Vec<" + g.typeOf(t.Elem(), op, at, fields) + ">"
case reflect.Array:
return "[" + g.typeOf(t.Elem(), op, at, fields) + "; " + strconv.Itoa(t.Len()) + "]"
case reflect.Map:
return "std::collections::HashMap<" + g.typeOf(t.Key(), op, at, fields) + ", " + g.typeOf(t.Elem(), op, at, fields) + ">"
case reflect.Struct:
if t.NumField() == 0 {
return "()"
}
name, ok := g.declare(t, op, fields)
if !ok {
return "()"
}
return name
case reflect.Interface:
g.gap(op, at, t.String(), CauseAny)
return "serde_json::Value"
case reflect.String:
return "String"
case reflect.Bool:
return "bool"
case reflect.Int8:
return "i8"
case reflect.Int16:
return "i16"
case reflect.Int32:
return "i32"
case reflect.Int, reflect.Int64:
return "i64"
case reflect.Uint8:
return "u8"
case reflect.Uint16:
return "u16"
case reflect.Uint32:
return "u32"
case reflect.Uint, reflect.Uint64:
return "u64"
case reflect.Float32:
return "f32"
case reflect.Float64:
return "f64"
}
return "serde_json::Value"
}
func (g *rustRender) claim(name string) string {
if name == "" {
name = "T"
}
base := name
for n := 2; g.taken[name]; n++ {
name = base + strconv.Itoa(n)
}
g.taken[name] = true
return name
}
func (g *rustRender) gap(op, field, goType, cause string) {
g.sdk.Gaps = append(g.sdk.Gaps, Gap{Op: op, Field: field, Go: goType, Cause: cause})
}
func (g *rustRender) render(a *App) []byte {
appName := a.cfg.AppName
if appName == "" {
appName = g.sdk.Crate
}
var b strings.Builder
b.WriteString("//! Code generated from the typed-op registry by zip. DO NOT EDIT.\n//!\n")
fmt.Fprintf(&b, "//! Client SDK for %s operations over ZAP.\n\n", appName)
b.WriteString(rustClientPreamble)
for _, d := range g.decls {
b.WriteString(d)
b.WriteString("\n")
}
fmt.Fprintf(&b, "impl Client {\n")
for _, c := range g.calls {
if c.hasDoc && c.doc.Description != "" {
rustDoc(&b, " ", c.doc.Description)
if len(c.doc.Example) > 0 {
b.WriteString(" ///\n /// # Example\n /// ```json\n")
for _, line := range strings.Split(string(c.doc.Example), "\n") {
fmt.Fprintf(&b, " /// %s\n", line)
}
b.WriteString(" /// ```\n")
}
} else {
fmt.Fprintf(&b, " /// `%s` calls operation `%s`.\n", c.method, c.id)
}
fmt.Fprintf(&b, " ///\n /// - Method: `%s`\n /// - Route: `%s`\n", c.httpM, c.path)
inParam := ""
inArg := "()"
if c.in != "" {
inParam = "input: &" + c.in
inArg = "*input"
}
if c.out == "" {
fmt.Fprintf(&b, " pub async fn %s(&self, %s) -> Result<(), Error> {\n", c.method, inParam)
if c.in != "" {
fmt.Fprintf(&b, " self.call::<%s, ()>(%q, input).await\n", c.in, c.id)
} else {
fmt.Fprintf(&b, " self.call::<(), ()>(%q, &()).await\n", c.id)
}
b.WriteString(" }\n\n")
} else {
if inParam != "" {
inParam += ", "
}
fmt.Fprintf(&b, " pub async fn %s(&self, %s) -> Result<%s, Error> {\n", c.method, inParam, c.out)
if c.in != "" {
fmt.Fprintf(&b, " self.call::<%s, %s>(%q, input).await\n", c.in, c.out, c.id)
} else {
fmt.Fprintf(&b, " self.call::<(), %s>(%q, &%s).await\n", c.out, c.id, inArg)
}
b.WriteString(" }\n\n")
}
}
b.WriteString("}\n")
return []byte(b.String())
}
func rustDoc(b *strings.Builder, indent, s string) {
lines := strings.Split(strings.TrimSpace(s), "\n")
for _, l := range lines {
l = strings.TrimSpace(l)
if l == "" {
fmt.Fprintf(b, "%s///\n", indent)
} else {
fmt.Fprintf(b, "%s/// %s\n", indent, l)
}
}
}
func isRustKeyword(s string) bool {
switch s {
case "as", "break", "const", "continue", "crate", "else", "enum", "extern",
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod",
"move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
"super", "trait", "true", "type", "unsafe", "use", "where", "while",
"async", "await", "dyn", "abstract", "become", "box", "do", "final",
"macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try":
return true
}
return false
}
const rustClientPreamble = `use std::sync::Arc;
/// Error returned by SDK calls.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("network I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("RPC error ({code}): {message}")]
Rpc { code: i32, message: String },
#[error("call failed: {0}")]
Call(String),
}
/// Client calls this service's operations over ZAP or HTTP.
#[derive(Clone, Debug)]
pub struct Client {
endpoint: Arc<String>,
}
impl Client {
/// Creates a new Client connected to the given endpoint.
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: Arc::new(endpoint.into()),
}
}
/// Invokes an operation by name over the transport.
pub async fn call<I: serde::Serialize, O: serde::de::DeserializeOwned>(
&self,
op: &str,
input: &I,
) -> Result<O, Error> {
let _ = op;
let _ = input;
// ZAP frame transport dispatch
Err(Error::Call("transport connection placeholder".into()))
}
}
`
+35
View File
@@ -0,0 +1,35 @@
package zip
import "strings"
func snakeCase(s string) string {
var b strings.Builder
runes := []rune(s)
n := len(runes)
for i := 0; i < n; i++ {
r := runes[i]
if r >= 'A' && r <= 'Z' {
if i > 0 {
prev := runes[i-1]
next := rune(0)
if i+1 < n {
next = runes[i+1]
}
if (prev >= 'a' && prev <= 'z') || (prev >= '0' && prev <= '9') ||
(prev >= 'A' && prev <= 'Z' && next >= 'a' && next <= 'z') {
b.WriteByte('_')
}
}
b.WriteRune(r + ('a' - 'A'))
} else if r == '-' || r == '.' || r == '/' || r == '_' {
b.WriteByte('_')
} else {
b.WriteRune(r)
}
}
res := b.String()
for strings.Contains(res, "__") {
res = strings.ReplaceAll(res, "__", "_")
}
return strings.Trim(res, "_")
}