resources: Add a utility to map from struct fields

For GAPI front ends that want to know what fields they can use and which
they map to, these two functions can be used.
This commit is contained in:
James Shubin
2017-06-17 11:49:30 -04:00
parent 5a3bd3ca67
commit e341256627
2 changed files with 142 additions and 0 deletions

View File

@@ -22,11 +22,18 @@ import (
"encoding/base64" "encoding/base64"
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"reflect"
"sort" "sort"
"strings"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
const (
// StructTag is the key we use in struct field names for key mapping.
StructTag = "lang"
)
// ResourceSlice is a linear list of resources. It can be sorted. // ResourceSlice is a linear list of resources. It can be sorted.
type ResourceSlice []Res type ResourceSlice []Res
@@ -77,3 +84,52 @@ func B64ToRes(str string) (Res, error) {
} }
return res, nil return res, nil
} }
// StructTagToFieldName returns a mapping from recommended alias to actual field
// name. It returns an error if it finds a collision. It uses the `lang` tags.
func StructTagToFieldName(res Res) (map[string]string, error) {
// TODO: fallback to looking up yaml tags, although harder to parse
result := make(map[string]string) // `lang` field tag -> field name
st := reflect.TypeOf(res).Elem() // elem for ptr to res
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
name := field.Name
// TODO: golang 1.7+
// if !ok, then nothing is found
//if alias, ok := field.Tag.Lookup(StructTag); ok { // golang 1.7+
if alias := field.Tag.Get(StructTag); alias != "" { // golang 1.6
if val, exists := result[alias]; exists {
return nil, fmt.Errorf("field `%s` uses the same key `%s` as field `%s`", name, alias, val)
}
// empty string ("") is a valid value
if alias != "" {
result[alias] = name
}
}
}
return result, nil
}
// LowerStructFieldNameToFieldName returns a mapping from the lower case version
// of each field name to the actual field name. It only returns public fields.
// It returns an error if it finds a collision.
func LowerStructFieldNameToFieldName(res Res) (map[string]string, error) {
result := make(map[string]string) // lower field name -> field name
st := reflect.TypeOf(res).Elem() // elem for ptr to res
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
name := field.Name
if strings.Title(name) != name { // must have been a priv field
continue
}
if alias := strings.ToLower(name); alias != "" {
if val, exists := result[alias]; exists {
return nil, fmt.Errorf("field `%s` uses the same key `%s` as field `%s`", name, alias, val)
}
result[alias] = name
}
}
return result, nil
}

View File

@@ -152,3 +152,89 @@ func TestMiscEncodeDecode2(t *testing.T) {
t.Error("The input and output Res values do not match!") t.Error("The input and output Res values do not match!")
} }
} }
func TestStructTagToFieldName0(t *testing.T) {
type TestStruct struct {
// TODO: switch this to TestRes when it is in git master
NoopRes // so that this struct implements `Res`
Alpha bool `lang:"alpha" yaml:"nope"`
Beta string `yaml:"beta"`
Gamma string
Delta int `lang:"surprise"`
}
mapping, err := StructTagToFieldName(&TestStruct{})
if err != nil {
t.Errorf("failed: %+v", err)
return
}
expected := map[string]string{
"alpha": "Alpha",
"surprise": "Delta",
}
if !reflect.DeepEqual(mapping, expected) {
t.Errorf("expected: %+v", expected)
t.Errorf("received: %+v", mapping)
}
}
func TestLowerStructFieldNameToFieldName0(t *testing.T) {
type TestStruct struct {
// TODO: switch this to TestRes when it is in git master
NoopRes // so that this struct implements `Res`
Alpha bool
skipMe bool
Beta string
IAmACamel uint
pass *string
Gamma string
Delta int
}
mapping, err := LowerStructFieldNameToFieldName(&TestStruct{})
if err != nil {
t.Errorf("failed: %+v", err)
return
}
expected := map[string]string{
"noopres": "NoopRes", // TODO: switch this to TestRes when it is in git master
"alpha": "Alpha",
//"skipme": "skipMe",
"beta": "Beta",
"iamacamel": "IAmACamel",
//"pass": "pass",
"gamma": "Gamma",
"delta": "Delta",
}
if !reflect.DeepEqual(mapping, expected) {
t.Errorf("expected: %+v", expected)
t.Errorf("received: %+v", mapping)
}
}
func TestLowerStructFieldNameToFieldName1(t *testing.T) {
type TestStruct struct {
// TODO: switch this to TestRes when it is in git master
NoopRes // so that this struct implements `Res`
Alpha bool
skipMe bool
Beta string
// these two should collide
DoubleWord bool
Doubleword string
IAmACamel uint
pass *string
Gamma string
Delta int
}
mapping, err := LowerStructFieldNameToFieldName(&TestStruct{})
if err == nil {
t.Errorf("expected failure, but passed with: %+v", mapping)
return
}
}