Files
zip/doc_test.go
T
zeekayandHanzo Dev 170e572463 cli: the registry is the CLI, and the doc comment is what describes it
One registry, three projections. A typed op already produced a REST route, an
OpenAPI path and an MCP tool; it now also produces a command. cli.go derives
`<service> <operation>` from the op's own identity, so adding a route can never
rename an existing command, and LocalInvoke runs it through the op's own invoke
seam — a fused binary needs no client because the command IS the handler call.
clispec.go builds the same commands from a served spec, which is what lets the
CLI drive a service it was not linked against.

cmd/zipdoc + internal/zipdoc close the gap Go leaves: doc comments do not exist
at run time, so prose reaches the spec only via a build-time AST pass. Field
docs and examples are extracted from source and keyed "<Type>.<field>".

Two defects found by making the projections agree:

commandName treated "ends in a parameter" as the only way to address one thing,
so GET /v1/paas/apps/:app/deploy became apps-deploy-LIST — named for a
collection, returning one deploy. The route already says which it is past its
parameter: /apps/:app/deploy is singular, /nodes/:node/jobs is plural.

OpenAPI parameters carried no description while the registry had one, so the
same argument had help in-process and none from the spec. Both now read the
same key, and query parameters get it too.

internal/jsontag exists so the reflect view of the wire-name rule and the AST
view cannot drift; transport gains a dial-only https, because reaching a
published API is an ordinary Mount and terminating TLS stays the ingress's job.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 17:02:36 -07:00

68 lines
2.2 KiB
Go

package zip_test
import (
"context"
"encoding/json"
"strings"
"testing"
"github.com/zap-proto/zip"
)
type invIn struct {
Limit int `json:"limit"`
Org string `json:"org" validate:"required"`
}
type invOut struct {
Invoices []string `json:"invoices"`
}
// TestDoc_ReachesTheSpec proves an extracted doc comment becomes the operation's
// description, its fields' descriptions, and its request/response examples —
// i.e. that the comment IS the spec rather than something written twice.
func TestDoc_ReachesTheSpec(t *testing.T) {
app := zip.New(zip.Config{DisableStartupMessage: true})
// What cmd/zipdoc emits for this operation.
zip.Describe("POST /v1/billing/invoices", zip.Doc{
Description: "ListInvoices returns every invoice for the caller's org, newest first. Results are capped by limit.",
Fields: map[string]string{
"invIn.limit": "Maximum invoices to return.",
"invIn.org": "Org whose invoices to list.",
"invOut.invoices": "Invoice ids, newest first.",
},
Example: json.RawMessage(`{"limit":25,"org":"hanzo"}`),
Response: json.RawMessage(`{"invoices":["inv_2","inv_1"]}`),
})
zip.Post(app, "/v1/billing/invoices", func(_ context.Context, in *invIn) (*invOut, error) {
return &invOut{}, nil
})
spec := app.OpenAPISpec()
raw, err := json.Marshal(spec)
if err != nil {
t.Fatalf("marshal spec: %v", err)
}
got := string(raw)
for _, want := range []string{
`newest first`, // description
`Maximum invoices to return.`, // In field doc
`Invoice ids, newest first.`, // Out field doc
`"limit":25`, // request example
`"invoices":["inv_2","inv_1"]`, // response example
} {
if !strings.Contains(got, want) {
t.Errorf("spec is missing %q", want)
}
}
// The summary falls back to the comment's first sentence, which Go's own
// convention already makes a summary — so nobody writes it twice.
ops := spec["paths"].(map[string]map[string]any)["/v1/billing/invoices"]
post := ops["post"].(map[string]any)
if s, _ := post["summary"].(string); !strings.HasPrefix(s, "ListInvoices returns") || strings.Contains(s, "capped by limit") {
t.Errorf("summary = %q, want just the first sentence", s)
}
}