Files
zip/manifest.go
zeekayandHanzo Dev bf9844f9a3 A registration is filed under the package and the line that wrote it
The callsite is recorded by counting frames out to the programmer's line, and a
verb method on a scope sits one frame further away than the package-level
function does. The count said otherwise, so every conflict, cycle and post-seal
write in a scoped service cited a line somewhere above the registration instead
of the two lines actually in conflict.

Two tests now hold both halves of that attribution — the package an op is filed
under and the line it names — across all three forms a registration takes,
including a handler another package composed, which is the shape that once
published seven ops with no description at all.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 00:20:27 -07:00

579 lines
19 KiB
Go

// SPDX-License-Identifier: BSD-3-Clause-Eco
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zip
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
)
// The registry, said without Go in it.
//
// Every projection in this package — the OpenAPI document, the MCP tool list,
// the CLI, the GraphQL schema, the ZAP IDL, the SDKs — reads a *registeredOp,
// and a registeredOp holds two reflect.Types and three closures. That is why
// they are Go's alone: a Rust service has no reflect.Type to hand them, so the
// only way it ever reached them was to have a Go program declare its ops for
// it, which is not a Rust service declaring anything.
//
// A Manifest is the same registry with the Go taken out: an op is a method, a
// path, an id, its prose and the NAMES of its input and output; a type is a
// name, a kind, and a list of fields with their wire names, their prose and
// their slots. Everything the projections read is here and nothing here is Go,
// so the projections can read this instead — and then a front end that can fill
// one in gets all of them, whatever language it is written in.
//
// There are three such front ends. Go fills a Manifest in by reflection, at run
// time, from the types it already holds ([App.Manifest]). Rust fills it in at
// COMPILE time, from a proc-macro that reads the handler's signature, its path
// attribute and its doc comments in one pass. C++ fills it in at BUILD time,
// from a libclang walk of the same declarations. None of them is privileged and
// none of them needs another.
//
// The manifest is a description, never a program: it carries no closure, no
// address and no way to run anything. What runs an op stays in the language the
// op was written in.
// ManifestVersion is the shape of the document. It is a number in the document
// itself so a projector reading one written by an older front end says so
// rather than mis-reading a field that moved.
const ManifestVersion = 1
// Manifest is one app, described so that anything can project it.
type Manifest struct {
Manifest int `json:"manifest"`
// App is the name the app declared for itself. It names the ZAP interface
// and titles the document when nothing else does.
App string `json:"app"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Version string `json:"version,omitempty"`
// Ops are this app's typed operations, in registration order — the same
// order [App.Registry] returns, so a projection that sorts sorts once.
Ops []ManifestOp `json:"ops"`
// Types are every type the ops reach, each described once and referred to
// by id. A type reached by two ops is one entry both point at, which is
// what makes one type one schema across every projection.
Types []TypeDesc `json:"types"`
}
// ManifestOp is one operation.
type ManifestOp struct {
Method string `json:"method"`
Path string `json:"path"`
// ID is the op's identity — the OpenAPI operationId, the MCP tool name, the
// CLI's spelling and the key the call plane resolves. It is filled in by the
// front end from the SAME rule [ID] states, so a manifest that names an op
// names it the way every other surface does.
ID string `json:"id"`
Summary string `json:"summary,omitempty"`
// Description is the handler's whole doc comment, minus the Example and
// Response lines. The summary falls back to its first sentence.
Description string `json:"description,omitempty"`
Tags []string `json:"tags,omitempty"`
Statuses []int `json:"statuses,omitempty"`
ResponseHeaders []string `json:"responseHeaders,omitempty"`
// Pkg is the package that REGISTERED the op — where the registration is
// written, which is the source its prose was read from, not wherever the
// handler happens to be declared. Origin is the app that declared it when it
// arrived by composition. Together they say WHO owns this op, which is what
// keeps two services' /height apart.
Pkg string `json:"pkg,omitempty"`
Origin string `json:"origin,omitempty"`
// In and Out are TypeDesc ids. Empty means the op takes or answers nothing.
In string `json:"in,omitempty"`
Out string `json:"out,omitempty"`
// Example and Response are the request and answer the doc comment wrote,
// raw so they land in the document exactly as written.
Example json.RawMessage `json:"example,omitempty"`
Response json.RawMessage `json:"response,omitempty"`
// Gated says an Authorizer is in force, so the document publishes the held
// answer beside the finished one.
Gated bool `json:"gated,omitempty"`
// OAuth says the operation answers at an OAuth address, so its refusal is an
// RFC 6749 error rather than an RFC 9457 problem document.
OAuth bool `json:"oauth,omitempty"`
}
// TypeDesc is one type, described by what it is made of and by what it looks
// like on the wire — which are not always the same thing, and that is the
// reason both are here. A quoted decimal is a u64 to a fixed layout and a
// string to a JSON reader; a description that could only say one of those would
// make one of the two projections wrong.
type TypeDesc struct {
// ID is how fields refer to this type within one manifest.
ID string `json:"id"`
// Name is the type's declared name, empty for an anonymous one.
Name string `json:"name,omitempty"`
// Pkg is the module or crate the type was declared in. It is what tells two
// Config types apart when both reach one document.
Pkg string `json:"pkg,omitempty"`
// Kind is what a reader must do with it: "struct" has fields, "list" and
// "map" hold something, "scalar" is a value. It is STRUCTURAL and says
// nothing about the wire form — a type that states its own form says so in
// JSON, and the two are separate because they disagree on purpose: a quoted
// decimal is a string to a reader and eight bytes to a layout.
Kind string `json:"kind"`
// Spell is this type as the source language writes it. See [FieldDesc.Spell].
Spell string `json:"spell,omitempty"`
// JSON is the schema this type states for ITSELF — the answer a type gives
// when its fields are not its wire form. Present for a quoted number, a
// timestamp, a raw message. A projection that has one uses it and does not
// look inside.
JSON json.RawMessage `json:"json,omitempty"`
// Text says this type reads and writes itself as one word, so a URL can
// carry it whatever it is made of.
Text bool `json:"text,omitempty"`
// Repr is the fixed-layout spelling of a scalar: bool, i8..i64, u8..u64,
// f32, f64, text, bytes, bytes_fixed[N].
Repr string `json:"repr,omitempty"`
// Elem is what a named list or map holds. A named slice is still a list of
// something, and a description that forgot what would publish an array of
// untyped objects.
Elem *TypeRef `json:"elem,omitempty"`
// Len is a list's fixed length, 0 for one that has none. A fixed-length
// list of anything but bytes has no place in a layout — an element is one
// value at one width — so the length is what tells the two apart.
Len int `json:"len,omitempty"`
Fields []FieldDesc `json:"fields,omitempty"`
}
// FieldDesc is one field of a struct.
type FieldDesc struct {
// Name is the field as the source declares it; JSON is what the wire calls
// it, "-" for a field the body does not carry.
Name string `json:"name"`
JSON string `json:"json"`
// URL is the name a URL carries it under and Header the request header it
// reads. Both empty means it rides the body alone.
URL string `json:"url,omitempty"`
Header string `json:"header,omitempty"`
Required bool `json:"required,omitempty"`
Doc string `json:"doc,omitempty"`
Type TypeRef `json:"type"`
// Spell is this field's type as the SOURCE LANGUAGE writes it. It is the
// one string in a manifest that is deliberately not neutral, and it is read
// by exactly one reader: the ledger the ZAP projection writes for the person
// who has to go and change that declaration. Sending them to
// "map[string]info.LP" when the file says HashMap<String, Lp> is worse than
// saying nothing.
Spell string `json:"spell,omitempty"`
}
// TypeRef names a field's type: a primitive by its spelling, a described type
// by its id, or a container around either.
type TypeRef struct {
Prim string `json:"prim,omitempty"`
Ref string `json:"ref,omitempty"`
List *TypeRef `json:"list,omitempty"`
// Len is a fixed-length list's length. See [TypeDesc.Len].
Len int `json:"len,omitempty"`
// Map is a map's VALUE type; a key is a string on every wire that has maps.
Map *TypeRef `json:"map,omitempty"`
// Opt says the value may be absent — a pointer, an Option, a nullable.
Opt bool `json:"opt,omitempty"`
}
// Settle fills in what a front end may leave to the projector.
//
// An op's ID is the one thing a front end must NOT decide for itself. It is
// [ID]'s rule, and a rule stated in three languages is a rule that will be
// spelled three ways — so a manifest may arrive without one and this settles it,
// which is how a Rust op and a Go op at the same address come out carrying the
// same token. A front end that has an explicit id (an operation renamed on
// purpose) keeps it.
func (m *Manifest) Settle() {
if m.Manifest == 0 {
m.Manifest = ManifestVersion
}
for i := range m.Ops {
if m.Ops[i].ID == "" {
m.Ops[i].ID = ID(m.Ops[i].Method, m.Ops[i].Path)
}
}
}
// Check refuses a description a projection would silently mis-read.
//
// A field naming a type nobody described is a dangling reference, and it is
// caught HERE rather than in the document, where it would have become a schema
// of `{}` that every generated client reads as "any value".
func (m Manifest) Check() error {
if m.Manifest != ManifestVersion {
return fmt.Errorf("manifest version %d, this zip reads %d", m.Manifest, ManifestVersion)
}
if len(m.Ops) == 0 {
// An empty registry projects an empty document, which is worse than no
// document: it publishes that the service has nothing to offer.
return fmt.Errorf("no ops; there is nothing to project")
}
known := make(map[string]bool, len(m.Types))
for _, td := range m.Types {
if known[td.ID] {
return fmt.Errorf("two types described under %q; a name is how a field refers to a type, so it can only mean one", td.ID)
}
known[td.ID] = true
}
for _, td := range m.Types {
for _, f := range td.Fields {
if err := resolves(known, td.ID+"."+f.Name, f.Type); err != nil {
return err
}
}
if td.Elem != nil {
if err := resolves(known, td.ID+" element", *td.Elem); err != nil {
return err
}
}
}
for _, op := range m.Ops {
for what, id := range map[string]string{"input": op.In, "output": op.Out} {
if id != "" && !known[id] {
return fmt.Errorf("op %s names %s %q, which nothing described", op.ID, what, id)
}
}
}
return nil
}
func resolves(known map[string]bool, at string, r TypeRef) error {
switch {
case r.Ref != "":
if !known[r.Ref] {
return fmt.Errorf("%s is %q, which nothing described", at, r.Ref)
}
case r.List != nil:
return resolves(known, at, *r.List)
case r.Map != nil:
return resolves(known, at, *r.Map)
}
return nil
}
// Manifest describes this app the way any front end would, so the projections
// read the same document whichever language declared the ops.
func (a *App) Manifest() Manifest {
m := Manifest{
Manifest: ManifestVersion,
App: a.cfg.AppName,
Title: a.cfg.OpenAPI.Title,
Description: a.cfg.OpenAPI.Description,
Version: a.cfg.OpenAPI.Version,
}
d := &describer{
byType: map[reflect.Type]string{},
taken: map[string]bool{},
}
oauth := composeOAuth(a.plan())
for _, op := range a.Registry() {
doc, hasDoc := docFor(op.Pkg, op.Method, op.Path)
mo := ManifestOp{
Method: op.Method,
Path: op.Path,
ID: opName(op),
Summary: op.Summary,
Tags: op.Tags,
Statuses: op.Statuses,
ResponseHeaders: op.ResponseHeaders,
Pkg: op.Pkg,
Origin: op.Origin,
In: d.describe(op.InType, op.Origin),
Out: d.describe(op.OutType, op.Origin),
Gated: op.rule != nil && op.rule() != nil,
OAuth: oauth[op.Method+" "+op.Path],
}
if hasDoc {
mo.Description = doc.Description
mo.Example = doc.Example
mo.Response = doc.Response
d.prose(doc.Fields)
}
m.Ops = append(m.Ops, mo)
}
m.Types = d.types()
return m
}
// describer walks the types an op reaches and files each one once.
type describer struct {
byType map[reflect.Type]string
taken map[string]bool
out []*TypeDesc
anon int
}
// describe files t and returns its id, or "" for a type that carries nothing —
// which is what an op with no input or no answer has.
func (d *describer) describe(t reflect.Type, origin string) string {
if t == nil {
return ""
}
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() == reflect.Struct && t.NumField() == 0 && t.Name() == "" {
return "" // struct{}: the op takes nothing.
}
if id, ok := d.byType[t]; ok {
return id
}
id := d.id(t, origin)
d.byType[t] = id
td := &TypeDesc{ID: id, Name: t.Name(), Pkg: t.PkgPath(), Spell: goName(t), Text: readsText(t)}
d.out = append(d.out, td) // claimed before the fields are walked: the cycle guard.
if isMarshaler(t) {
switch {
case declaredSchema(t) != nil:
td.JSON = rawJSON(declaredSchema(t))
case t == timeType:
td.JSON = rawJSON(map[string]any{"type": "string", "format": "date-time"})
default:
td.JSON = json.RawMessage("{}")
}
}
switch t.Kind() {
case reflect.Struct:
td.Kind = "struct"
d.fields(td, t, origin)
case reflect.Slice, reflect.Array:
td.Repr = reprOf(t)
if t.Elem().Kind() == reflect.Uint8 {
td.Kind = "scalar"
d.state(td, t)
break
}
td.Kind = "list"
if t.Kind() == reflect.Array {
td.Len = t.Len()
}
e := d.ref(t.Elem(), origin)
td.Elem = &e
case reflect.Map:
td.Kind, td.Repr = "map", reprOf(t)
e := d.ref(t.Elem(), origin)
td.Elem = &e
default:
td.Kind, td.Repr = "scalar", reprOf(t)
d.state(td, t)
}
return id
}
// state gives a value type the shape it shows on the wire, since it has no
// fields to be described by. A type that already stated its own keeps it.
func (d *describer) state(td *TypeDesc, t reflect.Type) {
if len(td.JSON) == 0 {
td.JSON = rawJSON(schemaOf(t, nil, nil))
}
}
// fields describes the fields the wire carries: the struct's own and the
// promoted ones of anything it embeds, in the order a decoder resolves them.
func (d *describer) fields(td *TypeDesc, t reflect.Type, origin string) {
for _, f := range wireFields(t) {
fd := FieldDesc{
Name: f.Name,
JSON: jsonFieldName(f),
URL: urlFieldName(f),
Header: headerFieldName(f),
Required: strings.Contains(f.Tag.Get("validate"), "required"),
Type: d.ref(f.Type, origin),
Spell: goName(f.Type),
}
if fd.URL == fd.JSON {
fd.URL = "" // says nothing: the wire name is the URL name.
}
td.Fields = append(td.Fields, fd)
}
}
// ref names a field's type without describing it twice: a primitive by its
// spelling, anything with a shape by the id it is filed under.
func (d *describer) ref(t reflect.Type, origin string) TypeRef {
r := TypeRef{}
if t.Kind() == reflect.Pointer {
r.Opt = true
t = t.Elem()
}
// A named type with a wire form of its own is described, not spelled: a
// quoted decimal is not "u64" to a JSON reader and not "string" to a layout.
if t.Name() != "" && (isMarshaler(t) || readsText(t) || t.Kind() == reflect.Struct) {
r.Ref = d.describe(t, origin)
return r
}
switch t.Kind() {
case reflect.Struct:
r.Ref = d.describe(t, origin)
case reflect.Slice, reflect.Array:
if t.Elem().Kind() == reflect.Uint8 {
r.Prim = "bytes"
if t.Kind() == reflect.Array {
r.Prim = "bytes_fixed[" + strconv.Itoa(t.Len()) + "]"
}
return r
}
e := d.ref(t.Elem(), origin)
r.List = &e
if t.Kind() == reflect.Array {
r.Len = t.Len()
}
case reflect.Map:
e := d.ref(t.Elem(), origin)
r.Map = &e
default:
r.Prim = primOf(t.Kind())
}
return r
}
// prose files the doc comments the extraction found onto the fields they
// describe. The key is "<Type>.<wire name>", which is what cmd/zipdoc wrote and
// what a Rust or C++ front end fills in directly from the comment above the
// field — so one key reaches a field's prose whichever language declared it.
func (d *describer) prose(fields map[string]string) {
for _, td := range d.out {
for i := range td.Fields {
if td.Fields[i].Doc != "" {
continue
}
if s := fields[td.Name+"."+td.Fields[i].JSON]; s != "" {
td.Fields[i].Doc = s
}
}
}
}
// types is the described set, in id order so one app yields one document
// whatever order its ops were registered in.
func (d *describer) types() []TypeDesc {
out := make([]TypeDesc, 0, len(d.out))
for _, td := range d.out {
out = append(out, *td)
}
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}
// id is the key a type is filed under: its name, qualified by the app that
// declared it when it came in by composition, then by its package when a
// different type already holds that name. It is [schemaRegistry.nameFor]'s rule,
// stated once more where there is no reflect.Type to key a map by.
func (d *describer) id(t reflect.Type, origin string) string {
qual := ""
if origin != "" {
qual = origin + "."
}
if t.Name() == "" {
d.anon++
return fmt.Sprintf("#%d", d.anon)
}
base := qual + t.Name()
if !d.taken[base] {
d.taken[base] = true
return base
}
if p := t.PkgPath(); p != "" {
base = qual + p[strings.LastIndexByte(p, '/')+1:] + "." + t.Name()
}
for name, n := base, 2; ; n++ {
if !d.taken[name] {
d.taken[name] = true
return name
}
name = base + strconv.Itoa(n)
}
}
// primOf is a scalar's neutral spelling — the one a fixed layout and a schema
// both read, since the width is what a generated accessor needs and "integer"
// alone does not carry it.
func primOf(k reflect.Kind) string {
switch k {
case reflect.Bool:
return "bool"
case reflect.String:
return "string"
case reflect.Int8:
return "i8"
case reflect.Int16:
return "i16"
case reflect.Int32:
return "i32"
case reflect.Int, reflect.Int64:
return "i64"
case reflect.Uint8:
return "u8"
case reflect.Uint16:
return "u16"
case reflect.Uint32:
return "u32"
case reflect.Uint, reflect.Uint64:
return "u64"
case reflect.Float32:
return "f32"
case reflect.Float64:
return "f64"
}
return "any"
}
// reprOf is a named scalar's fixed-layout spelling.
func reprOf(t reflect.Type) string {
switch t.Kind() {
case reflect.Slice:
if t.Elem().Kind() == reflect.Uint8 {
return "bytes"
}
return "list"
case reflect.Array:
if t.Elem().Kind() == reflect.Uint8 {
return "bytes_fixed[" + strconv.Itoa(t.Len()) + "]"
}
return "list"
case reflect.Map:
return "bytes"
case reflect.String:
return "text"
}
return primOf(t.Kind())
}
func rawJSON(v any) json.RawMessage {
b, err := json.Marshal(v)
if err != nil {
return json.RawMessage("{}")
}
return b
}