mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
v1.24.2 shipped two fixes. The operation-id one stays and is untouched here: a declared WithOperationID survives composition verbatim (measured on hanzoai/o11y — 353 ops, 217 declare an id, all 217 were being renamed by a host prefix and now are not). The address one, App.Shadow(), is removed. Shadow existed to make an op declarable in one place and answerable in another: a service installing its published table of ops onto the router its implementation already serves on, a test registering a second handler at an address deliberately. Both are the same shape — a declaration and the handler that satisfies it, written apart, with a framework verb holding the two ends together. That is a braid, and a framework that grows a verb per braid grows one verb per caller. The fix belongs in the caller: a typed registration is ALREADY both halves, the handler the router installs and the op the document, MCP tool list, CLI and SDK all read, so the declaration and the answer can simply be the same line. Use composes definitions, Group scopes them, and between them every shape Shadow was written for is expressible with one claim per address. So the address check goes back to what it was, exactly: one registration at an address, both parties named when there are two, no scope and no option that can talk it out of an accident. addrClaim, addressErrors and addressRules are gone with it — 190 lines of walk.go that existed only to reason about the SHAPE of multiple claims at one address, a question the language no longer asks. Removed: App.Shadow, OpScope.Shadow, route.shadow, App.shadow, wrapRouter.shadow and yields, App.method's shadow parameter. wrapChain and OpScope lose their nil-wrap branches, which existed only for a Shadow-only scope with no middleware to compose. No caller outside this repo referenced any of it at a released tag. Tests: shadow_test.go deleted (250 lines, ten cases, all about the removed verb). oneclaim_test.go replaces it with three that pin the rule from both sides — the braid is refused with both parties named, one registration declares AND answers, and Use+Group place a definition's ops wherever a host wants them with one claim per address. Everything else is unchanged, including all of declaredid_test.go and the unnamedApp/billingApp split in compose_test.go, which are the id fix and not this one. 319 root-package tests pass on a clean testcache and under -race. Co-authored-by: Hanzo Dev <[email protected]>
102 lines
3.8 KiB
Go
102 lines
3.8 KiB
Go
package zip
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// ONE ADDRESS, ONE REGISTRATION — and that registration both declares and answers.
|
|
//
|
|
// v1.24.2 shipped App.Shadow(), a scope whose registrations yielded their
|
|
// address so that an op could be DECLARED in one place and ANSWERED in another.
|
|
// It was removed in v1.24.3. The shape it accommodated is a braid: a
|
|
// declaration and the handler that satisfies it, written apart, held together
|
|
// by a framework verb. Unbraiding costs nothing, because a typed registration
|
|
// is ALREADY both halves — the handler the router installs and the op every
|
|
// projection reads — so the two can simply be the same line.
|
|
//
|
|
// These tests pin both directions: the braid is refused, the unbraided program
|
|
// is complete.
|
|
|
|
// braidedImpl answers the address.
|
|
func braidedImpl() *App {
|
|
a := quiet("impl")
|
|
a.Post("/v1/o11y/roles", func(c *Ctx) error { return c.String(200, "runtime") })
|
|
return a
|
|
}
|
|
|
|
// braidedTable declares the same address somewhere else. Two claims.
|
|
func braidedTable() *App {
|
|
t := quiet("table")
|
|
Post(t.Group("/v1/o11y"), "/roles", func(_ context.Context, in *roleIn) (*roleOut, error) {
|
|
return &roleOut{ID: in.Name}, nil
|
|
}, WithOperationID("CreateRole"))
|
|
return t
|
|
}
|
|
|
|
// TestDeclaringApartFromTheHandlerIsRefused: there is no scope, no option and
|
|
// no verb that makes two registrations at one address legal. The check has one
|
|
// behaviour and cannot be talked out of it.
|
|
func TestDeclaringApartFromTheHandlerIsRefused(t *testing.T) {
|
|
host := quiet("host")
|
|
host.Use(braidedImpl())
|
|
host.Use(braidedTable())
|
|
|
|
err := host.Build()
|
|
if err == nil {
|
|
t.Fatal("a declaration registered apart from its handler was accepted — " +
|
|
"two claims at one address must stay a refused program")
|
|
}
|
|
// Both parties named, in the words the check has always used.
|
|
for _, want := range []string{"POST /v1/o11y/roles", "declared by", "impl", "table"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the refusal does not mention %q: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestOneRegistrationDeclaresAndAnswers is the same intent, unbraided: the op
|
|
// reaches the document under the id its author wrote, AND it is what answers
|
|
// the address. Nothing to keep in sync, because there is only one thing.
|
|
func TestOneRegistrationDeclaresAndAnswers(t *testing.T) {
|
|
svc := quiet("o11y")
|
|
Post(svc.Group("/v1/o11y"), "/roles", func(_ context.Context, in *roleIn) (*roleOut, error) {
|
|
return &roleOut{ID: in.Name}, nil
|
|
}, WithOperationID("CreateRole"))
|
|
|
|
host := quiet("host")
|
|
host.Use(svc)
|
|
if err := host.Build(); err != nil {
|
|
t.Fatalf("build: %v", err)
|
|
}
|
|
|
|
reg := host.Registry()
|
|
if len(reg) != 1 {
|
|
t.Fatalf("%d ops, want 1", len(reg))
|
|
}
|
|
if reg[0].OperationID != "CreateRole" || reg[0].Path != "/v1/o11y/roles" {
|
|
t.Fatalf("op = %s at %s, want CreateRole at /v1/o11y/roles", reg[0].OperationID, reg[0].Path)
|
|
}
|
|
if status, body := postJSON(t, host, "/v1/o11y/roles", `{"name":"admin"}`); status != 200 || !strings.Contains(body, `"admin"`) {
|
|
t.Fatalf("POST /v1/o11y/roles: status=%d body=%q — the declaration must also be what answers", status, body)
|
|
}
|
|
}
|
|
|
|
// TestGroupIsTheOnlyScopeARegistrationNeeds: Use and Group between them place a
|
|
// definition's ops wherever a host wants them, and the address check still sees
|
|
// exactly one claim per address. This is the whole replacement for Shadow.
|
|
func TestGroupIsTheOnlyScopeARegistrationNeeds(t *testing.T) {
|
|
host := quiet("host")
|
|
host.Group("/v1").Use(service()) // service() declares CreateRole at /roles
|
|
host.Get("/v1/health", func(c *Ctx) error { return c.String(200, "ok") })
|
|
|
|
if err := host.Build(); err != nil {
|
|
t.Fatalf("build: %v", err)
|
|
}
|
|
reg := host.Registry()
|
|
if len(reg) != 1 || reg[0].OperationID != "CreateRole" || reg[0].Path != "/v1/roles" {
|
|
t.Fatalf("registry = %+v, want CreateRole at /v1/roles", reg)
|
|
}
|
|
}
|