Godoc: https://pkg.go.dev/github.com/pbedat/expose
exposeRPC allows you to create RPC interfaces, without the usual boilerplate. Methods can be exposed directly in Go code, without any further generation or definition steps. The resulting http interface provides an OpenAPI specification, that can be used to create type safe clients, to call the functions you exposed.
Expose the functions Inc and Get as RPC endpoints:
package main
import (
"context"
"log"
"net/http"
"sync/atomic"
"github.com/pbedat/expose"
)
var i = &atomic.Int32{}
func Inc(_ context.Context, delta int) (int, error) {
return int(i.Add(int32(delta))), nil
}
func Get(context.Context, expose.Void) (int, error) {
return int(i.Load()), nil
}
func main() {
h, err := expose.NewHandler(
[]expose.Function{
expose.Func("/counter/inc", Inc),
expose.Func("/counter/get", Get),
},
)
if err != nil {
panic(err)
}
http.Handle("/", h)
http.ListenAndServe(":8000", nil)
}Perform the RPC calls:
curl -H "content-type: application/json" --data 1 localhost:8000/rpc/counter/inc
curl -X POST localhost:8000/rpc/counter/get
> 1Get the OpenAPI Spec:
curl localhost:8000/rpc/swagger.json{
"components": {
"schemas": {
"int": {
"type": "integer"
}
}
},
"info": {
"title": "Starter Example",
"version": ""
},
"openapi": "3.0.2",
"paths": {
"/counter/get": {
"post": {
"operationId": "counter#get",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/int"
}
}
}
},
"default": {
"description": ""
}
},
"tags": ["counter"]
}
},
"/counter/inc": {
"post": {
"operationId": "counter#inc",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/int"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/int"
}
}
}
},
"default": {
"description": ""
}
},
"tags": ["counter"]
}
}
},
"servers": [
{
"url": "http://localhost:8000/rpc"
}
]
}Exposed functions can be annotated with the standard openapi operation fields (summary, description, tags, deprecated). Well written summaries and descriptions make the spec self describing, so consumers (e.g. AI agents) can figure out on their own which endpoints they need - and in which order - to achieve a given goal:
expose.Func("/orders/submit", submitOrder,
expose.Summary("Submit an order"),
expose.Description("Creates the order and reserves the stock. Requires a prior call to /cart/checkout."),
)Functions registered via expose.Struct can be annotated afterwards with expose.Describe:
fns := expose.Struct("/api", &myService{})
for i, fn := range fns {
if fn.Path() == "/api/orders/submit" {
fns[i] = expose.Describe(fn, expose.Summary("Submit an order"))
}
}For anything beyond the standard fields, expose.Extension adds custom
specification extensions and expose.OperationCustomizer gives full control
over the reflected operation:
expose.Func("/orders/submit", submitOrder,
expose.Extension("x-requires-auth", true),
expose.OperationCustomizer(func(op *openapi3.Operation) {
op.ExternalDocs = &openapi3.ExternalDocs{URL: "https://example.com/docs/orders"}
}))A full spec of a large service can be too much context for an agent to take in at once.
The spec endpoint (default /swagger.json) therefore supports progressive discovery
via the query parameters paths.prefix and paths.depth:
# compact overview: only the top level, everything below is collapsed
curl "localhost:8000/rpc/swagger.json?paths.depth=1"Collapsed path groups are returned as stubs without operations. They carry a
description and an x-expose-expand hint with the query string, that expands them:
"/guestlist": {
"description": "Manage the guestlist of an event.\n\n7 operations",
"x-expose-expand": "?paths.prefix=/guestlist&paths.depth=1"
}# drill down into a section
curl "localhost:8000/rpc/swagger.json?paths.prefix=/guestlist&paths.depth=1"Every response is a valid OpenAPI document and contains only the schemas, that are referenced by the paths it actually includes. Without the parameters the full spec is served as before.
Use expose.Module to document a whole path section. The description shows up as
the group header in Swagger UI and as the description of the discovery stubs, so an
agent can tell from the overview alone, which section it needs to expand:
fns := expose.Module("/guestlist", "Manage the guestlist of an event.",
expose.Func("/guestlist/add", Add),
expose.Func("/guestlist/vip/upgrade", Upgrade),
)Module wraps the functions, so the documentation travels with the route definitions.
Nested modules are supported - just wrap an inner expose.Module(...) again.
Services registered with expose.Struct document their section by implementing
ModuleDoc() string. Nested struct fields can do the same and get their own section:
type Guestlist struct {
Vip VipService
}
func (Guestlist) ModuleDoc() string { return "Manage the guestlist of an event." }More examples: https://github.com/pbedat/expose/tree/main/examples