mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
App, Group, Operation. A group is a prefix and what holds under it, an operation
is one typed route declared there, and the verb methods are generic methods —
which Go 1.27 allows on a concrete type and still refuses on an interface. So In
and Out come from the handler and the caller writes no type arguments:
users := app.Group("/v1/users")
users.Post("/", api.Create)
mint := users.Group("/mint").With(middleware.Mint(gate))
mint.Post("/deposit", api.Deposit)
A Group is a CLOSED HANDLE: two unexported fields, and no method that hands back
the app, the parent or the fiber underneath. A subsystem given the group at its
own prefix cannot reach above it, so confinement is a property of the type
instead of a rule something has to police — which is what a host needs in order
to hand out a bounded surface and know what comes back.
That is bought by giving up the router interface, deliberately. Router and
OpTarget are gone, and with them the ability to implement a route table outside
this package: a decorator is no longer a router, a gate is middleware on a group,
and a bound is a path. The five package-level verb functions go the same way —
a registration is a method on the thing it registers into.
Raw carries what is not a typed operation: a stream, an upgrade, a probe, a
foreign subtree. It names its method, and it is the longer spelling because
opting out of the typed model is the exception and should read like one.
Co-authored-by: Hanzo Dev <[email protected]>
208 lines
5.9 KiB
Go
208 lines
5.9 KiB
Go
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}`),
|
|
})
|
|
app.Post("/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")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRustCLI_GeneratesRustCLIBindings(t *testing.T) {
|
|
app := crossApp()
|
|
cli, err := app.RustCLI("ledger")
|
|
if err != nil {
|
|
t.Fatalf("RustCLI error: %v", err)
|
|
}
|
|
src := string(cli.Source)
|
|
if !strings.Contains(src, "pub struct CommandDescriptor") {
|
|
t.Errorf("Rust CLI missing CommandDescriptor struct")
|
|
}
|
|
if !strings.Contains(src, "pub trait CliRunner") {
|
|
t.Errorf("Rust CLI missing CliRunner trait")
|
|
}
|
|
if !strings.Contains(src, "pub async fn run_cli") {
|
|
t.Errorf("Rust CLI missing run_cli dispatcher")
|
|
}
|
|
}
|
|
|
|
func TestCppCLI_GeneratesCppCLIBindings(t *testing.T) {
|
|
app := crossApp()
|
|
cli, err := app.CppCLI("ledger")
|
|
if err != nil {
|
|
t.Fatalf("CppCLI error: %v", err)
|
|
}
|
|
hdr := string(cli.Header)
|
|
if !strings.Contains(hdr, "struct CommandDescriptor") {
|
|
t.Errorf("C++ CLI missing CommandDescriptor struct")
|
|
}
|
|
if !strings.Contains(hdr, "class CliRunner") {
|
|
t.Errorf("C++ CLI missing CliRunner class")
|
|
}
|
|
if !strings.Contains(hdr, "inline std::string run_cli") {
|
|
t.Errorf("C++ CLI missing run_cli dispatcher")
|
|
}
|
|
}
|