这是indexloc提供的服务,不要输入任何密码
Skip to content

feat: groups #728

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions docs/docs/features/groups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
description: Group operations with common prefixes, middleware, transformers, and more.
---

# Groups

## Groups { .hidden }

Operations can be grouped under common route prefixes and share middleware, operation modifier functions, and response transformers. This is done using the `huma.Group` wrapper around a `huma.API` instance, which can then be passed to `huma.Register` and its convenience wrappers like `huma.Get`, `huma.Post`, etc.

```go
grp := huma.NewGroup(api, "/v1")
grp.UseMiddleware(authMiddleware)

huma.Get(grp, "/users", func(ctx context.Context, input *struct{}) (*UsersResponse, error) {
// ...
})
```

The above example will register a `GET /v1/users` operation with the `authMiddleware` running before the operation handler.

!!! info "Groups & Documentation"

Groups assume that `huma.Register` or one of its convenience wrappers is used to register operations. If you are not, then you may need to invoke `group.DocumentOperation(*huma.Operation)` to ensure that the operation is documented correctly.

## Group Features

Groups support the following features:

- One or more path prefixes for all operations in the group.
- Middleware that runs before each operation in the group.
- Operation modifiers that run at operation registration time.
- Response transformers that run after each operation in the group.

## Prefixes

Groups can have one or more path prefixes that are prepended to all operations in the group. This is useful for grouping related operations under a common prefix and is typically done with a single prefix.

```go
grp := huma.NewGroup(api, "/prefix1", "/prefix2", "...")
```

This is just a convenience for the following equivalent code:

```go
grp := huma.NewGroup(api)
grp.UseModifier(huma.PrefixModifier("/prefix1", "/prefix2", "..."))
```

