Skip to content

Commit

Permalink
Merge pull request #242 from asteris-llc/feature/fully-lowercase-fiel…
Browse files Browse the repository at this point in the history
…d-names

Feature/fully lowercase field names
  • Loading branch information
rebeccaskinner authored Sep 13, 2016
2 parents cf6594c + 9eeda35 commit d73211b
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 14 deletions.
115 changes: 103 additions & 12 deletions render/preprocessor/preprocessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import (
// references
var ErrUnresolvable = errors.New("field is unresolvable")

// fieldMapCache caches the results of field map generation to avoid
// recalculating it during execution.
var fieldMapCache = make(map[reflect.Type]map[string]string)

// Preprocessor is a template preprocessor
type Preprocessor struct {
vertices map[string]struct{}
Expand Down Expand Up @@ -118,7 +122,6 @@ func VertexSplit(g *graph.Graph, s string) (string, string, bool) {

// HasField returns true if the provided struct has the defined field
func HasField(obj interface{}, fieldName string) bool {
fieldName = toPublicFieldCase(fieldName)
var v reflect.Type
switch oType := obj.(type) {
case reflect.Type:
Expand All @@ -131,6 +134,10 @@ func HasField(obj interface{}, fieldName string) bool {
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
fieldName, err := LookupCanonicalFieldName(v, fieldName)
if err != nil {
return false
}
_, hasField := v.FieldByName(fieldName)
return hasField
}
Expand Down Expand Up @@ -169,7 +176,11 @@ func HasMethod(obj interface{}, methodName string) bool {

// EvalMember gets a member from a stuct, dereferencing pointers as necessary
func EvalMember(name string, obj interface{}) (reflect.Value, error) {
name = toPublicFieldCase(name)
name, err := LookupCanonicalFieldName(interfaceToConcreteType(obj), name)

if err != nil {
return reflect.Zero(reflect.TypeOf(obj)), err
}
v := reflect.ValueOf(obj)
for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
if v.IsNil() {
Expand Down Expand Up @@ -199,6 +210,11 @@ func HasPath(obj interface{}, terms ...string) error {
return errors.New("cannot access non-structure field")
}

term, err := LookupCanonicalFieldName(t, term)
if err != nil {
return err
}

field, ok := t.FieldByName(term)
if !ok {
validFields, fieldErrs := ListFields(t)
Expand All @@ -214,11 +230,6 @@ func HasPath(obj interface{}, terms ...string) error {

// EvalTerms acts as a left fold over a list of term accessors
func EvalTerms(obj interface{}, terms ...string) (interface{}, error) {

for idx, term := range terms {
terms[idx] = toPublicFieldCase(term)
}

if err := HasPath(obj, terms...); err != nil {
return nil, err
}
Expand All @@ -245,6 +256,91 @@ func EvalTerms(obj interface{}, terms ...string) (interface{}, error) {
return obj, nil
}

// For a given interface, fieldMap returns a map with keys being the lowercase
// versions of the string, and values being the correct version. It returns an
// error if the interface is not a struct, or a reflect.Type or reflect.Value of
// a struct.
func fieldMap(val interface{}) (map[string]string, error) {
fieldMap := make(map[string]string)
var t reflect.Type
switch typed := val.(type) {
case reflect.Type:
t = typed
case reflect.Value:
t = typed.Type()
default:
t = reflect.TypeOf(val)
}
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("cannot access fields of non-struct type %T", val)
}
return addFieldsToMap(fieldMap, t)
}

func addFieldsToMap(m map[string]string, t reflect.Type) (map[string]string, error) {
if cached, ok := fieldMapCache[t]; ok {
return cached, nil
}

for idx := 0; idx < t.NumField(); idx++ {
field := t.Field(idx)
if field.Anonymous {
var err error
if m, err = addFieldsToMap(m, interfaceToConcreteType(field.Type)); err != nil {
return nil, err
}
continue
}

name := field.Name
lower := strings.ToLower(name)
if _, ok := m[lower]; ok {
return nil, fmt.Errorf("multiple potential matches for %s", name)
}
m[lower] = name
}
fieldMapCache[t] = m
return m, nil
}

// LookupCanonicalFieldName takes a type and an arbitrarily cased field name and
// returns the field name with a case that matches the actual field.
func LookupCanonicalFieldName(t reflect.Type, term string) (string, error) {
term = strings.ToLower(term)
m, err := fieldMap(t)
if err != nil {
return "", err
}
correctCase, found := m[term]
if found {
return correctCase, nil
}
var fields []string
for key := range m {
fields = append(fields, key)
}
return "", fmt.Errorf("%s has no field that matches %s, should be one of %v", t, term, fields)
}

func interfaceToConcreteType(i interface{}) reflect.Type {
var t reflect.Type
switch typed := i.(type) {
case reflect.Type:
t = typed
case reflect.Value:
t = typed.Type()
default:
t = reflect.TypeOf(i)
}
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t
}

// mapToLower converts a string slice to all lower case
func mapToLower(strs []string) []string {
for idx, str := range strs {
Expand All @@ -253,11 +349,6 @@ func mapToLower(strs []string) []string {
return strs
}

// toPublicFieldCase converts the first letter in the string to capital
func toPublicFieldCase(s string) string {
return strings.ToUpper(string(s[0])) + s[1:]
}

func nilPtrError(v reflect.Value) error {
typeStr := v.Type().String()
return fmt.Errorf("cannot dereference nil pointer of type %s", typeStr)
Expand Down
104 changes: 102 additions & 2 deletions render/preprocessor/preprocessor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package preprocessor_test

import (
"reflect"
"testing"

"github.com/asteris-llc/converge/graph"
Expand All @@ -32,6 +33,19 @@ func Test_HasField_WhenStruct_ReturnsFieldPresentWhenPresent(t *testing.T) {

}

func Test_HasField_WhenEmbeddedStruct_ReturnsEmbeddedFieldPresent(t *testing.T) {
type Embedded struct {
A struct{}
}

type Embedding struct {
*Embedded
B struct{}
}
assert.True(t, preprocessor.HasField(Embedding{}, "B"))
assert.True(t, preprocessor.HasField(Embedding{}, "A"))
}

func Test_VertexSplit_WhenMatchingSubstring_ReturnsPrefixAndRest(t *testing.T) {
s := "a.b.c.d.e"
g := graph.New()
Expand Down Expand Up @@ -70,9 +84,9 @@ func Test_HasField_WhenStructPtr_ReturnsFieldPresentWhenPresent(t *testing.T) {
assert.False(t, preprocessor.HasField(&TestStruct{}, "FieldB"))
}

func Test_HasField_WhenGivenAsLowerCaseAndIsCapital_ReturnsTrueI(t *testing.T) {
func Test_HasField_WhenGivenAsLowerCaseAndIsCapital_ReturnsTrue(t *testing.T) {
assert.True(t, preprocessor.HasField(&TestStruct{}, "fieldA"))
assert.False(t, preprocessor.HasField(&TestStruct{}, "fielda"))
assert.True(t, preprocessor.HasField(&TestStruct{}, "fielda"))
assert.False(t, preprocessor.HasField(&TestStruct{}, "fieldB"))
}

Expand Down Expand Up @@ -123,6 +137,86 @@ func Test_EvalMember_ReturnsError_WhenNotExists(t *testing.T) {
assert.Error(t, err)
}

func Test_LookupCanonicalFieldName_ReturnsCanonicalFieldName_WhenStructOkay(t *testing.T) {
type TestStruct struct {
ABC struct{} // All upper
aaa struct{} // all lower

AcB struct{} // mixed case, initial capital
bAc struct{} // mixed case, initial lower

A struct{} // single letter upper
b struct{} // single letter lower
}
testType := reflect.TypeOf(TestStruct{})

actual, err := preprocessor.LookupCanonicalFieldName(testType, "ABC")
assert.NoError(t, err)
assert.Equal(t, "ABC", actual)
actual, err = preprocessor.LookupCanonicalFieldName(testType, "AbC")
assert.NoError(t, err)
assert.Equal(t, "ABC", actual)
actual, err = preprocessor.LookupCanonicalFieldName(testType, "abc")
assert.NoError(t, err)
assert.Equal(t, "ABC", actual)

actual, err = preprocessor.LookupCanonicalFieldName(testType, "aaa")
assert.NoError(t, err)
assert.Equal(t, "aaa", actual)
actual, err = preprocessor.LookupCanonicalFieldName(testType, "aAa")
assert.NoError(t, err)
assert.Equal(t, "aaa", actual)
actual, err = preprocessor.LookupCanonicalFieldName(testType, "AAA")
assert.NoError(t, err)
assert.Equal(t, "aaa", actual)

actual, err = preprocessor.LookupCanonicalFieldName(testType, "acb")
assert.NoError(t, err)
assert.Equal(t, "AcB", actual)
actual, err = preprocessor.LookupCanonicalFieldName(testType, "AcB")
assert.NoError(t, err)
assert.Equal(t, "AcB", actual)
actual, err = preprocessor.LookupCanonicalFieldName(testType, "aCb")
assert.NoError(t, err)
assert.Equal(t, "AcB", actual)

actual, err = preprocessor.LookupCanonicalFieldName(testType, "bac")
assert.NoError(t, err)
assert.Equal(t, "bAc", actual)
actual, err = preprocessor.LookupCanonicalFieldName(testType, "BaC")
assert.NoError(t, err)
assert.Equal(t, "bAc", actual)
actual, err = preprocessor.LookupCanonicalFieldName(testType, "BAC")
assert.NoError(t, err)
assert.Equal(t, "bAc", actual)

actual, err = preprocessor.LookupCanonicalFieldName(testType, "A")
assert.NoError(t, err)
assert.Equal(t, "A", actual)
actual, err = preprocessor.LookupCanonicalFieldName(testType, "a")
assert.NoError(t, err)
assert.Equal(t, "A", actual)

actual, err = preprocessor.LookupCanonicalFieldName(testType, "b")
assert.NoError(t, err)
assert.Equal(t, "b", actual)
actual, err = preprocessor.LookupCanonicalFieldName(testType, "B")
assert.NoError(t, err)
assert.Equal(t, "b", actual)

}

func Test_LookupCanonicalFieldName_ReturnsError_WhenOverlappingFieldNames(t *testing.T) {
type TestStruct struct {
Xyz struct{} // collision initial upper
XYz struct{} // collision first two upper
}

testType := reflect.TypeOf(TestStruct{})
_, err := preprocessor.LookupCanonicalFieldName(testType, "xyz")
assert.Error(t, err)
}

func Test_EvalTerms(t *testing.T) {
type C struct {
CVal string
Expand All @@ -140,9 +234,15 @@ func Test_EvalTerms(t *testing.T) {
val, err := preprocessor.EvalTerms(a, "AB", "BVal")
assert.NoError(t, err)
assert.Equal(t, val, "b")

val, err = preprocessor.EvalTerms(a, "AB", "BC", "CVal")
assert.NoError(t, err)
assert.Equal(t, val, "c")

val, err = preprocessor.EvalTerms(a, "ab", "bc", "cval")
assert.NoError(t, err)
assert.Equal(t, val, "c")

val, err = preprocessor.EvalTerms(a, "AVal")
assert.NoError(t, err)
assert.Equal(t, val, "a")
Expand Down

0 comments on commit d73211b

Please sign in to comment.