Files
zeekayandHanzo Dev 5f71d2496b The router is three nouns, and every one of them is concrete
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]>
2026-09-17 01:39:17 -07:00

107 lines
2.6 KiB
Go

// Command local-service is a zip service with one operation, backed by SQLite.
//
// The operation is declared once, in New, and every interface below is derived
// from that declaration:
//
// go run . serve # REST, MCP and OpenAPI on 127.0.0.1:8080
// go run . notes create --text "buy milk" # the same operation as a command
// go run . openapi openapi.json # the OpenAPI document, written to a file
//
// Notes are stored in notes.db in the working directory.
package main
//go:generate go tool zipdoc
import (
"context"
"database/sql"
"fmt"
"os"
"github.com/hanzoai/sqlite"
"github.com/zap-proto/zip"
)
// Note is one stored note.
type Note struct {
// ID is the row id SQLite assigned.
ID int64 `json:"id"`
// Text is the note as written.
Text string `json:"text"`
}
// AddIn is a note to store.
type AddIn struct {
// Text is the note body.
Text string `json:"text" validate:"required"`
}
// Store keeps notes in one SQLite file.
type Store struct {
db *sql.DB
}
// Open opens the SQLite file at path, creating it and its table if needed.
func (s *Store) Open(path string) error {
db, err := sqlite.OpenDB(path, nil)
if err != nil {
return err
}
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, text TEXT NOT NULL)`); err != nil {
db.Close()
return err
}
s.db = db
return nil
}
// Add stores a note and returns it with its id.
//
// Example: {"text": "buy milk"}
// Response: {"id": 1, "text": "buy milk"}
func (s *Store) Add(ctx context.Context, in *AddIn) (*Note, error) {
res, err := s.db.ExecContext(ctx, `INSERT INTO notes (text) VALUES (?)`, in.Text)
if err != nil {
return nil, err
}
id, err := res.LastInsertId()
if err != nil {
return nil, err
}
return &Note{ID: id, Text: in.Text}, nil
}
// New declares the service's operations. It does not touch the database, so the
// OpenAPI document can be written by a build step that has none.
func New(s *Store) *zip.App {
app := zip.New(zip.Config{AppName: "notes"})
app.Post("/v1/notes", s.Add)
return app
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
s := &Store{}
app := New(s)
// `local-service openapi <file>` and `local-service declare <file>` write a
// projection and stop here, before the database is opened.
if done, err := app.Described(); done {
return err
}
if err := s.Open("notes.db"); err != nil {
return err
}
if len(os.Args) > 1 && os.Args[1] == "serve" {
return app.Listen("http://127.0.0.1:8080")
}
cli := app.CLI()
cli.Out = os.Stdout
return cli.Run(context.Background(), os.Args[1:])
}