mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
The address an op answers at is a property of the CALL, not of the declaration: a group's prefix composes at build, and a definition composed under two hosts has two addresses and one handler. So a handler could not learn its own address, and a service that needed one composed it in a wrapper and threaded it in — which made every one of those registrations a call this pass cannot see, and took a whole service's operations out of the document, the tool list and the SDKs. OpOf reads it from the context instead, so the declaration stays the bound method the projections resolve. The seam that knows the occurrence states it: the REST route that matched, and the by-name seams, which looked the op up in the composed registry. Under that, the one contract every projection funnels through states the declaration, so a handler always has an operation to read and a direct call says there is none rather than guessing. Costs two allocations on a typed REST request, measured: 61 to 63 on Benchmark_TypedRoute, with bytes unchanged. Co-authored-by: Hanzo Dev <[email protected]>
82 lines
2.3 KiB
Go
82 lines
2.3 KiB
Go
// Package nested registers through a group: the verb methods are generic
|
|
// methods on a concrete type, so the declarations carry no type arguments and
|
|
// the handler is the bound method. zipdoc has to read that shape, because it is
|
|
// the shape a service is written in.
|
|
package nested
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
//go:generate zipdoc
|
|
|
|
// OpenIn asks for a new account.
|
|
type OpenIn struct {
|
|
// Name the account answers to.
|
|
Name string `json:"name" validate:"required"`
|
|
}
|
|
|
|
// Account is one account.
|
|
type Account struct {
|
|
// ID addresses the account.
|
|
ID string `json:"id"`
|
|
// Name it answers to.
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Ledger is what an account holds.
|
|
type Ledger struct {
|
|
// Cents held.
|
|
Cents int64 `json:"cents"`
|
|
}
|
|
|
|
// API serves accounts.
|
|
type API struct{}
|
|
|
|
// Open opens an account for the caller's org.
|
|
func (a *API) Open(ctx context.Context, in *OpenIn) (*Account, error) {
|
|
return &Account{ID: "acct_1", Name: in.Name}, nil
|
|
}
|
|
|
|
// Read returns one account.
|
|
func (a *API) Read(ctx context.Context, in *struct{}) (*Account, error) {
|
|
return &Account{ID: "acct_1"}, nil
|
|
}
|
|
|
|
// Balance returns what the account holds.
|
|
func (a *API) Balance(ctx context.Context, in *struct{}) (*Ledger, error) {
|
|
return &Ledger{}, nil
|
|
}
|
|
|
|
// Watch streams the account's activity as it happens.
|
|
//
|
|
// It reads the address it was asked at rather than being handed one by a
|
|
// wrapper, which is what keeps the declaration below a bound method this pass
|
|
// can see.
|
|
func (a *API) Watch(ctx context.Context, in *struct{}) (*Ledger, error) {
|
|
if op, ok := zip.OpOf(ctx); ok {
|
|
_ = op.Path
|
|
}
|
|
return &Ledger{}, nil
|
|
}
|
|
|
|
// Register declares the accounts surface.
|
|
func Register(app *zip.App, api *API) {
|
|
accounts := app.Group("/v1/accounts").Tag("accounts")
|
|
|
|
// A metadata chain follows the declaration; the registration is the inner
|
|
// call, and zipdoc has to find it there.
|
|
accounts.Post("/open", api.Open).ID("accounts.open").Summary("Open an account")
|
|
accounts.Get("/:id", api.Read)
|
|
|
|
// A nested group composes its prefix.
|
|
accounts.Group("/:id").Get("/ledger", api.Balance)
|
|
|
|
// A handler that reads its own address is still a bound method here, so it
|
|
// is documented like any other. Composing the address in a helper instead is
|
|
// what made a whole service's operations invisible.
|
|
accounts.Get("/:id/watch", api.Watch)
|
|
}
|