diff --git a/resources/util.go b/resources/util.go index 35c5035f..c36d520a 100644 --- a/resources/util.go +++ b/resources/util.go @@ -22,11 +22,18 @@ import ( "encoding/base64" "encoding/gob" "fmt" + "reflect" "sort" + "strings" 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. type ResourceSlice []Res @@ -77,3 +84,52 @@ func B64ToRes(str string) (Res, error) { } 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 +} diff --git a/resources/util_test.go b/resources/util_test.go index 8912f8b6..c10b30f8 100644 --- a/resources/util_test.go +++ b/resources/util_test.go @@ -152,3 +152,89 @@ func TestMiscEncodeDecode2(t *testing.T) { 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 + } +}