Skip to content

Commit

Permalink
versioned names (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
muir committed Feb 2, 2023
1 parent 7534b3a commit abaed17
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 0 deletions.
7 changes: 7 additions & 0 deletions README.md
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
@@ -0,0 +1,3 @@
package foo

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

type Bar struct {}
107 changes: 107 additions & 0 deletions names.go
@@ -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
@@ -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)))
}
}

0 comments on commit abaed17

Please sign in to comment.