Documentation
¶
Overview ¶
Package aprot is a Go library for building type-safe real-time APIs with automatic TypeScript client generation. It supports both WebSocket and SSE+HTTP transports.
Overview ¶
aprot follows a define-register-serve-generate workflow:
- Define handler methods on Go structs — each becomes a callable RPC endpoint.
- Register handlers (with optional middleware) on a Registry.
- Create a Server and mount it on your HTTP router.
- Run the Generator to emit a fully typed TypeScript client.
The generated client includes standalone functions, React hooks (optional), typed error checking, enum const objects, and push event handlers — all derived from your Go source code. For details on the generated TypeScript API (hook behavior, cancellation, loading states), browse the committed example output at example/react/client/src/api/ and example/vanilla/client/static/api/ in the repository.
Handlers ¶
Handler methods are ordinary Go methods on a struct. They must accept context.Context as the first parameter and return either error (void) or (T, error):
func (h *Handlers) CreateUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) { ... }
func (h *Handlers) DeleteUser(ctx context.Context, id string) error { ... }
func (h *Handlers) ListUsers(ctx context.Context) ([]User, error) { ... }
func (h *Handlers) Add(ctx context.Context, a int, b int) (*SumResult, error) { ... }
Parameters are positional — each Go parameter becomes a separate argument in the TypeScript client. Parameter names are extracted from Go source via AST parsing, so the names you choose in Go are the names your TypeScript client uses.
Streaming Handlers ¶
Handlers may return iter.Seq or iter.Seq2 instead of a single value to stream results incrementally. Each yielded element is delivered to the client as a separate wire message, so UIs can render rows as they arrive instead of waiting for the full response:
func (h *Handlers) ListUsers(ctx context.Context) (iter.Seq[*User], error) {
return func(yield func(*User) bool) {
for cursor.Next(ctx) {
var u User
cursor.Scan(&u)
if !yield(&u) {
return // client canceled
}
}
}, nil
}
func (h *Handlers) Prices(ctx context.Context) (iter.Seq2[string, float64], error) { ... }
The generator emits an AsyncIterable on the TypeScript side, so clients consume streams with a `for await` loop. Cancellation is bidirectional: breaking out of the loop (or calling the hook's cancel function) cancels the handler's context immediately, and the next `yield` returns false.
Streaming is WebSocket/SSE only. Registry.RegisterREST and Registry.EnableREST panic at registration time for streaming handlers because REST cannot deliver multi-message responses over a single HTTP request. See OnStreamComplete in the Middleware section for observing the real end of a stream from logging / metrics middleware.
Input Transformation ¶
Request struct fields can be normalized before handler dispatch using "transform" struct tags. Transforms run after JSON decoding and before struct validation, so validator rules see the cleaned value:
type SignupRequest struct {
Email string `json:"email" transform:"trim,lowercase" validate:"required,email"`
Username string `json:"username" transform:"trim" validate:"required,min=3"`
Slug string `json:"slug" transform:"trimleft=/,lowercase"`
Tags []string `json:"tags" transform:"trim,removeempty"`
}
Supported ops (applied in the order listed in the tag):
- trim strings.TrimSpace
- trimleft[=cutset] TrimLeft; optional cutset (defaults to whitespace)
- trimright[=cutset] TrimRight; optional cutset (defaults to whitespace)
- uppercase strings.ToUpper
- lowercase strings.ToLower
- removeempty []string only — drop empty elements
Ops apply to string, *string (nil-safe), and []string fields, and the walker recurses into nested structs, *struct, and []struct so nested tags are picked up automatically. There is no registry opt-in — a field is transformed if and only if it carries a "transform" tag.
Every "transform" tag reachable from a handler's param types is statically checked at registration time via ValidateTransformTags. Unknown ops, "removeempty" on a non-[]string field, or a "transform" tag on an unsupported field type (int, bool, time.Time, …) cause Registry.Register to panic when the server boots, rather than turning every request into a CodeInvalidParams response at runtime. ApplyTransforms is also exposed so the same walker can be invoked on ad-hoc values outside the handler flow.
Request Validation ¶
Request struct fields can declare validation rules via the "validate" struct tag, using the vocabulary from github.com/go-playground/validator. Validation is opt-in per registry: nothing happens until Registry.SetValidator is called with a StructValidator. The supplied NewPlaygroundValidator wraps the go-playground implementation and produces a structured error payload that flows through to the generated TypeScript client:
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=13,lte=120"`
}
registry := aprot.NewRegistry()
registry.Register(&Handlers{})
registry.SetValidator(aprot.NewPlaygroundValidator())
Validation runs after ApplyTransforms inside HandlerInfo.Call, so rules like "required,min=1" observe the already-normalized value. Failures are returned as a ProtocolError with CodeValidationFailed and a []FieldError payload describing every rule that failed, which the generated TypeScript client exposes via its ApiError type.
Registry ¶
A Registry collects handler groups, push events, enums, and custom errors for both server dispatch and code generation:
registry := aprot.NewRegistry()
registry.Register(&PublicHandlers{})
registry.Register(&AdminHandlers{}, authMiddleware)
registry.RegisterPushEventFor(&PublicHandlers{}, UserCreatedEvent{})
registry.RegisterEnumFor(&PublicHandlers{}, StatusValues())
registry.RegisterError(sql.ErrNoRows, "NotFound")
Each Registry.Register call creates a handler group with its own middleware chain and a corresponding TypeScript file.
Middleware ¶
Middleware wraps handlers to add cross-cutting behavior. It follows the standard func(next) -> func pattern:
func LoggingMiddleware() aprot.Middleware {
return func(next aprot.Handler) aprot.Handler {
return func(ctx context.Context, req *aprot.Request) (any, error) {
start := time.Now()
result, err := next(ctx, req)
log.Printf("[%s] %s took %v", req.ID, req.Method, time.Since(start))
return result, err
}
}
}
Server-level middleware applies to all handlers. Per-handler middleware applies only to the handlers registered in the same Registry.Register call. Execution order: server middleware (outer) → handler middleware (inner) → handler.
server.Use(LoggingMiddleware()) // all handlers
registry.Register(&ProtectedHandlers{}, AuthMiddleware()) // this group only
Middleware sees a streaming handler "return" as soon as its iter.Seq value is constructed — before any items have been yielded — so a naive `time.Since(start)` measurement logs 0ms for every stream. Call OnStreamComplete from middleware to register a callback that fires when the stream has actually terminated (via exhaustion, handler panic, or client cancellation), with the terminal error and the number of items delivered:
func LoggingMiddleware() aprot.Middleware {
return func(next aprot.Handler) aprot.Handler {
return func(ctx context.Context, req *aprot.Request) (any, error) {
start := time.Now()
aprot.OnStreamComplete(ctx, func(err error, items int) {
log.Printf("stream %s done in %s items=%d err=%v",
req.Method, time.Since(start), items, err)
})
return next(ctx, req)
}
}
}
Calling OnStreamComplete on a unary-handler context is a no-op, so the same middleware can log both streaming and unary handlers without branching on handler kind.
Server ¶
A Server handles WebSocket upgrades, SSE streams, and HTTP POST dispatch. Mount it directly for WebSocket, or use Server.HTTPTransport for SSE+HTTP:
server := aprot.NewServer(registry)
http.Handle("/ws", server) // WebSocket
http.Handle("/sse", server.HTTPTransport()) // SSE+HTTP
http.Handle("/sse/", server.HTTPTransport())
Both transports can run simultaneously and share connection tracking — Server.Broadcast, Server.PushToUser, and Server.ConnectionCount work across all connections regardless of transport.
Handlers can additionally be exposed over REST/HTTP alongside (or instead of) WebSocket. Use Registry.RegisterREST for REST-only handlers, or Registry.EnableREST to mark an existing WebSocket handler for REST as well. NewRESTAdapter returns an http.Handler that serves every REST-exposed handler in the registry:
registry.Register(&UserHandlers{}) // WebSocket only
registry.RegisterREST(&TodoHandlers{}) // REST only
registry.Register(&BothHandlers{}) // WebSocket...
registry.EnableREST(&BothHandlers{}) // ...and also REST
http.Handle("/api/", aprot.NewRESTAdapter(registry))
HTTP method and path are derived from the handler method name by convention (e.g. CreateUser → POST /users/create-user), and path parameters are mapped from the Go parameter list. Streaming handlers cannot be exposed via REST and will panic at registration — use WebSocket or SSE for those.
Use ServerOptions to configure client reconnection behavior. The server sends this configuration to clients on connect; TypeScript clients apply it automatically.
Connection Lifecycle ¶
Server.OnConnect and Server.OnDisconnect hooks react to connection events. OnConnect hooks can reject connections by returning an error:
server.OnConnect(func(ctx context.Context, conn *aprot.Conn) error {
session, err := validateSession(conn.Info().Cookies)
if err != nil {
return aprot.ErrConnectionRejected("invalid session")
}
conn.SetUserID(session.UserID)
conn.Set(principalKey{}, session.User)
return nil
})
Each Conn has a unique ID, HTTP request info captured at connection time (via Conn.Info), and key-value storage (via Conn.Set, Conn.Get, Conn.Load) for caching authentication state or other per-connection data.
Conn.SetUserID / Conn.UserID is a routing identity used for push targeting (Server.PushToUser). It is not a security boundary — use the stored principal for authorization decisions.
Push Events ¶
Push events are server-to-client messages broadcast to all connected clients or targeted to specific users:
server.Broadcast(&UserCreatedEvent{ID: "1", Name: "Alice"})
server.PushToUser("user_123", &NotificationEvent{Message: "hello"})
Push event types must be registered with Registry.RegisterPushEventFor. The event name on the wire is derived from the Go type name.
Subscription Refresh ¶
Subscription refresh automatically pushes updated query results to clients when related data changes. Query handlers declare trigger keys with RegisterRefreshTrigger, and mutation handlers fire them with TriggerRefresh:
// Query handler — declares dependency on "users" trigger key
func (h *H) ListUsers(ctx context.Context) ([]User, error) {
aprot.RegisterRefreshTrigger(ctx, "users")
return h.db.ListUsers(ctx)
}
// Mutation handler — fires trigger to refresh all subscribed clients
func (h *H) CreateUser(ctx context.Context, req *CreateReq) (*User, error) {
user, err := h.db.CreateUser(ctx, req)
if err != nil {
return nil, err
}
aprot.TriggerRefresh(ctx, "users")
return user, nil
}
Multiple TriggerRefresh calls within a single request are batched and deduplicated. TriggerRefreshNow flushes the queue immediately — use it in long-running handlers that make observable state transitions over time.
From background goroutines, cron jobs, webhook fan-in, or any other code path that runs outside of a request handler, use the Server.TriggerRefresh method instead — it flushes immediately and does not require a request context:
go func() {
for range ticker.C {
server.TriggerRefresh("users")
}
}()
RegisterRefreshTrigger takes variadic strings that form a composite key. It is a no-op when called from a non-subscribe request. The package-level TriggerRefresh is a no-op outside a request context. Subscriptions are cleaned up automatically on client disconnect.
Error Handling ¶
Return ProtocolError values from handlers to send structured errors to clients. Built-in helpers cover common cases:
aprot.ErrUnauthorized("invalid token") // -32001
aprot.ErrForbidden("access denied") // -32003
aprot.ErrInvalidParams("name is required") // -32602
aprot.ErrInternal(err) // -32603
Register Go errors with Registry.RegisterError for automatic conversion. The generated TypeScript client includes typed error checking:
registry.RegisterError(sql.ErrNoRows, "NotFound") // In TypeScript: err.isNotFound(), ErrorCode.NotFound
Connection Errors (TypeScript Client) ¶
Connection-level failures surface as a structured ConnectionError on the generated TypeScript client — separate from ApiError, which represents a structured server-side error response. ConnectionError extends Error and exposes a typed `reason` field so apps can render appropriate UI:
'offline' — navigator.onLine was false at failure time.
'server-rejected' — server sent ApiError with code ConnectionRejected
before closing; the original ApiError is attached as
err.cause.
'server-closed' — transport closed cleanly after the WebSocket upgrade
completed; err.closeCode and err.closeReason carry
the WebSocket CloseEvent fields.
'network-error' — pre-upgrade failure or close code 1006 (refused,
unreachable, TLS, HTTP error during upgrade).
Browsers deliberately collapse these into one bucket.
'manual' — caller invoked client.disconnect().
In-flight request and requestStream calls reject with a ConnectionError when the connection drops. Calls issued while disconnected reject with the most recent ConnectionError, falling back to 'offline' or 'manual' when none is available. Use client.onConnectionError(listener) to drive UI like an "Offline" banner; client.getLastConnectionError() returns the most recently observed error or null.
The 'server-rejected' bucket is also surfaced via the existing onConnectionRejected ApiClientOption callback — both fire for the same underlying event, kept for backward compatibility.
Enum Support ¶
Register Go enum types with Registry.RegisterEnumFor or Registry.RegisterEnum to generate TypeScript const objects with full type safety. String-based enums derive names by capitalizing values; int-based enums use the String() method:
type Status string
const (
StatusActive Status = "active"
StatusExpired Status = "expired"
)
func StatusValues() []Status { return []Status{StatusActive, StatusExpired} }
registry.RegisterEnumFor(handler, StatusValues())
Struct fields with enum types generate TypeScript fields typed as the enum union (not raw string/number).
Code Generation ¶
Generator reads a Registry and emits TypeScript client code. It supports two output modes: OutputVanilla (standalone functions + subscribe helpers) and OutputReact (adds React hooks with auto-refetch and mutation state):
gen := aprot.NewGenerator(registry).WithOptions(aprot.GeneratorOptions{
OutputDir: "./client/src/api",
Mode: aprot.OutputReact,
})
gen.Generate()
The generator creates split files: client.ts (base client), one file per handler group, and optional shared type files for types used across groups. Use NamingPlugin to customize TypeScript name conventions.
Setting [GeneratorOptions.Zod] emits a companion `.schema.ts` file for every handler group whose request types carry "validate" tags. The resulting Zod schemas mirror the server-side validation rules field for field — so the TypeScript client can reject bad input before it hits the wire, using the same constraints the server will enforce on arrival:
gen := aprot.NewGenerator(registry).WithOptions(aprot.GeneratorOptions{
OutputDir: "./client/src/api",
Mode: aprot.OutputReact,
Zod: true,
})
For REST-exposed handlers, NewOpenAPIGenerator produces an OpenAPI 3.0 document describing every REST endpoint in the registry. Go doc comments on handler methods become `summary` / `description`, struct and field doc comments flow into JSON Schema descriptions, and "validate" tags become JSON Schema constraints:
oag := aprot.NewOpenAPIGenerator(registry, "My API", "1.0.0") spec, err := oag.Generate() // or: jsonBytes, err := oag.GenerateJSON()
Use OpenAPIGenerator.WithBasePath when the API is mounted behind a proxy or at a non-root path.
TypeScript Mutation Patterns (React) ¶
aprot does not generate per-handler mutation hooks (no useXxxMutation()). Mutations use one of two patterns:
Pattern 1 — query-scoped mutate(action) (refetch on completion). Every useXxx() query hook returns a `mutate` helper alongside data / isLoading / error. It accepts either a Promise or a (client: ApiClient) => Promise<unknown> thunk; it runs the action, captures any thrown error in `error`, and refetches the query on success. Loading / error state is shared with the query so a single indicator covers both the action and the refresh:
const { data, mutate, isLoading, error } = useListTodos();
<button onClick={() => mutate((client) => addTodo(client, { title: 'Buy milk' }))} />
The thunk receives the same ApiClient the hook is bound to, so callers don't need a separate useApiClient() at the call site. A bare Promise is also accepted, useful when composing operations:
mutate(Promise.all([addTodo(c, a), addTodo(c, b)]));
Pattern 2 — raw async function via useApiClient(). Generated standalone functions (addTodo, createUser, etc.) are typed Promise<TRes> that throw ApiError / ConnectionError on failure. Use them when there's no surrounding query (no list to refetch) or when you need conditional try/catch:
const client = useApiClient();
try {
const todo = await addTodo(client, { title: 'Buy milk' });
} catch (err) {
if (err instanceof ApiError && err.isValidationFailed()) { ... }
else { throw err; }
}
You manage isLoading / error / AbortController yourself. The trade-off is honest: the function does what its type says — no hidden state machine.
Earlier versions of aprot generated useXxxMutation() hooks whose mutate() swallowed errors and returned `undefined as TRes` on failure. They were removed because the Promise<TRes> type lied at runtime, void mutations could not distinguish success from "not yet called", and the only correct after-success pattern (useEffect([data])) was non-obvious. See the repository's MIGRATION_MUTATION_HOOKS.md for a rewrite prompt.
Global Error Capture (TypeScript Client) ¶
Wrap a region of the React tree in <ApiClientErrorProvider> to surface every API error inside a single hook, instead of wiring per-call try/catch. Errors from imperative client.request() / requestStream() / subscribe() calls AND from generated query / stream / mutate hooks all flow through the provider, because every hook retrieves its client via useApiClient() and useApiClient() returns a Proxy-wrapped client when the provider is mounted above it:
import { ApiClientProvider, ApiClientErrorProvider, useApiClient,
useApiClientError } from './api/client';
<ApiClientProvider value={client}>
<ApiClientErrorProvider>
<App />
</ApiClientErrorProvider>
</ApiClientProvider>
Read with useApiClientError():
const { error, source, clear } = useApiClientError();
// source is { struct, method } | null — null exactly when error is null.
// A failure from client.request('Todos.CreateTodo', …) yields
// source = { struct: 'Todos', method: 'CreateTodo' }, so a banner can
// name the failing call without each call site reporting itself.
The source is parsed from the wire name on the first dot. Calls whose wire name has no dot set struct to ” and put the full name in method.
Only the latest error is held (newer overrides older); clear() resets both error and source. The provider observes errors but does not swallow them — wrapped client calls still throw, so per-hook `error` fields and explicit try/catch keep working. Without <ApiClientErrorProvider> above, useApiClient() returns the raw client unchanged and useApiClientError() throws — adoption is opt-in.
React Suspense ¶
In addition to per-handler hooks like `useListUsers()` that return `{data, isLoading, error}`, OutputReact also emits `useQuerySuspense` -- a single generic hook that pairs aprot's promise-returning query functions with React 19's `use()` and `<Suspense>` boundaries:
import { Suspense } from 'react'
import { useQuerySuspense } from './api/client'
import { listUsers, getUser } from './api/handlers'
function UsersList() {
const data = useQuerySuspense(listUsers) // no params
return data.users.map(u => <li key={u.id}>{u.name}</li>)
}
function UserView({ id }: { id: string }) {
const user = useQuerySuspense(getUser, id) // typed params
return <h1>{user.name}</h1>
}
<Suspense fallback={<Spinner />}>
<ErrorBoundary fallback={<ErrorView />}>
<UsersList />
</ErrorBoundary>
</Suspense>
`useQuerySuspense` opens a server subscription on first read (using the same TriggerRefresh machinery as `useQuery`), suspends the component until the first response arrives, and replaces the cached promise with a new resolved one on each subsequent server push -- so live updates flow without re-suspending. Errors thrown by the handler are propagated to the nearest error boundary.
The hook works directly with the generated query functions because each one carries a `.method` property identifying its wire method:
export function listUsers(client: ApiClient, options?: RequestOptions): Promise<ListUsersResponse> { ... }
listUsers.method = 'PublicHandlers.ListUsers' as const;
No per-handler Suspense hook is generated; the single generic hook plus the metadata is enough. Requires React 19+. Streams continue to use `useStream`, and mutations use the patterns described above (query.mutate or the raw async function) — only queries fit the Suspense paradigm cleanly.
Context Helpers ¶
Several functions extract request-scoped values from context:
- Progress — returns the ProgressReporter for long-running operations
- Connection — returns the Conn for the current request
- HandlerInfoFromContext — returns HandlerInfo metadata
- RequestFromContext — returns the Request with ID, method, and params
- CancelCause — returns why the request was canceled (see ErrClientCanceled, ErrConnectionClosed, ErrServerShutdown)
Graceful Shutdown ¶
Server.Stop rejects new connections (503), sends close frames, waits for in-flight requests to complete, and runs disconnect hooks. It is safe to call multiple times:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() server.Stop(ctx)
Type Mapping ¶
Go types are mapped to TypeScript during generation:
- string → string
- int, float64, etc. → number
- bool → boolean
- []T → T[]
- map[K]V → Record<K, V>
- *T → T (optional field)
- time.Time → string (RFC 3339)
- sql.NullString → string | null (all sql.Null* types supported)
- struct → interface
- Registered enum → const object + union type
Wire Protocol ¶
Messages are JSON objects with a "type" field. Client-to-server: request, cancel, subscribe, unsubscribe. Server-to-client: response, error, progress, push, config, connected (SSE only).
Example ¶
package main
import (
"context"
"fmt"
"net/http"
"github.com/marrasen/aprot"
)
// Request and response types
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessItemsRequest struct {
Items []string `json:"items"`
}
type ProcessItemsResponse struct {
Processed int `json:"processed"`
}
// Push event types
type UserUpdatedEvent struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessingCompleteEvent struct {
Count int `json:"count"`
}
// Handlers struct
type MyHandlers struct {
server *aprot.Server
}
// CreateUser handles user creation
func (h *MyHandlers) CreateUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) {
progress := aprot.Progress(ctx)
progress.Update(1, 2, "Validating...")
if req.Name == "" {
return nil, aprot.ErrInvalidParams("name is required")
}
progress.Update(2, 2, "Creating user...")
conn := aprot.Connection(ctx)
if conn != nil {
conn.Push(&UserUpdatedEvent{ID: "123", Name: req.Name})
}
return &CreateUserResponse{ID: "123", Name: req.Name}, nil
}
// ProcessItems demonstrates progress reporting with cancellation
func (h *MyHandlers) ProcessItems(ctx context.Context, req *ProcessItemsRequest) (*ProcessItemsResponse, error) {
progress := aprot.Progress(ctx)
total := len(req.Items)
for i, item := range req.Items {
select {
case <-ctx.Done():
return nil, aprot.ErrCanceled()
default:
}
progress.Update(i+1, total, fmt.Sprintf("Processing %s...", item))
}
h.server.Broadcast(&ProcessingCompleteEvent{Count: total})
return &ProcessItemsResponse{Processed: total}, nil
}
func main() {
// Create registry and register handlers
registry := aprot.NewRegistry()
handlers := &MyHandlers{}
registry.Register(handlers)
registry.RegisterPushEventFor(handlers, UserUpdatedEvent{})
registry.RegisterPushEventFor(handlers, ProcessingCompleteEvent{})
// Create server
server := aprot.NewServer(registry)
handlers.server = server
// Start HTTP server with WebSocket endpoint
mux := http.NewServeMux()
mux.Handle("/ws", server)
// http.ListenAndServe(":8080", mux)
fmt.Println("Server ready")
}
Output: Server ready
Example (DualTransport) ¶
This example shows how to set up a server with both WebSocket and SSE transports running simultaneously.
package main
import (
"context"
"fmt"
"net/http"
"github.com/marrasen/aprot"
)
// Request and response types
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessItemsRequest struct {
Items []string `json:"items"`
}
type ProcessItemsResponse struct {
Processed int `json:"processed"`
}
// Push event types
type UserUpdatedEvent struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessingCompleteEvent struct {
Count int `json:"count"`
}
// Handlers struct
type MyHandlers struct {
server *aprot.Server
}
// CreateUser handles user creation
func (h *MyHandlers) CreateUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) {
progress := aprot.Progress(ctx)
progress.Update(1, 2, "Validating...")
if req.Name == "" {
return nil, aprot.ErrInvalidParams("name is required")
}
progress.Update(2, 2, "Creating user...")
conn := aprot.Connection(ctx)
if conn != nil {
conn.Push(&UserUpdatedEvent{ID: "123", Name: req.Name})
}
return &CreateUserResponse{ID: "123", Name: req.Name}, nil
}
// ProcessItems demonstrates progress reporting with cancellation
func (h *MyHandlers) ProcessItems(ctx context.Context, req *ProcessItemsRequest) (*ProcessItemsResponse, error) {
progress := aprot.Progress(ctx)
total := len(req.Items)
for i, item := range req.Items {
select {
case <-ctx.Done():
return nil, aprot.ErrCanceled()
default:
}
progress.Update(i+1, total, fmt.Sprintf("Processing %s...", item))
}
h.server.Broadcast(&ProcessingCompleteEvent{Count: total})
return &ProcessItemsResponse{Processed: total}, nil
}
func main() {
registry := aprot.NewRegistry()
registry.Register(&MyHandlers{})
server := aprot.NewServer(registry)
mux := http.NewServeMux()
mux.Handle("/ws", server) // WebSocket
mux.Handle("/sse", server.HTTPTransport()) // SSE+HTTP
mux.Handle("/sse/", server.HTTPTransport()) // SSE sub-routes (rpc, cancel)
fmt.Println("Dual transport ready")
}
Output: Dual transport ready
Example (Generate) ¶
package main
import (
"context"
"fmt"
"io"
"github.com/marrasen/aprot"
)
// Request and response types
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessItemsRequest struct {
Items []string `json:"items"`
}
type ProcessItemsResponse struct {
Processed int `json:"processed"`
}
// Push event types
type UserUpdatedEvent struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessingCompleteEvent struct {
Count int `json:"count"`
}
// Handlers struct
type MyHandlers struct {
server *aprot.Server
}
// CreateUser handles user creation
func (h *MyHandlers) CreateUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) {
progress := aprot.Progress(ctx)
progress.Update(1, 2, "Validating...")
if req.Name == "" {
return nil, aprot.ErrInvalidParams("name is required")
}
progress.Update(2, 2, "Creating user...")
conn := aprot.Connection(ctx)
if conn != nil {
conn.Push(&UserUpdatedEvent{ID: "123", Name: req.Name})
}
return &CreateUserResponse{ID: "123", Name: req.Name}, nil
}
// ProcessItems demonstrates progress reporting with cancellation
func (h *MyHandlers) ProcessItems(ctx context.Context, req *ProcessItemsRequest) (*ProcessItemsResponse, error) {
progress := aprot.Progress(ctx)
total := len(req.Items)
for i, item := range req.Items {
select {
case <-ctx.Done():
return nil, aprot.ErrCanceled()
default:
}
progress.Update(i+1, total, fmt.Sprintf("Processing %s...", item))
}
h.server.Broadcast(&ProcessingCompleteEvent{Count: total})
return &ProcessItemsResponse{Processed: total}, nil
}
func main() {
// Create registry with handlers
registry := aprot.NewRegistry()
myHandlers := &MyHandlers{}
registry.Register(myHandlers)
// Register push events on the registry
registry.RegisterPushEventFor(myHandlers, UserUpdatedEvent{})
// Generate TypeScript to a file (or any io.Writer)
gen := aprot.NewGenerator(registry)
gen.GenerateTo(io.Discard)
fmt.Println("Generated")
}
Output: Generated
Index ¶
- Constants
- Variables
- func ApplyTransforms(v any) error
- func CancelCause(ctx context.Context) error
- func HTTPRequestFromContext(ctx context.Context) *http.Request
- func OnStreamComplete(ctx context.Context, hook StreamCompleteHook)
- func RegisterRefreshTrigger(ctx context.Context, keys ...string)
- func SQLNullGoKind(t reflect.Type, kindResolver func(reflect.Type) string) string
- func SQLNullTSType(t reflect.Type, typeResolver func(reflect.Type) string) string
- func TriggerRefresh(ctx context.Context, keys ...string)
- func TriggerRefreshNow(ctx context.Context, keys ...string)
- func ValidateTransformTags(t reflect.Type) error
- func WithTestConnection(ctx context.Context, id uint64) context.Context
- type Broadcaster
- type CancelReason
- type Components
- type ConfigMessage
- type Conn
- func (c *Conn) Context() context.Context
- func (c *Conn) Get(key any) any
- func (c *Conn) ID() uint64
- func (c *Conn) Info() ConnInfo
- func (c *Conn) Load(key any) (value any, ok bool)
- func (c *Conn) Push(data any) error
- func (c *Conn) RemoteAddr() string
- func (c *Conn) ServerBroadcaster() Broadcaster
- func (c *Conn) Set(key, value any)
- func (c *Conn) SetUserID(userID string)
- func (c *Conn) UserID() string
- type ConnInfo
- type ConnectHook
- type ConnectedMessage
- type DefaultNaming
- func (d DefaultNaming) ErrorMethodName(errorName string) string
- func (d DefaultNaming) FileName(groupName string) string
- func (d DefaultNaming) HandlerName(eventName string) string
- func (d DefaultNaming) HookName(name string) string
- func (d DefaultNaming) MethodName(name string) string
- func (d DefaultNaming) PathPrefix(groupName string) string
- func (d DefaultNaming) PathSegment(methodName string) string
- type DisconnectHook
- type EnumInfo
- type EnumValueInfo
- type ErrorCodeInfo
- type ErrorMessage
- type FieldError
- type Generator
- type GeneratorOptions
- type HTTPMethod
- type Handler
- type HandlerGroup
- type HandlerInfo
- type HandlerKind
- type IncomingMessage
- type JSONSchema
- type MarshalTSType
- type MediaType
- type MessageType
- type Middleware
- type NamingPlugin
- type OpenAPIGenerator
- type OpenAPIInfo
- type OpenAPISpec
- type Operation
- type OutputMode
- type ParamInfo
- type Parameter
- type PathItem
- type PlaygroundValidator
- type PreserveNaming
- func (p PreserveNaming) ErrorMethodName(errorName string) string
- func (p PreserveNaming) FileName(groupName string) string
- func (p PreserveNaming) HandlerName(eventName string) string
- func (p PreserveNaming) HookName(name string) string
- func (p PreserveNaming) MethodName(name string) string
- func (p PreserveNaming) PathPrefix(groupName string) string
- func (p PreserveNaming) PathSegment(methodName string) string
- type ProgressMessage
- type ProgressReporter
- type ProtocolError
- func ErrCanceled() *ProtocolError
- func ErrConnectionRejected(message string) *ProtocolError
- func ErrForbidden(message string) *ProtocolError
- func ErrInternal(cause error) *ProtocolError
- func ErrInvalidParams(reason string) *ProtocolError
- func ErrMethodNotFound(method string) *ProtocolError
- func ErrUnauthorized(message string) *ProtocolError
- func NewError(code int, message string) *ProtocolError
- func WrapError(code int, message string, cause error) *ProtocolError
- type PushEventInfo
- type PushMessage
- type RESTAdapter
- type RESTOption
- type Registry
- func (r *Registry) EnableREST(handler any)
- func (r *Registry) Enums() []EnumInfo
- func (r *Registry) ErrorCodes() []ErrorCodeInfo
- func (r *Registry) GenerateHooks() []func(results map[string]string, mode OutputMode)
- func (r *Registry) Get(method string) (*HandlerInfo, bool)
- func (r *Registry) GetEnum(t reflect.Type) *EnumInfo
- func (r *Registry) GetMiddleware(method string) []Middleware
- func (r *Registry) GroupMiddleware(groupName string) []Middleware
- func (r *Registry) Groups() map[string]*HandlerGroup
- func (r *Registry) Handlers() map[string]*HandlerInfo
- func (r *Registry) IsREST(groupName string) bool
- func (r *Registry) LookupError(err error) (int, bool)
- func (r *Registry) Methods() []string
- func (r *Registry) OnGenerate(hook func(results map[string]string, mode OutputMode))
- func (r *Registry) OnServerInit(hook func(s *Server))
- func (r *Registry) PushEvents() []PushEventInfo
- func (r *Registry) RESTGroups() map[string]bool
- func (r *Registry) Register(handler any, middleware ...Middleware)
- func (r *Registry) RegisterEnum(values any)
- func (r *Registry) RegisterEnumFor(handler any, values any)
- func (r *Registry) RegisterError(err error, name string)
- func (r *Registry) RegisterErrorCode(name string) int
- func (r *Registry) RegisterPushEventFor(handler any, dataType any)
- func (r *Registry) RegisterREST(handler any, middleware ...Middleware)
- func (r *Registry) SetValidator(v StructValidator)
- func (r *Registry) SharedEnums() []EnumInfo
- type Request
- type RequestBody
- type Response
- type ResponseMessage
- type RouteInfo
- type Server
- func (s *Server) Broadcast(data any)
- func (s *Server) ConnectionCount() int
- func (s *Server) ForEachConn(fn func(conn *Conn))
- func (s *Server) HTTPTransport() http.Handler
- func (s *Server) OnConnect(hook ConnectHook)
- func (s *Server) OnDisconnect(hook DisconnectHook)
- func (s *Server) OnStop(hook func())
- func (s *Server) PushToUser(userID string, data any)
- func (s *Server) Registry() *Registry
- func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (s *Server) SetCheckOrigin(f func(r *http.Request) bool)
- func (s *Server) Stop(ctx context.Context) error
- func (s *Server) TriggerRefresh(keys ...string)
- func (s *Server) Use(mw ...Middleware)
- func (s *Server) WebSocket() http.Handler
- type ServerOptions
- type StreamCompleteHook
- type StreamEndMessage
- type StreamItemMessage
- type StructValidator
- type TestPushConn
- type ValidateRule
Examples ¶
Constants ¶
const ( CodeParseError = -32700 CodeInvalidRequest = -32600 CodeMethodNotFound = -32601 CodeInvalidParams = -32602 CodeInternalError = -32603 CodeValidationFailed = -32604 CodeCanceled = -32800 CodeConnectionRejected = -32002 CodeForbidden = -32003 )
Standard error codes.
Variables ¶
var ( // ErrClientCanceled indicates the client explicitly canceled the request. ErrClientCanceled = &CancelReason{"client canceled"} // ErrConnectionClosed indicates the client disconnected. ErrConnectionClosed = &CancelReason{"connection closed"} // ErrServerShutdown indicates the server is shutting down. ErrServerShutdown = &CancelReason{"server shutdown"} )
Functions ¶
func ApplyTransforms ¶ added in v0.40.0
ApplyTransforms walks v (a struct or pointer to struct) and applies the operations declared in `transform:""` tags on its exported fields. It mutates the value in place.
Supported ops:
- trim strings.TrimSpace
- trimleft[=cutset] TrimLeft (default cutset: whitespace)
- trimright[=cutset] TrimRight (default cutset: whitespace)
- uppercase strings.ToUpper
- lowercase strings.ToLower
- removeempty ([]string only) drop empty elements
Ops apply in the order listed in the tag, so `transform:"trim,removeempty"` on a []string first trims each element and then drops the empties.
Non-struct inputs are a no-op. Unknown op names or type mismatches (e.g. removeempty on a non-slice field) return an *ProtocolError with CodeInvalidParams.
func CancelCause ¶
CancelCause returns the reason the request context was canceled. Returns nil if the context has not been canceled or no cause was set. The returned error will be one of ErrClientCanceled, ErrConnectionClosed, or ErrServerShutdown.
func HTTPRequestFromContext ¶ added in v0.37.0
HTTPRequestFromContext returns the *http.Request associated with a REST handler call. Returns nil if the context is not from a REST request.
func OnStreamComplete ¶ added in v0.39.0
func OnStreamComplete(ctx context.Context, hook StreamCompleteHook)
OnStreamComplete registers a callback that fires after a streaming handler's iterator has finished. Typically called from middleware before dispatching to the next handler, so the callback closure can capture the start time and emit a log entry once iteration finishes.
Example logging middleware that logs duration and item count for both unary and streaming handlers:
func Logging(next aprot.Handler) aprot.Handler {
return func(ctx context.Context, req *aprot.Request) (any, error) {
start := time.Now()
aprot.OnStreamComplete(ctx, func(err error, items int) {
slog.Info("stream done",
"method", req.Method,
"dur", time.Since(start),
"items", items,
"err", err)
})
result, err := next(ctx, req)
if info := aprot.HandlerInfoFromContext(ctx); info != nil &&
info.Kind != aprot.HandlerKindUnary {
// Streaming handler: hook will fire later (unless this is
// a preflight error, in which case log it here).
if err != nil {
slog.Error("stream preflight error",
"method", req.Method,
"dur", time.Since(start),
"err", err)
}
return result, err
}
slog.Info("unary done",
"method", req.Method, "dur", time.Since(start), "err", err)
return result, err
}
}
Calling OnStreamComplete on a unary handler's context is a no-op — the hooks slot is only populated for requests whose handler returns an iter.Seq shape.
func RegisterRefreshTrigger ¶
RegisterRefreshTrigger registers trigger keys that this subscription depends on. When called from a regular (non-subscribe) request, this is a no-op. Keys are variadic strings that form a composite trigger key.
func SQLNullGoKind ¶ added in v0.37.1
SQLNullGoKind returns the unwrapped Go kind string ("string", "int", "float", "bool") for database/sql nullable wrappers, parallel to SQLNullTSType. Returns "" if t is not a sql.Null type. kindResolver handles the generic Null[T] case and is typically goKindString.
This is the Zod-side companion to SQLNullTSType: rather than sniffing the TypeScript output for a " | null" suffix, we derive the unwrapped kind directly from reflection so fieldData carries the information explicitly.
func SQLNullTSType ¶
SQLNullTSType checks whether t is a database/sql nullable type (NullString, NullInt64, NullBool, NullFloat64, NullInt32, NullInt16, NullByte, NullTime, or the generic Null[T]) and returns the corresponding TypeScript type (e.g. "string | null"). Returns "" if t is not a sql.Null type.
typeResolver is called for the generic Null[T] case to convert the inner Go type to its TypeScript representation.
func TriggerRefresh ¶
TriggerRefresh queues a refresh for all subscriptions matching the given keys. Called from mutation handlers to notify subscribed clients of data changes. Triggers are batched per-request and deduplicated by subscription when the request handler completes. This is a no-op outside a request context — for background goroutines, cron jobs, or other out-of-request callers, use Server.TriggerRefresh instead.
func TriggerRefreshNow ¶
TriggerRefreshNow is like TriggerRefresh but flushes the refresh queue immediately instead of deferring until the handler returns. Use this in long-running handlers when you want subscribers to observe intermediate state transitions before the handler completes.
TriggerRefreshNow flushes every key queued so far (including keys passed to prior TriggerRefresh calls), not just the keys in this call. Subsequent TriggerRefresh / TriggerRefreshNow calls start with an empty queue, so a handler can fire multiple rounds of refreshes by calling TriggerRefreshNow at each state transition.
This is a no-op outside a request context and during subscription re-execution (cascading refreshes are prevented).
Concurrency: triggered subscription handlers run in their own goroutines, concurrently with the rest of the calling handler. If the calling handler mutates shared state that the subscription handler reads, the subscription must return a defensive copy (or otherwise be safe to read without coordination) to avoid data races. The same applies to any response marshaling — a shared slice or map returned by the subscription may still be marshaled after the caller continues executing.
func ValidateTransformTags ¶ added in v0.40.0
ValidateTransformTags statically checks every `transform:""` tag reachable from t (a struct type or pointer to one). It catches unknown op names, ops used on unsupported field kinds, and `removeempty` on anything other than `[]string` — all at registration time, so the problem surfaces when the server boots rather than on the first request that happens to hit the handler.
Non-struct types are a no-op; caller is responsible for passing the param type. Recursion into nested structs, *struct, and slices/arrays of struct (or *struct) elements mirrors the runtime walker. Cycles are broken by tracking visited types.
Types ¶
type Broadcaster ¶
type Broadcaster interface {
Broadcast(data any)
}
Broadcaster is an interface for broadcasting push events to all clients. The event name is derived from the Go type of data, which must have been registered via RegisterPushEventFor.
type CancelReason ¶
type CancelReason struct {
// contains filtered or unexported fields
}
CancelReason represents why a request context was canceled.
func (*CancelReason) Error ¶
func (r *CancelReason) Error() string
type Components ¶ added in v0.37.0
type Components struct {
Schemas map[string]*JSONSchema `json:"schemas,omitempty"`
}
Components holds reusable schema definitions.
type ConfigMessage ¶
type ConfigMessage struct {
Type MessageType `json:"type"`
ReconnectInterval int `json:"reconnectInterval,omitempty"`
ReconnectMaxInterval int `json:"reconnectMaxInterval,omitempty"`
ReconnectMaxAttempts int `json:"reconnectMaxAttempts,omitempty"`
}
ConfigMessage represents server-pushed configuration for the client.
type Conn ¶
type Conn struct {
// contains filtered or unexported fields
}
Conn represents a single client connection.
func Connection ¶
Connection returns the Connection from the context. Returns nil if not present.
func (*Conn) Context ¶
Context returns the context from the HTTP request. This is useful for accessing request-scoped values like zerolog loggers.
func (*Conn) Get ¶
Get retrieves a value previously stored with Set. Returns nil if the key was never set (or was set to nil). Use Load to distinguish between an unset key and a key set to nil. Safe for concurrent use.
func (*Conn) Load ¶
Load retrieves a value previously stored with Set. The ok result indicates whether the key was found. This follows the sync.Map convention and allows callers to distinguish between an unset key and a key explicitly set to nil. Safe for concurrent use.
func (*Conn) Push ¶
Push sends a push message to this connection. The event name is derived from the Go type of data, which must have been registered via RegisterPushEventFor.
func (*Conn) RemoteAddr ¶
RemoteAddr returns the remote address of the connection.
func (*Conn) ServerBroadcaster ¶
func (c *Conn) ServerBroadcaster() Broadcaster
ServerBroadcaster returns the server as a Broadcaster. This allows external packages to broadcast push events without exposing the *Server type directly.
func (*Conn) Set ¶
Set stores a value on the connection, keyed by an arbitrary key. This is useful for caching connection-scoped data (e.g. an authenticated principal) that persists for the connection's lifetime. The map is lazily initialized on first call, so connections that never call Set pay no cost. Keep stored values small — they live for the entire connection lifetime. Safe for concurrent use.
Example ¶
This example shows how to use connection-scoped state to store and retrieve per-connection data such as authenticated principals.
package main
import (
"context"
"fmt"
"github.com/marrasen/aprot"
)
// PublicHandlers has endpoints that require no authentication.
type PublicHandlers struct{}
func (h *PublicHandlers) Ping(ctx context.Context) (string, error) { return "pong", nil }
func main() {
type principalKey struct{}
registry := aprot.NewRegistry()
registry.Register(&PublicHandlers{})
server := aprot.NewServer(registry)
server.OnConnect(func(ctx context.Context, conn *aprot.Conn) error {
conn.Set(principalKey{}, "user_123")
return nil
})
// Later, in any handler or middleware:
// val := conn.Get(principalKey{}).(string)
// Use Load to distinguish "not set" from "set to nil":
// v, ok := conn.Load(principalKey{})
fmt.Println("Connection state configured")
}
Output: Connection state configured
type ConnInfo ¶
type ConnInfo struct {
RemoteAddr string
Header http.Header
Cookies []*http.Cookie
URL string
Host string
}
ConnInfo contains HTTP request information captured at connection time.
type ConnectHook ¶
ConnectHook is called when a new connection is established. Return an error to reject the connection.
type ConnectedMessage ¶
type ConnectedMessage struct {
Type MessageType `json:"type"`
ConnectionID string `json:"connectionId"`
}
ConnectedMessage is sent as the first SSE event to provide the connection ID.
type DefaultNaming ¶
type DefaultNaming struct {
FixAcronyms bool
}
DefaultNaming reproduces the current naming behavior. Set FixAcronyms to true to treat consecutive uppercase letters as a single word (e.g. "BulkXMLHandlers" → "bulk-xml-handlers" instead of "bulk-x-m-l-handlers").
func (DefaultNaming) ErrorMethodName ¶
func (d DefaultNaming) ErrorMethodName(errorName string) string
func (DefaultNaming) FileName ¶
func (d DefaultNaming) FileName(groupName string) string
func (DefaultNaming) HandlerName ¶
func (d DefaultNaming) HandlerName(eventName string) string
func (DefaultNaming) HookName ¶
func (d DefaultNaming) HookName(name string) string
func (DefaultNaming) MethodName ¶
func (d DefaultNaming) MethodName(name string) string
func (DefaultNaming) PathPrefix ¶ added in v0.37.0
func (d DefaultNaming) PathPrefix(groupName string) string
func (DefaultNaming) PathSegment ¶ added in v0.37.0
func (d DefaultNaming) PathSegment(methodName string) string
type DisconnectHook ¶
DisconnectHook is called when a connection is closed.
type EnumInfo ¶
type EnumInfo struct {
Name string // e.g., "StrState"
Type reflect.Type // the reflect.Type for lookup
IsString bool // true for string-based, false for int-based
Values []EnumValueInfo
}
EnumInfo describes a registered enum type.
type EnumValueInfo ¶
type EnumValueInfo struct {
Name string // e.g., "Pending"
Value any // e.g., "pending" (string) or 0 (int)
}
EnumValueInfo describes a single enum value.
type ErrorCodeInfo ¶
ErrorCodeInfo describes a custom error code for code generation.
type ErrorMessage ¶
type ErrorMessage struct {
Type MessageType `json:"type"`
ID string `json:"id"`
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
ErrorMessage represents an error response from server to client.
type FieldError ¶ added in v0.37.0
type FieldError struct {
Field string `json:"field"` // JSON field name
Tag string `json:"tag"` // validation tag that failed, e.g. "required", "min"
Value any `json:"value"` // the rejected value
Param string `json:"param"` // tag parameter, e.g. "3" for min=3
Message string `json:"message"` // human-readable message
}
FieldError describes a single field-level validation failure.
type Generator ¶
type Generator struct {
// contains filtered or unexported fields
}
Generator generates TypeScript client code from a registry.
func NewGenerator ¶
NewGenerator creates a new TypeScript generator.
func (*Generator) Generate ¶
Generate writes TypeScript client code for all handler groups. Returns a map of filename to content, or writes to OutputDir if set. Generates:
- client.ts: Base client with ApiClient, ApiError, ErrorCode, etc.
- {handler-name}.ts: Handler-specific interfaces and methods for each handler group
func (*Generator) GenerateTo ¶
GenerateTo writes TypeScript client code to a single writer. This combines all handler groups into one file (legacy behavior).
func (*Generator) WithOptions ¶
func (g *Generator) WithOptions(opts GeneratorOptions) *Generator
WithOptions sets generator options.
type GeneratorOptions ¶
type GeneratorOptions struct {
// OutputDir is the directory to write generated files to.
// If empty, files are written to current directory.
OutputDir string
// Mode specifies vanilla or react output.
Mode OutputMode
// Naming controls how Go names are transformed into TypeScript names.
// If nil, DefaultNaming{} is used (preserving current behavior).
Naming NamingPlugin
// Zod enables generation of Zod validation schemas alongside TypeScript interfaces.
// When enabled, {handler-name}.schema.ts files are generated for structs with validate tags.
Zod bool
}
GeneratorOptions configures the code generator.
type HTTPMethod ¶ added in v0.37.0
type HTTPMethod string
HTTPMethod represents an HTTP method.
const ( HTTPGet HTTPMethod = "GET" HTTPPost HTTPMethod = "POST" HTTPPut HTTPMethod = "PUT" HTTPPatch HTTPMethod = "PATCH" HTTPDelete HTTPMethod = "DELETE" )
type HandlerGroup ¶
type HandlerGroup struct {
Name string
Handlers map[string]*HandlerInfo
PushEvents []PushEventInfo
Enums []EnumInfo
// contains filtered or unexported fields
}
HandlerGroup contains all methods from a single handler struct.
func (*HandlerGroup) SourceDir ¶
func (g *HandlerGroup) SourceDir() string
SourceDir returns the directory containing the handler's Go source files. Discovered automatically via runtime.FuncForPC during Register().
type HandlerInfo ¶
type HandlerInfo struct {
Name string
Params []ParamInfo // handler parameters (after ctx), empty for no-params handlers
ResponseType reflect.Type
StructName string
IsVoid bool // true when handler returns only error
Kind HandlerKind // unary, stream, or stream2
// StreamKeyType is set only when Kind == HandlerKindStream2; it holds the
// key type K of iter.Seq2[K, V]. ResponseType holds the value type V.
StreamKeyType reflect.Type
// contains filtered or unexported fields
}
HandlerInfo contains metadata about a registered handler method.
func HandlerInfoFromContext ¶
func HandlerInfoFromContext(ctx context.Context) *HandlerInfo
HandlerInfoFromContext returns the HandlerInfo from the context. Returns nil if not present.
func (*HandlerInfo) Call ¶
Call invokes a unary handler with the given context and JSON params and returns its (response, error) pair. Streaming handlers must use CallStream. Params must be a JSON array (positional arguments) or empty/nil for no-params handlers.
func (*HandlerInfo) CallStream ¶ added in v0.38.0
func (info *HandlerInfo) CallStream(ctx context.Context, params jsontext.Value) (reflect.Value, error)
CallStream invokes a streaming handler and returns the raw reflect.Value of the returned iter.Seq / iter.Seq2. The caller is responsible for driving the iterator (typically via reflect.MakeFunc). If the handler returns a preflight error it is returned without invoking the iterator.
type HandlerKind ¶ added in v0.38.0
type HandlerKind uint8
HandlerKind categorizes a handler by its return shape so the dispatcher knows how to invoke and marshal its output.
const ( // HandlerKindUnary: func(ctx, ...) error | (*T, error) | (T, error) HandlerKindUnary HandlerKind = iota // HandlerKindStream: func(ctx, ...) (iter.Seq[T], error) HandlerKindStream // HandlerKindStream2: func(ctx, ...) (iter.Seq2[K, V], error) HandlerKindStream2 )
type IncomingMessage ¶
type IncomingMessage struct {
Type MessageType `json:"type"`
ID string `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Params jsontext.Value `json:"params,omitempty"`
}
IncomingMessage represents a message from client to server. Method uses qualified "Group.Method" format (e.g., "PublicHandlers.CreateUser").
type JSONSchema ¶ added in v0.37.0
type JSONSchema struct {
Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"`
Properties map[string]*JSONSchema `json:"properties,omitempty"`
Required []string `json:"required,omitempty"`
Items *JSONSchema `json:"items,omitempty"`
Enum []any `json:"enum,omitempty"`
Ref string `json:"$ref,omitempty"`
MinLength *int `json:"minLength,omitempty"`
MaxLength *int `json:"maxLength,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty"`
ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty"`
Pattern string `json:"pattern,omitempty"`
Description string `json:"description,omitempty"`
Nullable bool `json:"nullable,omitempty"`
AdditionalProperties *JSONSchema `json:"additionalProperties,omitempty"`
}
JSONSchema represents a JSON Schema object (subset used by OpenAPI 3.0).
type MarshalTSType ¶
type MarshalTSType struct {
TSType string // e.g. "string", "number", "boolean", "Record<string, number>", "string[]"
}
MarshalTSType is the result of inferring a TypeScript type from a Go type's JSON marshaling behavior.
func InferTypeFromMarshal ¶
func InferTypeFromMarshal(t reflect.Type) *MarshalTSType
InferTypeFromMarshal checks whether t implements json.Marshaler or encoding.TextMarshaler and, if so, marshals a zero value to determine the TypeScript type. Returns nil when the type does not implement either interface, when marshaling produces null, or when the type is an interface.
type MediaType ¶ added in v0.37.0
type MediaType struct {
Schema *JSONSchema `json:"schema"`
}
MediaType describes a media type with schema.
type MessageType ¶
type MessageType string
MessageType represents the type of protocol message.
const ( TypeRequest MessageType = "request" TypeCancel MessageType = "cancel" TypeResponse MessageType = "response" TypeError MessageType = "error" TypeProgress MessageType = "progress" TypePush MessageType = "push" TypeConfig MessageType = "config" TypeConnected MessageType = "connected" TypeSubscribe MessageType = "subscribe" TypeUnsubscribe MessageType = "unsubscribe" TypeStreamItem MessageType = "stream_item" TypeStreamEnd MessageType = "stream_end" )
type Middleware ¶
Middleware wraps a Handler to add cross-cutting behavior.
type NamingPlugin ¶
type NamingPlugin interface {
// FileName converts a handler group name (e.g. "PublicHandlers") to a
// filename stem (without extension). The generator appends ".ts".
FileName(groupName string) string
// MethodName converts a Go handler method name (e.g. "CreateUser") to a
// TypeScript function name (e.g. "createUser" or "CreateUser").
MethodName(name string) string
// HookName converts a Go handler/event name to a React hook name.
// Default: "use" + name (e.g. "useCreateUser").
HookName(name string) string
// HandlerName converts a push event name to an event handler callback name.
// Default: "on" + name (e.g. "onUserCreated").
HandlerName(eventName string) string
// ErrorMethodName converts a custom error code name to a type-guard method name.
// Default: "is" + name (e.g. "isNotFound").
ErrorMethodName(errorName string) string
// PathPrefix converts a handler group name to a URL path prefix.
// Default: "/" + kebab-case (e.g. "UserHandlers" → "/user-handlers").
PathPrefix(groupName string) string
// PathSegment converts a method name to a URL path segment.
// Default: kebab-case (e.g. "UpdateUser" → "update-user").
PathSegment(methodName string) string
}
NamingPlugin controls how Go names are transformed into TypeScript names during code generation and REST path construction.
type OpenAPIGenerator ¶ added in v0.37.0
type OpenAPIGenerator struct {
// contains filtered or unexported fields
}
OpenAPIGenerator generates an OpenAPI 3.0 spec from a Registry. Only handlers registered via RegisterREST are included in the spec.
func NewOpenAPIGenerator ¶ added in v0.37.0
func NewOpenAPIGenerator(registry *Registry, title, version string) *OpenAPIGenerator
NewOpenAPIGenerator creates an OpenAPI spec generator.
func (*OpenAPIGenerator) Generate ¶ added in v0.37.0
func (g *OpenAPIGenerator) Generate() (*OpenAPISpec, error)
Generate produces an OpenAPI 3.0 spec.
func (*OpenAPIGenerator) GenerateJSON ¶ added in v0.37.0
func (g *OpenAPIGenerator) GenerateJSON() ([]byte, error)
GenerateJSON produces the OpenAPI spec as formatted JSON bytes.
func (*OpenAPIGenerator) WithBasePath ¶ added in v0.37.0
func (g *OpenAPIGenerator) WithBasePath(path string) *OpenAPIGenerator
WithBasePath sets a prefix prepended to all paths in the generated spec. Use this when the API is mounted behind a proxy or at a non-root path.
Example:
oag.WithBasePath("/rest/api/v1.0")
// paths: "/rest/api/v1.0/todos/create-todo", etc.
func (*OpenAPIGenerator) WithNaming ¶ added in v0.37.0
func (g *OpenAPIGenerator) WithNaming(n NamingPlugin) *OpenAPIGenerator
WithNaming sets the naming plugin for path generation.
type OpenAPIInfo ¶ added in v0.37.0
OpenAPIInfo describes the API metadata.
type OpenAPISpec ¶ added in v0.37.0
type OpenAPISpec struct {
OpenAPI string `json:"openapi"`
Info OpenAPIInfo `json:"info"`
Paths map[string]*PathItem `json:"paths"`
Components *Components `json:"components,omitempty"`
}
OpenAPISpec represents an OpenAPI 3.0 document.
type Operation ¶ added in v0.37.0
type Operation struct {
OperationID string `json:"operationId"`
Tags []string `json:"tags,omitempty"`
Summary string `json:"summary,omitempty"`
Description string `json:"description,omitempty"`
Parameters []Parameter `json:"parameters,omitempty"`
RequestBody *RequestBody `json:"requestBody,omitempty"`
Responses map[string]Response `json:"responses"`
}
Operation represents a single API operation on a path.
type OutputMode ¶
type OutputMode string
OutputMode specifies the type of client code to generate.
const ( OutputVanilla OutputMode = "vanilla" OutputReact OutputMode = "react" )
type ParamInfo ¶
type ParamInfo struct {
Type reflect.Type // the actual parameter type (e.g., string, *CreateUserRequest)
Variadic bool // true for the last param if the method is variadic
}
ParamInfo describes a single handler parameter (after context.Context).
type Parameter ¶ added in v0.37.0
type Parameter struct {
Name string `json:"name"`
In string `json:"in"` // "path", "query", "header"
Required bool `json:"required"`
Schema *JSONSchema `json:"schema"`
}
Parameter represents a single operation parameter.
type PathItem ¶ added in v0.37.0
type PathItem struct {
Get *Operation `json:"get,omitempty"`
Post *Operation `json:"post,omitempty"`
Put *Operation `json:"put,omitempty"`
Patch *Operation `json:"patch,omitempty"`
Delete *Operation `json:"delete,omitempty"`
}
PathItem represents the operations on a single path.
type PlaygroundValidator ¶ added in v0.37.0
type PlaygroundValidator struct {
// contains filtered or unexported fields
}
PlaygroundValidator wraps github.com/go-playground/validator/v10.
func NewPlaygroundValidator ¶ added in v0.37.0
func NewPlaygroundValidator() *PlaygroundValidator
NewPlaygroundValidator creates a StructValidator backed by go-playground/validator.
func (*PlaygroundValidator) Validate ¶ added in v0.37.0
func (p *PlaygroundValidator) Validate() *validator.Validate
Validate returns the underlying validator for registering custom validations.
func (*PlaygroundValidator) ValidateStruct ¶ added in v0.37.0
func (p *PlaygroundValidator) ValidateStruct(v any) error
ValidateStruct validates a struct and returns a *ProtocolError on failure.
type PreserveNaming ¶
type PreserveNaming struct {
FixAcronyms bool
}
PreserveNaming keeps Go PascalCase method names unchanged in the generated TypeScript. Filenames still use kebab-case (filesystem convention).
func (PreserveNaming) ErrorMethodName ¶
func (p PreserveNaming) ErrorMethodName(errorName string) string
func (PreserveNaming) FileName ¶
func (p PreserveNaming) FileName(groupName string) string
func (PreserveNaming) HandlerName ¶
func (p PreserveNaming) HandlerName(eventName string) string
func (PreserveNaming) HookName ¶
func (p PreserveNaming) HookName(name string) string
func (PreserveNaming) MethodName ¶
func (p PreserveNaming) MethodName(name string) string
func (PreserveNaming) PathPrefix ¶ added in v0.37.0
func (p PreserveNaming) PathPrefix(groupName string) string
func (PreserveNaming) PathSegment ¶ added in v0.37.0
func (p PreserveNaming) PathSegment(methodName string) string
type ProgressMessage ¶
type ProgressMessage struct {
Type MessageType `json:"type"`
ID string `json:"id"`
Current *int `json:"current,omitempty"`
Total *int `json:"total,omitempty"`
Message string `json:"message,omitempty"`
}
ProgressMessage represents a request-level progress update from server to client.
type ProgressReporter ¶
type ProgressReporter interface {
// Update sends a progress update to the client.
Update(current, total int, message string)
}
ProgressReporter allows handlers to report progress during long operations.
func Progress ¶
func Progress(ctx context.Context) ProgressReporter
Progress returns the ProgressReporter from the context. Returns a no-op reporter if not present.
type ProtocolError ¶
type ProtocolError struct {
Code int
Message string
Cause error
Data any // optional structured data (e.g., []FieldError for validation errors)
}
ProtocolError represents an error that can be sent to the client.
func ErrConnectionRejected ¶
func ErrConnectionRejected(message string) *ProtocolError
ErrConnectionRejected returns a connection rejected error.
func ErrForbidden ¶
func ErrForbidden(message string) *ProtocolError
ErrForbidden returns a forbidden error.
func ErrInternal ¶
func ErrInternal(cause error) *ProtocolError
ErrInternal returns an internal error.
func ErrInvalidParams ¶
func ErrInvalidParams(reason string) *ProtocolError
ErrInvalidParams returns an invalid params error.
func ErrMethodNotFound ¶
func ErrMethodNotFound(method string) *ProtocolError
ErrMethodNotFound returns a method not found error.
func ErrUnauthorized ¶
func ErrUnauthorized(message string) *ProtocolError
ErrUnauthorized returns an unauthorized error.
func NewError ¶
func NewError(code int, message string) *ProtocolError
NewError creates a new protocol error.
func WrapError ¶
func WrapError(code int, message string, cause error) *ProtocolError
WrapError creates a new protocol error wrapping an existing error.
func (*ProtocolError) Error ¶
func (e *ProtocolError) Error() string
func (*ProtocolError) Unwrap ¶
func (e *ProtocolError) Unwrap() error
type PushEventInfo ¶
PushEventInfo describes a push event for code generation.
type PushMessage ¶
type PushMessage struct {
Type MessageType `json:"type"`
Event string `json:"event"`
Data any `json:"data"`
}
PushMessage represents a server-initiated push message.
type RESTAdapter ¶ added in v0.37.0
type RESTAdapter struct {
// contains filtered or unexported fields
}
RESTAdapter serves registered handlers over HTTP/REST. Only handlers registered via RegisterREST are exposed. It implements http.Handler and can be mounted on any stdlib-compatible router.
func NewRESTAdapter ¶ added in v0.37.0
func NewRESTAdapter(registry *Registry, opts ...RESTOption) *RESTAdapter
NewRESTAdapter creates an HTTP/REST adapter from a registry. Handlers are mapped to REST endpoints using naming conventions.
func (*RESTAdapter) Routes ¶ added in v0.37.0
func (a *RESTAdapter) Routes() []RouteInfo
Routes returns all computed routes for inspection or documentation.
func (*RESTAdapter) ServeHTTP ¶ added in v0.37.0
func (a *RESTAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler.
type RESTOption ¶ added in v0.37.0
type RESTOption func(*RESTAdapter)
RESTOption configures a RESTAdapter.
func WithRESTMiddleware ¶ added in v0.37.0
func WithRESTMiddleware(mw ...Middleware) RESTOption
WithRESTMiddleware adds middleware to all REST endpoints.
func WithRESTNaming ¶ added in v0.37.0
func WithRESTNaming(n NamingPlugin) RESTOption
WithRESTNaming sets the naming plugin for path generation.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds registered handlers and their methods.
func (*Registry) EnableREST ¶ added in v0.37.0
EnableREST marks an already-registered handler for REST/HTTP exposure in addition to WebSocket. Streaming handlers cannot be exposed via REST and will panic at registration time.
func (*Registry) Enums ¶
Enums returns all registered enum types across all handler groups and shared enums.
func (*Registry) ErrorCodes ¶
func (r *Registry) ErrorCodes() []ErrorCodeInfo
ErrorCodes returns all registered custom error codes.
func (*Registry) GenerateHooks ¶
func (r *Registry) GenerateHooks() []func(results map[string]string, mode OutputMode)
GenerateHooks returns the registered generation hooks.
func (*Registry) Get ¶
func (r *Registry) Get(method string) (*HandlerInfo, bool)
Get returns the handler info for the given method name.
func (*Registry) GetEnum ¶
GetEnum returns the EnumInfo for a registered enum type, or nil if not registered.
func (*Registry) GetMiddleware ¶
func (r *Registry) GetMiddleware(method string) []Middleware
GetMiddleware returns the middleware for a specific handler method.
func (*Registry) GroupMiddleware ¶ added in v0.37.0
func (r *Registry) GroupMiddleware(groupName string) []Middleware
GroupMiddleware returns the middleware for a handler group by group name. Works for both WS-registered and REST-only handlers.
func (*Registry) Groups ¶
func (r *Registry) Groups() map[string]*HandlerGroup
Groups returns all registered handler groups.
func (*Registry) Handlers ¶
func (r *Registry) Handlers() map[string]*HandlerInfo
Handlers returns all registered handler infos.
func (*Registry) IsREST ¶ added in v0.37.0
IsREST reports whether the named handler group was registered via RegisterREST.
func (*Registry) LookupError ¶
LookupError checks if an error matches any registered error mapping. Returns the code and true if found, or 0 and false if not.
func (*Registry) OnGenerate ¶
func (r *Registry) OnGenerate(hook func(results map[string]string, mode OutputMode))
OnGenerate registers a hook called after code generation. The hook receives the results map (filename → content) and the output mode. Hooks can modify existing entries or add new files.
func (*Registry) OnServerInit ¶
OnServerInit registers a hook called during NewServer after the server is constructed but before the run loop starts. This lets subpackages like tasks/ defer server-side setup to server creation time.
func (*Registry) PushEvents ¶
func (r *Registry) PushEvents() []PushEventInfo
PushEvents returns all registered push events.
func (*Registry) RESTGroups ¶ added in v0.37.0
RESTGroups returns the set of handler group names registered via RegisterREST.
func (*Registry) Register ¶
func (r *Registry) Register(handler any, middleware ...Middleware)
Register registers all valid handler methods from the given struct. Optional middleware will be applied to all methods in this handler. A valid handler method must accept context.Context as its first parameter (after the receiver), followed by any number of additional parameters of any type. It must return either error or (*T, error).
Example signatures:
func(ctx context.Context) error func(ctx context.Context) (*Resp, error) func(ctx context.Context, req *T) (*Resp, error) func(ctx context.Context, name string, age int) (*Resp, error) func(ctx context.Context, items ...string) error
Example:
registry.Register(&PublicHandlers{}) // No middleware
registry.Register(&UserHandlers{}, authMiddleware) // With auth
registry.Register(&AdminHandlers{}, authMiddleware, adminMiddleware)
func (*Registry) RegisterEnum ¶
RegisterEnum registers an enum type that is not tied to any handler group. The enum will be generated in a shared TypeScript file, importable by all handler files. Pass the result of a Values() function (e.g. EventTypeValues()).
Example:
registry.RegisterEnum(EventTypeValues()) registry.RegisterEnum(RSVPStatusValues())
func (*Registry) RegisterEnumFor ¶
RegisterEnumFor registers an enum type associated with a handler group for TypeScript generation. The enum will be generated in the handler's TypeScript file. Pass the handler instance (same one used in Register) and the result of the Values() function.
Example:
handler := &MyHandlers{}
registry.Register(handler)
registry.RegisterEnumFor(handler, StrStateValues()) // string-based
registry.RegisterEnumFor(handler, IntStatusValues()) // int-based with Stringer
Example ¶
This example shows how to register enums for TypeScript generation. String-based enums derive names by capitalizing values; int-based enums use the String() method.
package main
import (
"context"
"fmt"
"github.com/marrasen/aprot"
)
// Request and response types
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessItemsRequest struct {
Items []string `json:"items"`
}
type ProcessItemsResponse struct {
Processed int `json:"processed"`
}
// Push event types
type UserUpdatedEvent struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessingCompleteEvent struct {
Count int `json:"count"`
}
// Handlers struct
type MyHandlers struct {
server *aprot.Server
}
// CreateUser handles user creation
func (h *MyHandlers) CreateUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) {
progress := aprot.Progress(ctx)
progress.Update(1, 2, "Validating...")
if req.Name == "" {
return nil, aprot.ErrInvalidParams("name is required")
}
progress.Update(2, 2, "Creating user...")
conn := aprot.Connection(ctx)
if conn != nil {
conn.Push(&UserUpdatedEvent{ID: "123", Name: req.Name})
}
return &CreateUserResponse{ID: "123", Name: req.Name}, nil
}
// ProcessItems demonstrates progress reporting with cancellation
func (h *MyHandlers) ProcessItems(ctx context.Context, req *ProcessItemsRequest) (*ProcessItemsResponse, error) {
progress := aprot.Progress(ctx)
total := len(req.Items)
for i, item := range req.Items {
select {
case <-ctx.Done():
return nil, aprot.ErrCanceled()
default:
}
progress.Update(i+1, total, fmt.Sprintf("Processing %s...", item))
}
h.server.Broadcast(&ProcessingCompleteEvent{Count: total})
return &ProcessItemsResponse{Processed: total}, nil
}
func main() {
type Status string
const (
StatusActive Status = "active"
StatusExpired Status = "expired"
)
registry := aprot.NewRegistry()
handlers := &MyHandlers{}
registry.Register(handlers)
registry.RegisterEnumFor(handlers, []Status{StatusActive, StatusExpired})
fmt.Println("Enum registered")
}
Output: Enum registered
func (*Registry) RegisterError ¶
RegisterError registers a Go error with a name for code generation. When handlers return this error, it will be automatically converted to a ProtocolError with the assigned code. Codes are auto-assigned starting at 1000.
Example ¶
This example shows how to register Go errors for automatic conversion to typed TypeScript error codes.
package main
import (
"context"
"fmt"
"github.com/marrasen/aprot"
)
// Request and response types
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessItemsRequest struct {
Items []string `json:"items"`
}
type ProcessItemsResponse struct {
Processed int `json:"processed"`
}
// Push event types
type UserUpdatedEvent struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessingCompleteEvent struct {
Count int `json:"count"`
}
// Handlers struct
type MyHandlers struct {
server *aprot.Server
}
// CreateUser handles user creation
func (h *MyHandlers) CreateUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) {
progress := aprot.Progress(ctx)
progress.Update(1, 2, "Validating...")
if req.Name == "" {
return nil, aprot.ErrInvalidParams("name is required")
}
progress.Update(2, 2, "Creating user...")
conn := aprot.Connection(ctx)
if conn != nil {
conn.Push(&UserUpdatedEvent{ID: "123", Name: req.Name})
}
return &CreateUserResponse{ID: "123", Name: req.Name}, nil
}
// ProcessItems demonstrates progress reporting with cancellation
func (h *MyHandlers) ProcessItems(ctx context.Context, req *ProcessItemsRequest) (*ProcessItemsResponse, error) {
progress := aprot.Progress(ctx)
total := len(req.Items)
for i, item := range req.Items {
select {
case <-ctx.Done():
return nil, aprot.ErrCanceled()
default:
}
progress.Update(i+1, total, fmt.Sprintf("Processing %s...", item))
}
h.server.Broadcast(&ProcessingCompleteEvent{Count: total})
return &ProcessItemsResponse{Processed: total}, nil
}
func main() {
registry := aprot.NewRegistry()
handlers := &MyHandlers{}
registry.Register(handlers)
// Registered errors are auto-converted when returned from handlers.
// Codes are auto-assigned starting at 1000.
registry.RegisterError(context.DeadlineExceeded, "Timeout")
// Register code-only for manual use with NewError.
_ = registry.RegisterErrorCode("InsufficientBalance")
fmt.Println("Errors registered")
}
Output: Errors registered
func (*Registry) RegisterErrorCode ¶
RegisterErrorCode registers a custom error code name without an error mapping. Use this for errors that will be created manually with NewError().
func (*Registry) RegisterPushEventFor ¶
RegisterPushEventFor registers a push event associated with a specific handler. The handler must have been previously registered via Register(). The event name is the Go type name (e.g., UserCreatedEvent → "UserCreatedEvent"). Broadcasting an unregistered push type will panic.
Example:
registry.RegisterPushEventFor(publicHandlers, UserCreatedEvent{})
func (*Registry) RegisterREST ¶ added in v0.37.0
func (r *Registry) RegisterREST(handler any, middleware ...Middleware)
RegisterREST registers a handler for REST/HTTP only. The handler is NOT available via WebSocket — only through the REST adapter and OpenAPI generator.
Streaming handlers (iter.Seq / iter.Seq2 return shapes) cannot be exposed via REST and will panic at registration time.
To expose a handler via both WebSocket and REST, use Register + EnableREST:
registry.Register(&UserHandlers{}) // WebSocket only
registry.RegisterREST(&TodoHandlers{}) // REST only
registry.Register(&BothHandlers{}) // WebSocket...
registry.EnableREST(&BothHandlers{}) // ...and also REST
func (*Registry) SetValidator ¶ added in v0.37.0
func (r *Registry) SetValidator(v StructValidator)
SetValidator sets the struct validator used for automatic parameter validation. When set, struct parameters are validated before handler dispatch. Pass nil to disable validation.
Example:
registry.SetValidator(aprot.NewPlaygroundValidator())
func (*Registry) SharedEnums ¶
SharedEnums returns enum types registered via RegisterEnum (not tied to any handler group).
type Request ¶
type Request struct {
ID string // Request ID for correlation
Method string // Method name being called
Params jsontext.Value // Raw JSON parameters
}
Request contains information about the incoming request.
func RequestFromContext ¶
RequestFromContext returns the Request from the context. Returns nil if not present.
type RequestBody ¶ added in v0.37.0
type RequestBody struct {
Required bool `json:"required"`
Content map[string]MediaType `json:"content"`
}
RequestBody represents the request body.
type Response ¶ added in v0.37.0
type Response struct {
Description string `json:"description"`
Content map[string]MediaType `json:"content,omitempty"`
}
Response represents a single response.
type ResponseMessage ¶
type ResponseMessage struct {
Type MessageType `json:"type"`
ID string `json:"id"`
Result any `json:"result"`
}
ResponseMessage represents a successful response from server to client.
type RouteInfo ¶ added in v0.37.0
type RouteInfo struct {
HTTPMethod HTTPMethod
Pattern string // e.g., "GET /users/update-user/{id}"
Path string // e.g., "/users/update-user/{id}"
GroupName string
MethodName string
WireMethod string // e.g., "Users.UpdateUser"
PathParams []routeParam
BodyParam *ParamInfo
HandlerInfo *HandlerInfo
}
RouteInfo describes one HTTP endpoint derived from a handler method.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server manages WebSocket connections and handler dispatch.
func NewServer ¶
func NewServer(registry *Registry, opts ...ServerOptions) *Server
NewServer creates a new WebSocket server with the given registry. An optional ServerOptions can be passed to configure server behavior.
Example (Options) ¶
This example shows how to configure client reconnection behavior. The server sends this configuration to clients on connect.
package main
import (
"fmt"
"github.com/marrasen/aprot"
)
func main() {
registry := aprot.NewRegistry()
server := aprot.NewServer(registry, aprot.ServerOptions{
ReconnectInterval: 2000, // initial delay (ms)
ReconnectMaxInterval: 60000, // max delay (ms)
ReconnectMaxAttempts: 10, // 0 = unlimited
})
_ = server
fmt.Println("Server with options")
}
Output: Server with options
func (*Server) Broadcast ¶
Broadcast sends a push message to all connected clients. The event name is derived from the Go type of data, which must have been registered via RegisterPushEventFor.
func (*Server) ConnectionCount ¶
ConnectionCount returns the number of active connections.
func (*Server) ForEachConn ¶
ForEachConn iterates over all active connections under a read lock. The callback must not block or call methods that acquire the server's write lock.
func (*Server) HTTPTransport ¶
HTTPTransport returns an http.Handler for SSE+HTTP transport. Routes:
- GET / — SSE event stream
- POST /rpc — RPC calls
- POST /cancel — Request cancellation
func (*Server) OnConnect ¶
func (s *Server) OnConnect(hook ConnectHook)
OnConnect registers a hook to be called when a new connection is established. Hooks are called in the order they are registered. If a hook returns an error, the connection is rejected and subsequent hooks are not called.
Example ¶
This example shows how to validate connections and reject unauthorized clients using OnConnect. The stored principal can be checked in per-handler middleware.
package main
import (
"context"
"fmt"
"log"
"github.com/marrasen/aprot"
)
// Request and response types
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessItemsRequest struct {
Items []string `json:"items"`
}
type ProcessItemsResponse struct {
Processed int `json:"processed"`
}
// Push event types
type UserUpdatedEvent struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProcessingCompleteEvent struct {
Count int `json:"count"`
}
// Handlers struct
type MyHandlers struct {
server *aprot.Server
}
// CreateUser handles user creation
func (h *MyHandlers) CreateUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) {
progress := aprot.Progress(ctx)
progress.Update(1, 2, "Validating...")
if req.Name == "" {
return nil, aprot.ErrInvalidParams("name is required")
}
progress.Update(2, 2, "Creating user...")
conn := aprot.Connection(ctx)
if conn != nil {
conn.Push(&UserUpdatedEvent{ID: "123", Name: req.Name})
}
return &CreateUserResponse{ID: "123", Name: req.Name}, nil
}
// ProcessItems demonstrates progress reporting with cancellation
func (h *MyHandlers) ProcessItems(ctx context.Context, req *ProcessItemsRequest) (*ProcessItemsResponse, error) {
progress := aprot.Progress(ctx)
total := len(req.Items)
for i, item := range req.Items {
select {
case <-ctx.Done():
return nil, aprot.ErrCanceled()
default:
}
progress.Update(i+1, total, fmt.Sprintf("Processing %s...", item))
}
h.server.Broadcast(&ProcessingCompleteEvent{Count: total})
return &ProcessItemsResponse{Processed: total}, nil
}
func main() {
registry := aprot.NewRegistry()
registry.Register(&MyHandlers{})
server := aprot.NewServer(registry)
type principalKey struct{}
server.OnConnect(func(ctx context.Context, conn *aprot.Conn) error {
for _, cookie := range conn.Info().Cookies {
if cookie.Name == "session" {
// validate the session...
conn.SetUserID("user_123")
conn.Set(principalKey{}, "authenticated-user")
return nil
}
}
return nil // allow unauthenticated for public endpoints
})
server.OnDisconnect(func(ctx context.Context, conn *aprot.Conn) {
log.Printf("Client %d disconnected", conn.ID())
})
fmt.Println("Lifecycle hooks configured")
}
Output: Lifecycle hooks configured
func (*Server) OnDisconnect ¶
func (s *Server) OnDisconnect(hook DisconnectHook)
OnDisconnect registers a hook to be called when a connection is closed. Hooks are called in the order they are registered. The connection's UserID is still available when the hook is called.
func (*Server) OnStop ¶
func (s *Server) OnStop(hook func())
OnStop registers a hook called during Server.Stop after in-flight requests drain. Used by the tasks package to shut down the taskManager.
func (*Server) PushToUser ¶
PushToUser sends a push message to all connections for a specific user. The event name is derived from the Go type of data, which must have been registered via RegisterPushEventFor.
func (*Server) ServeHTTP ¶
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler for WebSocket upgrades.
func (*Server) SetCheckOrigin ¶
SetCheckOrigin sets the origin check function for the WebSocket upgrader.
func (*Server) Stop ¶
Stop gracefully shuts down the server. It rejects new connections, closes existing connections with a close frame, waits for in-flight requests to complete, and waits for disconnect hooks to finish. Returns nil on clean shutdown, or ctx.Err() if the context expires.
Example ¶
This example shows graceful shutdown with a timeout.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/marrasen/aprot"
)
func main() {
registry := aprot.NewRegistry()
server := aprot.NewServer(registry)
// In production, this would be triggered by a signal handler.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Stop(ctx); err != nil {
log.Printf("shutdown timed out: %v", err)
}
fmt.Println("Server stopped")
}
Output: Server stopped
func (*Server) TriggerRefresh ¶ added in v0.39.0
TriggerRefresh fires a refresh for all subscriptions matching the given keys, across every connection. Unlike the package-level TriggerRefresh — which batches within a request handler and flushes after the handler returns — this method flushes immediately and is safe to call from background goroutines, cron jobs, webhook fan-in, or any other out-of-request code path.
Matching subscriptions are re-executed once, each in its own goroutine, the same way request-scoped triggers are dispatched. Cascading refreshes remain prevented because subscription re-execution runs without a refresh queue in its context (TriggerRefresh calls from inside a re-executed subscription handler are no-ops).
Keys are variadic strings that form a single composite key, matching the convention used by RegisterRefreshTrigger and TriggerRefresh. To fire multiple distinct keys, call this method multiple times.
func (*Server) Use ¶
func (s *Server) Use(mw ...Middleware)
Use adds middleware to the chain. Middleware is executed in the order it is added.
Example ¶
This example shows how to define and apply middleware. Server-level middleware applies to all handlers; per-handler middleware applies only to the handlers registered in the same Register call.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/marrasen/aprot"
)
// PublicHandlers has endpoints that require no authentication.
type PublicHandlers struct{}
func (h *PublicHandlers) Ping(ctx context.Context) (string, error) { return "pong", nil }
// ProtectedHandlers has endpoints that require authentication.
type ProtectedHandlers struct{}
func (h *ProtectedHandlers) Secret(ctx context.Context) (string, error) { return "classified", nil }
func main() {
loggingMiddleware := func(next aprot.Handler) aprot.Handler {
return func(ctx context.Context, req *aprot.Request) (any, error) {
start := time.Now()
result, err := next(ctx, req)
log.Printf("[%s] %s took %v", req.ID, req.Method, time.Since(start))
return result, err
}
}
authMiddleware := func(next aprot.Handler) aprot.Handler {
return func(ctx context.Context, req *aprot.Request) (any, error) {
conn := aprot.Connection(ctx)
if _, ok := conn.Load(struct{}{}); !ok {
return nil, aprot.ErrUnauthorized("not authenticated")
}
return next(ctx, req)
}
}
registry := aprot.NewRegistry()
registry.Register(&PublicHandlers{}) // no handler middleware
registry.Register(&ProtectedHandlers{}, aprot.Middleware(authMiddleware)) // with auth
server := aprot.NewServer(registry)
server.Use(aprot.Middleware(loggingMiddleware)) // applies to all handlers
fmt.Println("Middleware configured")
}
Output: Middleware configured
type ServerOptions ¶
type ServerOptions struct {
// ReconnectInterval is the initial reconnect delay in milliseconds. Default: 1000
ReconnectInterval int
// ReconnectMaxInterval is the maximum reconnect delay in milliseconds. Default: 30000
ReconnectMaxInterval int
// ReconnectMaxAttempts is the maximum number of reconnect attempts. 0 = unlimited. Default: 0
ReconnectMaxAttempts int
}
ServerOptions configures the server behavior.
type StreamCompleteHook ¶ added in v0.39.0
StreamCompleteHook is invoked once per streaming request after the handler's iterator has finished executing — whether through clean completion, client cancellation, connection loss, server shutdown, a mid-stream panic, or a transport send failure.
Parameters:
- err is nil for clean completion, or the cause of termination otherwise. For cancellation-driven termination, err is the context cause sentinel: one of ErrClientCanceled, ErrConnectionClosed, or ErrServerShutdown — use errors.Is to distinguish. For panics it is a wrapped recover value. For transport failures it is the underlying transport error.
- items is the number of elements successfully yielded to the client. For iter.Seq2, each (key, value) pair counts as one.
Preflight errors — i.e. handlers that return (nil, err) before any iterator is produced — do NOT invoke the hook. They travel as the regular error return from the middleware chain and should be logged there, the same way unary handler errors are today.
type StreamEndMessage ¶ added in v0.38.0
type StreamEndMessage struct {
Type MessageType `json:"type"`
ID string `json:"id"`
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Data any `json:"data,omitempty"`
}
StreamEndMessage terminates a server-streamed iterator for a request. Code/Message/Data are set only on abnormal termination (handler panic, unexpected error). Clean completion (including client cancellation) produces an empty StreamEndMessage.
type StreamItemMessage ¶ added in v0.38.0
type StreamItemMessage struct {
Type MessageType `json:"type"`
ID string `json:"id"`
Item any `json:"item"`
}
StreamItemMessage carries one element of a server-streamed iterator.
For HandlerKindStream handlers the Item holds the element value. For HandlerKindStream2 handlers the Item is a 2-element JSON array [K, V].
type StructValidator ¶ added in v0.37.0
StructValidator validates struct parameters before handler dispatch. Implementations must return a *ProtocolError with CodeValidationFailed and structured FieldError data when validation fails.
type TestPushConn ¶
type TestPushConn struct {
Conn *Conn
// contains filtered or unexported fields
}
TestPushConn is a test-only Conn that records all messages sent via Push. Use NewTestPushConn to create one.
func NewTestPushConn ¶
func NewTestPushConn(id uint64, pushEvents ...any) *TestPushConn
NewTestPushConn creates a Conn backed by a recording transport and a Server whose registry has the given push events registered. This allows Conn.Push to work in tests. The returned TestPushConn provides access to captured messages.
func (*TestPushConn) Messages ¶
func (tc *TestPushConn) Messages() [][]byte
Messages returns all raw JSON messages sent through the connection.
func (*TestPushConn) WithContext ¶
func (tc *TestPushConn) WithContext(ctx context.Context) context.Context
WithTestPushConn returns a context carrying the conn from tc.
type ValidateRule ¶ added in v0.37.0
type ValidateRule struct {
Tag string // e.g., "required", "min", "email"
Param string // e.g., "3" for min=3, "" for email
}
ValidateRule represents a single parsed validation constraint.
func ParseValidateTag ¶ added in v0.37.0
func ParseValidateTag(tag string) []ValidateRule
ParseValidateTag splits a validate tag string into structured rules. For example: "required,gte=12,lte=110" -> [{Tag:"required"}, {Tag:"gte", Param:"12"}, {Tag:"lte", Param:"110"}]
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
gentestpkg
Package gentestpkg exists only to host test fixture types in a Go package whose name is different from "aprot", so that generate_test.go can exercise code paths where shared types and shared enums live in separate per-package shared TypeScript files and reference each other across packages.
|
Package gentestpkg exists only to host test fixture types in a Go package whose name is different from "aprot", so that generate_test.go can exercise code paths where shared types and shared enums live in separate per-package shared TypeScript files and reference each other across packages. |
|
Package tasks provides hierarchical task trees with progress tracking, output streaming, and both request-scoped and shared (broadcast) task systems for the github.com/marrasen/aprot framework.
|
Package tasks provides hierarchical task trees with progress tracking, output streaming, and both request-scoped and shared (broadcast) task systems for the github.com/marrasen/aprot framework. |