expose

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 18 Imported by: 0

README

Godoc: https://pkg.go.dev/github.com/pbedat/expose

exposeRPC

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.

Example

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

Get 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"
    }
  ]
}

Operation Metadata

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

Agent discovery

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

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrApplication = errors.New("application error")
View Source
var JsonEncoding = Encoding{
	MimeType: "application/json",
	GetEncoder: func(w io.Writer) Encoder {
		enc := json.NewEncoder(w)
		return EncoderFunc(func(v any) error {
			return enc.Encode(v)
		})
	},
	GetDecoder: func(r io.Reader) Decoder {
		dec := json.NewDecoder(r)

		return DecoderFunc(func(v any) error {
			return dec.Decode(v)
		})
	},
}

Functions

func DefaultSchemaIdentifier

func DefaultSchemaIdentifier(t reflect.Type) string

DefaultSchemaIdentifier creates a schema identifier for the provided type `t` in the form of '<path>.<to>.<my>.<package>.<name>

func FilterSpec added in v0.3.0

func FilterSpec(spec openapi3.T, prefix string, depth int) openapi3.T

FilterSpec returns a reduced view of the provided spec for progressive discovery: paths are filtered by `prefix` and collapsed at `depth` path segments (counted relative to the prefix). Collapsed groups appear as stub path items without operations, carrying a description and an "x-expose-expand" extension with the query string that expands the group. Schemas that are no longer referenced by any remaining operation are removed from components/schemas. A depth <= 0 disables collapsing, so only the prefix filter applies. The result is a valid openapi document; the provided spec is not modified.

func GetErrCode

func GetErrCode(err error) (string, bool)

func ReflectSpec

func ReflectSpec(root openapi3.T, fns []Function, opts ...reflectSpecOpt) (openapi3.T, error)

ReflectSpec reflects all provided exposed functions `fns` and generates an openapi3 specification. The provided spec is the template for the resulting specification. Use it e.g. to define the spec info or additional schemas and operations

func SetErrCode

func SetErrCode(err error, code string) error

func ShortSchemaIdentifier

func ShortSchemaIdentifier(t reflect.Type) string

ShortSchemaIdentifier creates a schema identifier for the provided type `t` in the form of '<package.<name>'

func SkipExtractSubSchemas

func SkipExtractSubSchemas(skip ...bool) reflectSpecOpt

SkipExtractSubSchemas prevents the extraction sub schemas into compeonents/schemas while reflecting a spec

func WithSchemaCustomizers added in v0.2.0

func WithSchemaCustomizers(customizers ...SchemaCustomizer) reflectSpecOpt

WithSchemaCustomizers appends custom schema customizers to the reflection pipeline. Customizers run after setID but before the built-in mapper, custom type, and required-properties pipes. Multiple calls to WithSchemaCustomizers are cumulative.

func WithSchemaIdentifier

func WithSchemaIdentifier(namer SchemaIdentifier) reflectSpecOpt

WithSchemaIdentifier sets an alternative SchemaIdentifier. Default: DefaultSchemaIdentifier

func WithSchemaMapper

func WithSchemaMapper(mapper SchemaMapper) reflectSpecOpt

Types

type Decoder

type Decoder interface {
	Decode(v any) error
}

type DecoderFunc

type DecoderFunc func(v any) error

func (DecoderFunc) Decode

func (f DecoderFunc) Decode(v any) error

type Encoder

type Encoder interface {
	Encode(v any) error
}

type EncoderFunc

type EncoderFunc func(v any) error

func (EncoderFunc) Encode

func (f EncoderFunc) Encode(v any) error

type Encoding

type Encoding struct {
	MimeType   string
	GetDecoder func(r io.Reader) Decoder
	GetEncoder func(w io.Writer) Encoder
}

Encoding is used for content negotiating. Request arguments and response values are encoded and decoded with the encoding that is matching the `Content-Type` or `Accept` header.

type ErrWithCode

type ErrWithCode struct {
	// contains filtered or unexported fields
}

func (ErrWithCode) Code

func (e ErrWithCode) Code() string

func (ErrWithCode) Error

func (e ErrWithCode) Error() string

type ErrorHandler

type ErrorHandler func(w http.ResponseWriter, enc Encoder, err error) (handled bool)

ErrorHandler is called, when a exposed function returns an error. Returning `handled == true` cancels any further error handling.

type FuncOpt

type FuncOpt func(s *functionSettings)

func Deprecated added in v0.3.0

func Deprecated() FuncOpt

Deprecated marks the operation as deprecated in the openapi spec

