diff --git a/README.md b/README.md index 8b48451..b290f0e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/internal/foo/foo.go b/internal/foo/foo.go new file mode 100644 index 0000000..221b1da --- /dev/null +++ b/internal/foo/foo.go @@ -0,0 +1,3 @@ +package foo + +type Bar struct {} diff --git a/internal/foo/v2/foo.go b/internal/foo/v2/foo.go new file mode 100644 index 0000000..221b1da --- /dev/null +++ b/internal/foo/v2/foo.go @@ -0,0 +1,3 @@ +package foo + +type Bar struct {} diff --git a/names.go b/names.go new file mode 100644 index 0000000..049043f --- /dev/null +++ b/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() { + case reflect.Ptr: + 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, ", ") + ")" + } +} diff --git a/names_test.go b/names_test.go new file mode 100644 index 0000000..a4a739b --- /dev/null +++ b/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 + } + 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))) + } +}