Skip to content

Commit

Permalink
Merge pull request #200 from danielgtaylor/fullname
Browse files Browse the repository at this point in the history
fix: enhanced generic schema naming
  • Loading branch information
danielgtaylor authored Jan 5, 2024
2 parents 4b5f0d1 + 59a2951 commit 1d337f1
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 16 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ mem.out
docs/site
docs/__pycache__
docs/.cache
.idea/
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down Expand Up @@ -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
Expand Down
34 changes: 20 additions & 14 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ 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(`\[[^\]]+\]`)
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

// Registry creates and stores schemas and their references, and supports
// marshalling to JSON/YAML for use as an OpenAPI #/components/schemas object.
Expand All @@ -34,15 +32,23 @@ type Registry interface {
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, "]", "")
// 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`).
result += cases.Title(language.Und, cases.NoLower).String(base)
}
name = result

if name == "" {
name = hint
Expand Down
55 changes: 55 additions & 0 deletions registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package huma

import (
"reflect"
"testing"
"time"

"github.com/danielgtaylor/huma/v2/examples/protodemo/protodemo"
"github.com/stretchr/testify/assert"
)

type Output[T any] struct{}

type Embedded[P any] struct{}

type EmbeddedTwo[P, V any] struct{}

type S struct{}

type ü struct{}

type MP4 struct{}

func TestDefaultSchemaNamer(t *testing.T) {
type Renamed Output[*[]Embedded[protodemo.User]]

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)
})
}
}
2 changes: 1 addition & 1 deletion schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down

0 comments on commit 1d337f1

Please sign in to comment.