func Description added in v0.3.0

func Description(description string) FuncOpt

Description sets the description of the operation in the openapi spec. Descriptions are the place to document behavior, that is not visible in the request/response schemas, e.g. side effects, preconditions or ordering constraints.

func Extension added in v0.3.0

func Extension(key string, value any) FuncOpt

Extension adds an arbitrary specification extension to the operation. Keys should be prefixed with "x-" to produce a valid openapi spec. The value must be serializable by the encoding used to serve the spec (JSON by default).

func OperationCustomizer added in v0.3.0

func OperationCustomizer(customize func(op *openapi3.Operation)) FuncOpt

OperationCustomizer registers a callback that can modify the reflected operation without restrictions. It runs after all other metadata has been applied.

func Summary added in v0.3.0

func Summary(summary string) FuncOpt

Summary sets the summary of the operation in the openapi spec

func Tags added in v0.3.0

func Tags(tags ...string) FuncOpt

Tags adds additional tags to the operation in the openapi spec. The module of the function is always added as a tag.

func Validate

func Validate(validate bool) FuncOpt

Validate enables the json schema validation for requests

type Function

type Function interface {
	// Name is the name of the exposed function.
	// The name is part of the operationId in the spec.
	Name() string
	// Module is a qualifier, used in the operationId and as tag in the operation.
	Module() string
	// Path is the actual path, where the function is registered
	Path() string
	// Req returns an empty instance of the functions request argument.
	// Used for schema reflection.
	Req() any
	// Res returns an empty instance of the functions result value.
	// Used for schema reflection.
	Res() any
	// Apply calls the actual function by decoding the http request and passing it to the function
	Apply(ctx context.Context, dec Decoder, spec openapi3.T) (any, error)
}

Function defines a function, that should be registered as RPC endpoint in the Handler. It carries all information, that is necessary to include it as an operation in the openapi spec of the Handler, as well as the actual function wrapped in `Apply`

func Describe added in v0.3.0

func Describe(fn Function, opts ...FuncOpt) Function

Describe wraps a Function and attaches additional operation metadata to it. This is useful for functions that were not created via Func and friends, e.g. functions registered with Struct:

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

Note: Describe only affects the openapi spec. Options that change the runtime behavior (e.g. Validate) are ignored, since the wrapped function handles the invocation.

func Func

func Func[TReq any, TRes any](
	mountpoint string,
	fn func(ctx context.Context, req TReq) (TRes, error), opts ...FuncOpt) Function

Func creates an Function that can be registered with the Handler. The provided `fn` is then callable at the provided path. If you want to expose a function without an input or output parameter, you can parametrize with Void, use FuncVoid or FuncNullary instead.

func FuncNullary

func FuncNullary[TRes any](mountpoint string, fn func(ctx context.Context) (TRes, error), opts ...FuncOpt) Function

FuncNullary creates an Function for functions without a request argument. See Func.

func FuncNullaryVoid

func FuncNullaryVoid(mountpoint string, fn func(ctx context.Context) error, opts ...FuncOpt) Function

FuncNullaryVoid creates an Function for functions without a request argument and return no result. See Func

func FuncVoid

func FuncVoid[TReq any](mountpoint string, fn func(ctx context.Context, req TReq) error, opts ...FuncOpt) Function

FuncVoid creates an Function for functions that do not return values. Shortcut for using Func with Void as request argument.

func Module added in v0.3.0

func Module(pathPrefix string, description string, fns ...Function) []Function

Module attaches a description to all provided functions, documenting the path section they live under. This keeps the documentation colocated with the route definitions:

expose.Module("/guestlist", "How the guest list works ...",
	expose.Func("/guestlist/add", Add),
	expose.Func("/guestlist/remove", Remove),
)

The description ends up as the tag description of the module in the openapi spec and as the description of the collapsed group in the discovery view (see FilterSpec). Structs registered with Struct can provide their module description by implementing `ModuleDoc() string` instead.

func Struct

func Struct(basePath string, v any, opts ...FuncOpt) []Function

Struct traverses the provided struct recursively and registers all public methods that match the function signatures supported by Func, FuncVoid, FuncNullary, or FuncNullaryVoid. The basePath is used as the prefix for all registered functions.

type Handler

type Handler struct {
	http.Handler
}

Handler handles RPC requests. See NewHandler

func NewHandler

func NewHandler(fns []Function, options ...HandlerOption) (*Handler, error)