The built-in [`huma.PrefixModifier`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#PrefixModifier) will adjust the operation's ID and tags when more than one prefix is used. If you with to customize this behavior, you can write your own operation modifier.

## Middleware

Middleware functions are run before each operation handler in the group. They can be used for common tasks like authentication, logging, and error handling. Middleware functions are registered using the `UseMiddleware` method on a group.

```go
grp.UseMiddleware(func(ctx huma.Context, next func(huma.Context)) {
// Do something before the operation runs
next(ctx)
})
```

## Operation Modifiers

Operation modifiers are functions that run at operation registration time. They can be used to modify the operation before it is registered. Operation modifiers are registered using the `UseModifier` method on a group.

```go
grp.UseModifier(func(op *huma.Operation, next func(*huma.Operation)) {
op.Summary = "A summary for all operations in this group"
op.Tags = []string{"my-tag"}
next(op)
})
```

There is also a simplified form you can use:

```go
grp.UseSimpleModifier(func(op *huma.Operation)) {
op.Summary = "A summary for all operations in this group"
op.Tags = []string{"my-tag"}
})
```

## Response Transformers

Response transformers are functions that run after each operation handler in the group. They can be used to modify the response before it is returned to the client. Response transformers are registered using the `UseResponseTransformer` method on a group.

```go
grp.UseTransformer(func(ctx huma.Context, status string, v any) (any, error) {
// Do something with the output
return output, nil
})
```

## Customizing Documentation

Groups implement [`huma.OperationDocumenter`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#OperationDocumenter) which bypasses the normal flow of documentation generation and instead calls a function. This allows you to customize the documentation for all operations in the group. You can override the `DocumentOperation` method to customize the documentation if needed:

```go
type MyGroup huma.Group

func (g *MyGroup) DocumentOperation(op *huma.Operation) {
g.ModifyOperation(op, func(op *huma.Operation) {
if documenter, ok := g.API.(huma.OperationDocumenter); ok {
// Support nested operation documenters (i.e. groups of groups).
documenter.DocumentOperation(op)
} else {
// Default behavior to add operations.
if op.Hidden {
return
}
g.OpenAPI().AddOperation(op)
}
})
}
```

## Dive Deeper

- Features
- [Operations](./operations.md) registration & workflows
- [Middleware](./middleware.md) for operations
- [Response Transformers](./response-transformers.md) to modify response bodies
- Reference
- [`huma.Register`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Register) register an operation
- [`huma.Middlewares`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Middlewares) list of middleware
- [`huma.Transformer`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Transformer) response transformers
- [`huma.OperationDocumenter`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#OperationDocumenter) to customize OpenAPI generation
- [`huma.API`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#API) the API instance
4 changes: 2 additions & 2 deletions docs/docs/features/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func MyMiddleware(ctx huma.Context, next func(huma.Context)) {

Then you can get the value in the handler context:

``` go title="handler.go"
```go title="handler.go"
huma.Get(api, "/greeting/{name}", func(ctx context.Context, input *struct{
Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
}) (*GreetingOutput, error) {
Expand Down Expand Up @@ -201,7 +201,7 @@ It's also possible for global middleware to run only for certain paths by checki

- Reference
- [`huma.Context`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Context) a router-agnostic request/response context
- [`huma.Middlewares`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Middlewares) the API instance
- [`huma.Middlewares`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Middlewares) list of middleware
- [`huma.ReadCookie`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#ReadCookie) reads a named cookie from a request
- [`huma.ReadCookies`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#ReadCookies) reads cookies from a request
- [`huma.WriteErr`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#WriteErr) function to write error responses
Expand Down
3 changes: 2 additions & 1 deletion docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ nav:
- "Model Validation": features/model-validation.md
- "Operations":
- "Operations": features/operations.md
- "Groups": features/groups.md
- "Requests":
- "Request Inputs": features/request-inputs.md
- "Validation": features/request-validation.md
Expand All @@ -46,8 +47,8 @@ nav:
- "Streaming": features/response-streaming.md
- "Transformers": features/response-transformers.md
- "Extra Packages":
- "Conditional Requests": features/conditional-requests.md
- "Auto PATCH Operations": features/auto-patch.md
- "Conditional Requests": features/conditional-requests.md
- "Server Sent Events (SSE)": features/server-sent-events-sse.md
- "Test Utilities": features/test-utilities.md
- "Clients":
Expand Down
170 changes: 170 additions & 0 deletions group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package huma

import (
"strings"
)

// OperationDocumenter is an interface that can be implemented by an API or
// group to document operations in the OpenAPI document. This bypasses the
// normal `huma.Register` logic and provides complete customization of how
// operations are documented.
type OperationDocumenter interface {
// DocumentOperation adds an operation to the OpenAPI document. This is
// called by `huma.Register` when a new operation is registered.
DocumentOperation(op *Operation)
}

// PrefixModifier provides a fan-out to register one or more operations with
// the given prefix for every one operation added to a group.
func PrefixModifier(prefixes []string) func(o *Operation, next func(*Operation)) {
return func(o *Operation, next func(*Operation)) {
for _, prefix := range prefixes {
modified := *o
if len(prefixes) > 1 && prefix != "" {
// If there are multiple prefixes, update the ID and tags so you can
// differentiate between them in clients & the UI.
friendlyPrefix := strings.ReplaceAll(strings.Trim(prefix, "/"), "/", "-")
modified.OperationID = friendlyPrefix + "-" + modified.OperationID
tags := append([]string{}, modified.Tags...)
modified.Tags = append(tags, friendlyPrefix)
}
modified.Path = prefix + modified.Path
next(&modified)
}
}
}

// groupAdapter is an Adapter wrapper that registers multiple operation handlers
// with the underlying adapter based on the group's prefixes.
type groupAdapter struct {
Adapter
group *Group
}

func (a *groupAdapter) Handle(op *Operation, handler func(Context)) {
a.group.ModifyOperation(op, func(op *Operation) {
a.Adapter.Handle(op, handler)
})
}

// Group is a collection of routes that share a common prefix and set of
// operation modifiers, middlewares, and transformers.
//
// This is useful for grouping related routes together and applying common
// settings to them. For example, you might create a group for all routes that
// require authentication.
type Group struct {
API
prefixes []string
adapter Adapter
modifiers []func(o *Operation, next func(*Operation))
middlewares Middlewares
transformers []Transformer
}

// NewGroup creates a new group of routes with the given prefixes, if any. A
// group enables a collection of operations to have the same prefix and share
// operation modifiers, middlewares, and transformers.
//
// grp := huma.NewGroup(api, "/v1")
// grp.UseMiddleware(authMiddleware)
//
// huma.Get(grp, "/users", func(ctx huma.Context, input *MyInput) (*MyOutput, error) {
// // Your code here...
// })
func NewGroup(api API, prefixes ...string) *Group {
group := &Group{API: api, prefixes: prefixes}
group.adapter = &groupAdapter{Adapter: api.Adapter(), group: group}
if len(prefixes) > 0 {
group.UseModifier(PrefixModifier(prefixes))
}
return group
}

func (g *Group) Adapter() Adapter {
return g.adapter
}

// DocumentOperation adds an operation to the OpenAPI document. This is called
// by `huma.Register` when a new operation is registered. All modifiers will be
// run on the operation before it is added to the OpenAPI document, so for
// groups with multiple prefixes this will result in multiple operations in the
// OpenAPI document.
func (g *Group) DocumentOperation(op *Operation) {
g.ModifyOperation(op, func(op *Operation) {
if documenter, ok := g.API.(OperationDocumenter); ok {
documenter.DocumentOperation(op)
} else {
if op.Hidden {
return
}
g.OpenAPI().AddOperation(op)
}
})
}

// UseModifier adds an operation modifier function to the group that will be run
// on all operations in the group. Use this to modify the operation before it is
// registered with the router or OpenAPI document. This behaves similar to
// middleware in that you should invoke `next` to continue the chain. Skip it
// to prevent the operation from being registered, and call multiple times for
// a fan-out effect.
func (g *Group) UseModifier(modifier func(o *Operation, next func(*Operation))) {
g.modifiers = append(g.modifiers, modifier)
}

// UseSimpleModifier adds an operation modifier function to the group that
// will be run on all operations in the group. Use this to modify the operation
// before it is registered with the router or OpenAPI document.
func (g *Group) UseSimpleModifier(modifier func(o *Operation)) {
g.modifiers = append(g.modifiers, func(o *Operation, next func(*Operation)) {
modifier(o)
next(o)
})
}

// ModifyOperation runs all operation modifiers in the group on the given
// operation, in the order they were added. This is useful for modifying an
// operation before it is registered with the router or OpenAPI document.
func (g *Group) ModifyOperation(op *Operation, next func(*Operation)) {
chain := next
for i := len(g.modifiers) - 1; i >= 0; i-- {
// Use an inline func to provide a closure around the index & chain.
func(i int, n func(*Operation)) {
chain = func(op *Operation) { g.modifiers[i](op, n) }
}(i, chain)
}
chain(op)
}

// UseMiddleware adds one or more middleware functions to the group that will be
// run on all operations in the group. Use this to add common functionality to
// all operations in the group, e.g. authentication/authorization.
func (g *Group) UseMiddleware(middlewares ...func(ctx Context, next func(Context))) {
g.middlewares = append(g.middlewares, middlewares...)
}

func (g *Group) Middlewares() Middlewares {
m := append(Middlewares{}, g.API.Middlewares()...)
return append(m, g.middlewares...)
}

// UseTransformer adds one or more transformer functions to the group that will
// be run on all responses in the group.
func (g *Group) UseTransformer(transformers ...Transformer) {
g.transformers = append(g.transformers, transformers...)
}

func (g *Group) Transform(ctx Context, status string, v any) (any, error) {
v, err := g.API.Transform(ctx, status, v)
if err != nil {
return v, err
}
for _, transformer := range g.transformers {
v, err = transformer(ctx, status, v)
if err != nil {
return v, err
}
}
return v, nil
}
Loading
Loading