mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
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:
@@ -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
@@ -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
@@ -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;
|
||||
`
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()))
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -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, "_")
|
||||
}
|
||||
Reference in New Issue
Block a user