NewHandler creates a http handler, that provides the exposed functions as HTTP POST endpoints. see Handler Requests and responses are encoded with JSON by default. The handler also provides the openapi spec at the path '/swagger.json'

When an exposed function returns an error, the handler will respond with HTTP status 500 Internal Server Error by default. When the error is (see errors.Is) an ErrApplication, the status 422 Unprocessable Entity will be returned instead. Errors can be marked with custom codes SetErrCode, which will be included in the error response. To customize the error handling further, a ErrorHandler can be provided.

type HandlerOption

type HandlerOption func(settings *handlerSettings)

func WithDefaultSpec

func WithDefaultSpec(spec *openapi3.T) HandlerOption

WithDefaultSpec allows you to define a base spec. The handler fills this base spec with the operations and schemas reflected from the exposed functions.

func WithEncodings

func WithEncodings(encodings ...Encoding) HandlerOption

WithEncodings registers additional encodings. Encodings are selected based on the provided "Content-Type" and "Accept" headers

func WithErrorHandler

func WithErrorHandler(h ErrorHandler) HandlerOption

WithErrorHandler registers a custom ErrorHandler

func WithPathPrefix

func WithPathPrefix(prefixPath string) HandlerOption

WithPathPrefix defines the path prefix of the handler. When using it with WithSwaggerUI, make sure that your `Servers` section in the default spec WithDefaultSpec adds this prefix as well

func WithReflection

func WithReflection(opts ...reflectSpecOpt) HandlerOption

WithReflection sets options for the schema reflection

func WithSwaggerJSONPath

func WithSwaggerJSONPath(path string) HandlerOption

WithSwaggerJSONPath overrides the default path (/swagger.json), where the spec is served

func WithSwaggerUI

func WithSwaggerUI(path string) HandlerOption

WithSwaggerUI, adds a SwaggerUI handler at the provided `path`

type Middleware

type Middleware func(next http.Handler) http.Handler

type ModuleDoc added in v0.3.0

type ModuleDoc struct {
	// Path is the path prefix of the module, e.g. "/guestlist"
	Path string
	// Description documents all endpoints below Path. It may contain markdown.
	Description string
}

ModuleDoc documents a path section ("module") of the api.

type ModuleDocumenter added in v0.3.0

type ModuleDocumenter interface {
	ModuleDocs() []ModuleDoc
}

ModuleDocumenter is implemented by Function values that carry documentation for the module(s) they belong to. ReflectSpec registers these docs as tag descriptions in the spec, where they are rendered as group headers by swagger ui and picked up by the discovery filter FilterSpec as the description of collapsed path groups.

type OperationAnnotator added in v0.3.0

type OperationAnnotator interface {
	AnnotateOperation(op *openapi3.Operation)
}

OperationAnnotator is implemented by Function values that carry metadata for their openapi operation. ReflectSpec calls AnnotateOperation after the operation has been reflected from the function signature.

type SchemaCustomizer added in v0.2.0

type SchemaCustomizer func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) (stop bool, err error)

SchemaCustomizer is a function that customizes an OpenAPI schema during reflection. It receives the field name, the Go reflect.Type, the struct tag, and the schema being built. Returning stop=true halts the customizer pipeline for this schema; no further customizers will run.

type SchemaIdentifier

type SchemaIdentifier func(t reflect.Type) string

TypeNamers are used to generate a schema identifier for a go type

type SchemaMapper

type SchemaMapper func(t reflect.Type) *openapi3.Schema

type SchemaProvider

type SchemaProvider interface {
	JSONSchema(gen *openapi3gen.Generator, schemas openapi3.Schemas) (*openapi3.SchemaRef, error)
}

SchemaProvider overrides the schema reflection with the provided custom type

type SwaggerUIHandler

type SwaggerUIHandler struct {
	http.Handler
}

func NewSwaggerUIHandler

func NewSwaggerUIHandler(defaultSpec openapi3.T, fns []Function) *SwaggerUIHandler

type Void

type Void struct{}

Void is a placeholder for input or output parameters. When an input parameter is Void. The function is treated as nullary. When the output paramtere is Void, the function is treated as function without a return parameter.

func (*Void) UnmarshalJSON

func (v *Void) UnmarshalJSON(b []byte) error

type WithCode

type WithCode interface {
	error
	Code() string
}

Directories

Path Synopsis
examples
01_starter command
03_ts_codegen command
04_fx command
05_struct command
exposefx contains helper methods to expose functions to a fx app and to provide the handler
exposefx contains helper methods to expose functions to a fx app and to provide the handler

Jump to

Keyboard shortcuts

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