mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
The documentation map is one map for a whole process, keyed by an address — and an address is unique only within the app that serves it. A node runs twelve chains on one binary and the P-Chain and the X-Chain both answer GET /height, GET /block, GET /tx, GET /utxos, GET /balance and POST /tx: six addresses where whichever init() ran last published its prose for the other chain routes. Nothing failed. The document and the MCP tool carried the wrong sentence for the right route, which is why it survived every test until someone read the output — and renaming a path to dodge it would have been the wrong repair twice over, since the mount already tells the chains apart and every future chain would inherit the dodge. So the key carries the declaring package, read off the handler itself at registration. It is the namespacing the schema registry already does for named types: who declared a thing is a property of the code. An unqualified key is still read, so a generated file written before this is still correct for the single-app process it was written for.
166 lines
4.4 KiB
Go
166 lines
4.4 KiB
Go
package zipdoc
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/format"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Header marks the emitted file. It is the standard form `go generate` and every
|
|
// Go tool recognises, and the guard that stops this command from ever writing
|
|
// over something a person wrote.
|
|
const Header = "// Code generated by zipdoc; DO NOT EDIT."
|
|
|
|
// DefaultFile is the name written into each package's own directory, so the
|
|
// prose sits next to the source it came from and moves with it.
|
|
const DefaultFile = "zipdoc_gen.go"
|
|
|
|
// Render is the file for a package's operations, gofmt'd. Nil when the package
|
|
// registers nothing typed — there is no such thing as an empty documentation
|
|
// file, only one that should not exist.
|
|
func (p Package) Render() ([]byte, error) {
|
|
if len(p.Ops) == 0 {
|
|
return nil, nil
|
|
}
|
|
ops := append([]Op(nil), p.Ops...)
|
|
sort.Slice(ops, func(i, j int) bool { return ops[i].Key() < ops[j].Key() })
|
|
|
|
// zip documenting itself needs no import and no qualifier.
|
|
q := "zip."
|
|
if p.Path == ZipPkg {
|
|
q = ""
|
|
}
|
|
raw := false
|
|
for _, o := range ops {
|
|
raw = raw || o.Example != "" || o.Response != ""
|
|
}
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "%s\n\npackage %s\n\n", Header, p.Name)
|
|
if raw || q != "" {
|
|
b.WriteString("import (\n")
|
|
if raw {
|
|
b.WriteString("\t\"encoding/json\"\n")
|
|
}
|
|
if q != "" {
|
|
if raw {
|
|
b.WriteString("\n")
|
|
}
|
|
fmt.Fprintf(&b, "\t%q\n", ZipPkg)
|
|
}
|
|
b.WriteString(")\n\n")
|
|
}
|
|
b.WriteString("func init() {\n")
|
|
for _, o := range ops {
|
|
fmt.Fprintf(&b, "\t%sDescribe(%q, %sDoc{\n", q, p.Path+" "+o.Key(), q)
|
|
if o.Description != "" {
|
|
fmt.Fprintf(&b, "\t\tDescription: %s,\n", strconv.Quote(o.Description))
|
|
}
|
|
if len(o.Fields) > 0 {
|
|
b.WriteString("\t\tFields: map[string]string{\n")
|
|
keys := make([]string, 0, len(o.Fields))
|
|
for k := range o.Fields {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
fmt.Fprintf(&b, "\t\t\t%q: %s,\n", k, strconv.Quote(o.Fields[k]))
|
|
}
|
|
b.WriteString("\t\t},\n")
|
|
}
|
|
if o.Example != "" {
|
|
fmt.Fprintf(&b, "\t\tExample: json.RawMessage(%s),\n", goString(o.Example))
|
|
}
|
|
if o.Response != "" {
|
|
fmt.Fprintf(&b, "\t\tResponse: json.RawMessage(%s),\n", goString(o.Response))
|
|
}
|
|
b.WriteString("\t})\n")
|
|
}
|
|
b.WriteString("}\n")
|
|
|
|
src, err := format.Source([]byte(b.String()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generated source for %s does not parse: %w", p.Path, err)
|
|
}
|
|
return src, nil
|
|
}
|
|
|
|
// goString prefers a raw literal, so JSON reads as JSON, and falls back to a
|
|
// quoted one when the content would break out of it.
|
|
func goString(s string) string {
|
|
if !strings.ContainsAny(s, "`\r") {
|
|
return "`" + s + "`"
|
|
}
|
|
return strconv.Quote(s)
|
|
}
|
|
|
|
// Write puts the rendered file in the package directory, and removes a stale one
|
|
// when the package no longer registers anything. Reports whether it changed
|
|
// anything.
|
|
func (p Package) Write(name string) (bool, error) {
|
|
path := filepath.Join(p.Dir, name)
|
|
want, err := p.Render()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
got, generated, err := read(path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if bytes.Equal(got, want) {
|
|
return false, nil
|
|
}
|
|
if got != nil && !generated {
|
|
return false, fmt.Errorf("%s exists and was not generated by zipdoc; refusing to overwrite it", path)
|
|
}
|
|
if want == nil {
|
|
return true, os.Remove(path)
|
|
}
|
|
return true, os.WriteFile(path, want, 0o644)
|
|
}
|
|
|
|
// Stale reports why the file on disk does not match the source, or "" when it
|
|
// does. This is what CI runs: the spec is derived, so a stale file is a
|
|
// description that has drifted from the code it describes.
|
|
func (p Package) Stale(name string) (string, error) {
|
|
path := filepath.Join(p.Dir, name)
|
|
want, err := p.Render()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
got, generated, err := read(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
switch {
|
|
case bytes.Equal(got, want):
|
|
return "", nil
|
|
case got == nil:
|
|
return path + ": missing", nil
|
|
case !generated:
|
|
return path + ": not generated by zipdoc", nil
|
|
case want == nil:
|
|
return path + ": stale (the package no longer registers typed handlers)", nil
|
|
default:
|
|
return path + ": stale", nil
|
|
}
|
|
}
|
|
|
|
// read returns the file's bytes (nil if absent) and whether it carries the
|
|
// generated header.
|
|
func read(path string) ([]byte, bool, error) {
|
|
b, err := os.ReadFile(path)
|
|
if os.IsNotExist(err) {
|
|
return nil, false, nil
|
|
}
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
return b, bytes.HasPrefix(b, []byte(Header)), nil
|
|
}
|