Files
zeekay 999e2153b3 Type every wire: bytes, forms, streams, sockets, redirects, unions
A typed op could only take one JSON value and answer one, so uploads,
signed webhooks, form posts, YAML and merge-patch bodies, PDFs, SSE,
WebSockets, OAuth redirects and upstream relays stayed untyped routes
with no schema, tool, command or SDK method. Each is now a typed op and
its REST wire is unchanged.

Requests: a zip.Body field takes the body and Content-Type exactly as
sent; an In implementing Parser decodes itself; form: fields and
*zip.File parts bind url-encoded and multipart forms; cookie: binds a
request cookie as an in: cookie parameter. Consumes names the media.

Answers: zip.Body (bytes, filename, its own status and headers, a
Reader streams chunked), Verbatim[T] (a relay documented as T), Sse[T]
(events with keep-alive), Socket[M] (an upgrade after the contract
runs; 426 without one, 501 over ZAP), Redirect, CookieCoder, Produces,
and unions: a type whose OneOf method's results are the alternatives,
with Or[A,B] as the stock one filing each under the status it states.

Projections follow from the manifest: the document publishes binary
media, form schemas, x-events, x-socket with 101, 3xx with Location
and oneOf; MCP takes bytes as base64 and returns bytes, text or
collected events as tool content, listing a socket op in _meta as no
tool; the CLI reads --body @file|-, --field name=@file and --out,
prints events as they arrive and bridges a socket to stdin and stdout.
tools/list is ProjectMCP over the manifest, the projection MCPTools
already was.

Prose reaches promoted fields (filed under the declaring struct), a
type defined over another struct, and the type arguments of zip's
kinds and OneOf results; an instantiated generic is published under
its name followed by its argument names instead of an illegal
component key. The unused reflect-only document and tool builders go.
2026-10-09 09:42:00 -07:00

239 lines
9.4 KiB
Go

