Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions adapters/humamux/humamux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}
26 changes: 20 additions & 6 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"unicode/utf8"
)
Expand Down Expand Up @@ -124,17 +125,30 @@ func (r *mapRegistry) Schema(t reflect.Type, allowRef bool, hint string) *Schema

if getsRef {
if s, ok := r.schemas[name]; ok {
if _, ok = r.seen[t]; !ok {
// The name matches but the type is different, so we have a dupe.
if _, seen := r.seen[t]; seen {
// The name and type both match, so reuse the existing schema.
if allowRef {
return &Schema{Ref: r.prefix + name}
}

panic(fmt.Errorf("duplicate name: %s, new type: %s, existing type: %s", name, t, r.types[name]))
return s
}

if allowRef {
return &Schema{Ref: r.prefix + name}
// The name matches but the type is different. Named types are a
// genuine duplicate the caller must resolve (e.g. with a custom
// namer), but unnamed inline types share a hint-derived name by
// design, so disambiguate them with an incrementing numeric suffix.
if t.Name() != "" {
panic(fmt.Errorf("duplicate name: %s, new type: %s, existing type: %s", name, t, r.types[name]))
}

return s
base := name
for i := 1; ; i++ {
name = base + strconv.Itoa(i)
if _, exists := r.schemas[name]; !exists {
break
}
}
}
}

Expand Down
47 changes: 47 additions & 0 deletions registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,53 @@ func TestAllowAdditionalPropertiesByDefault(t *testing.T) {
})
}

func TestInlineTypeDisambiguation(t *testing.T) {
// Distinct inline (unnamed) structs share the same hint-derived name and
// must be disambiguated rather than panic. See issue reproduced in #893.
r := NewMapRegistry("#/components/schemas/", DefaultSchemaNamer)

first := r.Schema(reflect.TypeOf(struct {
Field1 string `json:"field1"`
}{}), true, "Request")
second := r.Schema(reflect.TypeOf(struct {
Field2 string `json:"field2"`
}{}), true, "Request")

assert.Equal(t, "#/components/schemas/Request", first.Ref)
assert.Equal(t, "#/components/schemas/Request1", second.Ref)
assert.Len(t, r.Map(), 2)
}

func TestInlineTypeDeduplication(t *testing.T) {
// Identical inline structs are the same reflect.Type and must still
// deduplicate to a single schema.
r := NewMapRegistry("#/components/schemas/", DefaultSchemaNamer)

first := r.Schema(reflect.TypeOf(struct {
Field string `json:"field"`
}{}), true, "Request")
second := r.Schema(reflect.TypeOf(struct {
Field string `json:"field"`
}{}), true, "Request")

assert.Equal(t, "#/components/schemas/Request", first.Ref)
assert.Equal(t, first.Ref, second.Ref)
assert.Len(t, r.Map(), 1)
}

func TestDuplicateNamedTypePanics(t *testing.T) {
// Genuinely different named types that collide on a name remain a hard
// error, directing the caller to a custom namer.
r := NewMapRegistry("#/components/schemas/", func(reflect.Type, string) string {
return "Collision"
})

r.Schema(reflect.TypeFor[S](), true, "")
assert.Panics(t, func() {
r.Schema(reflect.TypeFor[Output[int]](), true, "")
})
}

func TestRegistryConfigValue(t *testing.T) {
r := NewMapRegistry("/schemas", DefaultSchemaNamer)

Expand Down
Loading