Documentation
¶
Overview ¶
Package rpc is the transport-agnostic controller layer for Sov.
A consumer declares router structs and methods, then hands them to an Engine. The engine reflects the method signatures, maps wire names to Go methods, and dispatches incoming requests by router + method name.
type WidgetRouter struct {
Repo *widget.Repo
}
type CreateParams struct {
Name string `json:"name"`
}
func (r *WidgetRouter) Create(ctx *rpc.Context, p *CreateParams) (*Widget, error) {
if p.Name == "" {
return nil, rpc.BadRequest("name required")
}
return r.Repo.Create(ctx, p.Name)
}
engine := rpc.NewEngine()
engine.Register(&WidgetRouter{Repo: repo})
Wire shape (the only contract):
POST /rpc/{Router}/{method}
Body: {"args": [paramsObject]}
Resp 200: {"data": <result>}
Resp ≥400:{"error": {"message": "...", "code": "BAD_REQUEST"}}
The engine does not know about HTTP. Transport adapters in sov/rpc/httpx and sov/rpc/fiberx own the HTTP boundary; both call Engine.Dispatch. The gateway in sov/gateway wraps an Engine for in-process services (the modular-monolith case) AND a Resolver chain for remote services (the microservice case), simultaneously.
Auth is NOT the framework's concern. Consumers wire their own middleware that fills Context.User before the engine dispatches. Per-request integrity signing lives in sov/rpc/signing as a middleware that runs BEFORE the engine; it does not produce identity claims.
Index ¶
- Constants
- func DecodeEnvelope(body []byte, status int, out any) error
- func Handle[P any, R any](e *Engine, router, method string, fn func(ctx *Context, p *P) (R, error))
- func HandleErr[P any](e *Engine, router, method string, fn func(ctx *Context, p *P) error)
- func IsReservedMarker(name string) bool
- func MarshalError(e *Error) []byte
- func MarshalSuccess(data any) []byte
- func OperationTitle(name string) string
- func RequireSubject(c *Context) (string, error)
- func RouterTitle(name string) string
- func SplitRPCPath(p string) (router, method string, ok bool)
- func TSPreviewForMethod(entry *methodEntry) (request, response string)
- func UserFromContext(c *Context) (any, error)
- type Claims
- type Context
- type Engine
- func (e *Engine) Describe() []RouterDescriptor
- func (e *Engine) Dispatch(ctx *Context, router, method string, body []byte) (status int, respBody []byte)
- func (e *Engine) HardHiddenMethods(router string) []string
- func (e *Engine) HasRouter(router string) bool
- func (e *Engine) HiddenMethods(router string) []string
- func (e *Engine) Lookup(router, method string) (*methodEntry, bool)
- func (e *Engine) Methods(router string) []string
- func (e *Engine) PublicMethods(router string) []string
- func (e *Engine) Register(router any)
- func (e *Engine) Routers() []string
- type Error
- func BadRequest(msg string, args ...any) *Error
- func BadRequestCode(errorCode, msg string, args ...any) *Error
- func Conflict(msg string, args ...any) *Error
- func DecodeErrorBody(body []byte, status int) (e *Error, ok bool)
- func Forbidden(msg string, args ...any) *Error
- func ForbiddenCode(errorCode, msg string, args ...any) *Error
- func Internal(msg string, args ...any) *Error
- func NotFound(msg string, args ...any) *Error
- func NotImplemented(msg string, args ...any) *Error
- func TooManyRequests(msg string, args ...any) *Error
- func Unauthorized(msg string, args ...any) *Error
- type ErrorResponse
- type FieldInfo
- type FieldMap
- type HardHiddenLister
- type HiddenLister
- type MethodDescriptor
- type ParamField
- type PublicLister
- type Request
- type RouterDescriptor
- type SuccessResponse
Constants ¶
const ContextKeyClaims = "sov.claims"
ContextKeyClaims is the key under which the gateway stashes *Claims on rpc.Context.State during local dispatch. Handlers reach in via (*Context).Claims() — never type-assert the value directly.
Variables ¶
This section is empty.
Functions ¶
func DecodeEnvelope ¶
DecodeEnvelope is the canonical client-side response decoder. For status >= 400 it returns the decoded *Error (falling back to an INTERNAL error carrying the raw body when the envelope won't parse). For success it unmarshals the `data` field into out (a nil out, empty data, or null data is a no-op success).
func Handle ¶
Handle registers a typed RPC method with NO reflection in the dispatch hot path. The handler is called directly through a closure built once at boot — no reflect.Value.Call, no reflect.New per request. Reflection is used only here, at registration, to build the introspect descriptor and the param field map.
rpc.Handle(eng, "Chirp", "post",
func(ctx *rpc.Context, p *chirps.PostParams) (*chirps.Chirp, error) { ... })
Two wins over the reflective Register path:
- the handler signature is checked at COMPILE time (a wrong shape is a build error, not a boot panic); and
- dispatch skips method-invoke reflection (the part that hurt).
Field decoding still uses the boot-built FieldMap, so both wire arg shapes (positional + named) and `sov` tags work identically to Register. Handle and Register coexist on the same Engine; use Handle for hot methods you want type-checked and reflection-free to call.
For methods that return only an error, use HandleErr. No-arg methods are cheap to dispatch reflectively — keep them on Register, or pass a zero-field params struct.
func HandleErr ¶
HandleErr registers a typed method that returns only an error (no result body). Same boot-time, reflection-free dispatch as Handle.
func IsReservedMarker ¶
IsReservedMarker reports whether name is a framework marker or plugin hook method the reflection scanner skips rather than treating as an RPC method. Exposed so the gateway package's sanity test can assert that every plugin sub-interface method is covered here.
func MarshalError ¶
MarshalError builds the JSON body for an Error. Transport adapters use this when writing the response.
func MarshalSuccess ¶
MarshalSuccess builds the JSON body for a successful result.
func OperationTitle ¶
OperationTitle turns a Go method name into a product-facing label: "ListInvoices" → "List invoices".
func RequireSubject ¶
RequireSubject returns the subject id stashed on ctx.User by the gateway, or 401 UNAUTHORIZED if the request was anonymous. The canonical one-liner gate at the top of any handler that needs an authenticated caller:
uid, err := rpc.RequireSubject(ctx)
if err != nil { return nil, err }
Identity-required policy lives next to the method that needs it, not centralized in main(). The AuthzService (when bound on the gateway) is the other layer: it sees every request including anonymous ones and can return {Authenticate: true} to surface 401 before dispatch.
func RouterTitle ¶
RouterTitle turns a router wire name into a product-facing label: "Workspace" → "Workspace", "TicketKey" → "Ticket Key".
func SplitRPCPath ¶
SplitRPCPath parses a request path of the form /rpc/{router}/{method} and returns the router + method segments, or ok=false on anything else. Rejects paths where method contains a `/` (defense against service-level _X smuggling — the gateway treats /rpc/Foo/_x as a distinct, refused branch).
One canonical implementation; gateway, signing middleware, and any future transport adapter consume it instead of forking three nearly identical helpers.
func TSPreviewForMethod ¶
func TSPreviewForMethod(entry *methodEntry) (request, response string)
TSPreviewForMethod returns one-line TypeScript previews of the params type and the result type for an RPC method. Thin shim over tsrender.RenderInline — the shared renderer is the source of truth for both this preview path and the sovgen CLI's full `.d.ts` emission. Output unchanged from the pre-shim implementation.
func UserFromContext ¶
UserFromContext returns the authenticated user, or an Unauthorized Error if Context.User is nil. Routers reach for this rather than type-asserting Context.User directly so the error path is consistent.
Types ¶
type Claims ¶
type Claims struct {
Subject string `json:"sub"` // opaque caller id
Issuer string `json:"iss,omitempty"` // which AuthService minted this
Scopes []string `json:"scopes,omitempty"` // OAuth scope semantics — what powers the TOKEN grants
ExpiresAt time.Time `json:"exp"` // cache TTL upper bound
Extra map[string]any `json:"extra,omitempty"` // escape hatch (tenant_id, device_id, ...)
}
Claims is the verified caller identity the AuthService returns. Shape is fixed by the framework so every downstream service knows what to expect — both on the wire (X-Sov-* headers, decomposed by the gateway) and in-process (ctx.Claims()).
Claims is identity + delegation only. Authorization state (role memberships, RBAC matrix) is queried by the AuthzService at decision time, not stamped here at issue time. Stuffing roles into Claims turns ambient identity into ambient policy and makes hot-reload of RBAC impossible — keep them separate.
type Context ¶
Context is the per-request value handed to every router method. It embeds the standard library context.Context so it can be passed wherever a context.Context is expected.
User is the authenticated subject id (an opaque string the gateway resolved from the bearer token). The framework does not produce it — consumers wire whatever auth middleware they need (JWT, session cookie, upstream gateway headers) before dispatch and set it here. Handlers that need the subject call rpc.RequireSubject(ctx); handlers that want the full structured Claims call ctx.Claims().
State is a free-form bag for adapter- and consumer-specific values (database handles, fiber.Ctx, request id, etc.). The framework does not read it; it is provided so consumers do not need to subclass Context or thread their own context type through every handler.
func NewContext ¶
NewContext returns a Context wrapping ctx.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine holds the registered routers and dispatches incoming requests to the right Go method by reflection. Safe for concurrent dispatch. Mutations (Register) are expected at boot, not under load.
func (*Engine) Describe ¶
func (e *Engine) Describe() []RouterDescriptor
Describe returns one RouterDescriptor per registered router, in registration order. Each descriptor includes method signatures rendered as TypeScript-shaped previews and JSON-tagged params expanded to ParamField records.
The gateway's /rpc/_introspect endpoint stitches this output across every service in the resolver chain into one org-wide catalog.
func (*Engine) Dispatch ¶
func (e *Engine) Dispatch(ctx *Context, router, method string, body []byte) (status int, respBody []byte)
Dispatch is the transport-agnostic entry. Adapters parse HTTP path/body, build a *Context, then call Dispatch. The return is the wire status and the JSON envelope already encoded — adapters set Content-Type and write the bytes.
body is the raw request body, typically `{"args":[...]}` or `{"args":{...}}`. An empty body is treated as no-args so methods that take no params can be invoked with no body.
func (*Engine) HardHiddenMethods ¶
HardHiddenMethods returns the HARD-hidden wire method names the router declared via the HardHiddenLister marker, or nil.
func (*Engine) HiddenMethods ¶
HiddenMethods returns the SOFT-hidden wire method names the router declared via the HiddenLister marker, or nil.
func (*Engine) Methods ¶
Methods returns the wire method names registered on router, in sorted order.
func (*Engine) PublicMethods ¶
PublicMethods returns the wire method names the router declared via the PublicLister marker interface, or nil if the router did not declare any. Used by the gateway/authz to default-allow without per-line configuration.
func (*Engine) Register ¶
Register reflects on the given router pointer and exposes its exported methods over the wire. The router type's name minus the "Router" suffix is used as the wire namespace.
Accepted method signatures:
func (r *X) Foo(ctx *rpc.Context) error func (r *X) Foo(ctx *rpc.Context) (T, error) func (r *X) Foo(ctx *rpc.Context, p *Params) error func (r *X) Foo(ctx *rpc.Context, p *Params) (T, error)
Anything else panics at boot — fail fast, never at request time.
type Error ¶
type Error struct {
Message string `json:"message"`
Code string `json:"code,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
Status int `json:"-"`
}
Error is the canonical error type returned by router methods.
Status maps to the HTTP status code the transport adapter sets. Code is the UPPERCASE_SNAKE category (BAD_REQUEST, NOT_FOUND, ...); it surfaces as JSON `"code"`. ErrorCode is an optional stable application-level reason ("WORKSPACE_SLUG_IN_USE") for client branching; it surfaces as JSON `"error_code"`.
func BadRequest ¶
BadRequest returns 400 BAD_REQUEST.
func BadRequestCode ¶
BadRequestCode returns 400 BAD_REQUEST with a stable application error_code.
func DecodeErrorBody ¶
DecodeErrorBody parses an `{"error":{message,code,error_code}}` envelope into an *Error stamped with status. ok is false when body is not a valid JSON error envelope — the caller then supplies its own fallback. Shared by the client, the auth verifier, and rpctest so the error-envelope shape lives in one place.
func ForbiddenCode ¶
ForbiddenCode returns 403 FORBIDDEN with a stable application error_code.
func Internal ¶
Internal returns 500 INTERNAL. The Message is logged server-side; the transport adapter substitutes a generic message on the wire so internal detail does not leak.
func NotImplemented ¶
NotImplemented returns 501 NOT_IMPLEMENTED. Use for RPC stubs.
func TooManyRequests ¶
TooManyRequests returns 429 RATE_LIMITED.
func Unauthorized ¶
Unauthorized returns 401 UNAUTHORIZED.
type ErrorResponse ¶
type ErrorResponse struct {
Error errorBody `json:"error"`
}
ErrorResponse is the canonical failure envelope.
type FieldInfo ¶
type FieldInfo struct {
GoName string
WireName string // wire/JSON name
StructIdx int // index into reflect.Type.Field
Position int // -1 = no positional slot
Required bool
Omitempty bool
Deprecated bool
Type reflect.Type
// Human-facing metadata from the sov tag `key=value` pairs. None
// affect dispatch — they flow into Describe(), the explorer UI,
// and codegen JSDoc.
Title string // short label, e.g. "Username"
Desc string // one-line hint shown as placeholder / helper text
Doc string // long-form documentation surfaced as tooltip / JSDoc body
Example string // example value the explorer can pre-fill
}
FieldInfo is the per-field resolution of the tag grammar.
type FieldMap ¶
type FieldMap struct {
Type reflect.Type
Fields []FieldInfo // source order
ByName map[string]int // wire name → index into Fields
ByPos []int // position → index into Fields (-1 if no field at that position)
MaxPos int // highest positional slot, or -1 if no positional fields
// Internal / InternalHard are set by a blank sentinel field
// _ struct{} `sov:"internal"` → Internal (soft hide)
// _ struct{} `sov:"internal,hard"` → InternalHard (hard hide)
// marking the method that takes this params struct as hidden from
// introspection. Method-level directive, not a wire field.
Internal bool
InternalHard bool
}
FieldMap is the boot-time-resolved layout of one params (or result) struct. It decouples wire shape from Go source shape so:
- Field source order can be changed for cache alignment or readability without breaking clients.
- The same struct can decode from EITHER `args:[positional]` OR `args:{named}` — clients pick the form that suits them.
- Fields can be renamed (Go) while the wire name stays stable, or vice versa.
- Introspection emits per-field metadata (required, omitempty, deprecated, position) so codegen and the explorer UI render the right thing without re-reading struct tags at request time.
FieldMap is built once per (Type) at Register time, validated, and cached on the methodEntry. Hot path is map / slice lookup, no reflection on struct tags per request.
func BuildFieldMap ¶
BuildFieldMap parses `sov:` (with `json:` fallback) tags on t and returns a validated FieldMap. Errors are reported with full field context so callers can panic at boot with a clear message.
t must be a struct type. Pointer-to-struct callers should pass t.Elem().
type HardHiddenLister ¶
type HardHiddenLister interface {
HardHiddenMethods() []string
}
HardHiddenLister is the optional marker a router implements to HARD-hide methods: the named methods are stripped from EVERY introspect payload — not even the X-Sov-Introspect-Internal header reveals them. Use for endpoints only callers who already know the path should find.
SECURITY: hard-hide removes discoverability, NOT access. The endpoint is still live and callable; authz, not hiding, is the access boundary.
type HiddenLister ¶
type HiddenLister interface {
HiddenMethods() []string
}
HiddenLister is the optional marker a router implements to SOFT-hide methods: the named methods are omitted from the default introspect report (and the explorer / codegen / federation) but remain in the full payload served under the X-Sov-Introspect-Internal header, so the explorer's "show internal" toggle can reveal them. The engine reads the list once at Register and skips the marker method when reflecting.
Hiding is discoverability only — the methods stay dispatchable.
type MethodDescriptor ¶
type MethodDescriptor struct {
Method string `json:"method"` // wire name (camelCase) — URL segment
Title string `json:"title"` // product-facing label derived from goName
PostPath string `json:"postPath"` // /rpc/{Router}/{method}
HasParams bool `json:"hasParams"`
Params []ParamField `json:"params,omitempty"`
RequestTypeScript string `json:"requestTypeScript"`
ResponseTypeScript string `json:"responseTypeScript"`
// ResponseTypeName is the Go type name of the method's non-error
// return when it's a named struct (possibly via pointer/slice).
// Empty for primitive/map results. The type catalog uses it to tag
// the type's usage role as "response" (data-ownership inference).
ResponseTypeName string `json:"responseTypeName,omitempty"`
// Internal marks a SOFT-hidden method: omitted from the default
// introspect report, but present (with this flag set) in the full
// payload served under the X-Sov-Introspect-Internal header so the
// explorer's "show internal" toggle can reveal it.
Internal bool `json:"internal,omitempty"`
// HardHidden marks a method stripped from EVERY introspect payload —
// the framework auth/authz hooks and any author HardHiddenMethods().
// json:"-" because hard methods are removed before marshal; the flag
// only needs to survive Describe() → the gateway's strip pass within a
// single gateway and never crosses the wire.
HardHidden bool `json:"-"`
// NestedTypes are the named struct types referenced by Params
// (transitively). Lets the IntrospectReport.Types catalog include
// every type the generated client needs without losing reflect
// access at catalog-build time. Keyed by the Go type's Name.
NestedTypes map[string][]ParamField `json:"nestedTypes,omitempty"`
}
MethodDescriptor is one exported router method.
type ParamField ¶
type ParamField struct {
JSONName string `json:"jsonName"`
SchemaType string `json:"schemaType"` // OpenAPI-shaped: string|number|boolean|array|object
DesignerHint string `json:"designerHint,omitempty"` // short human label
Required bool `json:"required"` // sov:"required" VALIDATION intent — NOT wire presence
Position int `json:"position"` // -1 = no positional slot
Omitempty bool `json:"omitempty,omitempty"`
// Nullable is true when the Go field is a pointer — it may be absent or
// null on the wire. With Omitempty it drives codegen optionality: a
// field is OPTIONAL in the generated type iff it can be absent
// (Omitempty || Nullable); a non-omitempty non-pointer field is always
// present and so required. (Required is validation-only and does NOT
// imply presence — see the optionality note in WIRE_CONTRACT.)
Nullable bool `json:"nullable,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
TypeName string `json:"typeName,omitempty"` // Go type name when SchemaType=="object", OR the NAMED slice-element type when SchemaType=="array"
// ElemType is the element's OpenAPI schema when SchemaType=="array"
// (string|number|boolean|object|array). Lets codegen type a primitive
// slice ([]string → string[]) instead of falling back to unknown[].
// For arrays of named structs, TypeName carries the element type name
// and ElemType=="object".
ElemType string `json:"elemType,omitempty"`
// Human-facing metadata from the sov tag `key=value` pairs.
// Surfaced by the explorer UI + codegen JSDoc; ignored by dispatch.
Title string `json:"title,omitempty"`
Desc string `json:"desc,omitempty"`
Doc string `json:"doc,omitempty"`
Example string `json:"example,omitempty"`
}
ParamField describes one JSON field on a method's params object. Used by Explorer / codegen / OpenAPI emission downstream.
type PublicLister ¶
type PublicLister interface {
PublicMethods() []string
}
PublicLister is the optional marker a router implements to declare which methods are public (no authentication required). The engine reads the list once at Register, exposes it via PublicMethods(router) and Describe(), and skips the marker method itself when reflecting the RPC surface.
type Request ¶
type Request struct {
Args json.RawMessage `json:"args"`
}
Request is the canonical wire body. The `args` field accepts either of two shapes; clients pick per request:
- Positional: {"args":[v0, v1, v2]} — bound by sov tag position (or source order when no sov tag is present)
- Named: {"args":{"name":v, ...}} — bound by sov tag name (or json tag, or snake_case(GoFieldName))
Both decode into the same Go params struct; dispatch picks the path by inspecting the first non-whitespace byte (`[` vs `{`).
type RouterDescriptor ¶
type RouterDescriptor struct {
Router string `json:"router"` // wire name (URL segment)
Title string `json:"title"` // group label for explorers
Methods []MethodDescriptor `json:"methods"`
}
RouterDescriptor describes one registered router.
type SuccessResponse ¶
type SuccessResponse struct {
Data any `json:"data"`
}
SuccessResponse is the canonical success envelope.