Files
zip/opof_test.go
zeekayandHanzo Dev d1ac3e430e A handler is told the address it was asked at
The address an op answers at is a property of the CALL, not of the declaration:
a group's prefix composes at build, and a definition composed under two hosts
has two addresses and one handler. So a handler could not learn its own address,
and a service that needed one composed it in a wrapper and threaded it in —
which made every one of those registrations a call this pass cannot see, and
took a whole service's operations out of the document, the tool list and the
SDKs.

OpOf reads it from the context instead, so the declaration stays the bound
method the projections resolve. The seam that knows the occurrence states it:
the REST route that matched, and the by-name seams, which looked the op up in
the composed registry. Under that, the one contract every projection funnels
through states the declaration, so a handler always has an operation to read and
a direct call says there is none rather than guessing.

Costs two allocations on a typed REST request, measured: 61 to 63 on
Benchmark_TypedRoute, with bytes unchanged.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 08:57:42 -07:00

115 lines
3.7 KiB
Go

package zip_test
import (
"context"
"encoding/json"
"net/http/httptest"
"sort"
"testing"
"github.com/zap-proto/zip"
)
type tailIn struct {
Follow bool `json:"follow"`
}
// where is the address the handler was asked at.
type where struct {
Address string `json:"address"`
ID string `json:"id"`
}
// tail is the shape a service takes when it forwards what it was asked for: a
// BOUND METHOD registered directly, reading its own address from the context.
type tail struct{}
// Livetail streams matching records as they arrive.
func (tail) Livetail(ctx context.Context, _ *tailIn) (*where, error) {
op, ok := zip.OpOf(ctx)
if !ok {
return &where{Address: "", ID: "no operation"}, nil
}
return &where{Address: op.Path, ID: op.OperationID}, nil
}
func addressAt(t *testing.T, app *zip.App, path string) where {
t.Helper()
resp, err := app.Test(httptest.NewRequest("GET", path, nil))
if err != nil {
t.Fatalf("%s: %v", path, err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != 200 {
t.Fatalf("%s: status %d", path, resp.StatusCode)
}
var w where
if err := json.NewDecoder(resp.Body).Decode(&w); err != nil {
t.Fatalf("%s: %v", path, err)
}
return w
}
// A HANDLER IS TOLD THE ADDRESS IT WAS ASKED AT, and the registration stays the
// bound method — no closure, no factory, nothing between the source and the
// projections that read it.
//
// This is what a registration-time wrapper was for: the address was composed by
// hand and threaded in, which made every such op invisible to cmd/zipdoc,
// because a registration made inside a helper is not a registration this pass
// can see. Reading it from the context leaves the declaration direct.
func TestOpOf_IsTheAddressTheHandlerWasAskedAt(t *testing.T) {
app := zip.New(zip.Config{AppName: "o11y", DisableStartupMessage: true})
app.Group("/v1/o11y").Get("/logs/livetail", tail{}.Livetail)
if got := addressAt(t, app, "/v1/o11y/logs/livetail").Address; got != "/v1/o11y/logs/livetail" {
t.Errorf("address = %q, want the composed path", got)
}
// And the op is in the registry under that address, so what the handler reads
// and what the document publishes are the same string.
var addrs []string
for _, op := range app.Manifest().Ops {
addrs = append(addrs, op.Method+" "+op.Path)
}
sort.Strings(addrs)
if len(addrs) != 1 || addrs[0] != "GET /v1/o11y/logs/livetail" {
t.Errorf("registry = %v, want the one op at the address the handler reads", addrs)
}
}
// ONE HANDLER, TWO ADDRESSES. A definition composed under two hosts answers at
// both, and each call is told where IT was asked — which is the whole reason
// this is a property of the call and not something a group could hand out when
// it was declared.
func TestOpOf_ADefinitionComposedTwiceAnswersAtBoth(t *testing.T) {
child := zip.New(zip.Config{AppName: "tail", DisableStartupMessage: true})
child.Get("/livetail", tail{}.Livetail)
host := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
host.Group("/east").Use(child)
host.Group("/west").Use(child)
if err := host.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
if got := addressAt(t, host, "/east/livetail").Address; got != "/east/livetail" {
t.Errorf("east address = %q", got)
}
if got := addressAt(t, host, "/west/livetail").Address; got != "/west/livetail" {
t.Errorf("west address = %q — one handler, and each call is told its own", got)
}
}
// Called directly rather than through the declaration that registered it, there
// is no operation, and saying so beats guessing an address.
func TestOpOf_SaysWhenThereIsNoOperation(t *testing.T) {
w, err := tail{}.Livetail(context.Background(), &tailIn{})
if err != nil {
t.Fatal(err)
}
if w.Address != "" {
t.Errorf("address = %q, want empty outside a registration", w.Address)
}
}