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]>
86 lines
3.1 KiB
Go
86 lines
3.1 KiB
Go
// The host. It composes a linked-in service and a plugin with the same verb,
|
|
// and never imports the plugin's code.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
// Health is what the probe answers.
|
|
type Health struct {
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// Nothing is an operation that takes no input.
|
|
type Nothing struct{}
|
|
|
|
// health is a linked-in unit: a constructor that RETURNS a definition. A unit
|
|
// that takes dependencies returns the same thing — func(deps) *zip.App — so a
|
|
// composition root only ever sees *App, whether the unit is linked in or lives
|
|
// in its own binary.
|
|
//
|
|
// It registers a typed op, exactly as the separately-built plugin does. Both
|
|
// halves of the composition contribute to one document, one tool list and one
|
|
// command tree, which is what makes "linked in" versus "its own binary" a
|
|
// deployment decision and nothing more.
|
|
func health() *zip.App {
|
|
a := zip.New(zip.Config{AppName: "health", DisableStartupMessage: true})
|
|
a.Get("/health", func(context.Context, *Nothing) (*Health, error) {
|
|
return &Health{Status: "ok"}, nil
|
|
})
|
|
return a
|
|
}
|
|
|
|
func main() {
|
|
app := zip.New(zip.Config{AppName: "host"})
|
|
|
|
// Both arguments are Services. The first is linked into this binary; the
|
|
// second is a separate binary started on its own unix socket and reached
|
|
// over ZAP. Moving a service between those two lines is a deployment
|
|
// decision, not a code change.
|
|
//
|
|
// Path is used here so the example runs from a fresh checkout. In a real
|
|
// deployment you embed the binary instead and still ship one artifact:
|
|
//
|
|
// //go:embed bin/billing
|
|
// var billingBin []byte
|
|
// zip.Load(zip.Plugin{Name: "billing", Bin: billingBin}, "/v1/billing")
|
|
//
|
|
// Or install the one CI published, which is what decouples the two build
|
|
// cycles entirely — the plugin is built ONCE, per OS/arch, and every host
|
|
// picks up the same bits. Sum is required: the digest is verified before the
|
|
// file is ever made executable, and it doubles as the cache key, so a
|
|
// restart is offline and a rollback is free. See ../README.md.
|
|
//
|
|
// zip.Load(zip.Plugin{
|
|
// Name: "billing",
|
|
// URL: "https://github.com/hanzoai/billing/releases/download/v1.2.3/billing-linux-arm64",
|
|
// Sum: "9f2c…",
|
|
// Dir: "/var/lib/hanzo", // not /tmp — that is RAM on most hosts
|
|
// }, "/v1/billing")
|
|
//
|
|
// Or point at one that is already running elsewhere, unchanged otherwise:
|
|
//
|
|
// zip.Load(zip.Plugin{Name: "billing", Addr: os.Getenv("BILLING_ADDR")}, "/v1/billing")
|
|
billing, err := zip.Load(zip.Plugin{Name: "billing", Path: "./bin/billing"}, "/v1/billing")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
// ONE verb. Middleware, a linked-in unit and a plugin in its own binary all
|
|
// arrive the same way, because they are the same kind of thing: something
|
|
// that answers addresses.
|
|
app.Use(health(), billing)
|
|
|
|
// A new build of the plugin can replace the running one without dropping a
|
|
// request — the replacement must be listening before any traffic moves to
|
|
// it, so a bad build cannot take the route down:
|
|
//
|
|
// app.Reload("billing", newBin)
|
|
//
|
|
// The child is killed and cleaned up on Shutdown.
|
|
log.Fatal(app.Listen(":9653", "http://:8080"))
|
|
}
|