mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
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]>
72 lines
2.5 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|