openapi

package
v1.801.477 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package openapi projects the LIVE zip/fiber router into an OpenAPI 3.1 document. The spec is not a description of the router — it IS the router, read through app.Fiber().GetRoutes(). There is no second route registry, so a document and the app that produced it cannot drift: the only way to change it is to change the routes it is read from. (Register/Describe add a registry of BODIES and PROSE, never of routes: a declaration renders only on a route the router carries, so the paths remain the router's alone.)

THAT GUARANTEE IS PER APP, AND IT ENDS AT THE APP. Spec and Mount read a router that is right there. The FLEET document — what api.hanzo.ai serves — cannot: the light host mounts no subsystem, so MountFleet weaves the projections 116 app binaries wrote when they were BUILT (fleet.go). Between the projection and the request sit two gaps no reading of any router closes: the subset can be older than the code (mk/fleet.mk surface-check regenerates it from source and refuses the diff), and the deployed front door can hand the path to somebody else entirely (only a probe of the live host sees that). See fleetInfo for what the published document may therefore claim.

This mirrors the rule zapface/wire.go states for transports — two transports, ONE dispatch path. ZAP and OpenAPI are two PROJECTIONS of one route table.

Reading the router is not a workaround; it is the only total source

Two properties make the LIVE router the only honest source, and no static scan of the source tree a substitute:

  • Routes are composed at runtime. POST /v1/kms/auth/login is registered as Group("/v1/kms/auth").Post("/login") — that path literal does not exist anywhere in the tree, so no grep can find it. Only the assembled router knows it.
  • The route set is a function of DEPLOYMENT CONFIG. Subsystems mount only when cfg.Enabled(name), and several gate routes internally (kms registers its secret routes only `if kc != nil`). The spec therefore VARIES PER DEPLOYMENT, correctly: a deployment that does not mount admin does not advertise admin. That is a feature — each deployment describes itself — and it is why the document is generated per-process, not built once in CI.

Two readings of one router, folded into one document

zip ships its own OpenAPI projection (zip/openapi.go) driven by the typed-op registry (zip.Get[In, Out] → app.ops), which carries the real request/response Go types and therefore real JSON Schema — plus the prose cmd/zipdoc lifts out of the handlers' doc comments at build time. It is the better reading, and it is not duplicated here: Typed READS it and Fold lays it over the router projection.

The two are not rivals because GetRoutes() is a strict SUPERSET of app.ops (registerTyped registers a fiber route too). So the router gives the TOTAL set of operations and the registry gives DETAIL for the subset that has any:

router   → every operation exists, with its address and product.
registry → the typed ones also carry schemas, parameters, responses,
           descriptions and examples.

One document, no gaps and no invention. Migrating a raw handler to a typed op is what earns it the second half, and it needs no generator change: the same fold picks it up on the next run.

Register and Describe are the seam through which a subsystem DECLARES what the router cannot derive: the payload types an operation binds (Register) and, for an operation whose handler the wire refuses to let become a typed op, its prose (Describe). The projector (From) reads only two sources: the live route table, which says WHICH operations exist, and this registry, which says what a declared operation's bodies CONTAIN and what its refused handler DOES. The drift-proof property survives because the registry cannot add an operation: a declaration whose route is not in the router simply never renders, so the document still cannot disagree with the router — schemas and prose are additive metadata on routes that exist.

The schema itself is derived by reflection from the very Go structs the handler binds (json tags), stated once at the registration site next to the route. There is no hand-written schema to fall out of sync with the code: change the struct and the document follows.

Index

Constants

View Source
const CommandPath = "/v1/commands"

CommandPath is the command projection's address. House law: /v1/ only, no /api/ prefix, and never a v2.

View Source
const Compat = "compat"

Compat marks an operation whose ADDRESS is a legacy spelling of another operation's: served so a consumer pinned to it does not break, and not part of the contract.

The router cannot know this. `/v1/iam/get-users` and `/v1/iam/users` are two live routes with no relation the route table can see; only the code that registers them knows one replaced the other. So the SERVING code declares it — hanzoai/iam tags the fifty-one entity verbs it inherited (`get-users`, `add-application`, `set-preferred-mfa`) and the singular `application` address it had before the kind was pluralized — and the declaration rides the weave out to hanzoai/openapi, whose merge keeps those addresses out of the published document. Without it the customer surface carries two spellings of every one of those operations: two SDK methods, two docs entries, two `hanzo iam` commands.

It is a SECOND tag, never the first: the product tag is the axis every generator files an operation under, and this is an orthogonal fact about the same operation.

View Source
const Path = "/v1/openapi.json"

Path is the canonical spec endpoint. House law: /v1/ only, no /api/ prefix, and never a v2 — the document's own shape is versioned by its `openapi` field.

Variables

This section is empty.

Functions

func Complete added in v1.801.360

func Complete(doc *Document, owner Owner) error

Complete refuses a document a reader cannot read.

One law, stated as a bijection between operations and prose: EVERY OPERATION SAYS WHAT IT DOES, AND EVERYTHING SAID IS SAID ABOUT AN OPERATION. Both halves fail the same way — a consumer is handed an address and no sentence — so both are refused here rather than in two places under two names.

Why the producer refuses instead of a consumer coping

A projection cannot repair its source, and every branch that handles "the source didn't say" hides the defect at the one place it could be fixed. That is not a design argument, it is a post-mortem: hanzoai/cli's generated tree used to fall back to the operation's own HTTP ROUTE when it had no sentence, so `hanzo platform health` shipped "GET /v1/platform/health" as its help. Nobody filed a bug, because a mechanical line reads exactly like a deliberate one — the fallback did not report the gap, it DISGUISED it, for as long as it existed. The CLI has since deleted the fallback and refuses to emit an undescribed command; this is the same refusal one step upstream, where the sentence can actually be written. Between them there is no place left for a route to stand in for prose.

And there is no placeholder here either. A visible "TODO: describe this operation" is the same fallback with better manners: it would travel into eight generated SDKs, the MCP tool list and docs.hanzo.ai — every surface that cannot fix it — and would be no more visible in the one that can. A gap belongs in a build failure that names a file, not in a published artifact.

The remedy is always the same shape, and the message names it

Prose reaches this document by exactly two seams, and both are already the operation's own source: a typed op carries the Go doc comment on its handler (zipdoc lifts it at build time), and a route the wire refuses to let become a typed op declares it with Describe beside that route. Neither is a table of strings anybody edits separately — which is why the second half of the law matters as much as the first. Describe renders nothing when its key is not a live route (that is what makes the registry unable to invent an operation), so a mis-keyed declaration is prose that was written, reviewed, and then silently dropped: POST /v1/store/storefront-token published an operationId and nothing else for as long as its description sat under the store's old /v1/store/token address. Silence in the safe direction, and total silence in the unsafe one.

An orphan is judged only inside the surface THIS document publishes. The prose registry is process-wide and every app binary links cloud's core, so a subset is built with declarations belonging to subsystems it does not mount; those are not this app's business and are skipped. A declaration that lands on an address this document's own apps own, on a route it does not serve, is this app's defect.

The unit of that judgement is the APP, never the product segment

A product is not an app, and reading one as the other is how this gate produced its own false positive. Product collapses /v1/s3 and /v1/s3/buckets to "s3", but the manifest gives them to different apps on purpose: /v1/s3 PROVISIONS an s3 resource (provisioning) and /v1/s3/buckets is the DATA plane (storage), separated by longest prefix exactly as the router separates them. Keyed on the segment, storage was charged with provisioning's POST /v1/s3 — a declaration provisioning both serves and describes — so an app with nothing wrong with it could not project its own document, and `make surface-check` died there.

That is not one awkward pair to special-case: FOURTEEN products are answered by more than one app (billing, catalog, finance, plans, platform, search, usage, vector among them), so the segment is simply the wrong value. owner is the right one — it is the routing rule itself (manifest.OwnerOf), asked rather than re-derived, which is what keeps this from drifting away from what the host actually does.

func Describe added in v1.801.350

func Describe(path, method, summary, description string)

Describe declares the PROSE for one route — the summary and description a consumer reads — keyed by the fiber pattern exactly as the route is registered, like Register. Called from the owning subsystem's init, next to the wire fact that keeps the handler untyped.

It exists for the operation a typed op cannot carry. A typed op's prose is lifted from its doc comment by zipdoc, so the ONLY operations with nowhere to state prose are the ones the wire refuses to let become typed ops — SSE streams, raw proxies, redirects. Leaving those bare publishes an operationId and NOTHING else: every SDK generated off the document offers a call it cannot explain, and a spec-derived CLI a command with no help text. Describe is that prose's seam, with the same drift-proof property Register has: a description whose route is not in the router never renders, so prose is additive metadata on routes that exist — the registry still cannot add an operation.

Empty prose is refused loudly: a Describe that states nothing is a programming error, not a declaration.

func DescribeRest added in v1.801.350

func DescribeRest(path, summary, description string)

DescribeRest declares prose for every method at path that nothing has described yet, and is how an address bound with All() is covered WITHOUT a hand-copied list of methods.

A wildcard route accepts every method this generator publishes, but the interesting ones are described individually — GET reads, POST acts, PUT is not routed — and each site then enumerated the methods it cared about and stopped. Every one of them stopped at the same five, so OPTIONS and TRACE were published bare from six different addresses: the same omission written six times, which is what a hand-copied list of a thing the generator owns always becomes.

So the leftovers are asked for rather than listed. Call it AFTER the per-method prose for that path; it skips what is already declared, so it cannot collide with them, and a method added to the generator is covered here the day it appears instead of the day somebody notices.

func DescribeSPA added in v1.801.477

func DescribeSPA(prefix, name string)

DescribeSPA declares the prose for the two addresses an embedded single-page app serves: the prefix itself and everything under it.

Every app that embeds a UI (spa.Handler) binds exactly those two with All(), which publishes each of them under EVERY method this generator knows — ten operations that the fleet's prose gate requires a sentence for, and that no handler doc comment can supply because a static bundle has no typed op to lift one from. Written per app it was ~40 lines of near-identical text three times, and text repeated three times drifts: tasks still promised that a missing asset answers "200 with HTML rather than 404" for a while after the handler had begun answering 404, so the published document described behaviour the binary no longer had.

So the sentences are derived from the ONE fact that differs — which app, at which prefix — and the serving policy they describe is spa.Handler's, stated in the one place that policy is implemented.

prefix is the mount ("/tasks"), and its API subtree is "/v1"+prefix by construction — the same-origin shape every embed uses. name is what the product IS to the person reading the document ("tasks console", "tracker board"), and reads directly after "The ".

func Door added in v1.801.455

func Door(path string) bool

Door reports whether path is one [serve] registers — the document and its command projection.

The doors are the host's, and they are the only operations that belong to NO app: they are declared in this package rather than beside any subsystem, no manifest row names them, and EVERY deployment serves them whatever subset of the fleet it runs. Three gates need exactly that fact and each had written it as the one literal that was true when it was written:

Complete                     skips a description no app can own
cmd/cloud/openapi_test.go    exempts them from a scoped deployment's surface
manifest/openapi_test.go     refuses an app row that claims one

So it is stated once, beside the code that makes it true. The second door arrived and all three were wrong the same afternoon — the cost of a literal is that it is right until it isn't and says nothing when it stops.

func Fold added in v1.801.299

func Fold(doc *Document, reg Registry) error

Fold lays the registry's detail over the router's shape, in place. A typed op REPLACES the structural operation at its address — it is strictly richer, including the path parameters, which zip derives from the same pattern.

Two things the fold keeps from the router, both because the router is the authority on them:

  • the product tag, which is the path's first /v1/ segment and nothing else. zip's per-op tags name a different axis, and the one that carries a fact the router cannot know survives beside it (Compat, below).
  • membership. A typed op with no live route is a contradiction — registering one registers a fiber route — so it means the two readings disagree about a path (a translation bug), and the honest answer is to refuse rather than to invent the operation or drop it silently.

func Front added in v1.801.360

func Front(r Relay)

Front declares what is behind a door. Called from the owning subsystem's Mount, next to the registration of the door itself — the two are one fact and must be written in one place.

It panics on a duplicate prefix, the way Register and Describe do: two relays at one door is a programming error at wire time, not a runtime condition to degrade through.

func Methods added in v1.801.350

func Methods() []string

Methods returns the methods this generator publishes, sorted.

It exists for ONE caller shape: a subsystem whose whole surface is an All() registration. All() binds every method at one path, so there is no per-method registration site to hang prose on — the subsystem declares its prose in a loop instead, and that loop has to cover EXACTLY what the document renders. Reading the projection's own set is what makes it exact: a method added or dropped here moves both halves at once, so the loop can neither describe an operation nobody publishes nor miss one that is published.

func Mount

func Mount(app *zip.App, info Info, servers ...Server)

Mount serves the document at Path off app's OWN live router — the app it is registered on is the app it reads, so what it serves is that process's actual route table and nothing else.

It is UNAUTHENTICATED, deliberately:

  • It is an API description of a public API, and it grants no capability. Every route it names stays individually auth-gated; reading the map does not open a door. Withholding it would be obscurity, not access control.
  • The `hanzo` CLI must build its command tree BEFORE a user logs in. Gating the spec would make `hanzo --help` require credentials.
  • It carries no schemas, no examples, no secrets — only addresses.
  • Enablement already scopes it: the document is generated from what THIS deployment mounted, so a deployment that does not enable admin does not list admin routes. The blast radius is the deployment's own surface.

The cost accepted is path enumeration on a deployment that mounts admin. That is a recon convenience, not an authorization change. If a deployment ever needs it closed, the lever is a guard here — one line, one place.

The document is built once, lazily, on first request: the route table is fixed after boot, and building lazily (rather than at Mount) means it includes every route — including this one, and any registered after Mount.

The light HOST mounts no subsystem, so its live router is not the API — see MountFleet, the other document source at this same one address.

func MountFleet added in v1.801.350

func MountFleet(app *zip.App, subsets func() ([]Part, error))

MountFleet serves the FLEET's document at Path: the composition of what this deployment's plugins serve, woven from the subsets their binaries projected when they were built.

It exists because Mount's answer is WRONG on the light host, and wrong in the way that is hardest to see. The host mounts no subsystem — that laziness is what makes 113 of them affordable — so its live router is 113 proxy prefixes and a console catch-all, and reading it describes the ROUTER, not the API. Nor can the host mount them to find out: waking the fleet to answer a public GET is exactly the cost lazy mounting exists to avoid.

So the host answers from the build-time projection instead. It is the same question every other projection answers ("what does the fleet serve") sourced from the only place the host can honestly read it. The weave runs ONCE, on the first request, off bytes already in the binary: no subsystem starts, no socket opens, and a deployment that never gets asked never pays.

Unauthenticated for the reasons stated on Mount: same document, same door.

func Product

func Product(path string) string

Product is the product axis, and it is mechanical: the first path segment after /v1/ IS the product (/v1/kms/* → kms, /v1/billing/* → billing). No judgment, no table to maintain, nothing to keep in sync.

It is deliberately NOT the subsystem name: clients/billing serves both /v1/billing/* and /v1/finance/* (finance.go:57), so the mount that owns a route and the product a caller names are different values. The CLI wants the one in the URL.

Returns "" when the first segment is not a product name — a parameter (:org), a wildcard (*), or a file (openapi.json) — and for anything outside /v1 (/health, /.well-known/*, /git/*, /tasks/*). Those get no tag rather than a fabricated one.

func Products added in v1.801.360

func Products(tags []string) []string

Products drops Compat from an operation's tag list.

Compat is an orthogonal fact about ONE address, not a product. Everything that reads tags to answer "which products does this document publish" — the floor ratchet, the weave's ownership claims, the document's own tag list — has to ask this instead of iterating Tags, or `compat` arrives as a product with twenty-three operations in it and a doc-site heading nothing answers to.

func Project added in v1.801.360

func Project(doc *Document, rs []Relay) error

Project replaces each declared door with the registry behind it.

A relay whose door is NOT in doc does not apply. That is the same law Register and Describe obey — a declaration renders only on a route this process carries — and it is what makes one binary per app work: the relay registry is process-wide, a describe run mounts one subsystem, and a relay declared by a subsystem that is not mounted has no door to replace. The fleet-level guarantee that a door never quietly goes missing is not this function's job and cannot be: it is the ratchet in floor.go, which is the only place that can see the whole surface at once.

func Register added in v1.801.299

func Register(path, method string, req, resp any)

Register declares the request and response body types for one route, keyed by the fiber pattern exactly as the route is registered. Pass the zero value of the handler's own binding struct (or a slice of the view type for list endpoints), Binary for a raw byte body, and nil for a side that has no body. Called from the owning subsystem's init, next to its route table.

A duplicate registration for the same (method, path) is a programming error at init time and panics loudly rather than letting two declarations race for one operation.

func Synopsis added in v1.801.350

func Synopsis(dir string) string

Synopsis is the one-sentence summary of the package that backs the app whose binary lives in dir (a plugin/<name> directory), or "" when there is none.

The OWNER is read from the app's own composition root: plugin/<name>/main.go imports exactly the package it mounts, so the import IS the mapping. Nothing else could be — four apps are not named after their package (audit → auditlog, evals → eval, plugins → plugin), so a name-derived guess would be right 107 times and silently wrong 4. (It was wrong 5 while apps/account backed a second app, account-bridge; reading the import means a package taking on or shedding a mount changes nothing here.) An app whose subsystem is another MODULE (authz, licensing, metrics) imports no package here, has no doc comment to read, and gets "" — the honest answer, not a fabricated one.

Empty on any failure, deliberately. This is prose: a describe run from outside the source tree (a container) still has to produce a document, and a missing sentence is a document without a sentence, never a failed projection.

Types

type Binary added in v1.801.350

type Binary struct{}

Binary is the request declaration for a body that is not JSON at all: opaque bytes under the caller's own content type — an uploaded PDF or image, an OFX/QFX/CSV bank statement, a script. Pass it as Register's req for a route whose handler reads the body raw.

It exists because "no declaration" and "a byte body" were rendering IDENTICALLY, and they are opposite facts. An operation with no requestBody is what a route that takes no body publishes, so every SDK generator reading the document emitted a call with no payload parameter for routes that cannot work without one (POST /v1/books/scan eats a receipt; /v1/books/bank/import eats a statement). OpenAPI's own spelling for an opaque body is a string of format binary, which is what this renders — the honest declaration a Go struct cannot make, since no struct describes a file.

It is the REQUEST half; Bytes is the response half, which states its media type because a served asset has one.

type Bytes added in v1.801.437

type Bytes struct{ Type string }

Bytes is the RESPONSE declaration for a body that is not JSON: an asset the caller loads rather than decodes. Type is the media type the handler actually sets, because a document that says application/json over JavaScript is a document that lies to whoever generates against it.

openapi.Register("/v1/event.js", "GET", nil, openapi.Bytes{Type: "application/javascript"})

Empty Type means opaque bytes (application/octet-stream). Like Binary it names no component: an asset has no fields.

type Components added in v1.801.299

type Components struct {
	Schemas map[string]any `json:"schemas,omitempty"`
}

Components holds the named schemas operations reference by $ref, so an SDK generator mints one named type per Go struct instead of an anonymous shape per operation. Open-typed: Register contributes *Schema values, the typed fold contributes zip's JSON Schema verbatim — both marshal to the same vocabulary.

type Conflict added in v1.801.299

type Conflict struct {
	Kind string // "operation" or "schema"
	Name string // "GET /v1/x" or the schema name
	A, B string // the two apps that claim it, in weave order
}

Conflict is two apps disagreeing about one name. Typed rather than a bare error string so a caller (and the gate) can assert on the KIND of collision without matching prose.

func (*Conflict) Error added in v1.801.299

func (c *Conflict) Error() string

type Document

type Document struct {
	OpenAPI    string              `json:"openapi"`
	Info       Info                `json:"info"`
	Servers    []Server            `json:"servers,omitempty"`
	Tags       []Tag               `json:"tags,omitempty"`
	Paths      map[string]PathItem `json:"paths"`
	Components *Components         `json:"components,omitempty"`
}

Document is an OpenAPI 3.1 document.

What IS derivable from the router, and what is NOT

The router is a dispatch table: pattern → handler. It knows how to MATCH a request, not what a request or response CONTAINS. Concretely, fiber's Route struct (fiber/v3@v3.2.1 router.go:46) is only {Method, Name, Path, Params, Handlers} — there is no payload information in it to read.

Derivable (asserted here, all of it structural):

  • method — the stack a route is registered in.
  • path — the registered pattern, verbatim.
  • path parameters — the pattern's own :name segments; a router that could not name them could not match them. Always required:true (a fiber path param is optional only if written :name?, which cloud does not use).
  • product tag — the first /v1/ segment of that same pattern.

NOT derivable from the router (no amount of router-reading changes it — these come from the typed registry via Fold, and are absent on a route that has no typed op):

  • request body schema. The router holds a func(*zip.Ctx) error. The request type is a LOCAL VARIABLE inside the handler body — clients/kms putSecret is the representative case: `var req secretPutRequest; json.Unmarshal( ctx.Body(), &req)`. The type exists in the package but never appears in the handler's signature, and Go has no reflection from a func value to the types it unmarshals internally. cloud.Handle[S] does not help: its type parameter S is the SERVICE (service.go:90), not the payload. cloud.Typed does not help either: it is an any→*zip.App mount adapter. What cannot be DERIVED can still be DECLARED: Register (register.go) is the seam a subsystem uses to state its binding structs once, next to its route table, and the schema is reflected from those structs.
  • response body schema / status codes — same dead end, at the far end, with the same declaration seam (the success shape under a "2XX" range, because the exact code lives in the handler body).
  • query and header parameters — read positionally via c.Query("k") at runtime; not part of the match, so the router has never heard of them.
  • auth requirements — enforced by middleware and by guards wrapped around handlers (guard(s, cloud.Handle(s, listSecrets))), invisible as data.
  • summaries/descriptions — prose that exists only in Go comments (a typed op's zipdoc lift, or a refused route's Describe declaration).
  • wildcard semantics — a fiber `*` matches MULTIPLE segments greedily; OpenAPI's {param} matches one. The emitted {wildcardN} is the closest honest approximation and is NOT equivalent.
  • whether a route is a real endpoint or a PROXY PREFIX. A catch-all like app.Post("/v1/billing/*") forwards to another service; the operations behind it (POST /v1/billing/deposit and friends) are not routes in this process and cannot appear. For products whose whole surface is one catch-all, this document can name the prefix and nothing under it.

The consequence for a consumer: this document is a complete and exact map of the API's SHAPE (every operation, its address, its product), and it describes PAYLOADS exactly as far as the typed registry does. A CLI can build its full command tree — `hanzo <product> <resource> <verb>` — and bind path params from it with no judgment calls, everywhere. It can typecheck a request body and pretty-print a response only for a typed op, and behind a catch-all it cannot enumerate subcommands at all.

The path to more schema is not a better reader. Two seams exist, both anchored in the handler's own Go types so neither can drift:

  • Register (register.go): a subsystem declares its binding structs for a route it already serves — no handler change, schema by reflection. A registration renders ONLY when the router carries the route, so the document still cannot disagree with the router; schemas are additive metadata on routes that exist.
  • zip's typed ops (zip.Get[In,Out]), which carry the In/Out types in the handler signature itself and also earn an MCP tool and a CLI command from the same registry entry (zip's third projection, zip/mcp.go). That is a per-handler refactor of business logic, and it composes with this: GetRoutes() already includes typed ops, so migration adds detail through the fold that is already running.

Chained handlers are not collisions

Fiber MERGES byte-identical route patterns into ONE Route carrying both handlers chained, so a duplicate registration is invisible to an entry count and shows only as len(Handlers) > 1. That is true — but the converse does not hold, and this generator does NOT use that signal:

app.Post("/v1/billing/recharge/run-all",
    commercemid.RequestContext(), commercemid.TokenRequired(),
    commercemid.PlatformOnly(), commercebilling.RunAutoRechargeAllOrgs)

is ONE registration with FOUR handlers — three middleware and a terminal handler (apps/commerce.go:151). The whole /v1/store/* surface is the same shape. 34 live routes carry chained handlers and every one is legitimate. A merged duplicate and a middleware chain are INDISTINGUISHABLE through the public API, so "handlers > 1" cannot mean "collision" fleet-wide; treating it as one would refuse a spec for a healthy router. (clients/bots/routes_test.go asserts exactly that rule, and is right to: it is a local truth for the bots/visor/runtime surface, where nothing chains middleware. It is not a global one.)

This costs the document nothing. Merged or chained, one pattern is one operation — which is what the spec emits either way. Detecting routing bugs is the bots guard's job, at the seam where the premise holds; the spec's only requirement is that (method, path) → operation stay injective, which From enforces via operationId uniqueness.

func Fleet added in v1.801.350

func Fleet(subsets []Part) (*Document, error)

Fleet is THE published Hanzo Cloud API document: the weave of every app's own subset plus core.

ONE definition, called by both things that must agree about it — the gate that WRITES openapi.yaml (openapi/weave_test.go) and the host that SERVES it (MountFleet). That is what makes "the served document is the committed artifact" true by construction rather than by two pieces of code happening to agree; a drift between them is not expressible.

func FleetSpec added in v1.801.299

func FleetSpec(app *zip.App) (*Document, error)

FleetSpec projects app into THE Hanzo Cloud API document: Spec, carrying the one identity above.

Every producer of a published spec calls this — the monolith's golden, each app binary's subset, and the weave's output. Spec stays exported for the callers that want a document of their own (the live endpoint names the deployment's brand), but nothing that writes an artifact should be choosing its own title.

func From

func From(rs []Route, info Info, servers ...Server) (*Document, error)

From builds the document from route data. No router, no I/O — its inputs are the routes, plus the package registry of declared bodies (Register), which is written only at init time and is therefore fixed by the time any document is built.

It refuses on a duplicate operationId rather than emit a document a generator would mis-consume. That check subsumes the only ambiguity the spec can suffer: two routes sharing a (method, path) derive the same id, so an injective (method, path) → operation map is exactly what uniqueness buys. It refuses equally when two DIFFERENT Go types claim one component name — a silent merge would hand an SDK generator a lie.

func Spec

func Spec(app *zip.App, info Info, servers ...Server) (*Document, error)

Spec reads the live router and every projection of it — the one call a caller wants.

Three readings, in the order each earns: the router gives the total set of addresses, the typed registry gives detail for the ones that have any, and the relays give what is BEHIND the addresses that are doors rather than endpoints. Project runs last because a door is only a door once the router has been read, and because what it substitutes is already a finished document.

func Weave added in v1.801.299

func Weave(parts []Part) (*Document, error)

Weave composes the parts into the fleet document, or refuses.

Identity (openapi version, info, servers) comes from THIS package, not from the parts: a subset is a partial view of one API, so the composed document is that same API — not a fifth one assembled out of whatever titles the parts happened to carry.

type Floor added in v1.801.360

type Floor struct {
	Paths      int            `json:"paths"`
	Operations int            `json:"operations"`
	Products   map[string]int `json:"products"`
}

THE RATCHET: a published surface may grow and may not quietly shrink.

Every gate this package already has compares the document to something that moved with it. The weave compares the golden to the subsets — both derived, so they can agree while both are wrong. surface-check regenerates the subsets from source — which catches a stale artifact, and passes cleanly on an artifact that is freshly and correctly generated from code that stopped registering half its routes. Nothing held a line across time.

So a surface can lose products with every gate green, and it has: the CLI's capture went from 151 products to 4 in one bad reading and shipped 46 products short, and plugin/ingress lost eight paths — /v1/ingress/routes, /services, /middlewares, /tls, /status and their :id forms — because a subset was never regenerated. Both were invisible for the same reason: NOTHING COMPARED THE SURFACE TO ITS OWN PAST. A smaller document is indistinguishable from a smaller API, and only the history says which one happened.

floor.json is that history, in the only shape that is cheap to keep honest: the counts, per product. A regeneration that meets or beats every one of them raises the floor and the new file is committed alongside the document it describes. A regeneration that comes in UNDER any of them fails, names the products and the deltas, and writes nothing.

Why counts, and why per product

The floor is not a second copy of the document — that would be one more derived artifact free to drift, which is the disease. It is the one fact a shrink cannot hide behind: a product with 161 operations that comes back with 4 is a hole whatever the four are called, and a product that comes back absent is the whole hole. Per product rather than in total, because totals net out — the run that lost all 143 of iam's paths added enough elsewhere to keep the total moving up.

Lowering it is allowed, and is meant to be an ACT

A deliberate deletion lowers the floor by editing floor.json in the same commit that deletes the routes, where a reviewer sees the number go down next to the reason. That is the whole design: not "never shrink", but "never shrink by accident and never shrink silently".

func Measure added in v1.801.360

func Measure(d *Document) Floor

Measure counts what a document publishes.

Products are read off the OPERATIONS' tags, not off the document's tag list: the tag list is itself derived, and a bug that emptied it would make every product vanish from the floor at once — the ratchet would then be measuring its own blind spot. An operation with no tag (every address outside /v1) is counted in Operations and in no product, which is exactly what it is.

func ReadFloor added in v1.801.360

func ReadFloor(path string) (Floor, error)

ReadFloor reads a committed floor. A MISSING file is refused rather than treated as zero: an absent floor makes every shrink legal, so the failure mode of losing the file would be losing the guarantee — silently, which is the one thing this must never do.

func (Floor) Raise added in v1.801.360

func (f Floor) Raise(now Floor) (Floor, error)

Raise returns the floor for now, or refuses because now is smaller.

The returned floor is the pointwise MAXIMUM, not now: a product that legitimately shrank has already been let through by a hand edit to floor.json, and taking the max means that one edit is not silently undone by the next unrelated regeneration.

func (Floor) Write added in v1.801.360

func (f Floor) Write(path string) error

Write renders the floor as the same indented, newline-terminated JSON every other artifact in this repo is written as, so it reviews as a diff.

type Info

type Info struct {
	Title       string `json:"title"`
	Description string `json:"description,omitempty"`
	Version     string `json:"version"`
}

Info is the OpenAPI info block.

type Media added in v1.801.299

type Media struct {
	Schema *Schema `json:"schema,omitempty"`
}

Media is an OpenAPI media type object: the schema of one content type.

type OneOf added in v1.801.350

type OneOf []any

OneOf is the request declaration for a body whose wire is POLYMORPHIC: one path that accepts several unrelated JSON shapes, all decoded by the same handler. Pass the zero value of each shape, in the order a reader should meet them:

openapi.Register("/v1/event", "POST",
    openapi.OneOf{Event{}, []Event{}, CaptureBatch{}}, CaptureResult{})

It exists for the same reason Binary does — the honest declaration a single Go struct cannot make. The alternative was to name ONE of the shapes and call it the wire, which is a document that omits the two forms every batching client actually sends, and an SDK whose only ingest call cannot send a batch. OpenAPI's own spelling for "several shapes, caller picks" is `oneOf`, which is what this renders; each alternative is derived by reflection exactly as a lone req is, so a named struct among them still becomes one shared component.

REQUEST-only, like Binary: a response that varies by shape is a fact no route here needs stated yet, and inventing the second half before one asks is how one seam becomes two.

type Operation

type Operation struct {
	OperationID string      `json:"operationId"`
	Summary     string      `json:"summary,omitempty"`
	Description string      `json:"description,omitempty"`
	Tags        []string    `json:"tags,omitempty"`
	Parameters  []Parameter `json:"parameters,omitempty"`
	RequestBody any         `json:"requestBody,omitempty"`
	Responses   any         `json:"responses,omitempty"`
	App         string      `json:"x-app,omitempty"`
}

Operation is one operation.

Everything past Parameters is omitempty and comes from exactly two seams, both anchored in the handler's own Go types so neither can drift:

  • Register/Describe (register.go): a subsystem declares its binding structs — schema by reflection — and, for a handler the wire refuses to let become a typed op, its prose; both attach only when the router carries the route.
  • zip's typed ops, folded in from the registry (Fold): Summary/Description from the lifted godoc, query parameters and bodies from the In/Out types.

A route neither seam knows keeps exactly the structural facts the router can prove, and asserts no status code, content type or prose it has no evidence for — OpenAPI 3.1 makes `responses` OPTIONAL (3.0 required it), so absent stays valid and absent beats invented.

RequestBody and Responses are `any` because the two seams produce different (JSON-identical) shapes: Register builds the closed *RequestBody / map[string]*Response, the fold reuses zip's open maps verbatim. App is PROVENANCE: the registry that registered this operation. It is the one question a reader of a wrong operation has — who do I file this against — and until it was recorded the answer took a bisect of 116 subsets. For an app's own route it is the app name, so the code is apps/<name> in this repo; for an operation reached through a door it is the module behind that door, so the code is that repo. Either way it names a place to go.

It is written exactly once per operation, by whichever producer knows: Project stamps the relay's source as the operation enters, Weave stamps the part's app for everything else, and neither overwrites a value already there.

type Owner added in v1.801.384

type Owner func(path string) string

Owner reports which app the fleet routes path to, and "" when nothing does.

It is a PARAMETER and not an import because the dependency runs one way only: manifest is the routing table, and manifest's own tests read this package to pin the spec door's address (manifest/openapi_test.go), so an openapi that imported manifest back would make manifest's test binary an import cycle — the compiler says so. The fleet passes manifest.OwnerOf, at the one producer (describe.go).

nil is refused rather than defaulted. A caller with no routing table cannot tell a misfiled declaration from another app's, and the safe-looking default — judge nothing — is a gate that passes everything while still being called.

type Parameter

type Parameter struct {
	Name        string          `json:"name"`
	In          string          `json:"in"`
	Required    bool            `json:"required"`
	Description string          `json:"description,omitempty"`
	Schema      map[string]any  `json:"schema,omitempty"`
	Example     json.RawMessage `json:"example,omitempty"`
}

Parameter is an OpenAPI parameter object.

From the ROUTER only path parameters are emitted: they are structural — the router matches on them — so they are derivable. From the REGISTRY a typed op's query parameters come too, with descriptions, because its input type says what they are. Schema is an open map because JSON Schema is an open vocabulary: the router asserts `{"type": "string"}` and nothing more, while a typed op's field can be any shape zip derives from its Go type.

Example is that field's value in the op's own example. A BODYLESS op — every GET and, since zip v1.18.0, every DELETE — has no requestBody for the doc comment's Example to live in, so zip splits it across the parameters that carry it. Without a name for it here the round-trip through Typed dropped it, and every GET and DELETE reached the published reference with no example at all.

type Part added in v1.801.299

type Part struct {
	App string
	Doc *Document
}

Part is one app's contribution: its document, and the name a conflict is reported against. The name is the app's, not the file's — a message naming two paths a human then has to map back to apps is a worse message.

func Subsets added in v1.801.350

func Subsets(apps []string, read func(app string) []byte) ([]Part, error)

Subsets decodes the apps' own documents, in the order given — which callers take from manifest.Names(), the fleet's mount order, so a conflict is reported as the router would meet it.

read answers with one app's subset bytes. WHERE those bytes come from is the caller's, because there are two callers and ONE set of files: the drift gate reads the working tree it is about to compare against (openapi/weave_test.go), and the light host reads what it embedded from that same tree at build time (plugin.Spec). Neither is a second source — surface-check regenerates the files both read, from source, and fails on any diff.

A missing subset is refused rather than skipped. Skipping it would publish a fleet document with one app's whole surface quietly absent, which is precisely the failure mode plugin/ingress cost eight paths to.

AND EACH SUBSET IS CHECKED FOR THE INVARIANT ITS GENERATOR ALREADY OWES, at the one place the app's NAME is still in hand. From refuses to emit a document whose operationIds collide, so a generated subset cannot arrive broken — but "generated" was an assumption about a committed file, and a file can be edited. One was: /v1/billing/methods was hand-written into commerce's subset by copying the /v1/billing/portal/methods block, operationId and prose together, so two paths claimed get_v1_billing_portal_methods and the fleet could not be woven at all. Weave did catch it — but a collision INSIDE one part reaches Weave as a collision between two paths with no app attached, so the report named the two addresses and left which of 123 subsets to a search. Here the answer is the loop variable.

It is the same check, not a second one: uniqueOperationIDs is the single statement of the rule, asked once per part here and once over the whole composition there, because a part being injective and the weave being injective are different facts and neither implies the other.

type PathItem

type PathItem map[string]*Operation

PathItem maps a lowercased HTTP method to its operation.

type Registry added in v1.801.299

type Registry struct {
	Ops     map[string]*Operation
	Schemas map[string]any
}

Registry is zip's typed-op projection, reduced to what the router cannot supply: the operations that carry schema and prose, keyed by the same "METHOD /templated/path" identity a Document is indexed by, and the schemas they $ref.

func Typed added in v1.801.299

func Typed(app *zip.App) (Registry, error)

Typed reads the typed-op registry off the app — the SAME value zip serves at /.well-known/openapi.json, so there is one generator for schemas and this is not it.

It comes back through JSON rather than by walking zip's map[string]any because the map IS a JSON document; decoding it into the same Operation the router projection builds is what makes the two foldable at all, and it means a field zip adds later arrives here without a change (or is dropped honestly, if nothing here has a name for it).

type Relay added in v1.801.360

type Relay struct {
	// Source is WHO registered these operations — a module path, because the
	// point of recording it is that a misplaced operation names the repo to file
	// against. It travels onto every operation as x-app.
	Source string
	// Prefix is the door's own address with no wildcard: "/v1/iam", "/v1". Every
	// operation behind the door must be under it.
	Prefix string
	// Yields are subtrees INSIDE Prefix that the door does not reach, because the
	// fleet delivers them to a sibling registered in front of it. A wildcard is
	// matched last, so a registry behind one can hold routes that no request ever
	// arrives at: ai registers /v1/metrics and /v1/admin/providers, and both 404 on
	// api.hanzo.ai because the metrics and admin apps receive those prefixes and do
	// not serve them. Publishing such a route is publishing a phantom, which is the
	// exact defect a router-derived document exists to make impossible.
	//
	// It is the door's owner that fills this, from the fleet's own routing table
	// (manifest.Elsewhere), never a list written here — a second copy of the
	// routing order is how the pair goes wrong while each half stays sensible.
	Yields []string
	Behind func() (*Document, error)
}

Relay is a door and the registry behind it.

Behind is a func, not a Document: every app binary in production registers its relays at mount and never projects anything, so building the sub-document at declaration time would be work no serving process needs. It is called once, by Spec, and its error is the caller's.

func Mounted added in v1.801.360

func Mounted(source, prefix string, sub *zip.App) Relay

Mounted is a relay over a sub-app the host mounts behind its door: the SAME Spec the host's own document is, over the app that actually answers.

It is the richest shape a relay has — the sub-app carries its typed registry, so its operations arrive with schemas, parameters and the prose zipdoc lifted from its handlers, exactly as the host's own typed ops do.

The caller passes the app it MOUNTED, not a fresh one. A second construction would be a second route table, free to differ from the one answering requests — which is the whole defect this package exists to make unexpressible.

func Table added in v1.801.360

func Table(source, prefix string, patterns func() map[string][]string, prose func() map[string]Said) Relay

Table is a relay over a route table, for a registry that is not a zip app.

hanzoai/ai is the case: its surface is a beego ControllerRegister reached through one adapter, so what it hands a host is not an App but the two readings an App would have given — `path -> methods` (routers.App.Patterns) and `"METHOD /path" -> sentence` (routers.Prose). Those are exactly Live and Typed for a registry that is not zip's, so they compose the same way: the table is the total set of addresses, and the sentences are detail for them.

Structure therefore comes from From, the same builder the router projection uses, so a relayed operation earns the SAME operationId, path parameters and product tag a native one does — one convention across the document, whoever registered the route. Only the prose is the registry's, because only the registry has it. It earns no schemas: a table has none, and that is a true statement about the table rather than a gap in the projection.

A "*" method means the table dispatches EVERY method at that address. It expands to Methods, the set this generator publishes, which is the same expansion the door itself gets — so the two halves of one wildcard cannot disagree about which verbs exist, and each expanded verb inherits the one handler's sentence, because one handler is what answers all of them.

type RequestBody added in v1.801.299

type RequestBody struct {
	Content map[string]Media `json:"content"`
}

RequestBody is an OpenAPI request body object.

type Response added in v1.801.299

type Response struct {
	Description string           `json:"description"`
	Content     map[string]Media `json:"content,omitempty"`
}

Response is an OpenAPI response object. Description is required by the spec.

type Route

type Route struct {
	Method string
	Path   string // fiber pattern, e.g. /v1/kms/orgs/:org/secrets/*
}

Route is one live route, reduced to what the router actually knows: where a request goes. Method and Path are the whole of it.

There is deliberately no handler count here. See Document's note on chained handlers for why that number cannot be interpreted.

func Live

func Live(app *zip.App) []Route

Live reads the router. It is the SOLE adapter from fiber to data — every other function here is a pure function of []Route, so the projection is testable without a router and the fiber coupling has exactly one home.

GetRoutes(true) drops Use() middleware entries: middleware matches path PREFIXES and is not an operation. That filter is fiber's own (app.go:822), not a reimplementation of it.

type Said added in v1.801.360

type Said struct{ Summary, Description string }

Said is what a foreign registry says about one of its operations: the sentence a reader is owed, and the whole comment it opens.

type Schema

type Schema struct {
	Type                 string             `json:"type,omitempty"`
	Format               string             `json:"format,omitempty"`
	Ref                  string             `json:"$ref,omitempty"`
	Items                *Schema            `json:"items,omitempty"`
	Properties           map[string]*Schema `json:"properties,omitempty"`
	AdditionalProperties *Schema            `json:"additionalProperties,omitempty"`
	OneOf                []*Schema          `json:"oneOf,omitempty"`
}

Schema is the sliver of JSON Schema this generator can honestly assert as a CLOSED struct: primitive types for path parameters, and — for routes whose subsystem declared its bodies via Register — objects, arrays, and $refs derived by reflection from the handler's own binding structs. Typed-op schemas do NOT pass through it: zip already derives arbitrary JSON Schema from the In/Out Go types, and restating that open vocabulary here would be a second, lossier copy — they travel as `any` (see Fold). Format qualifies Type where the type alone is not the whole fact. It carries exactly one value today — "binary" on the string that stands for a raw byte body (Binary) — because that is the only distinction this generator can make that a consumer acts on: an SDK generator emits a file/bytes parameter for `string/binary` and a text parameter for a bare `string`. OneOf carries the alternatives of a POLYMORPHIC body (OneOf the declaration value) and is empty on every other schema. It is the one place this generator says "several shapes, and the caller picks" — a fact a single Go type cannot state, which is why declaring one alternative and calling it the wire would under-describe a route that accepts three.

type Server

type Server struct {
	URL string `json:"url"`
}

Server is an OpenAPI server entry.

type Tag

type Tag struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
}

Tag is an OpenAPI tag — one per product, so a consumer can read the product list off the document without walking every path.

Description says what the product IS, in the words of the package that implements it (Synopsis). It is omitted rather than filled: a product whose package carries no doc comment is a product nobody has described yet, and inventing a sentence here would make that indistinguishable from one somebody wrote. The NAME is never conditional on it — the tag list stays a function of the document's operations, which is the one thing a consumer enumerating products can rely on.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL