2020-08-26 22:25:47 -07:00
package huma
import (
2021-05-03 22:31:01 +02:00
"context"
2020-08-26 22:25:47 -07:00
"fmt"
2021-08-04 19:49:58 +02:00
"net"
2020-08-26 22:25:47 -07:00
"net/http"
"reflect"
2020-09-04 21:10:40 -07:00
"strings"
2020-08-26 22:25:47 -07:00
"time"
"github.com/Jeffail/gabs/v2"
"github.com/danielgtaylor/huma/schema"
)
2021-05-03 22:31:01 +02:00
// 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 { } ,
}
}
2020-08-26 22:25:47 -07:00
// Operation represents an operation (an HTTP verb, e.g. GET / PUT) against
// a resource attached to a router.
type Operation struct {
2021-03-19 17:18:54 +01:00
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
2020-08-26 22:25:47 -07:00
}
2020-09-17 20:45:12 -07:00
func newOperation ( resource * Resource , method , id , docs string , responses [ ] Response ) * Operation {
summary , desc := splitDocs ( docs )
2020-08-26 22:25:47 -07:00
return & Operation {
resource : resource ,
method : method ,
id : id ,
2020-09-17 20:45:12 -07:00
summary : summary ,
description : desc ,
2020-08-26 22:25:47 -07:00
responses : responses ,
// 1 MiB body limit by default
maxBodyBytes : 1024 * 1024 ,
// 15 second timeout by default
2021-08-03 22:04:46 +02:00
bodyReadTimeout : resource . router . defaultBodyReadTimeout ,
2020-08-26 22:25:47 -07:00
}
}
2020-12-18 16:21:01 -08:00
func ( o * Operation ) toOpenAPI ( components * oaComponents ) * gabs . Container {
2020-08-26 22:25:47 -07:00
doc := gabs . New ( )
2020-09-17 20:45:12 -07:00
doc . Set ( o . id , "operationId" )
if o . summary != "" {
doc . Set ( o . summary , "summary" )
}
2020-08-26 22:25:47 -07:00
if o . description != "" {
2020-09-17 20:45:12 -07:00
doc . Set ( o . description , "description" )
2020-08-26 22:25:47 -07:00
}
2020-08-27 23:01:58 -07:00
// Request params
for _ , param := range o . params {
if param . Internal {
// Skip documenting internal-only params.
continue
}
doc . ArrayAppend ( param , "parameters" )
}
2020-08-26 22:25:47 -07:00
// Request body
if o . requestSchema != nil {
ct := o . requestContentType
if ct == "" {
ct = "application/json"
}
2021-03-19 17:18:54 +01:00
ref := ""
if o . requestSchemaOverride {
2022-08-19 23:49:30 +08:00
ref = components . AddExistingSchema ( o . requestSchema , o . id + "-request" , ! o . resource . router . disableSchemaProperty )
2021-03-19 17:18:54 +01:00
} else {
// Regenerate with ModeAll so the same model can be used for both the
// input and output when possible.
2022-08-19 23:49:30 +08:00
ref = components . AddSchema ( o . requestModel , schema . ModeAll , o . id + "-request" , ! o . resource . router . disableSchemaProperty )
2021-03-19 17:18:54 +01:00
}
2021-01-04 11:01:56 -08:00
doc . Set ( ref , "requestBody" , "content" , ct , "schema" , "$ref" )
2020-08-26 22:25:47 -07:00
}
// responses
2022-03-18 22:47:33 -07:00
for i , resp := range o . responses {
2020-08-26 22:25:47 -07:00
status := fmt . Sprintf ( "%v" , resp . status )
2022-04-21 23:24:04 -07:00
if resp . status == 0 {
status = "default"
}
2020-08-26 22:25:47 -07:00
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
2020-09-17 13:25:43 -07:00
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" )
2020-08-26 22:25:47 -07:00
}
if resp . model != nil {
2022-08-19 23:49:30 +08:00
ref := components . AddSchema ( resp . model , schema . ModeAll , o . id + "-response" , ! o . resource . router . disableSchemaProperty )
2022-03-18 22:47:33 -07:00
o . responses [ i ] . modelRef = ref
2020-12-18 16:21:01 -08:00
doc . Set ( ref , "responses" , status , "content" , resp . contentType , "schema" , "$ref" )
2020-08-26 22:25:47 -07:00
}
}
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
}
2021-03-11 10:52:22 -08:00
// 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
2021-03-19 17:18:54 +01:00
o . requestSchemaOverride = true
2021-03-11 10:52:22 -08:00
}
2020-08-26 22:25:47 -07:00
// 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 { } ) {
2020-09-04 21:46:53 -07:00
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 ) )
}
2020-08-26 22:25:47 -07:00
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
2020-09-25 10:51:26 -07:00
default :
panic ( fmt . Errorf ( "Unknown HTTP verb: %s" , o . method ) )
2020-08-26 22:25:47 -07:00
}
t := reflect . TypeOf ( handler )
if t . Kind ( ) == reflect . Func && t . NumIn ( ) > 1 {
var err error
input := t . In ( 1 )
// Get parameters
2020-08-27 23:01:58 -07:00
o . params = getParamInfo ( input )
2020-09-04 21:10:40 -07:00
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 ) )
}
}
}
2020-08-26 22:25:47 -07:00
2022-04-12 13:20:58 -07:00
possible := [ ] int { http . StatusBadRequest }
if _ , ok := input . FieldByName ( "Body" ) ; ok || len ( o . params ) > 0 {
// Invalid parameter values or body values can cause a 422.
possible = append ( possible , http . StatusUnprocessableEntity )
}
2020-08-26 22:25:47 -07:00
// Get body if present.
if body , ok := input . FieldByName ( "Body" ) ; ok {
2021-01-04 11:01:56 -08:00
o . requestModel = body . Type
2022-04-12 13:20:58 -07:00
possible = append ( possible ,
http . StatusRequestEntityTooLarge ,
http . StatusRequestTimeout ,
)
2021-03-11 10:52:22 -08:00
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 ) )
}
2020-08-26 22:25:47 -07:00
}
}
2020-08-31 22:05:34 -07:00
2022-04-12 13:20:58 -07:00
// It's possible for the inputs to generate a few different errors, so
// generate them if not already present.
found := map [ int ] bool { }
2020-08-31 22:05:34 -07:00
for _ , r := range o . responses {
2022-04-12 13:20:58 -07:00
found [ r . status ] = true
2020-08-31 22:05:34 -07:00
}
2022-04-12 13:20:58 -07:00
for _ , s := range possible {
if ! found [ s ] {
o . responses = append ( o . responses , NewResponse ( s , http . StatusText ( s ) ) . ContentType ( "application/problem+json" ) . Model ( & ErrorModel { } ) )
}
2020-08-31 22:05:34 -07:00
}
2020-08-26 22:25:47 -07:00
}
2020-08-29 14:29:37 -07:00
// Future improvement idea: use a sync.Pool for the input structure to save
// on allocations if the struct has a Reset() method.
2020-08-26 22:25:47 -07:00
register ( "/" , func ( w http . ResponseWriter , r * http . Request ) {
2021-05-03 22:31:01 +02:00
// 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 ... )
2020-08-26 22:25:47 -07:00
ctx := & hcontext {
2022-03-18 22:47:33 -07:00
Context : r . Context ( ) ,
ResponseWriter : w ,
r : r ,
op : o ,
2022-06-01 15:06:24 -07:00
docsPath : o . resource . router . DocsPath ( ) ,
schemasPath : o . resource . router . SchemasPath ( ) ,
specPath : o . resource . router . OpenAPIPath ( ) ,
2022-03-18 22:47:33 -07:00
urlPrefix : o . resource . router . urlPrefix ,
disableSchemaProperty : o . resource . router . disableSchemaProperty ,
2022-04-13 12:03:09 -07:00
errorCode : http . StatusBadRequest ,
2020-08-26 22:25:47 -07:00
}
2020-09-04 21:46:53 -07:00
// 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 )
2021-08-04 19:49:58 +02:00
// 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 ) )
}
}
2020-09-04 21:46:53 -07:00
setFields ( ctx , ctx . r , input , inputType )
2022-04-12 13:20:58 -07:00
if ! ctx . HasError ( ) {
// No errors yet, so any errors that come after should be treated as a
// semantic rather than structural error.
2022-04-13 12:03:09 -07:00
ctx . errorCode = http . StatusUnprocessableEntity
2022-04-12 13:20:58 -07:00
}
2021-02-09 10:08:26 -08:00
resolveFields ( ctx , "" , input )
2020-09-04 21:46:53 -07:00
if ctx . HasError ( ) {
2022-04-13 12:03:09 -07:00
ctx . WriteError ( ctx . errorCode , "Error while processing input parameters" )
2020-09-04 21:46:53 -07:00
return
}
2021-08-04 19:49:58 +02:00
// 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 { } )
}
2020-09-04 21:46:53 -07:00
// Call the handler with the context and newly populated input struct.
in := [ ] reflect . Value { reflect . ValueOf ( ctx ) , input . Elem ( ) }
reflect . ValueOf ( handler ) . Call ( in )
2020-08-26 22:25:47 -07:00
} )
}