diff --git a/README.md b/README.md index 7ebc85f2..409878c7 100644 --- a/README.md +++ b/README.md @@ -177,13 +177,40 @@ http.ListenAndServe(":8888", r) ### Middleware -Huma v1 came with its own middleware, but v2 does not. You can use any middleware you want, or even write your own. This is for two reasons: +Huma v2 has support two variants of middlewares: -1. Middleware is often router-specific and Huma is designed to be router-agnostic. -2. Many organizations already have a set of middleware for logging, metrics, distributed tracing, panic recovery, etc. +1. Router-specific - works at the router level, i.e. before router-specific middleware, you can use any middleware that is implemented for your router. +2. Router-agnostic - runs in the Huma processing chain, i.e. after calls to router-specific middleware. +#### Router-specific +Each router implementation has its own middlewares, you can use this middlewares with huma v2 framework. + +Chi router example: +```go +router := chi.NewMux() +router.Use(jwtauth.Verifier(tokenAuth)) +api := humachi.New(router, defconfig) +``` > :whale: Huma v1 middleware is compatible with Chi, so if you use that router with v2 you can continue to use the v1 middleware in a v2 application. + +#### Router-agnostic +You can write you own huma v2 middleware without dependency to router implementation. + +Example: +```go +func MyMiddleware(ctx huma.Context, next func(huma.Context)) { + // I don't do anything + next(ctx) +} +func NewHumaAPI() huma.API { + // ... + api := humachi.New(router, config) + // OR api := humagin.New(router, config) + api.UseMiddleware(MyMiddleware) +} +``` + ## Open API Generation & Extensibility Huma generates Open API 3.1.0 compatible JSON/YAML specs and provides rendered documentation automatically. Every operation that is registered with the API is included in the spec by default. The operation's inputs and outputs are used to generate the request and response parameters / schemas. diff --git a/api.go b/api.go index c176e498..304aa828 100644 --- a/api.go +++ b/api.go @@ -123,6 +123,17 @@ type API interface { // Unmarshal unmarshals the given data into the given value. The content type Unmarshal(contentType string, data []byte, v any) error + + // UseMiddleware appends a middleware handler to the API middleware stack. + // + // The middleware stack for any API will execute before searching for a matching + // route to a specific handler, which provides opportunity to respond early, + // change the course of the request execution, or set request-scoped values for + // the next Middleware. + UseMiddleware(middlewares ...func(ctx Context, next func(Context))) + + // Middlewares returns a slice of middleware handler functions. + Middlewares() Middlewares } // Format represents a request / response format. It is used to marshal and @@ -141,6 +152,7 @@ type api struct { formats map[string]Format formatKeys []string transformers []Transformer + middlewares Middlewares } func (a *api) Adapter() Adapter { @@ -202,6 +214,14 @@ func (a *api) Marshal(ctx Context, respKey string, ct string, v any) error { return f.Marshal(ctx.BodyWriter(), v) } +func (a *api) UseMiddleware(middlewares ...func(ctx Context, next func(Context))) { + a.middlewares = append(a.middlewares, middlewares...) +} + +func (a *api) Middlewares() Middlewares { + return a.middlewares +} + func NewAPI(config Config, a Adapter) API { newAPI := &api{ config: config, diff --git a/chain.go b/chain.go new file mode 100644 index 00000000..3df6434b --- /dev/null +++ b/chain.go @@ -0,0 +1,32 @@ +package huma + +type Middlewares []func(ctx Context, next func(Context)) + +// Handler builds and returns a handler func from the chain of middlewares, +// with `endpoint func` as the final handler. +func (m Middlewares) Handler(endpoint func(Context)) func(Context) { + return m.chain(endpoint) +} + +// wrap user middleware func with the next func to one func +func wrap(fn func(Context, func(Context)), next func(Context)) func(Context) { + return func(ctx Context) { + fn(ctx, next) + } +} + +// chain builds a Middleware composed of an inline middleware stack and endpoint +// handler in the order they are passed. +func (m Middlewares) chain(endpoint func(Context)) func(Context) { + // Return ahead of time if there aren't any middlewares for the chain + if len(m) == 0 { + return endpoint + } + + // Wrap the end handler with the middleware chain + w := wrap(m[len(m)-1], endpoint) + for i := len(m) - 2; i >= 0; i-- { + w = wrap(m[i], w) + } + return w +} diff --git a/huma.go b/huma.go index d4952c8b..dd5510fe 100644 --- a/huma.go +++ b/huma.go @@ -521,7 +521,7 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) a := api.Adapter() - a.Handle(&op, func(ctx Context) { + a.Handle(&op, api.Middlewares().Handler(func(ctx Context) { var input I // Get the validation dependencies from the shared pool. @@ -856,7 +856,7 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) } else { ctx.SetStatus(status) } - }) + })) } // AutoRegister auto-detects operation registration methods and registers them