package zip import ( "fmt" "sort" fiber "github.com/zap-proto/fiber/v3" ) // Building is a projection, not a merge. // // Every artifact this package emits is a REDUCER over [walk]'s occurrence // slice: the router, the op registry (and through it the OpenAPI document, the // MCP tool list, the CLI commands and the by-name call plane), the conflict // report and the staged-composition lint. Adding a projection means writing a // reducer, never touching composition. // methodAll is the sentinel [App.All] registers under: one entry answering // every method, kept as one entry so the program says what was written. The // router expands it; [App.Declaration] reads the expansion back off the router. // MethodAll is the method [App.Raw] and [Group.Raw] take to answer a path // whatever the verb — the rare case a proxy, a catch-all or a legacy mount // needs. Every other method is its own name, as net/http spells it. const MethodAll = "ALL" // methodAll is the internal spelling, kept so the walk and the router read one // constant. const methodAll = MethodAll // plan is the walk result the live generation was built from. Each generation // RETAINS it, so the reducers serving that generation — the OpenAPI endpoint, // the MCP list, llms.txt — reuse it rather than rewalking per request. func (a *App) plan() []occurrence { return a.mustBuild().occ } // hostSet is every definition this composition reaches, including this one. func (a *App) hostSet() []*App { return a.mustBuild().hosts } // computeHosts reduces the walk result to that set. func computeHosts(root *App, occ []occurrence) []*App { out := []*App{root} seen := map[*App]bool{root: true} for _, o := range occ { if def, ok := o.app(); ok && !seen[def] { seen[def] = true out = append(out, def) } } return out } // mustBuild is liveOrBuild for the accessors that CANNOT report an error. // // Registry and Declaration return values, not (value, error), so a program that // does not compose has no honest answer for them — and the dishonest one is what // this replaced: an invalid build rendered as an EMPTY build, 0 ops and 0 routes, // which every downstream projection faithfully reproduced. // // So it panics, carrying the joined error. That is the same category as mutating // a frozen App, and for the same reason: you cannot obtain a projection of a // program that does not compose. A sentinel would lose here precisely because a // sentinel is something a caller can ignore, and a caller ignoring it is the // failure this exists to stop. // // The error comes back from liveOrBuild rather than being read off the App: the // draft is written under buildMu, so a read outside it races a concurrent build. func (a *App) mustBuild() *generation { g, err := a.liveOrBuild() if err != nil { panic("zip: this program does not compose, so it has no projection:\n\t" + err.Error()) } return g } // Registry is the op registry as a PROJECTION: every typed op in the // composition, at the path and under the id its occurrence gives it. // // This is the value that replaced five verbs with one. It used to be a FIELD // that composition appended to at compose time, which is why composing an app // needed a verb of its own and why type-erasing one into an http.Handler // destroyed the OpenAPI document, the MCP tool list, the CLI commands, the // by-name call plane and the [Declaration] all at once. A projection cannot be // destroyed by composition, because composition no longer writes it. // // Lock-free once a generation is live — it is read from serving goroutines by // the OpenAPI endpoint and the MCP tool listing, on every request. // // Keys on the OCCURRENCE for surface (path, operationId, tags) and on the // DEFINITION for types: the *registeredOp's InType/OutType are the definition's // own reflect.Types, so one Invoice struct included twice is one schema, not // two identical copies under two names. func (a *App) Registry() []*registeredOp { if g := a.live.Load(); g != nil { return g.ops // the live generation's, computed once, read without a lock } // MUTABLE PHASE. No host exists, so the program can still change and there is // no generation to be consistent with: recompute per call, and do not // memoise. Caching here is what produced a stale answer once already — a // draft keyed on a counter that one mutation forgot to bump — and the phase // where this matters is codegen and tests, never the served path. // // Deliberately NOT goroutine-safe: a program under construction has one // writer by definition, and a lock here would buy nothing while implying a // guarantee the phase cannot make. occ, err := walk(a) if err == nil { if verr := structural(occ); verr != nil { err = verr } else { err = derived(occ) } } if err != nil { panic("zip: this program does not compose, so it has no projection:\n\t" + err.Error()) } return composeOps(occ) } // router returns the live generation's router, building a draft if nothing is // live yet. It never installs — see [App.liveOrBuild]. func (a *App) router() *fiber.App { return a.mustBuild().router } // composeOps reduces a walk to the op registry. func composeOps(occ []occurrence) []*registeredOp { out := make([]*registeredOp, 0, len(occ)) for _, o := range occ { r, ok := o.route() if !ok || r.op == nil { continue } if o.ctx.depth == 0 && o.ctx.prefix == "" { // The served app's own op, at the root: untouched, so an app that // composes nothing publishes byte-for-byte what it declared. out = append(out, r.op) continue } c := *r.op // a copy: composing never edits what a definition says about itself c.Path = o.abs(r.path) c.OperationID = occurrenceID(o.ctx.prefix, r.op) if c.Origin == "" { // Who DECLARED the type — a property of the code, never of where it // was deployed. An op that already names its declarer keeps that // name, so a definition two levels deep still credits its author. // [scope.origin] is what makes that last clause true: the enclosing // definition is a GROUP for every real app, and a group has no name. c.Origin = o.ctx.origin } if len(c.Tags) == 0 && c.Origin != "" { c.Tags = []string{c.Origin} } out = append(out, &c) } return out } // materialise replays an occurrence slice onto a fresh router, in order, and // RETURNS it. It is pure with respect to the App — the caller decides whether // the result becomes a generation — which is what lets a failed build be // discarded without the live system ever having seen it. // // The replay IS the semantics, because this fiber fork resolves same-run routes // by SPECIFICITY and treats every Use as a barrier between runs. Flattened // preorder inlines an included definition's entries at its inclusion site, so // replaying that order reproduces snapshot semantics exactly and for free: // entries written after an inclusion site land after that subtree and cannot // reach back into it. // // Middleware is placed by DEPTH, and this is the one place the two composition // models actually differ on the wire: // // - depth 0 — the app being served. Registered as router middleware, so it // runs for every request including ones that match no route (404 logging, // CORS preflight, recovery). Identical to what a single-app program has // always done. // - depth > 0 — declared inside an included definition. Composed into the // CHAIN of that subtree's own routes instead. Router middleware here would // escape the definition: a child's pathless app.Use(guard) registered on the // host's router is a barrier for the whole host binary, which is precisely // the failure the old delegating verb existed to avoid. Composing it into // the subtree's routes keeps it inside the definition that declared it, and // costs the definition's middleware its coverage of unmatched paths — a // definition does not answer for addresses it does not declare. func (a *App) materialise(occ []occurrence, ctl []route) *fiber.App { f := fiber.New(a.fiberConfig(composeOAuth(occ))) // The report goes on FIRST, so it is outermost: it observes every request // this router answers, including the ones that match no route and the ones a // later middleware refuses. An app gets it by being built, which is the whole // of what "no per-app code" means — there is no call to forget and no order // to get wrong. See [App.report]. if h := a.telemetryHandler(); h != nil { f.Use(h) } // Beside the report and for the same reason: an app says where its own // description lives by being BUILT, so there is no call to forget. It runs // after the report so the report still observes every request, and before // every route so it covers the answers no route matched. See [App.linkHandler]. f.Use(a.linkHandler()) for _, o := range occ { switch o.kind() { case kindMiddleware: if o.ctx.depth == 0 { h, _ := o.middleware() f.Use(toFiberHandler(a, h)) } case kindRoute: r, _ := o.route() a.installRoute(f, o.ctx.prefix, o.ctx.mw.included(), r) } } // zip's OWN projection routes go on last, and are not entries at all: a // control route SERVES what the program computes, so putting it in the // program would make rendering a projection a mutation, and would make it // inheritable by any host that included this definition. for _, c := range ctl { a.installRoute(f, "", nil, c) } return f } // installRoute puts one route on one router: its absolute path, its subtree's // middleware composed in front of it, then the handler the definition wrote. // // fiber runs a route's handlers in ARGUMENT order and advances through them on // Next(), which is the same rule zip's own registration chain already uses — so // composed middleware is passed as leading arguments and needs no wrapper type. func (a *App) installRoute(f *fiber.App, prefix string, mw []Handler, r route) { p := joinPath(prefix, r.path) args := make([]any, 0, len(mw)+len(r.chain)+1) for _, h := range mw { args = append(args, toFiberHandler(a, h)) } // a, not the definition: every handler in one request must be handed the // same *Ctx, and requestCtx keys that on the app it was bound to. for _, h := range r.chain { args = append(args, toFiberHandler(a, h)) } if r.serve != nil { args = append(args, r.serve) } if r.method == methodAll { f.All(p, args[0], args[1:]...) return } f.Add([]string{r.method}, p, args[0], args[1:]...) } // addRoute is the ONE place a route entry is appended, so every route method, // every typed registration and zip's own control plane record the same thing in // the same shape with a real call site — and so the definition's error // vocabulary reaches the entry once instead of at each of those. func (a *App) addRoute(site callsite, r route) { r.oauth = a.oauth r.undeclared = a.undeclared a.appendEntry(entry{n: r, site: site}) } // Lint reports staged composition: middleware appended to a receiver AFTER an // App was included in it. // // It is not an error and not a warning tier. The pattern is INTENTIONAL when // the two lines sit together — // // app.Use(publicAPI) // app.Use(auth) // app.Use(privateAPI) // // — and a latent bug when they are written far apart, in different functions or // different files, where the author of the later line cannot see which subtrees // they just failed to cover. No walk can tell those two apart, because the // difference is authorial intent and the only evidence of it is co-location. // So this reports the fact and the two call sites and lets a human read them; // promoting it to an error would break the legitimate case, and hiding it would // leave the dangerous one silent. // // Reported per receiver, naming the included app, the middleware, and both // sites. func (a *App) Lint() []string { // A reducer, like every other. Filtering the walk's result by the definition // that WROTE each entry recovers that definition's own entries in order, // which is all this needs — and it keeps the tree from being reopened here. type seen struct { included []occurrence } per := map[*App]*seen{} var order []*App var out []string for _, o := range a.plan() { def := o.ctx.in st, ok := per[def] if !ok { st = &seen{} per[def] = st order = append(order, def) } switch o.kind() { case kindApp: st.included = append(st.included, o) case kindMiddleware: for _, inc := range st.included { child, _ := inc.app() out = append(out, fmt.Sprintf( "zip: staged composition in %s: middleware at %s was appended after %s was included at %s — "+ "it does not apply to that subtree; co-located this is intentional, far apart it is a missing seam", def.who(), o.site, child.who(), inc.site)) } } } _ = order sort.Strings(out) return out } // fiberConfig is the App's Config as fiber wants it. One place, so New and // every rematerialisation agree. oauth is the address set the error handler // answers RFC 6749 at, which is a property of the routes this generation holds // and so arrives with them. func (a *App) fiberConfig(oauth map[string]bool) fiber.Config { fcfg := fiber.Config{ AppName: a.cfg.AppName, BodyLimit: a.cfg.BodyLimit, JSONEncoder: jsonMarshal, JSONDecoder: jsonUnmarshal, Concurrency: a.cfg.Concurrency, ReadBufferSize: a.cfg.ReadBufferSize, WriteBufferSize: a.cfg.WriteBufferSize, // Every string and body fiber hands out is a copy. Without this each is // a view of the connection's RequestCtx, which the next request on the // connection parses into in place, so a value kept past the handler (a // map key, a cache entry, a goroutine, a usage record) silently becomes // the next caller's. Not optional: zip has no knob for it, because a // handler cannot know which of its values someone downstream will keep. Immutable: true, } if a.cfg.ServerHeader != "-" { fcfg.ServerHeader = a.cfg.ServerHeader } // The proxy-trust knobs reach the router or they mean nothing: c.IP() is // where fiber applies the allowlist, and it reads them off this config. fcfg.TrustProxy = a.cfg.TrustProxy fcfg.ProxyHeader = a.cfg.ProxyHeader if len(a.cfg.TrustedProxies) > 0 { fcfg.TrustProxyConfig = fiber.TrustProxyConfig{Proxies: a.cfg.TrustedProxies} } if a.cfg.ErrorHandler != nil { fcfg.ErrorHandler = a.cfg.ErrorHandler } else { fcfg.ErrorHandler = refusing(oauth) } return fcfg } // hosts walks the composition and returns every App carrying plugin state, // including this one. // // A plugin used to be registered on whatever host happened to load it, so // Plugins/Reload/Unload could read one map. A plugin is now a DEFINITION // ([Load] returns one), so its state lives with it, and the host finds it the // way it finds everything else about its composition: by walking. That is the // same move the op registry made — one walk, and the projections stop keeping // private copies of what the program already says. func (a *App) hosts() []*App { return a.hostSet() } // pluginNamed finds the definition holding the named plugin, and the plugin. func (a *App) pluginNamed(name string) (*App, *plugin) { for _, h := range a.hosts() { h.plugMu.Lock() p := h.plugins[name] h.plugMu.Unlock() if p != nil { return h, p } } return nil, nil }