Files
zooqueenandHanzo Dev 6cc782199b an app reports because it serves: telemetry is the framework's
Every program built on zip now emits one log line per request through its own
luxfi/log logger — method, path, status, duration, trace and span, plus the
caller when the environment parked one — and RED metrics through luxfi/metric,
with W3C trace context joined when a request carries it, minted when it does
not, and propagated on Call with no handler code. /metrics renders the one
registry, and export speaks the shipped ZAP exporter, which is what o11y
ingests. Config.Telemetry{Addr, Off} is the only switch; the default is on.

The boundary is installed outermost by materialise, so requests that match
nothing — 404s, preflights — are still counted; per-route composition cannot
see them. Two defects the tests forced out: the status is derived from the
handler's error, because at the boundary the response still reads 200 (fiber
writes the real status after unwinding, so every refusal would have recorded
as success); and the route label reads the not-found sentinel, because an
unmatched request leaves the middleware's own registration in place and would
otherwise be indistinguishable from a real root leaf.

middleware.Logger and middleware.Telemetry are deleted, not deprecated:
telemetry an app can lack is the defect this closes.

luxfi/metric moves to v1.9.1 — v1.9.0 is the published build whose registry
constructors sit behind a build tag nothing sets, so counters accepted Inc()
into a registry nothing gathers and /metrics served empty.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 14:36:48 -07:00

87 lines
3.0 KiB
Go

// migrate-from-gin example — mechanical port of a gin-style API to zip.
//
// gin before:
//
// r := gin.Default()
// r.GET("/users/:id", func(c *gin.Context) {
// c.JSON(200, gin.H{"id": c.Param("id")})
// })
//
// zip after — Sinatra/Express idiom is preserved; gin.Context becomes
// *zip.Ctx; gin.H becomes any map-shaped value.
//
// # The port is two steps, and stopping after the first loses the point
//
// STEP 1 is mechanical and shown below: the handler shape is the same, so a
// route moves in one edit. It is a WAY-STATION. An untyped route registers no
// operation, so after step 1 the endpoint is in no OpenAPI document, is no MCP
// tool, has no command, and no service can reach it with zip.Call — which is
// exactly the surface gin gave you, and the surface you are migrating away from.
//
// STEP 2 declares the In/Out types the handler was already parsing by hand and
// registers the route as a typed op. That is where the projections turn on. Port
// mechanically to get it running, then type it; do not ship a service that
// stopped halfway.
//
// A gin codebase built on router groups keeps them: zip.Get takes the App or
// any Group of it, so a group's prefix is part of the op exactly as it is part
// of an untyped route. The port does not have to flatten the router to type it.
package main
import (
"context"
"log"
"github.com/zap-proto/zip"
"github.com/zap-proto/zip/middleware"
)
// GetUserIn addresses one user; `id` is the path segment.
type GetUserIn struct {
ID string `json:"id"`
}
// User is one user record.
type User struct {
ID string `json:"id"`
}
// CreateUserIn is the create body — the same field the untyped port bound by
// hand with c.Bind, declared once instead.
type CreateUserIn struct {
Name string `json:"name" validate:"required"`
}
func main() {
app := zip.New(zip.Config{AppName: "migrate-from-gin"})
app.Use(middleware.Recover(), middleware.RequestID())
// STEP 1 — the mechanical port. The same shape as the gin handler, running
// on zip after one edit, and invisible to every projection until step 2.
app.Get("/legacy/users/:id", func(c *zip.Ctx) error {
return c.JSON(200, map[string]string{"id": c.Param("id")})
})
// STEP 2 — the same routes as typed ops, declared on the group the gin
// router already had. The hand-written c.Param and c.Bind are gone: the In
// type says what the request IS, and one declaration feeds the route, the
// document, the tool list, the CLI and the call plane.
v1 := app.Group("/v1")
zip.Get(v1, "/users/:id", getUser)
zip.Post(v1, "/users", createUser)
log.Fatal(app.Listen("http://:8080"))
}
// GetUser returns one user by id.
func getUser(_ context.Context, in *GetUserIn) (*User, error) {
return &User{ID: in.ID}, nil
}
// CreateUser creates a user. A missing name is refused before the handler runs,
// by the same `validate:"required"` tag that makes the field required in the
// published schema.
func createUser(_ context.Context, in *CreateUserIn) (*CreateUserIn, error) {
return in, nil
}