Files
zip/summary_test.go
zeekayandHanzo Dev cc2d647ed0 openapi: four things the document said that were not true
Each of these shipped into openapi.yaml and therefore into every generated SDK,
and none of them could fail a build, because a wrong description is not an error.

A PHANTOM REQUEST BODY. hasBody asked only the method. So POST /things/:id/verify,
whose whole input binds from the path, published a requestBody marked required
whose one property WAS the path param — every generated client gained an argument
the caller must construct to repeat a value it already passes in the URL. Ten
instances were live in the fleet's published subsets. hasRequestBody refines
hasBody with the clause that was missing: the input must have at least one field
the URL does not already carry. The wire is untouched — bindURL binds the path
last, so a body repeating a path param never won anyway.

A TYPE DESCRIBED BY ITS PARTS INSTEAD OF ITS WIRE FORM. schemaOf reached the
custom-marshaler rule only inside the struct case, but the rule is about the
MARSHALER. json.RawMessage is a []byte whose MarshalJSON emits raw JSON, so it
fell to the slice rule and published as an array of integers — for relay ops whose
answer is frequently an array or null, the document asserted a shape it can never
have. time.Time fared worse: all its fields are unexported, so it published a
$ref to a schema with no properties, leaving every timestamp in every SDK
undescribed. The marshaler check now runs first, with time.Time named as the one
marshaler whose output shape is documented (RFC 3339, format: date-time). A plain
[]byte still reflects as a base64 string, because it has no marshaler.

A SUMMARY WITH A LINE BREAK IN IT. A doc comment wraps in Go source; a sentence
ends where the next begins, whether that is a space or a newline. The summary is
neither fact — it is one sentence on one line, in the spec, in CLI help, and in
the first docstring line of every generated SDK. sentenceEnd now accepts a break
as a terminator and the result is re-joined on single spaces.

A ROUTE THAT COULD NOT BE DECLARED AT ALL. bindURL matched on the json name, so a
path parameter and a body field that are the same word for two different things
(a worker's name in the path, its source in the body) collided: the body field was
silently overwritten by the path value, making the route structurally untypable.
Fields now name themselves for the URL with `url:` and opt out with `url:"-"` —
one tag answering one question, separate from what `json:` answers. The CLI reads
the same tag, so the opt-out gains a flag rather than losing one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-30 06:28:46 -07:00

72 lines
2.5 KiB
Go

package zip_test
import (
"context"
"testing"
"github.com/zap-proto/zip"
)
type wrapIn struct {
ID string `json:"id"`
}
type wrapOut struct {
OK bool `json:"ok"`
}
func wrapHandler(_ context.Context, _ *wrapIn) (*wrapOut, error) { return &wrapOut{OK: true}, nil }
// A doc comment WRAPS in Go source, and a sentence ends where the next one
// begins whether that is a space or a newline. The summary is neither of those
// facts: it is one sentence on one line, in the OpenAPI document, in the CLI and
// in the first docstring line of every generated SDK.
//
// Both shapes below shipped a raw line break into all three.
func TestSummary_IsOneSentenceOnOneLine(t *testing.T) {
for _, tc := range []struct {
name string
doc string
want string
}{{
name: "wrapped",
// The first sentence runs past the column a doc comment wraps at, which
// is the ordinary case, not an unusual one.
doc: "ListWorkers returns every worker script in the\naccount, newest first. Results are capped by limit.",
want: "ListWorkers returns every worker script in the account, newest first.",
}, {
name: "sentence ends at the line break",
// ". " never appears before the paragraph break, so the old rule kept
// scanning and swallowed the second paragraph — line break and all.
doc: "Ping answers while the service is up.\n\nUse it as a health check. It touches no storage.",
want: "Ping answers while the service is up.",
}, {
name: "no sentence at all",
// No period anywhere: the summary is the first line, as it has always
// been — just without the ragged edge.
doc: "Ping answers while the service is up\nand carries no body",
want: "Ping answers while the service is up",
}} {
t.Run(tc.name, func(t *testing.T) {
app := zip.New(zip.Config{AppName: "wrap", DisableStartupMessage: true})
zip.Describe("GET /v1/wrap/"+tc.name, zip.Doc{Description: tc.doc})
zip.Get(app, "/v1/wrap/"+tc.name, wrapHandler)
paths, _ := app.OpenAPISpec()["paths"].(map[string]map[string]any)
op, _ := paths["/v1/wrap/"+tc.name]["get"].(map[string]any)
if got, _ := op["summary"].(string); got != tc.want {
t.Errorf("OpenAPI summary = %q, want %q", got, tc.want)
}
// The CLI reads the same rule from the same function. A summary is
// one line there for a harder reason: it is printed in a list.
cmds := app.Commands()
if len(cmds) != 1 {
t.Fatalf("commands = %d, want 1", len(cmds))
}
if cmds[0].Summary != tc.want {
t.Errorf("CLI summary = %q, want %q", cmds[0].Summary, tc.want)
}
})
}
}