Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type APIModule ¶ added in v0.1.0
type APIModule interface {
model.ModuleNaming // provides ModelName() — identity
MountAPI(r Router)
}
APIModule is a module that exposes a server API. It is consumed by the server entry point (!wasm): which passes it the host's Router, and the module registers its own routes/handlers. Since Router is isomorphic, the module never imports net/http to describe its API. The concrete transport (binary upload, another protocol mounted as a route) is the module's internal decision.
type Caller ¶ added in v0.1.4
type Caller interface {
// Call invokes op and DECODES the response into `into` using the transport's
// codec — the call-side mirror of Context.Decode/Encode on the mount side. A
// caller (a view, a module) works in typed model values and NEVER imports a
// codec package (json, jsvalue); which codec runs is the transport's decision.
//
// into may be nil when the caller does not care about the response body (a
// save/delete that only needs the error). done reports the outcome; result/err
// arrive asynchronously (works for wasm fetch and for in-process test doubles
// alike). Implementations MUST propagate every error — never swallow.
Call(op string, args model.Encodable, into model.Decodable, done func(err error))
// Dispatch is fire-and-forget (no response expected).
Dispatch(op string, args model.Encodable)
}
Caller is how a client-side view invokes a named server operation without knowing the wire protocol or transport. It mirrors APIModule: APIModule is the mount-side contract, Caller is the call-side contract.
op is the logical operation name (e.g. "list_services") — NEVER a wire-level method. Translating op to the concrete envelope is the adapter's job (e.g. mcp.NewCaller adapts *mcp.Client). Test doubles satisfy Caller with canned results and no transport.
type Context ¶ added in v0.1.0
type Context interface {
Method() string
Path() string
Body() []byte
GetHeader(key string) string
SetHeader(key, value string)
WriteStatus(code int)
Write(b []byte) (int, error)
// Request-scoped values (middleware passes data to the next handler).
SetValue(key string, v any)
Value(key string) any
// Isomorphic cookies.
SetCookie(c Cookie) // writes a cookie to the response
Cookie(name string) (Cookie, bool) // reads a cookie from the request; ok=false if not found
// Request-scoped identity. An auth middleware records the caller;
// handlers and mounted modules read it.
SetUserID(id string) // records the authenticated identity (id "" = anonymous)
UserID() string // reads the identity; "" if no valid session
// Decode reads the request body through the transport's codec, into a typed
// destination — the handler never imports a codec package directly. Decode
// backed by JSON, jsvalue, or any other model.FieldReader implementation is
// the transport's decision, not the handler's.
Decode(into model.Decodable) error
// Encode writes v through the transport's codec as the response body.
// Same contract as Decode, the other direction.
Encode(v model.Encodable) error
}
Context is the minimal abstraction seen by a handler: request → response. Same interface signature for both native (!wasm) and edge/wasm targets.
Ownership: a Context belongs to ONE goroutine (the handler's); implementations are not required to be safe for concurrent use (same contract as http.ResponseWriter). To feed it from other goroutines, send the data over a channel to the owning goroutine — never share the Context itself.
type Cookie ¶ added in v0.1.1
type Cookie struct {
Name string // e.g. "session_id", "user_pref"
Value string // e.g. "abc123xyz789"
Path string // e.g. "/", "/api"; omit for "/"
Domain string // e.g. "example.com"; omit for current domain
MaxAge int // >0 seconds; 0 = session; <0 = delete now
Secure bool // true = HTTPS only
HttpOnly bool // true = no JavaScript access
SameSite SameSite // SameSiteLax, SameSiteStrict, SameSiteNone
}
Cookie is the isomorphic representation of an HTTP cookie. It does not reference net/http: each concrete implementer maps it to its transport (net/http.Cookie on native; Set-Cookie header on edge/wasm).
type HandlerFunc ¶ added in v0.1.0
type HandlerFunc func(Context)
HandlerFunc is the dispatch unit: receives a Context and responds to it.
type Middleware ¶ added in v0.1.0
type Middleware func(HandlerFunc) HandlerFunc
Middleware wraps a handler to add cross-cutting logic (auth, logging). Operate ONLY on Context — never on concrete transport types.
type OpModule ¶ added in v0.1.14
type OpModule interface {
model.ModuleNaming // provides ModelName() — identity
MountOps(reg OpRegistry)
}
OpModule is a reusable domain module: it exposes named operations and NOTHING transport-specific. Where APIModule registers HTTP routes and is therefore bound to an HTTP host, an OpModule depends only on this package's neutral contracts, so the SAME module serves any transport the composition root chooses to bind (mcp tools today). "Is this module transport-agnostic?" is a compile-time fact — whether it satisfies OpModule — not a convention to remember.
type OpRegistry ¶ added in v0.1.14
type OpRegistry interface {
// Op registers an operation by LOGICAL NAME. Route.Accepts declares its arg schema
// (what a transport advertising a catalogue, like mcp's tools/list, reads); the
// same Route.Requires/Public/Authenticated gate applies as to any HTTP route.
Op(name string, h HandlerFunc) Route
}
OpRegistry is the transport-neutral surface a reusable domain module registers its operations on. It carries ONLY named operations — no HTTP verb, no path — so one module description projects onto whatever transport the host binds: mcp harvests each Op as a tool; a future REST/gRPC/stdio binding would map the name its own way.
It is the mount-side MIRROR of Caller (the call-side, also transport-neutral): Caller.Call(name, args, into, done) invokes exactly what Op(name, h) registered. Both live here, next to Context and Route, because that is what an Op handler needs.
It is deliberately NOT a method on Router. Router is the HTTP-shaped surface (Get/Post/path/cookies/status); a transport that only harvests operations (mcp) must never be forced to impersonate an HTTP router — panicking on Get/Post it can neither honour nor need — just to be handed the operation list. A concrete HTTP router MAY also satisfy OpRegistry, but the domain module sees only this.
type Route ¶ added in v0.1.1
type Route interface {
// Requires binds an RBAC permission to the route: the (resource, action) pair.
//
// Both are typed (model.Resource, model.Action). They used to be two bare strings in a
// row, so swapping them compiled — and the failure was not an error but a SILENT denial
// at runtime, in the one place where silence is unacceptable. Now it does not compile.
//
// The resource is open vocabulary: the app declares its own ("service_catalog"). The
// action is a closed CRUD set (model.Read, model.Update, …): persistence has four verbs
// and no tool in this ecosystem ever needed a fifth.
Requires(resource model.Resource, action model.Action) Route
// Authenticated marks the route as reachable by any identity, with no permission check.
// For operations on the CALLER themselves, where authentication already is the check.
Authenticated() Route
// Public marks the route as reachable with no identity at all.
Public() Route
// Accepts declares the typed schema of the request body — the Args a caller
// must send. It is the counterpart of RouteInfo.Args: a transport that needs
// to advertise a schema (mcp's tools/list) reads it from here instead of the
// module hand-rolling wire metadata. nil means "no args" (a Route that never
// calls Accepts has Args == nil, the same as passing nil explicitly).
Accepts(args model.Fielder) Route
}
Route describes a registered route and allows annotating it. It is returned by each Router registration method. Annotations are declarative: the contract does not enforce them — each concrete implementer (native server, edge runtime) enforces them.
type RouteInfo ¶ added in v0.1.1
type RouteInfo struct {
Method string // e.g. "GET", "POST"
Path string // e.g. "/api/users", "/api/orders/:id"
Resource model.Resource // required by AccessGuarded; must be empty otherwise
Action model.Action // e.g. model.Read; 0 = none
// Access is what the route declared. The ZERO VALUE is model.AccessGuarded: a route that
// annotates nothing is unreachable until it declares a Resource, and an enforcer must
// reject it loudly at startup.
//
// It replaced a `Public bool` alongside an empty-or-not Resource. That encoding made an
// illegal state writable — a route could be Public AND carry a Requires, and the gate
// silently dropped the permission check: a route that looked protected and was not.
Access model.Access
// Dir is the directory served by PublicDir; "" for every other route.
// It exists so a whole served directory is visible to introspection instead of
// being smuggled past the router by a file-server fallback.
Dir string
// Args is the schema a caller must send, set via Route.Accepts; nil = no args.
// It is a Go-side value read directly by a transport that needs it (mcp builds
// its tools/list schema from it) — deliberately NOT part of EncodeFields: a
// Fielder's schema is a different shape of data than this route-metadata wire
// view, and serializing it is that transport's concern, not RouteInfo's.
Args model.Fielder
}
RouteInfo is the read-only view of a registered route — for introspection.
func (RouteInfo) EncodeFields ¶ added in v0.1.11
func (r RouteInfo) EncodeFields(w model.FieldWriter)
EncodeFields makes RouteInfo a model.Encodable, so it is serialized by tinywasm/json through this DECLARED shape instead of by reflection over its Go fields.
Reflection got it actively wrong, and wrong in the worst direction. `Access` and `Action` are numeric types, so a reflection-based encoder emitted them as bare numbers: the ZERO value of Access is AccessGuarded, so the MOST protected route in the server reported itself as `"Access":0` — which any human or agent reading the routes endpoint takes for "nothing declared", the exact opposite of the truth. `"Action":6` was equally unreadable. An endpoint whose whole job is to expose the security posture of a server must not invert it.
Here the shape is stated, not guessed: the enums travel as the words they already know how to render, and `Dir` — an internal detail of PublicDir — stays out of the wire.
type Router ¶
type Router interface {
Get(path string, h HandlerFunc) Route
Post(path string, h HandlerFunc) Route
Put(path string, h HandlerFunc) Route
Delete(path string, h HandlerFunc) Route
Options(path string, h HandlerFunc) Route
Handle(method, path string, h HandlerFunc) Route
Stream(path string, h StreamFunc) Route
Socket(path string, h SocketFunc) Route
// PublicAsset registers ONE route serving ONE file to the browser: generated
// content such as index.html, the stylesheet, the JS bundle or the wasm binary.
//
// It is public by construction — a browser fetching an asset has no identity
// yet. It returns no Route: there is no permission to attach, so an asset can
// neither be left private by accident (a silent 403 on a blank page) nor be
// wrongly gated. Serving a file that DOES need permissions is a normal route:
// Get(path, h).Requires(resource, action) — which fails closed if forgotten.
PublicAsset(path string, h HandlerFunc)
// PublicDir serves a whole directory under a prefix (e.g. "web/public").
// Same contract as PublicAsset: public by construction, no Route to gate.
PublicDir(prefix string, dir string)
Use(m ...Middleware)
// Routes enumerates the registered routes and their metadata.
Routes() []RouteInfo
}
Router is what a module registers its routes on. A concrete implementer (native server, edge runtime) satisfies this interface; modules and hosts only consume it.
type SameSite ¶ added in v0.1.1
type SameSite int
SameSite types the SameSite policy — illegal state not representable (not a string).
type Socket ¶ added in v0.1.0
Socket is the bidirectional upgraded connection (WebSocket). Isomorphic abstraction: does not touch concrete upgrade mechanisms.
type SocketFunc ¶ added in v0.1.0
type SocketFunc func(Socket)
SocketFunc is a handler that receives a typed Socket.
type StreamFunc ¶ added in v0.1.0
type StreamFunc func(Streamer)
StreamFunc is a handler that receives a typed Streamer.
type Streamer ¶ added in v0.1.0
type Streamer interface {
Context
Flush() // sends to the client what has been written so far, without closing the response
}
Streamer is a Context that also flushes writes immediately. Used for incremental responses (SSE, streaming).
Ownership: same single-goroutine contract as Context. A push loop (SSE hub, broker) must deliver messages to the handler's goroutine via a channel; only that goroutine calls Write/Flush.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package conformance is the executable contract of router.Router.
|
Package conformance is the executable contract of router.Router. |