From c55a27997cbcd184e993c6a25b44b81fb586717c Mon Sep 17 00:00:00 2001 From: qdongxu Date: Fri, 5 Jan 2024 00:00:32 +0800 Subject: [PATCH 1/4] fix PR #195 PR #195 fixed ISSUE #198. But regex matched a shorter string when a generic parammeter is an slice( that means there's embedded brackets). --- .gitignore | 1 + registry.go | 21 ++++++++------------- registry_test.go | 27 +++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 registry_test.go diff --git a/.gitignore b/.gitignore index e8cd0ba7..a879595c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ mem.out docs/site docs/__pycache__ docs/.cache +.idea/ diff --git a/registry.go b/registry.go index 813a3de6..06011f8d 100644 --- a/registry.go +++ b/registry.go @@ -4,14 +4,9 @@ import ( "encoding/json" "fmt" "reflect" - "regexp" "strings" ) -// reGenericName helps to convert `MyType[path/to.SubType]` to `MyTypeSubType` -// when using the default schema namer. -var reGenericName = regexp.MustCompile(`\[[^\]]+\]`) - // Registry creates and stores schemas and their references, and supports // marshalling to JSON/YAML for use as an OpenAPI #/components/schemas object. // Behavior is implementation-dependent, but the design allows for recursive @@ -35,14 +30,14 @@ func DefaultSchemaNamer(t reflect.Type, hint string) string { name := deref(t).Name() // Fix up generics, if used, for nicer refs & URLs. - name = reGenericName.ReplaceAllStringFunc(name, func(s string) string { - // Convert `MyType[path/to.SubType]` to `MyType[SubType]`. - parts := strings.Split(s, ".") - return parts[len(parts)-1] - }) - // Remove square brackets. - name = strings.ReplaceAll(name, "[", "") - name = strings.ReplaceAll(name, "]", "") + // Convert `MyType[path/to.SubType]` to `MyTypepathtoSubType`. + replaced := strings.Builder{} + for _, c := range name { + if !strings.ContainsRune("[*/.]", c) { + replaced.WriteRune(c) + } + } + name = replaced.String() if name == "" { name = hint diff --git a/registry_test.go b/registry_test.go new file mode 100644 index 00000000..b188e563 --- /dev/null +++ b/registry_test.go @@ -0,0 +1,27 @@ +package huma + +import ( + "fmt" + "reflect" + "testing" + + "github.com/danielgtaylor/huma/v2/examples/protodemo/protodemo" + "github.com/stretchr/testify/assert" +) + +type Output[T any] struct { + data T +} + +type Embedded[P any] struct { + data P +} + +func TestDefaultSchemaNamer(t *testing.T) { + testUser := Output[*[]Embedded[protodemo.User]]{} + + name := DefaultSchemaNamer(reflect.TypeOf(testUser), "hint") + fmt.Println(reflect.TypeOf(testUser)) + fmt.Println(name) + assert.True(t, name == "Outputgithubcomdanielgtaylorhumav2Embeddedgithubcomdanielgtaylorhumav2examplesprotodemoprotodemoUser") +} From 899995e78d091eda15f06ab468f402ce17321de5 Mon Sep 17 00:00:00 2001 From: "Daniel G. Taylor" Date: Thu, 4 Jan 2024 21:41:45 -0800 Subject: [PATCH 2/4] fix: enhanced generic schema naming --- go.mod | 2 +- registry.go | 28 ++++++++++++++++++++-------- registry_test.go | 47 +++++++++++++++++++++++++++++++++++++++++------ schema_test.go | 2 +- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 800eaf29..6011babf 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/uptrace/bunrouter v1.0.21 golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc golang.org/x/net v0.17.0 + golang.org/x/text v0.14.0 google.golang.org/protobuf v1.30.0 ) @@ -83,7 +84,6 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.17.0 // indirect golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/registry.go b/registry.go index 06011f8d..8f20d06f 100644 --- a/registry.go +++ b/registry.go @@ -5,6 +5,9 @@ import ( "fmt" "reflect" "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" ) // Registry creates and stores schemas and their references, and supports @@ -29,15 +32,24 @@ type Registry interface { func DefaultSchemaNamer(t reflect.Type, hint string) string { name := deref(t).Name() - // Fix up generics, if used, for nicer refs & URLs. - // Convert `MyType[path/to.SubType]` to `MyTypepathtoSubType`. - replaced := strings.Builder{} - for _, c := range name { - if !strings.ContainsRune("[*/.]", c) { - replaced.WriteRune(c) - } + // Better support for lists, so e.g. `[]int` becomes `ListInt`. + name = strings.ReplaceAll(name, "[]", "List[") + + result := "" + for _, part := range strings.FieldsFunc(name, func(r rune) bool { + // Split on special characters. Note that `,` is used when there are + // multiple inputs to a generic type. + return r == '[' || r == ']' || r == '*' || r == ',' + }) { + // Split fully qualified names like `github.com/foo/bar.Baz` into `Baz`. + fqn := strings.Split(part, ".") + base := fqn[len(fqn)-1] + + // Add to result, and uppercase for better scalar support (`int` -> `Int`). + // Base is guaranteed to be at least one character long so `[1:]` is safe. + result += cases.Title(language.Und, cases.NoLower).String(base) } - name = replaced.String() + name = result if name == "" { name = hint diff --git a/registry_test.go b/registry_test.go index b188e563..457488ee 100644 --- a/registry_test.go +++ b/registry_test.go @@ -1,9 +1,9 @@ package huma import ( - "fmt" "reflect" "testing" + "time" "github.com/danielgtaylor/huma/v2/examples/protodemo/protodemo" "github.com/stretchr/testify/assert" @@ -17,11 +17,46 @@ type Embedded[P any] struct { data P } +type EmbeddedTwo[P, V any] struct { + data P + second V +} + +type S struct{} + +type ü struct{} + +type MP4 struct{} + func TestDefaultSchemaNamer(t *testing.T) { - testUser := Output[*[]Embedded[protodemo.User]]{} + type Renamed Output[*[]Embedded[protodemo.User]] - name := DefaultSchemaNamer(reflect.TypeOf(testUser), "hint") - fmt.Println(reflect.TypeOf(testUser)) - fmt.Println(name) - assert.True(t, name == "Outputgithubcomdanielgtaylorhumav2Embeddedgithubcomdanielgtaylorhumav2examplesprotodemoprotodemoUser") + for _, example := range []struct { + typ any + name string + }{ + {int(0), "Int"}, + {int64(0), "Int64"}, + {S{}, "S"}, + {time.Time{}, "Time"}, + {Output[int]{}, "OutputInt"}, + {Output[*int]{}, "OutputInt"}, + {Output[[]int]{}, "OutputListInt"}, + {Output[[]*int]{}, "OutputListInt"}, + {Output[[][]int]{}, "OutputListListInt"}, + {Output[map[string]int]{}, "OutputMapStringInt"}, + {Output[map[string][]*int]{}, "OutputMapStringListInt"}, + {Output[S]{}, "OutputS"}, + {Output[ü]{}, "OutputÜ"}, + {Output[MP4]{}, "OutputMP4"}, + {Output[Embedded[*protodemo.User]]{}, "OutputEmbeddedUser"}, + {Output[*[]Embedded[protodemo.User]]{}, "OutputListEmbeddedUser"}, + {Output[EmbeddedTwo[[]protodemo.User, **time.Time]]{}, "OutputEmbeddedTwoListUserTime"}, + {Renamed{}, "Renamed"}, + } { + t.Run(example.name, func(t *testing.T) { + name := DefaultSchemaNamer(reflect.TypeOf(example.typ), "hint") + assert.Equal(t, example.name, name) + }) + } } diff --git a/schema_test.go b/schema_test.go index 2e6b1128..c5baaa1d 100644 --- a/schema_test.go +++ b/schema_test.go @@ -501,7 +501,7 @@ func TestSchemaGenericNaming(t *testing.T) { b, _ := json.Marshal(s) assert.JSONEq(t, `{ - "$ref": "#/components/schemas/SchemaGenericint" + "$ref": "#/components/schemas/SchemaGenericInt" }`, string(b)) } From b49df9d34edd41be1e805ba2626fbefcaffba3de Mon Sep 17 00:00:00 2001 From: "Daniel G. Taylor" Date: Thu, 4 Jan 2024 21:49:46 -0800 Subject: [PATCH 3/4] fix: linter --- registry_test.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/registry_test.go b/registry_test.go index 457488ee..38109af0 100644 --- a/registry_test.go +++ b/registry_test.go @@ -9,18 +9,11 @@ import ( "github.com/stretchr/testify/assert" ) -type Output[T any] struct { - data T -} +type Output[T any] struct{} -type Embedded[P any] struct { - data P -} +type Embedded[P any] struct{} -type EmbeddedTwo[P, V any] struct { - data P - second V -} +type EmbeddedTwo[P, V any] struct{} type S struct{} From 59a29512cdbfa7d3da7b7db88a40bf5d28a0aef2 Mon Sep 17 00:00:00 2001 From: "Daniel G. Taylor" Date: Fri, 5 Jan 2024 08:46:48 -0800 Subject: [PATCH 4/4] chore: remove outdated comment --- registry.go | 1 - 1 file changed, 1 deletion(-) diff --git a/registry.go b/registry.go index 8f20d06f..dcf62518 100644 --- a/registry.go +++ b/registry.go @@ -46,7 +46,6 @@ func DefaultSchemaNamer(t reflect.Type, hint string) string { base := fqn[len(fqn)-1] // Add to result, and uppercase for better scalar support (`int` -> `Int`). - // Base is guaranteed to be at least one character long so `[1:]` is safe. result += cases.Title(language.Und, cases.NoLower).String(base) } name = result