Skip to content

Commit

Permalink
ensure default resolver can traverse embedded structs (#2341)
Browse files Browse the repository at this point in the history
Signed-off-by: James Phillips <jamesdphillips@gmail.com>
  • Loading branch information
jamesdphillips authored and Nikki Attea committed Nov 15, 2018
1 parent eaa6196 commit c07579c
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 35 deletions.
2 changes: 1 addition & 1 deletion backend/apid/graphql/globalid/entity.go
Expand Up @@ -13,7 +13,7 @@ var entityName = "entities"
// EntityTranslator global ID resource
var EntityTranslator = commonTranslator{
name: entityName,
encodeFunc: standardEncoder(entityName, "ID"),
encodeFunc: standardEncoder(entityName, "Name"),
decodeFunc: standardDecoder,
isResponsibleFunc: func(record interface{}) bool {
_, ok := record.(*types.Entity)
Expand Down
2 changes: 1 addition & 1 deletion backend/apid/graphql/globalid/handlers.go
Expand Up @@ -11,7 +11,7 @@ var handlerName = "handlers"
// HandlerTranslator global ID resource
var HandlerTranslator = commonTranslator{
name: handlerName,
encodeFunc: standardEncoder(handlerName, "ID"),
encodeFunc: standardEncoder(handlerName, "Name"),
decodeFunc: standardDecoder,
isResponsibleFunc: func(record interface{}) bool {
_, ok := record.(*types.Handler)
Expand Down
2 changes: 1 addition & 1 deletion backend/apid/graphql/globalid/silences.go
Expand Up @@ -11,7 +11,7 @@ var silenceName = "silences"
// SilenceTranslator global ID resource
var SilenceTranslator = commonTranslator{
name: silenceName,
encodeFunc: standardEncoder(silenceName, "ID"),
encodeFunc: standardEncoder(silenceName, "Name"),
decodeFunc: standardDecoder,
isResponsibleFunc: func(record interface{}) bool {
_, ok := record.(*types.Silenced)
Expand Down
74 changes: 42 additions & 32 deletions graphql/resolvers.go
Expand Up @@ -146,38 +146,8 @@ func DefaultResolver(source interface{}, fieldName string) (interface{}, error)

// Struct
if sourceVal.Type().Kind() == reflect.Struct {
fieldName = strings.Title(fieldName)
for i := 0; i < sourceVal.NumField(); i++ {
valueField := sourceVal.Field(i)
typeField := sourceVal.Type().Field(i)
if typeField.Name == fieldName {
// If ptr and value is nil return nil
if valueField.Type().Kind() == reflect.Ptr && valueField.IsNil() {
return nil, nil
}
return valueField.Interface(), nil
}
tag := typeField.Tag
checkTag := func(tagName string) bool {
t := tag.Get(tagName)
tOptions := strings.Split(t, ",")
if len(tOptions) == 0 {
return false
}
if tOptions[0] != fieldName {
return false
}
return true
}
if checkTag("json") || checkTag("graphql") {
return valueField.Interface(), nil
}
if valueField.Kind() == reflect.Struct && typeField.Anonymous {
return DefaultResolver(valueField.Interface(), fieldName)
}
continue
}
return nil, nil
_, val, err := findFieldInStruct(sourceVal, fieldName)
return val, err
}

// map[string]interface
Expand All @@ -198,6 +168,46 @@ func DefaultResolver(source interface{}, fieldName string) (interface{}, error)
return nil, nil
}

func findFieldInStruct(source reflect.Value, fieldName string) (bool, interface{}, error) {
for i := 0; i < source.NumField(); i++ {
fieldValue := source.Field(i)
fieldType := source.Type().Field(i)

if fieldType.Name == strings.Title(fieldName) {
// If ptr and value is nil return nil
if fieldValue.Type().Kind() == reflect.Ptr && fieldValue.IsNil() {
return true, nil, nil
}
return true, fieldValue.Interface(), nil
}

tag := fieldType.Tag
checkTag := func(tagName string) bool {
t := tag.Get(tagName)
tOptions := strings.Split(t, ",")
if len(tOptions) == 0 {
return false
}
if tOptions[0] != fieldName {
return false
}
return true
}
if checkTag("json") || checkTag("graphql") {
return true, fieldValue.Interface(), nil
}

if fieldValue.Kind() == reflect.Struct && fieldType.Anonymous {
if ok, val, err := findFieldInStruct(fieldValue, fieldName); ok {
return ok, val, err
}
}
continue
}

return false, nil, nil
}

type typeResolver interface {
ResolveType(interface{}, ResolveTypeParams) *Type
}
Expand Down
73 changes: 73 additions & 0 deletions graphql/resolvers_test.go
@@ -0,0 +1,73 @@
package graphql

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDefaultResolver(t *testing.T) {
type meta struct {
Name string `json:"firstname"`
}

type animal struct {
meta
Age int
}

fren := animal{meta: meta{Name: "bob"}, Age: 10}

testCases := []struct {
desc string
source interface{}
field string
out interface{}
}{
{
desc: "field on struct",
source: fren,
field: "name",
out: "bob",
},
{
desc: "second field on struct",
source: fren,
field: "age",
out: 10,
},
{
desc: "field on struct w/ tag",
source: fren,
field: "firstname",
out: "bob",
},
{
desc: "missing field on struct",
source: fren,
field: "surname",
out: nil,
},
{
desc: "field on map",
source: map[string]interface{}{"name": "bob"},
field: "name",
out: "bob",
},
{
desc: "missing field on map",
source: map[string]interface{}{"name": "bob"},
field: "firstname",
out: nil,
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
result, err := DefaultResolver(tc.source, tc.field)
require.NoError(t, err)
assert.EqualValues(t, result, tc.out)
})
}
}

0 comments on commit c07579c

Please sign in to comment.