A raw route's prose is reachable by its address

cmd/zipdoc files every entry under the package that declared it, raw routes
included. A raw route records no registrar — a method, a path and a handler
chain is all App.raw keeps — so an address is all anyone can ask by, and the
address lookup could not reach a qualified key. A service's whole untyped
half therefore documented as silent the moment its generated file was
rewritten: 57 of iam's routes at once.

So the keys are indexed by address as they are recorded, and only the
unqualified lookup reads that index. A caller that names a package asked
about that package, and answering it with another's sentence would document
one service with another's words.

An address two packages describe differently is dropped from the index
rather than resolved. One address answers once in one process, so two
claims on it are a conflict, and choosing either would be a guess; the
qualified lookup still reaches both.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-17 21:16:52 -07:00
co-authored by Hanzo Dev
parent 16323edc61
commit 0156f40d53
3 changed files with 150 additions and 4 deletions
+48 -4
View File
@@ -1,6 +1,9 @@
package zip
import "encoding/json"
import (
"encoding/json"
"strings"
)
// Documentation comes from the source, because the source is where it is already
// written.
@@ -60,6 +63,23 @@ type Doc struct {
// and it is what keeps two declarations of one name apart.
var docs = map[string]Doc{}
// byAddr indexes those keys by the address alone, for the reader that has a
// method and a path and no package to qualify them with. A raw route is that
// reader's whole subject: it records no registrar — [App.raw] files a method, a
// path and a handler chain — so an address is all anyone can ask by, while
// cmd/zipdoc files its comment under the package that declared it like every
// other entry. Without this index a service's untyped half documents as silent
// the moment its generated file is rewritten.
//
// An address two packages describe DIFFERENTLY is dropped from the index rather
// than resolved: in one process one address answers once, so two claims on it
// are a conflict, and answering with either would be a guess. The qualified
// lookup still reaches both.
var byAddr = map[string]string{}
// ambiguous marks an address more than one package describes.
const ambiguous = "\x00"
// DocKey is the key a package's operation is filed under. One function, so the
// generator that writes a key and the registry that reads one cannot spell it
// differently.
@@ -69,7 +89,19 @@ func DocKey(pkg, method, path string) string { return pkg + " " + method + " " +
// built with [DocKey]. Generated code calls it from an init(); hand-written
// calls are possible but defeat the point, since the comment is then no longer
// the single source.
func Describe(key string, d Doc) { docs[key] = d }
func Describe(key string, d Doc) {
docs[key] = d
f := strings.Fields(key)
if len(f) != 3 {
return // already unqualified: docs holds it under the address itself
}
addr := f[1] + " " + f[2]
if prev, seen := byAddr[addr]; seen && prev != key {
byAddr[addr] = ambiguous
return
}
byAddr[addr] = key
}
// docFor returns the extraction for an operation, if cmd/zipdoc ran.
//
@@ -80,8 +112,20 @@ func docFor(pkg, method, path string) (Doc, bool) {
if d, ok := docs[DocKey(pkg, method, path)]; ok {
return d, true
}
d, ok := docs[method+" "+path]
return d, ok
addr := method + " " + path
if d, ok := docs[addr]; ok {
return d, true
}
if pkg != "" {
// A caller that named a package asked about that package. Answering with
// another one's prose would document one service with another's sentence.
return Doc{}, false
}
key := byAddr[addr]
if key == "" || key == ambiguous {
return Doc{}, false
}
return docs[key], true
}
// Prose is what an operation says about itself: the sentence for a one-line
+54
View File
@@ -0,0 +1,54 @@
package zipdoc
import (
"regexp"
"strings"
"testing"
"github.com/zap-proto/zip"
)
var described = regexp.MustCompile(`zip\.Describe\("([^"]+)"`)
// The key this generator writes for a RAW route has to be the key the registry
// finds. A raw route records no registrar — a method, a path and a handler
// chain — so an address is all anyone can ask by, while the generated file
// files it under the declaring package like every other entry. When the two
// spellings drifted, a service's whole untyped half documented as silent.
//
// twoaddr is two Raw routes with a doc comment, which is the shape that broke.
func TestRender_ARawRoutesKeyIsFoundByItsAddress(t *testing.T) {
p := load(t, "twoaddr")
src, err := p.Render()
if err != nil {
t.Fatalf("Render: %v", err)
}
keys := described.FindAllStringSubmatch(string(src), -1)
if len(keys) != 2 {
t.Fatalf("rendered %d Describe calls, want 2:\n%s", len(keys), src)
}
for _, m := range keys {
key := m[1]
f := strings.Fields(key)
if len(f) != 3 {
t.Fatalf("key %q is not <pkg> <method> <path>", key)
}
pkg, method, path := f[0], f[1], f[2]
if pkg != p.Path {
t.Errorf("key %q names package %q, want %q", key, pkg, p.Path)
}
zip.Describe(key, zip.Doc{Description: "The generated sentence for " + method + " " + path + "."})
summary, _, ok := zip.Prose(method, path)
if !ok {
t.Errorf("%s: the generator wrote %q and the registry cannot find it by address", key, key)
continue
}
if !strings.Contains(summary, path) {
t.Errorf("%s: found some other entry: %q", key, summary)
}
}
}
+48
View File
@@ -0,0 +1,48 @@
package zip
import "testing"
// A raw route records no registrar — it has a method and a path and nothing
// else — so the only way to ask for its prose is by address. cmd/zipdoc files
// every entry under the package that declared it, raw routes included, so the
// address lookup has to reach a qualified key or a whole service's untyped
// half documents as silent.
func TestProse_ReachesAQualifiedKey(t *testing.T) {
Describe(DocKey("github.com/hanzoai/iam/internal/oidc", "GET", "/v1/iam/whoami"),
Doc{Description: "Whoami says which subject the presented credential names."})
summary, description, ok := Prose("GET", "/v1/iam/whoami")
if !ok {
t.Fatal("a raw route's prose is unreachable by address, so every untyped route documents as undocumented")
}
if summary != "Whoami says which subject the presented credential names." {
t.Fatalf("summary = %q", summary)
}
if description == "" {
t.Fatal("no description")
}
}
// Two packages describing one address is a conflict, not a choice: in one
// process one address answers once. Answering with either one would be a guess,
// so the address lookup declines and the qualified one still works.
func TestProse_DeclinesAnAddressTwoPackagesClaim(t *testing.T) {
Describe(DocKey("a/one", "GET", "/v1/contested"), Doc{Description: "One says this."})
Describe(DocKey("b/two", "GET", "/v1/contested"), Doc{Description: "Two says that."})
if _, _, ok := Prose("GET", "/v1/contested"); ok {
t.Fatal("answered an address two packages describe differently")
}
if d, ok := docFor("a/one", "GET", "/v1/contested"); !ok || d.Description != "One says this." {
t.Fatalf("the qualified lookup broke: %+v ok=%v", d, ok)
}
}
// An unqualified key still reads, which is what a generated file written
// before keys carried a package produces.
func TestProse_StillReadsAnUnqualifiedKey(t *testing.T) {
Describe("GET /v1/legacy", Doc{Description: "Legacy answers the old way."})
if _, _, ok := Prose("GET", "/v1/legacy"); !ok {
t.Fatal("an unqualified key stopped reading")
}
}