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]>
154 lines
5.4 KiB
Go
154 lines
5.4 KiB
Go
package zip
|
|
|
|
import (
|
|
"fmt"
|
|
)
|
|
|
|
// Middleware is a composable request transformer in the classic wrapping form:
|
|
// given the next Handler it returns a Handler that runs around it. This is a
|
|
// DIFFERENT tool from Use — they do different jobs and compose freely:
|
|
//
|
|
// - Use(Handler...) registers GLOBAL / prefix middleware. It runs for every
|
|
// matched route (or every route under a Group) in DECLARATION order and
|
|
// chains via c.Next(). Reach for it for ambient cross-cutting concerns
|
|
// that apply broadly: logging, recovery, request-id.
|
|
//
|
|
// - Middleware + With + Chain wrap ONE leaf handler explicitly, at
|
|
// registration time, with no c.Next() indirection. Reach for it when a
|
|
// specific endpoint needs a specific pipeline:
|
|
//
|
|
// app.With(RateLimit, CSRF).Post("/v1/keys", mintKey)
|
|
//
|
|
// wraps mintKey as RateLimit(CSRF(mintKey)): RateLimit is outermost and
|
|
// runs first, CSRF next, the handler last; any layer short-circuits by
|
|
// returning without calling next.
|
|
//
|
|
// A Middleware body is written by hand, no framework glue:
|
|
//
|
|
// func RequireCSRF(next zip.Handler) zip.Handler {
|
|
// return func(c *zip.Ctx) error {
|
|
// if !validCSRF(c) {
|
|
// return c.String(403, "bad csrf") // short-circuit
|
|
// }
|
|
// return next(c) // continue
|
|
// }
|
|
// }
|
|
type Middleware = func(next Handler) Handler
|
|
|
|
// Chain composes middleware left-to-right into one Middleware. Chain(a, b, c)
|
|
// nests as a(b(c(handler))): a is outermost (runs first inbound, last
|
|
// outbound), c innermost, wrapping the handler directly. Chain() with no
|
|
// arguments is the identity middleware.
|
|
func Chain(mw ...Middleware) Middleware {
|
|
return func(next Handler) Handler {
|
|
for i := len(mw) - 1; i >= 0; i-- {
|
|
next = mw[i](next)
|
|
}
|
|
return next
|
|
}
|
|
}
|
|
|
|
// wrapRouter decorates an inner Router so every leaf route it registers has its
|
|
// Handler wrapped by a Middleware chain first (see With). Non-leaf
|
|
// operations delegate to the inner Router; Group propagates the chain so
|
|
// leaves registered beneath it stay wrapped. Registration still flows
|
|
// through the same fiber path as any other route, so specificity precedence is
|
|
// unchanged — only the leaf handler is pre-wrapped.
|
|
type wrapRouter struct {
|
|
inner *App
|
|
wrap Middleware
|
|
}
|
|
|
|
// Use accepts middleware, which it wraps like any other leaf. It REFUSES a
|
|
// definition: With composes a chain around leaves registered THROUGH it, and a
|
|
// definition's leaves belong to the definition — wrapping them would mean
|
|
// editing an App that other hosts may also compose. Silently composing it
|
|
// ungated is the one thing that must not happen, since the chain people reach
|
|
// for With to install is usually the gate.
|
|
func (w *wrapRouter) Use(cs ...Component) Router {
|
|
for _, c := range cs {
|
|
if def, isApp := c.(*App); isApp {
|
|
panic(fmt.Sprintf("zip: With(...).Use(%s): a scoped chain cannot wrap a definition's "+
|
|
"own leaves — put the definition in a Group and scope the chain there: "+
|
|
"app.With(mw).Group(prefix).Use(def)", def.who()))
|
|
}
|
|
}
|
|
w.inner.Use(cs...)
|
|
return w
|
|
}
|
|
|
|
func (w *wrapRouter) Get(p string, hs ...Handler) Router {
|
|
w.inner.Get(p, w.wrapChain(hs)...)
|
|
return w
|
|
}
|
|
func (w *wrapRouter) Post(p string, hs ...Handler) Router {
|
|
w.inner.Post(p, w.wrapChain(hs)...)
|
|
return w
|
|
}
|
|
func (w *wrapRouter) Put(p string, hs ...Handler) Router {
|
|
w.inner.Put(p, w.wrapChain(hs)...)
|
|
return w
|
|
}
|
|
func (w *wrapRouter) Patch(p string, hs ...Handler) Router {
|
|
w.inner.Patch(p, w.wrapChain(hs)...)
|
|
return w
|
|
}
|
|
func (w *wrapRouter) Delete(p string, hs ...Handler) Router {
|
|
w.inner.Delete(p, w.wrapChain(hs)...)
|
|
return w
|
|
}
|
|
func (w *wrapRouter) Head(p string, hs ...Handler) Router {
|
|
w.inner.Head(p, w.wrapChain(hs)...)
|
|
return w
|
|
}
|
|
func (w *wrapRouter) Options(p string, hs ...Handler) Router {
|
|
w.inner.Options(p, w.wrapChain(hs)...)
|
|
return w
|
|
}
|
|
func (w *wrapRouter) All(p string, hs ...Handler) Router {
|
|
w.inner.All(p, w.wrapChain(hs)...)
|
|
return w
|
|
}
|
|
|
|
// wrapChain wraps the FINAL handler (the terminal) with w.wrap and passes any
|
|
// preceding middleware through untouched — With() composes around the leaf,
|
|
// never around the chain's middleware.
|
|
func (w *wrapRouter) wrapChain(hs []Handler) []Handler {
|
|
if len(hs) == 0 {
|
|
return hs
|
|
}
|
|
out := append([]Handler(nil), hs...)
|
|
out[len(out)-1] = w.wrap(out[len(out)-1])
|
|
return out
|
|
}
|
|
|
|
// OpScope carries the wrap down to the typed op, so `zip.Post(app.With(CSRF), …)`
|
|
// gates the op the same way `app.With(CSRF).Post(…)` gates an untyped route.
|
|
// Delegating without it would drop the middleware silently, which for the gates
|
|
// people reach for With to install is a hole, not an inconvenience — and it is
|
|
// why OpTarget is implementable from outside this package at all.
|
|
func (w *wrapRouter) OpScope() OpScope {
|
|
s := w.inner.OpScope()
|
|
if s.Middleware == nil {
|
|
s.Middleware = w.wrap
|
|
return s
|
|
}
|
|
s.Middleware = Chain(s.Middleware, w.wrap)
|
|
return s
|
|
}
|
|
|
|
// Group carries the wrap ONTO the group rather than around a decorator of it,
|
|
// because a group is an App and an App can hold its own wrap. Every leaf in the
|
|
// scope is wrapped, including ones registered on the returned App later — which
|
|
// is what a scoped With has to mean, and is why this cannot just delegate and
|
|
// drop the chain the way a decorator that returned a bare group would.
|
|
func (w *wrapRouter) Group(prefix string, handlers ...Handler) Router {
|
|
g := w.inner.group(here(1), prefix, handlers...)
|
|
if g.wrap == nil {
|
|
g.wrap = w.wrap
|
|
} else {
|
|
g.wrap = Chain(g.wrap, w.wrap)
|
|
}
|
|
return g
|
|
}
|