diff --git a/adapters/humamux/humagmux_test.go b/adapters/humamux/humagmux_test.go index 4d95beda..4a9c51d1 100644 --- a/adapters/humamux/humagmux_test.go +++ b/adapters/humamux/humagmux_test.go @@ -133,3 +133,63 @@ func BenchmarkHumaGorillaMux(b *testing.B) { } } } + +func TestOperationWithoutIDAndInlineRequestTypeDefinitionNotPanics(t *testing.T) { + r := mux.NewRouter() + api := New(r, huma.DefaultConfig("Test", "1.0.0")) + + var ( + op1 = huma.Operation{Method: http.MethodGet, Path: "/1"} // no OperationID + op2 = huma.Operation{Method: http.MethodGet, Path: "/2"} // no OperationID + ) + + huma.Register(api, op1, func(ctx context.Context, i *struct { // inline request input type definition + Body struct { + Test1 string // field name varying + } + }, + ) (*struct{}, error, + ) { + return nil, nil + }) + + huma.Register(api, op2, func(ctx context.Context, i *struct { // inline request input type definition + Body struct { + Test2 string // field name varying + } + }, + ) (*struct{}, error, + ) { + return nil, nil + }) +} + +func TestOperationWithoutIDAndInlineResponseTypeDefinitionNotPanics(t *testing.T) { + r := mux.NewRouter() + api := New(r, huma.DefaultConfig("", "")) + + var ( + op1 = huma.Operation{Method: http.MethodGet, Path: "/1"} // no OperationID + op2 = huma.Operation{Method: http.MethodGet, Path: "/2"} // no OperationID + ) + + huma.Register(api, op1, func(ctx context.Context, i *struct{}, + ) (*struct { // inline response output type definition + Body struct { + Test1 string // field name varying + } + }, error, + ) { + return nil, nil + }) + + huma.Register(api, op2, func(ctx context.Context, i *struct{}, + ) (*struct { // inline response output type definition + Body struct { + Test2 string // field name varying + } + }, error, + ) { + return nil, nil + }) +} diff --git a/huma.go b/huma.go index 6f98afaa..90af0d58 100644 --- a/huma.go +++ b/huma.go @@ -31,10 +31,12 @@ import ( var errDeadlineUnsupported = fmt.Errorf("%w", http.ErrNotSupported) -var bodyCallbackType = reflect.TypeOf(func(Context) {}) -var cookieType = reflect.TypeOf((*http.Cookie)(nil)).Elem() -var fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() -var stringType = reflect.TypeOf("") +var ( + bodyCallbackType = reflect.TypeOf(func(Context) {}) + cookieType = reflect.TypeOf((*http.Cookie)(nil)).Elem() + fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() + stringType = reflect.TypeOf("") +) // SetReadDeadline is a utility to set the read deadline on a response writer, // if possible. If not, it will not incur any allocations (unlike the stdlib @@ -714,7 +716,7 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) } } - var receiver = f + receiver := f if f.Addr().Type().Implements(reflect.TypeFor[ParamWrapper]()) { receiver = f.Addr().Interface().(ParamWrapper).Receiver() } @@ -1221,7 +1223,7 @@ func setRequestBodyFromBody(op *Operation, registry Registry, fBody reflect.Stru op.RequestBody.Content[contentType] = &MediaType{} } if op.RequestBody.Content[contentType].Schema == nil { - hint := getHint(inputType, fBody.Name, op.OperationID+"Request") + hint := getHint(inputType, fBody.Name, getDefaultHint(op.OperationID, registry, inputType, "Request")) if nameHint := fBody.Tag.Get("nameHint"); nameHint != "" { hint = nameHint } @@ -1230,6 +1232,24 @@ func setRequestBodyFromBody(op *Operation, registry Registry, fBody reflect.Stru } } +// getDefaultHint checks if the hint already exists in the registry and returns a new hint if it does. +// +// It suffixes the hint with an increasing number if it already exists, starting from 1. +// +// However, the operationID takes precedence if it is not empty. +func getDefaultHint(operationID string, registry Registry, inputType reflect.Type, defaultPrefix string) string { + if operationID != "" { + return operationID + defaultPrefix + } + + defaultHint := defaultPrefix + for i := 1; registry.NameExistsInSchema(inputType, defaultHint); i++ { + defaultHint = defaultPrefix + strconv.Itoa(i) + } + + return defaultHint +} + type rawBodyType int const ( @@ -1359,7 +1379,7 @@ func processOutputType(outputType reflect.Type, op *Operation, registry Registry op.Responses[statusStr].Headers = map[string]*Param{} } if !outBodyFunc { - hint := getHint(outputType, f.Name, op.OperationID+"Response") + hint := getHint(outputType, f.Name, getDefaultHint(op.OperationID, registry, outputType, "Response")) if nameHint := f.Tag.Get("nameHint"); nameHint != "" { hint = nameHint } diff --git a/registry.go b/registry.go index 6ee69153..562c5fe4 100644 --- a/registry.go +++ b/registry.go @@ -15,6 +15,7 @@ import ( // schemas to exist while being flexible enough to support other use cases // like only inline objects (no refs) or always using refs for structs. type Registry interface { + NameExistsInSchema(t reflect.Type, hint string) bool Schema(t reflect.Type, allowRef bool, hint string) *Schema SchemaFromRef(ref string) *Schema TypeFromRef(ref string) reflect.Type @@ -160,6 +161,11 @@ func (r *mapRegistry) RegisterTypeAlias(t reflect.Type, alias reflect.Type) { r.aliases[t] = alias } +func (r *mapRegistry) NameExistsInSchema(t reflect.Type, hint string) bool { + _, ok := r.schemas[r.namer(t, hint)] + return ok +} + // NewMapRegistry creates a new registry that stores schemas in a map and // returns references to them using the given prefix. func NewMapRegistry(prefix string, namer func(t reflect.Type, hint string) string) Registry {