package zip
import "fmt"
// A GROUP is a path prefix and everything declared under it; an OPERATION is
// one typed route. Three nouns carry the whole router — [App], [Group],
// [Operation] — and each is a concrete type, so the verb methods can be GENERIC
// METHODS, which Go 1.27 allows on concrete types and still refuses on
// interfaces. In and Out are inferred from the handler and the caller writes no
// type arguments at all:
//
// users := app.Group("/v1/users")
// users.Post("/", api.Create)
//
// mint := users.Group("/mint").Use(middleware.Mint(gate))
// mint.Post("/deposit", api.Deposit)
//
// THE HANDLER IS THE BOUND METHOD, passed directly rather than wrapped in a
// closure or built by a factory, so `api.Create` survives in the source where
// the projections resolve it back to the method and its doc comment. A wrapper
// erases that, and with it everything the operation publishes about itself.
//
// A Group is a CLOSED HANDLE. There is no App(), no Parent(), no Root() — a
// subsystem handed the group at its own prefix cannot reach above it, so
// confinement is a property of the type rather than a rule something has to
// police. That is what lets a host hand out a bounded surface and know what
// came back.
// Group is a prefix, the middleware that holds under it, and the options every
// operation declared there carries. [App.Group] opens one; [Group.Group] nests.
type Group struct {
// on is the definition this group registers into — an *App with a prefix,
// which is what a group has always been. It is unexported and has no
// accessor: see the note above.
on *App
opts []OpOption
}
// Group opens a group at prefix. Middleware passed here applies to every route
// under it.
func (a *App) Group(prefix string, mw ...Handler) *Group {
return &Group{on: a.group(here(1), prefix, mw...)}
}
// Group nests a group inside this one: the prefixes compose, and the outer
// group's options are carried into the inner one rather than shared with it, so
// a tag added below does not reach back up.
func (g *Group) Group(prefix string, mw ...Handler) *Group {
return &Group{
on: g.on.group(here(1), prefix, mw...),
opts: append([]OpOption(nil), g.opts...),
}
}
// Use composes middleware, or another [App] included by reference, into this
// group. It is the ONE composition verb.
func (g *Group) Use(cs ...Component) *Group {
// A scoped chain composes around leaves registered THROUGH this group, and a
// definition's leaves belong to the definition — wrapping them would mean
// editing an App other hosts may also compose, and composing it UNGATED is
// the one outcome that must not happen silently, since the chain people
// reach for With to install is usually the gate.
if g.on.wrap != nil {
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 — group the definition and scope the chain "+
"there: app.Group(prefix).Use(def)", def.who()))
}
}
}
g.on.Use(cs...)
return g
}
// Raw registers a route that is NOT a typed operation: a webhook that answers
// bytes, a stream, an upgrade, a health probe. There is no In and no Out to
// infer, so it is not projected into the document, the tool list or the SDKs —
// see [registeredOp].
//
// It is deliberately the longer spelling. Opting out of the typed model is the
// exception, and it should read like one.
func (g *Group) Raw(method, path string, handlers ...Handler) *Group {
g.on.raw(here(1), method, path, handlers)
return g
}
// Tag adds tags to every operation declared under this group.
func (g *Group) Tag(tags ...string) *Group {
g.opts = append(g.opts, func(op *registeredOp) { op.Tags = append(op.Tags, tags...) })
return g
}
// Defaults carries any [OpOption] as a default for this group's operations. An
// option the operation itself sets is applied after this one and wins.
func (g *Group) Defaults(opts ...OpOption) *Group {
g.opts = append(g.opts, opts...)
return g
}
// With returns a group whose every leaf handler is wrapped by mw — the chain
// composes around the TERMINAL, never around the middleware preceding it, so
// specificity precedence is untouched and a leaf registered on the returned
// group later is wrapped too.
//
// It differs from [Group.Use], which puts middleware in the chain as an entry
// of its own. Reach for With when the chain must hold at the leaf, for Use when
// it holds over the group.
func (g *Group) With(mw ...Middleware) *Group {
return (&Group{
on: g.on.group(here(1), ""),
opts: append([]OpOption(nil), g.opts...),
}).chain(mw)
}
// chain installs the middleware on a group already created for it. A group carries
// its parent's chain (see [App.group]), so a chain added here composes with
// that one rather than replacing it — a gate installed above stays installed.
func (g *Group) chain(mw []Middleware) *Group {
if chain := Chain(mw...); g.on.wrap == nil {
g.on.wrap = chain
} else {
g.on.wrap = Chain(g.on.wrap, chain)
}
return g
}
// OAuth marks every route declared on the group it returns as answering
// RFC 6749 §5.2 rather than RFC 9457, so the vocabulary is a property of where
// a route was declared and not of what its handler returns.
func (g *Group) OAuth() *Group {
n := g.Group("")
n.on.oauth = true
return n
}
// Undeclared marks every route declared on the group it returns as serving but
// left out of [App.Declaration].
func (g *Group) Undeclared() *Group {
n := g.Group("")
n.on.undeclared = true
return n
}
// Authorize installs fn as the rule over the ops declared on THIS group, and
// only those. It is [App.Authorize] at a prefix, with the same semantics: asked
// at invoke on the decoded input, a tighter rule than the one composed above it
// wins, and declaring none here falls through to that one rather than dropping
// it.
//
// The distinction is the whole point of having it. A subsystem that guards its
// own writes must put the rule on the GROUP it guards: on the group, every op
// under it answers the rule; on the host app, the rule becomes the HOST's, and
// [App.Authorize] covers every op the host serves — including a sibling
// subsystem's, whose ops would then be judged by rules written about a principal
// this guard never attached. Refusing every valid call to an unrelated
// subsystem is the failure that shape produces, and putting the rule nowhere is
// the other one: an authenticated write that nothing checked.
func (g *Group) Authorize(fn Authorizer) *Group {
g.on.authorizer = fn
return g
}
// Get declares a GET operation.
func (g *Group) Get[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out] {
return g.declare("GET", path, fn, opts)
}
// Post declares a POST operation.
func (g *Group) Post[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out] {
return g.declare("POST", path, fn, opts)
}
// Put declares a PUT operation.
func (g *Group) Put[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out] {
return g.declare("PUT", path, fn, opts)
}
// Patch declares a PATCH operation.
func (g *Group) Patch[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out] {
return g.declare("PATCH", path, fn, opts)
}
// Delete declares a DELETE operation. A DELETE addresses what it deletes with
// its URL and carries no request body — see [hasBody].
func (g *Group) Delete[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out] {
return g.declare("DELETE", path, fn, opts)
}
// declare is the one registration every verb method shares. The depth is one:
// the route belongs to the caller of the verb method, and this frame is the
// single one of this package between them. TestSite_IsTheWrittenLine holds it.
func (g *Group) declare[In, Out any](method, path string, fn TypedHandler[In, Out], opts []OpOption) *Operation[In, Out] {
all := append(append([]OpOption(nil), g.opts...), opts...)
return &Operation[In, Out]{op: registerTyped(1, g.on, method, path, fn, all...)}
}
// Operation is a declared typed route. Its methods carry the metadata that
// cannot be inferred — everything else (method, path, input, output, the
// handler's documentation) is already known from the declaration.
//
// It is the same record the authorizer sees as [Op] and the document publishes;
// this is the handle the declaring code holds.
type Operation[In, Out any] struct{ op *registeredOp }
// ID sets the operation id. Without one the method and path name the operation.
func (o *Operation[In, Out]) ID(id string) *Operation[In, Out] {
o.op.OperationID = id
return o
}
// Summary sets the one-line summary the document publishes.
func (o *Operation[In, Out]) Summary(s string) *Operation[In, Out] {
o.op.Summary = s
return o
}
// Tag adds tags to this operation, after any its group declared.
func (o *Operation[In, Out]) Tag(tags ...string) *Operation[In, Out] {
o.op.Tags = append(o.op.Tags, tags...)
return o
}
// Status declares the success codes this operation answers with — see
// [WithStatus] for what more than one means.
func (o *Operation[In, Out]) Status(codes ...int) *Operation[In, Out] {
return o.With(WithStatus(codes...))
}
// With applies any [OpOption], so the chain never becomes the only way to say
// something the option vocabulary already says.
func (o *Operation[In, Out]) With(opts ...OpOption) *Operation[In, Out] {
for _, opt := range opts {
opt(o.op)
}
o.op.settle()
return o
}