Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: TypeName() returns type names with versioned packages #12

Merged
merged 1 commit into from
Feb 2, 2023
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ err := GetTag(st, "foo").Fill(&tagInfo)
// tagInfo.Count will be 9
```

## Type names

The `TypeName()` function exists to disambiguate between type names that are
versioned. `reflect.Type.String()` will hides package versions. This doesn't
matter unless you've, unfortunately, imported multiple versions of the same
package.

## Development status

Reflectutils is used by several packages. Backwards compatability is expected.
Expand Down
3 changes: 3 additions & 0 deletions internal/foo/foo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package foo

type Bar struct {}
3 changes: 3 additions & 0 deletions internal/foo/v2/foo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package foo

type Bar struct {}
107 changes: 107 additions & 0 deletions names.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package reflectutils

import (
"path"
"reflect"
"regexp"
"strconv"
"strings"
)

var versionRE = regexp.MustCompile(`/v(\d+)$`)

// TypeName is an alternative to reflect.Type's .String() method. The only
// expected difference is that if there is a package that is versioned, the
// version will appear in the package name.
//
// For example, if there is a foo/v2 package with a Bar type, and you ask
// for for the TypeName, you'll get "foo/v2.Bar" instead of the "foo.Bar" that
// reflect returns.
func TypeName(t reflect.Type) string {
ts := t.String()
pkgPath := t.PkgPath()
if pkgPath != "" {
if versionRE.MatchString(pkgPath) {
version := path.Base(pkgPath)
pn := path.Base(path.Dir(pkgPath))
revised := strings.Replace(ts, pn, pn+"/"+version, 1)
if revised != ts {
return revised
}
return "(" + version + ")" + ts
}
return ts
}
switch t.Kind() { //nolint:exhaustive // not intended to be exhaustive
case reflect.Ptr: // TODO: change to Pointer when go 1.17 support lapses
return "*" + TypeName(t.Elem())
case reflect.Slice:
return "[]" + TypeName(t.Elem())
case reflect.Map:
return "map[" + TypeName(t.Key()) + "]" + TypeName(t.Elem())
case reflect.Array:
return "[" + strconv.Itoa(t.Len()) + "]" + TypeName(t.Elem())
case reflect.Func:
return "func" + fmtFunc(t)
case reflect.Chan:
switch t.ChanDir() {
case reflect.BothDir:
return "chan " + TypeName(t.Elem())
case reflect.SendDir:
return "chan<- " + TypeName(t.Elem())
case reflect.RecvDir:
return "<-chan " + TypeName(t.Elem())
default:
return ts
}
case reflect.Struct:
if t.NumField() == 0 {
return "struct {}"
}
fields := make([]string, t.NumField())
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if f.Anonymous {
fields[i] = TypeName(f.Type)
} else {
fields[i] = f.Name + " " + TypeName(f.Type)
}
}
return "struct { " + strings.Join(fields, "; ") + " }"
case reflect.Interface:
n := t.Name()
if n != "" {
return n
}
if t.NumMethod() == 0 {
return "interface {}"
}
methods := make([]string, t.NumMethod())
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
methods[i] = m.Name + fmtFunc(m.Type)
}
return "interface { " + strings.Join(methods, "; ") + " }"
default:
return ts
}
}

func fmtFunc(t reflect.Type) string {
inputs := make([]string, t.NumIn())
for i := 0; i < t.NumIn(); i++ {
inputs[i] = TypeName(t.In(i))
}
outputs := make([]string, t.NumOut())
for i := 0; i < t.NumOut(); i++ {
outputs[i] = TypeName(t.Out(i))
}
switch t.NumOut() {
case 0:
return "(" + strings.Join(inputs, ", ") + ")"
case 1:
return "(" + strings.Join(inputs, ", ") + ") " + outputs[0]
default:
return "(" + strings.Join(inputs, ", ") + ") (" + strings.Join(outputs, ", ") + ")"
}
}
119 changes: 119 additions & 0 deletions names_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package reflectutils

import (
"reflect"
"testing"

v1 "github.com/muir/reflectutils/internal/foo"
v2 "github.com/muir/reflectutils/internal/foo/v2"

"github.com/stretchr/testify/assert"
)

func TestVersionedNames(t *testing.T) {
type xbar struct {
v2.Bar
}
type ybar struct {
xbar
v1 v1.Bar //nolint:structcheck,unused // unused and we don't care
}
cases := []struct {
thing interface{}
want string
}{
{
thing: v1.Bar{},
},
{
thing: v2.Bar{},
want: "foo/v2.Bar",
},
{
thing: &v1.Bar{},
want: "*foo.Bar",
},
{
thing: &v2.Bar{},
want: "*foo/v2.Bar",
},
{
thing: []v2.Bar{},
want: "[]foo/v2.Bar",
},
{
thing: (func(*v1.Bar, *v2.Bar) (string, error))(nil),
want: "func(*foo.Bar, *foo/v2.Bar) (string, error)",
},
{
thing: [8]v2.Bar{},
want: "[8]foo/v2.Bar",
},
{
thing: make(chan *v2.Bar),
want: "chan *foo/v2.Bar",
},
{
thing: make(chan *v1.Bar),
want: "chan *foo.Bar",
},
{
thing: (chan<- *v1.Bar)(nil),
want: "chan<- *foo.Bar",
},
{
thing: (chan<- *v2.Bar)(nil),
want: "chan<- *foo/v2.Bar",
},
{
thing: (<-chan *v2.Bar)(nil),
want: "<-chan *foo/v2.Bar",
},
{
thing: (<-chan *v1.Bar)(nil),
want: "<-chan *foo.Bar",
},
{
thing: (func(interface {
V1() v1.Bar
V2() v2.Bar
}))(nil),
want: "func(interface { V1() foo.Bar; V2() foo/v2.Bar })",
},
{
thing: (func(interface{}))(nil),
},
{
thing: struct {
v1 v1.Bar
v2 v2.Bar
}{},
want: "struct { v1 foo.Bar; v2 foo/v2.Bar }",
},
{
thing: struct{}{},
},
{
thing: struct {
v2.Bar
v1 v1.Bar
}{},
want: "struct { foo/v2.Bar; v1 foo.Bar }",
},
{
thing: struct {
ybar
}{},
// want: "struct { reflectutils.ybar }",
},
}

for _, tc := range cases {
want := tc.want
if want == "" {
want = reflect.TypeOf(tc.thing).String()
}
t.Logf("%+v wanting %s", tc.thing, want)
assert.Equal(t, want, TypeName(reflect.TypeOf(tc.thing)))
}
}