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 78b286e3..82bb0286 100644 --- a/huma.go +++ b/huma.go @@ -1400,7 +1400,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 } @@ -1409,6 +1409,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 ( @@ -1538,7 +1556,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 8fa84b61..5d96793e 100644 --- a/registry.go +++ b/registry.go @@ -14,6 +14,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 @@ -184,6 +185,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 +} + func (r *mapRegistry) Config() registryConfig { return r.config }