mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
App, Group, Operation. A group is a prefix and what holds under it, an operation
is one typed route declared there, and the verb methods are generic methods —
which Go 1.27 allows on a concrete type and still refuses on an interface. So In
and Out come from the handler and the caller writes no type arguments:
users := app.Group("/v1/users")
users.Post("/", api.Create)
mint := users.Group("/mint").With(middleware.Mint(gate))
mint.Post("/deposit", api.Deposit)
A Group is a CLOSED HANDLE: two unexported fields, and no method that hands back
the app, the parent or the fiber underneath. A subsystem given the group at its
own prefix cannot reach above it, so confinement is a property of the type
instead of a rule something has to police — which is what a host needs in order
to hand out a bounded surface and know what comes back.
That is bought by giving up the router interface, deliberately. Router and
OpTarget are gone, and with them the ability to implement a route table outside
this package: a decorator is no longer a router, a gate is middleware on a group,
and a bound is a path. The five package-level verb functions go the same way —
a registration is a method on the thing it registers into.
Raw carries what is not a typed operation: a stream, an upgrade, a probe, a
foreign subtree. It names its method, and it is the longer spelling because
opting out of the typed model is the exception and should read like 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})
|
|
app.Get("/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)
|
|
}
|
|
})
|
|
}
|
|
}
|