This repository has been archived by the owner on Feb 23, 2022. It is now read-only.
forked from danielgtaylor/huma
-
Notifications
You must be signed in to change notification settings - Fork 1
/
operation.go
312 lines (271 loc) · 8.9 KB
/
operation.go
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
package huma
import (
"context"
"fmt"
"net"
"net/http"
"reflect"
"strings"
"time"
"github.com/Jeffail/gabs/v2"
"github.com/istreamlabs/huma/schema"
)
// OperationInfo describes an operation. It contains useful information for
// logging, metrics, auditing, etc.
type OperationInfo struct {
ID string
URITemplate string
Summary string
Tags []string
}
// GetOperationInfo returns information about the current Huma operation. This
// will only be populated *after* routing has been handled, meaning *after*
// `next.ServeHTTP(w, r)` has been called in your middleware.
func GetOperationInfo(ctx context.Context) *OperationInfo {
if oi := ctx.Value(opIDContextKey); oi != nil {
return oi.(*OperationInfo)
}
return &OperationInfo{
ID: "unknown",
Tags: []string{},
}
}
// Operation represents an operation (an HTTP verb, e.g. GET / PUT) against
// a resource attached to a router.
type Operation struct {
resource *Resource
method string
id string
summary string
description string
params map[string]oaParam
requestContentType string
requestSchema *schema.Schema
requestSchemaOverride bool
requestModel reflect.Type
responses []Response
maxBodyBytes int64
bodyReadTimeout time.Duration
}
func newOperation(resource *Resource, method, id, docs string, responses []Response) *Operation {
summary, desc := splitDocs(docs)
return &Operation{
resource: resource,
method: method,
id: id,
summary: summary,
description: desc,
responses: responses,
// 1 MiB body limit by default
maxBodyBytes: 1024 * 1024,
// 15 second timeout by default
bodyReadTimeout: resource.router.defaultBodyReadTimeout,
}
}
func (o *Operation) toOpenAPI(components *oaComponents) *gabs.Container {
doc := gabs.New()
doc.Set(o.id, "operationId")
if o.summary != "" {
doc.Set(o.summary, "summary")
}
if o.description != "" {
doc.Set(o.description, "description")
}
// Request params
for _, param := range o.params {
if param.Internal {
// Skip documenting internal-only params.
continue
}
doc.ArrayAppend(param, "parameters")
}
// Request body
if o.requestSchema != nil {
ct := o.requestContentType
if ct == "" {
ct = "application/json"
}
ref := ""
if o.requestSchemaOverride {
ref = components.AddExistingSchema(o.requestSchema, o.id+"-request")
} else {
// Regenerate with ModeAll so the same model can be used for both the
// input and output when possible.
ref = components.AddSchema(o.requestModel, schema.ModeAll, o.id+"-request")
}
doc.Set(ref, "requestBody", "content", ct, "schema", "$ref")
}
// responses
for _, resp := range o.responses {
status := fmt.Sprintf("%v", resp.status)
doc.Set(resp.description, "responses", status, "description")
headers := resp.headers
for _, name := range headers {
// TODO: get header description from shared registry
//header := headerMap[name]
header := name
doc.Set(header, "responses", status, "headers", name, "description")
typ := "string"
for _, param := range o.params {
if param.In == inHeader && param.Name == name {
if param.Schema.Type != "" {
typ = param.Schema.Type
}
break
}
}
doc.Set(typ, "responses", status, "headers", name, "schema", "type")
}
if resp.model != nil {
ref := components.AddSchema(resp.model, schema.ModeAll, o.id+"-response")
doc.Set(ref, "responses", status, "content", resp.contentType, "schema", "$ref")
}
}
return doc
}
// MaxBodyBytes sets the max number of bytes that the request body size may be
// before the request is cancelled. The default is 1MiB.
func (o *Operation) MaxBodyBytes(size int64) {
o.maxBodyBytes = size
}
// NoMaxBody removes the body byte limit, which is 1MiB by default. Use this
// if you expect to stream the input request or need to handle very large
// request bodies.
func (o *Operation) NoMaxBody() {
o.maxBodyBytes = 0
}
// BodyReadTimeout sets the amount of time a request can spend reading the
// body, after which it times out and the request is cancelled. The default
// is 15 seconds.
func (o *Operation) BodyReadTimeout(duration time.Duration) {
o.bodyReadTimeout = duration
}
// NoBodyReadTimeout removes the body read timeout, which is 15 seconds by
// default. Use this if you expect to stream the input request or need to
// handle very large request bodies.
func (o *Operation) NoBodyReadTimeout() {
o.bodyReadTimeout = 0
}
// RequestSchema allows overriding the generated input body schema, giving you
// more control over documentation and validation.
func (o *Operation) RequestSchema(s *schema.Schema) {
o.requestSchema = s
o.requestSchemaOverride = true
}
// Run registers the handler function for this operation. It should be of the
// form: `func (ctx huma.Context)` or `func (ctx huma.Context, input)` where
// input is your input struct describing the input parameters and/or body.
func (o *Operation) Run(handler interface{}) {
if reflect.ValueOf(handler).Kind() != reflect.Func {
panic(fmt.Errorf("Handler must be a function taking a huma.Context and optionally a user-defined input struct, but got: %s for %s %s", handler, o.method, o.resource.path))
}
var register func(string, http.HandlerFunc)
switch o.method {
case http.MethodPost:
register = o.resource.mux.Post
case http.MethodHead:
register = o.resource.mux.Head
case http.MethodGet:
register = o.resource.mux.Get
case http.MethodPut:
register = o.resource.mux.Put
case http.MethodPatch:
register = o.resource.mux.Patch
case http.MethodDelete:
register = o.resource.mux.Delete
default:
panic(fmt.Errorf("Unknown HTTP verb: %s", o.method))
}
t := reflect.TypeOf(handler)
if t.Kind() == reflect.Func && t.NumIn() > 1 {
var err error
input := t.In(1)
// Get parameters
o.params = getParamInfo(input)
for k, v := range o.params {
if v.In == inPath {
// Confirm each declared input struct path parameter is actually a part
// of the declared resource path.
if !strings.Contains(o.resource.path, "{"+k+"}") {
panic(fmt.Errorf("Parameter '%s' not in URI path: %s", k, o.resource.path))
}
}
}
// Get body if present.
if body, ok := input.FieldByName("Body"); ok {
o.requestModel = body.Type
if o.requestSchema == nil {
o.requestSchema, err = schema.GenerateWithMode(body.Type, schema.ModeWrite, nil)
if err != nil {
panic(fmt.Errorf("unable to generate JSON schema: %w", err))
}
}
}
// It's possible for the inputs to generate a 400, so add it if it wasn't
// explicitly defined.
found400 := false
for _, r := range o.responses {
if r.status == http.StatusBadRequest {
found400 = true
break
}
}
if !found400 {
o.responses = append(o.responses, NewResponse(http.StatusBadRequest, http.StatusText(http.StatusBadRequest)).ContentType("application/problem+json").Model(&ErrorModel{}))
}
}
// Future improvement idea: use a sync.Pool for the input structure to save
// on allocations if the struct has a Reset() method.
register("/", func(w http.ResponseWriter, r *http.Request) {
// Update the operation info for loggers/metrics/etc middlware to use later.
opInfo := GetOperationInfo(r.Context())
opInfo.ID = o.id
opInfo.URITemplate = o.resource.path
opInfo.Summary = o.summary
opInfo.Tags = append([]string{}, o.resource.tags...)
ctx := &hcontext{
Context: r.Context(),
ResponseWriter: w,
r: r,
op: o,
}
// If there is no input struct (just a context), then the call is simple.
if simple, ok := handler.(func(Context)); ok {
simple(ctx)
return
}
// Otherwise, create a new input struct instance and populate it.
v := reflect.ValueOf(handler)
inputType := v.Type().In(1)
input := reflect.New(inputType)
// Limit the request body size.
if r.Body != nil {
if o.maxBodyBytes > 0 {
r.Body = http.MaxBytesReader(w, r.Body, o.maxBodyBytes)
}
}
// Set a read deadline for reading/parsing the input request body, but
// only for operations that have a request body model.
var conn net.Conn
if o.requestModel != nil && o.bodyReadTimeout > 0 {
if conn = GetConn(r.Context()); conn != nil {
conn.SetReadDeadline(time.Now().Add(o.bodyReadTimeout))
}
}
setFields(ctx, ctx.r, input, inputType)
resolveFields(ctx, "", input)
if ctx.HasError() {
ctx.WriteError(http.StatusBadRequest, "Error while parsing input parameters")
return
}
// Clear any body read deadline if one was set as the body has now been
// read in. The one exception is when the body is streamed in via an
// `io.Reader` so we don't reset the deadline for that.
if conn != nil && o.requestModel != readerType {
conn.SetReadDeadline(time.Time{})
}
// Call the handler with the context and newly populated input struct.
in := []reflect.Value{reflect.ValueOf(ctx), input.Elem()}
reflect.ValueOf(handler).Call(in)
})
}