diff --git a/executor.go b/executor.go index 10d3799f..5e9623e0 100644 --- a/executor.go +++ b/executor.go @@ -902,6 +902,38 @@ type FieldResolver interface { Resolve(p ResolveParams) (interface{}, error) } +func defaultResolveStruct(sourceVal reflect.Value, fieldName string) (interface{}, error) { + for i := 0; i < sourceVal.NumField(); i++ { + valueField := sourceVal.Field(i) + typeField := sourceVal.Type().Field(i) + // try matching the field name first + if strings.EqualFold(typeField.Name, fieldName) { + return valueField.Interface(), nil + } + if typeField.Anonymous && typeField.Type.Kind() == reflect.Struct { + return defaultResolveStruct(valueField, fieldName) + } + 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 + } else { + continue + } + } + return nil, nil +} + // defaultResolveFn If a resolve function is not given, then a default resolve behavior is used // which takes the property of the source object of the same name as the field // and returns it as the result, or if it's a function, returns the result @@ -922,32 +954,7 @@ func DefaultResolveFn(p ResolveParams) (interface{}, error) { } if sourceVal.Type().Kind() == reflect.Struct { - for i := 0; i < sourceVal.NumField(); i++ { - valueField := sourceVal.Field(i) - typeField := sourceVal.Type().Field(i) - // try matching the field name first - if strings.EqualFold(typeField.Name, p.Info.FieldName) { - 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] != p.Info.FieldName { - return false - } - return true - } - if checkTag("json") || checkTag("graphql") { - return valueField.Interface(), nil - } else { - continue - } - } - return nil, nil + return defaultResolveStruct(sourceVal, p.Info.FieldName) } // try p.Source as a map[string]interface diff --git a/executor_resolve_test.go b/executor_resolve_test.go index 7430cd86..ce8a1495 100644 --- a/executor_resolve_test.go +++ b/executor_resolve_test.go @@ -191,7 +191,7 @@ func TestExecutesResolveFunction_UsesProvidedResolveFunction_SourceIsStruct_With func TestExecutesResolveFunction_UsesProvidedResolveFunction_SourceIsStruct_WithJSONTags(t *testing.T) { - // For structs without JSON tags, it will map to upper-cased exported field names + // For structs with JSON tags, it will use those tags as field names type SubObjectWithJSONTags struct { OtherField string `json:""` Str string `json:"str"` @@ -264,3 +264,85 @@ func TestExecutesResolveFunction_UsesProvidedResolveFunction_SourceIsStruct_With t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result.Data)) } } + +func TestExecutesResolveFunction_UsesProvidedResolveFunction_SourceIsStruct_WithContainedStruct(t *testing.T) { + + // For anonymous contained structs, it will use the field names within + type ContainedObjectWithJSONTags struct { + Str string `json:"str"` + Int int + } + + type SubObjectWithContained struct { + ContainedObjectWithJSONTags + AnotherStr string `json:"anotherStr"` + } + + schema := testSchema(t, &graphql.Field{ + Type: graphql.NewObject(graphql.ObjectConfig{ + Name: "SubObject", + Description: "Maps GraphQL Object `SubObject` to Go struct `SubObjectWithContained`", + Fields: graphql.Fields{ + "str": &graphql.Field{Type: graphql.String}, + "Int": &graphql.Field{Type: graphql.Int}, + }, + }), + Args: graphql.FieldConfigArgument{ + "aStr": &graphql.ArgumentConfig{Type: graphql.String}, + "aInt": &graphql.ArgumentConfig{Type: graphql.Int}, + }, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + aStr, _ := p.Args["aStr"].(string) + aInt, _ := p.Args["aInt"].(int) + return &SubObjectWithContained{ + ContainedObjectWithJSONTags: ContainedObjectWithJSONTags{ + Str: aStr, + Int: aInt, + }, + }, nil + }, + }) + + expected := map[string]interface{}{ + "test": map[string]interface{}{ + "str": "", + "Int": 0, + }, + } + result := graphql.Do(graphql.Params{ + Schema: schema, + RequestString: `{ test { str, Int } }`, + }) + + if !reflect.DeepEqual(expected, result.Data) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result.Data)) + } + + expected = map[string]interface{}{ + "test": map[string]interface{}{ + "str": "String!", + "Int": 0, + }, + } + result = graphql.Do(graphql.Params{ + Schema: schema, + RequestString: `{ test(aStr: "String!") { str, Int } }`, + }) + if !reflect.DeepEqual(expected, result.Data) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result.Data)) + } + + expected = map[string]interface{}{ + "test": map[string]interface{}{ + "str": "String!", + "Int": -123, + }, + } + result = graphql.Do(graphql.Params{ + Schema: schema, + RequestString: `{ test(aInt: -123, aStr: "String!") { str, Int } }`, + }) + if !reflect.DeepEqual(expected, result.Data) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result.Data)) + } +}