forked from danielgtaylor/huma
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgroup.go
More file actions
197 lines (180 loc) · 7.03 KB
/
group.go
File metadata and controls
197 lines (180 loc) · 7.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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 := func(op *Operation) {
// If this came from unmodified convenience functions, we may need to
// regenerate the operation ID and summary as they are based on things
// like the path which may have changed.
if op.Metadata != nil {
// Copy so we don't modify the original map.
meta := make(map[string]any, len(op.Metadata))
for k, v := range op.Metadata {
meta[k] = v
}
op.Metadata = meta
// If the conveniences are set, we need to regenerate the operation ID and
// summary based on the new path. We also update the metadata to reflect
// the new generated operation ID and summary so groups of groups can
// continue to modify them as needed.
if op.Metadata["_convenience_id"] == op.OperationID {
op.OperationID = GenerateOperationID(op.Method, op.Path, op.Metadata["_convenience_id_out"])
op.Metadata["_convenience_id"] = op.OperationID
}
if op.Metadata["_convenience_summary"] == op.Summary {
op.Summary = GenerateSummary(op.Method, op.Path, op.Metadata["_convenience_summary_out"])
op.Metadata["_convenience_summary"] = op.Summary
}
}
// Call the final handler.
next(op)
}
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
}