router

package module
v0.1.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 1 Imported by: 0

README

tinywasm/router

Isomorphic routing contract — Context, Router, HandlerFunc identical on native and edge/wasm targets. Defines request handling, streaming (SSE), WebSocket upgrade, and middleware. Modules mount their API via APIModule. The router defines the shape; concrete servers implement it.

Quick Start

import "github.com/tinywasm/router"

type MyModule struct{ name string }

func (m MyModule) ModelName() string { return m.name }
func (m MyModule) MountAPI(r router.Router) {
    r.Get("/api/data", func(ctx router.Context) {
        ctx.WriteStatus(200)
        ctx.Write([]byte("hello"))
    })
}

Caller — the call-side contract

func (v *MyView) Refresh() {
    v.Caller.Call("list_services", nil, func(res []byte, err error) {
        if err != nil {
            v.HandleError(err)
            return
        }
        v.Update(res)
    })
}

Modules and views depend on Caller to invoke server operations without knowing the wire protocol or transport. Adapters live with each transport (e.g. mcp.NewCaller in tinywasm/mcp adapts a JSON-RPC client), while tests use a mock.Caller.

Contracts

  • Context: minimal I/O (read method/path/body, write headers/status) + cookies (SetCookie/Cookie) + identity (SetUserID/UserID)
  • Cookie: isomorphic HTTP cookie type with SameSite policy (SameSiteDefault/Lax/Strict/None)
  • HandlerFunc: func(Context) — the unit of dispatch
  • Route: registration token; supports Requires(resource, action) for RBAC and Public() for explicit public access
  • RouteInfo: read-only view of a registered route with method, path, resource, action, and public flag
  • Router: register routes (Get/Post/Put/Delete/Handle) returning Route + streaming (Stream/Socket) + middleware (Use) + Routes() for introspection
  • Streamer: Context + Flush() for SSE/streaming responses
  • Socket: bidirectional connection (WebSocket)
  • Middleware: func(HandlerFunc) HandlerFunc — transversal logic (auth, logging)
  • APIModule: module + MountAPI(Router) — how modules publish APIs
  • Caller: call-side contract — how a client-side view invokes a named server operation
  • mock: subpackage with canonical test doubles (Router, Context, Route, Caller) — no net/http, WASM-safe

Design

No net/http in the public API. Handlers never import Go's standard library HTTP types. All routing is self-describing via signatures — no runtime type assertions, no hidden machinery.

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; result/err arrive asynchronously via callback
	// (works for wasm fetch and for in-process test doubles alike).
	// Implementations MUST propagate every error — never swallow.
	Call(op string, args model.Encodable, callback func(result []byte, 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 bytes 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
}

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 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 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
}

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
}

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.

func (RouteInfo) IsNil added in v0.1.11

func (r RouteInfo) IsNil() bool

IsNil satisfies model.Encodable; a RouteInfo is a value and never nil.

func (RouteInfo) IsPublic added in v0.1.9

func (r RouteInfo) IsPublic() bool

IsPublic reports whether the route is reachable with no identity.

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).

const (
	SameSiteDefault SameSite = iota // browser default behavior
	SameSiteLax                     // cross-site requests send cookie (default modern behavior)
	SameSiteStrict                  // never send cookie cross-site
	SameSiteNone                    // send cookie in all contexts (requires Secure=true)
)

type Socket added in v0.1.0

type Socket interface {
	Read() ([]byte, error)
	Write(b []byte) error
	Close() error
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL