Files
zip/router_decorator_test.go
zooqueenandHanzo Dev 7d22341185 router: the abstraction stops naming its implementation
Router carried `Fiber() *fiber.App` "for one-off escape". A Router is the type
a DECORATOR implements — OpTarget's own doc says so, and says how — and a
decorator wraps a Router, so it has no *fiber.App of its own to hand back.
Requiring one made the interface unimplementable outside this package: zip's
wrapRouter only ever delegated the method, hanzoai/cloud's `scope` and
hanzoai/commerce's `mintRouter` could not satisfy it at all, and a 157-file
v1.19 adoption in cloud stalled on a member nobody wanted.

The file already said the rule two lines up — "fiber.Ctx is the underlying type
the framework's users never see directly" — and then exposed *fiber.App. v1.19
made it worse than v1.18, which at least returned the fiber.Router interface
rather than the concrete app.

The escape hatch was never the problem; putting it on the ABSTRACTION was. It
stays on the concrete App, so a caller that genuinely needs the underlying
router still reaches it and has to hold an *App to do so, instead of every
Router in the estate quietly widening to fiber.

router_decorator_test.go is the guard: a decorator that embeds a Router, counts
registrations and imports no fiber, asserted against Router at compile time and
driven through a real registration so it cannot pass while swallowing routes. A
comment asking for the property is not the property.

Verified: build, vet and the decorator test pass. The suite's 6-7 socket and
release-fetch failures are pre-existing — measured against a stashed tree, the
failure sets are identical bar one flapping wire test.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 13:35:42 -07:00

58 lines
2.2 KiB
Go

package zip
import "testing"
// A decorator is the reason Router is an interface at all, so the interface has
// to be implementable by something that is NOT an *App and holds no fiber type.
// This file is a compile-time proof of exactly that.
//
// It exists because the property did not hold. Router carried
// `Fiber() *fiber.App` "for one-off escape", which every implementor had to
// satisfy — and a decorator wraps a Router, so it has no *fiber.App of its own.
// Outside this package it was unimplementable; inside it, zip's own wrapRouter
// only ever delegated the method. The estate paid for that: hanzoai/cloud's
// `scope` and hanzoai/commerce's `mintRouter` are both decorators, and both
// failed to compile against the interface — a 157-file migration stalled on a
// method nobody wanted.
//
// If someone adds a fiber-shaped member back to Router, this file stops
// compiling, which is the point. A comment asking for the property is not the
// property.
// countingRouter decorates a Router and counts registrations. It embeds the
// wrapped Router for everything it does not care about — the shape OpTarget's
// doc recommends — and imports no fiber.
type countingRouter struct {
Router
n *int
}
func (c countingRouter) Get(path string, handlers ...Handler) Router {
*c.n++
return c.Router.Get(path, handlers...)
}
// The proof. If Router names a fiber type again, this assignment fails.
var _ Router = countingRouter{}
// TestDecoratorNeedsNoFiber drives the decorator through a real registration so
// the guard is not merely structural: a type can satisfy an interface and still
// be useless if the embedded router is not actually reached.
func TestDecoratorNeedsNoFiber(t *testing.T) {
app := New(Config{AppName: "decorator", DisableStartupMessage: true})
n := 0
var r Router = countingRouter{Router: app, n: &n}
r.Get("/decorated", func(c *Ctx) error { return nil })
if n != 1 {
t.Fatalf("decorator did not observe the registration: got %d, want 1", n)
}
// And the registration must have reached the wrapped app, not stopped at the
// decorator — a decorator that swallows routes is worse than none.
if got := len(app.Fiber().GetRoutes(true)); got == 0 {
t.Fatal("decorated route never reached the underlying app")
}
}