Skip to content

Commit f05ace9

Browse files
authored
Add graphql reflect tag (#596)
* Add graphql reflect tag * Resolve ambiguity bug * Test for field tag name collision * Improve error message
1 parent 8545a50 commit f05ace9

File tree

4 files changed

+165
-18
lines changed

4 files changed

+165
-18
lines changed

examples_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,59 @@ func ExampleUseStringDescriptions() {
433433
// field: "title", description: ""
434434
// field: "tags", description: "Tags of the post."
435435
}
436+
437+
// ExampleFieldTag demonstrates the use of the graphql field tag.
438+
func Example_resolverFieldTag() {
439+
type resolver struct {
440+
Hello string
441+
HelloUnderscore string `graphql:"_hello"`
442+
HelloLower string `graphql:"hello"`
443+
HelloTitle string `graphql:"Hello"`
444+
HelloUpper string `graphql:"HELLO"`
445+
}
446+
447+
sdl := `
448+
type Query {
449+
_hello: String!
450+
hello: String!
451+
Hello: String!
452+
HELLO: String!
453+
}`
454+
455+
r := &resolver{
456+
Hello: "This field is not used during query execution!",
457+
HelloLower: "Hello, graphql!",
458+
HelloTitle: "Hello, GraphQL!",
459+
HelloUnderscore: "Hello, _!",
460+
HelloUpper: "Hello, GRAPHQL!",
461+
}
462+
463+
query := `
464+
{
465+
_hello
466+
hello
467+
Hello
468+
HELLO
469+
}
470+
`
471+
472+
schema := graphql.MustParseSchema(sdl, r, graphql.UseFieldResolvers())
473+
res := schema.Exec(context.Background(), query, "", nil)
474+
475+
enc := json.NewEncoder(os.Stdout)
476+
enc.SetIndent("", " ")
477+
err := enc.Encode(res)
478+
if err != nil {
479+
panic(err)
480+
}
481+
482+
// output:
483+
// {
484+
// "data": {
485+
// "_hello": "Hello, _!",
486+
// "hello": "Hello, graphql!",
487+
// "Hello": "Hello, GraphQL!",
488+
// "HELLO": "Hello, GRAPHQL!"
489+
// }
490+
// }
491+
}

graphql_test.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2529,7 +2529,7 @@ func TestInlineFragments(t *testing.T) {
25292529
},
25302530

25312531
{
2532-
Schema: socialSchema,
2532+
Schema: graphql.MustParseSchema(social.Schema, &social.Resolver{}, graphql.UseFieldResolvers()),
25332533
Query: `
25342534
query {
25352535
admin(id: "0x01") {
@@ -5634,3 +5634,68 @@ func TestSchemaExtension(t *testing.T) {
56345634
t.Fatalf(`expected an "awesome" schema directive, got %q`, dirs[0].Name.Name)
56355635
}
56365636
}
5637+
5638+
func TestGraphqlNames(t *testing.T) {
5639+
t.Parallel()
5640+
5641+
sdl1 := `
5642+
type Query {
5643+
hello: String!
5644+
}
5645+
`
5646+
type invalidResolver1 struct {
5647+
Field1 string `graphql:"hello"`
5648+
Field2 string `graphql:"hello"`
5649+
}
5650+
5651+
wantErr := fmt.Errorf(`*graphql_test.invalidResolver1 does not resolve "Query": multiple fields have a graphql reflect tag "hello"`)
5652+
_, err := graphql.ParseSchema(sdl1, &invalidResolver1{}, graphql.UseFieldResolvers())
5653+
if err == nil || err.Error() != wantErr.Error() {
5654+
t.Fatalf("want err %q, got %q", wantErr, err)
5655+
}
5656+
5657+
gqltesting.RunTests(t, []*gqltesting.Test{
5658+
{
5659+
Schema: graphql.MustParseSchema(`
5660+
type Query {
5661+
_hello: String!
5662+
hello: String!
5663+
Hello: String!
5664+
HELLO: String!
5665+
}`,
5666+
func() interface{} {
5667+
type helloTagResolver struct {
5668+
Hello string
5669+
HelloUnderscore string `graphql:"_hello"`
5670+
HelloLower string `graphql:"hello"`
5671+
HelloTitle string `graphql:"Hello"`
5672+
HelloUpper string `graphql:"HELLO"`
5673+
}
5674+
return &helloTagResolver{
5675+
Hello: "This field will not be used during query execution!",
5676+
HelloLower: "Hello, graphql!",
5677+
HelloTitle: "Hello, GraphQL!",
5678+
HelloUnderscore: "Hello, _!",
5679+
HelloUpper: "Hello, GRAPHQL!",
5680+
}
5681+
}(),
5682+
graphql.UseFieldResolvers()),
5683+
Query: `
5684+
{
5685+
_hello
5686+
hello
5687+
Hello
5688+
HELLO
5689+
}
5690+
`,
5691+
ExpectedResult: `
5692+
{
5693+
"_hello": "Hello, _!",
5694+
"hello": "Hello, graphql!",
5695+
"Hello": "Hello, GraphQL!",
5696+
"HELLO": "Hello, GRAPHQL!"
5697+
}
5698+
`,
5699+
},
5700+
})
5701+
}

internal/exec/resolvable/resolvable.go

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -444,18 +444,22 @@ func (b *execBuilder) makeObjectExec(typeName string, fields ast.FieldsDefinitio
444444

445445
Fields := make(map[string]*Field)
446446
rt := unwrapPtr(resolverType)
447-
fieldsCount := fieldCount(rt, map[string]int{})
447+
fieldsCount, fieldTagsCount := fieldCount(rt, map[string]int{}, map[string]int{})
448448
for _, f := range fields {
449449
var fieldIndex []int
450450
methodIndex := findMethod(resolverType, f.Name)
451451
if b.useFieldResolvers && methodIndex == -1 {
452-
if fieldsCount[strings.ToLower(stripUnderscore(f.Name))] > 1 {
452+
// If a resolver field is ambiguous thrown an error unless there is exactly one field with the given graphql
453+
// reflect tag. In that case use the field with the reflect tag.
454+
if fieldTagsCount[f.Name] > 1 {
455+
return nil, fmt.Errorf("%s does not resolve %q: multiple fields have a graphql reflect tag %q", resolverType, typeName, f.Name)
456+
} else if fieldsCount[strings.ToLower(stripUnderscore(f.Name))] > 1 && fieldTagsCount[f.Name] != 1 {
453457
return nil, fmt.Errorf("%s does not resolve %q: ambiguous field %q", resolverType, typeName, f.Name)
454458
}
455-
fieldIndex = findField(rt, f.Name, []int{})
459+
fieldIndex = findField(rt, f.Name, []int{}, fieldTagsCount)
456460
}
457461
if methodIndex == -1 && len(fieldIndex) == 0 {
458-
hint := ""
462+
var hint string
459463
if findMethod(reflect.PtrTo(resolverType), f.Name) != -1 {
460464
hint = " (hint: the method exists on the pointer type)"
461465
}
@@ -529,9 +533,7 @@ func (b *execBuilder) makeObjectExec(typeName string, fields ast.FieldsDefinitio
529533
var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
530534
var errorType = reflect.TypeOf((*error)(nil)).Elem()
531535

532-
func (b *execBuilder) makeFieldExec(typeName string, f *ast.FieldDefinition, m reflect.Method, sf reflect.StructField,
533-
methodIndex int, fieldIndex []int, methodHasReceiver bool) (*Field, error) {
534-
536+
func (b *execBuilder) makeFieldExec(typeName string, f *ast.FieldDefinition, m reflect.Method, sf reflect.StructField, methodIndex int, fieldIndex []int, methodHasReceiver bool) (*Field, error) {
535537
var argsPacker *packer.StructPacker
536538
var hasError bool
537539
var hasContext bool
@@ -662,17 +664,29 @@ func findMethod(t reflect.Type, name string) int {
662664
return -1
663665
}
664666

665-
func findField(t reflect.Type, name string, index []int) []int {
667+
func findField(t reflect.Type, name string, index []int, matchingTagsCount map[string]int) []int {
666668
for i := 0; i < t.NumField(); i++ {
667669
field := t.Field(i)
668670

669671
if field.Type.Kind() == reflect.Struct && field.Anonymous {
670-
newIndex := findField(field.Type, name, []int{i})
672+
newIndex := findField(field.Type, name, []int{i}, matchingTagsCount)
671673
if len(newIndex) > 1 {
672674
return append(index, newIndex...)
673675
}
674676
}
675677

678+
if gt, ok := field.Tag.Lookup("graphql"); ok {
679+
if name == gt {
680+
return append(index, i)
681+
}
682+
}
683+
684+
// The current field's tag didn't match, however, if the tag of another field matches,
685+
// then skip the name matching until we find the desired field with the correct tag.
686+
if matchingTagsCount[name] > 0 {
687+
continue
688+
}
689+
676690
if strings.EqualFold(stripUnderscore(name), stripUnderscore(field.Name)) {
677691
return append(index, i)
678692
}
@@ -682,26 +696,40 @@ func findField(t reflect.Type, name string, index []int) []int {
682696
}
683697

684698
// fieldCount helps resolve ambiguity when more than one embedded struct contains fields with the same name.
685-
func fieldCount(t reflect.Type, count map[string]int) map[string]int {
699+
// or when a field has a `graphql` reflect tag with the same name as some other field causing name collision.
700+
func fieldCount(t reflect.Type, count, tagsCount map[string]int) (map[string]int, map[string]int) {
686701
if t.Kind() != reflect.Struct {
687-
return nil
702+
return nil, nil
688703
}
689704

690705
for i := 0; i < t.NumField(); i++ {
691706
field := t.Field(i)
692-
fieldName := strings.ToLower(stripUnderscore(field.Name))
707+
var fieldName, gt string
708+
var hasTag bool
709+
if gt, hasTag = field.Tag.Lookup("graphql"); hasTag && gt != "" {
710+
fieldName = gt
711+
} else {
712+
fieldName = strings.ToLower(stripUnderscore(field.Name))
713+
}
693714

694715
if field.Type.Kind() == reflect.Struct && field.Anonymous {
695-
count = fieldCount(field.Type, count)
716+
count, tagsCount = fieldCount(field.Type, count, tagsCount)
696717
} else {
697718
if _, ok := count[fieldName]; !ok {
698719
count[fieldName] = 0
699720
}
700721
count[fieldName]++
722+
if !hasTag {
723+
continue
724+
}
725+
if _, ok := count[gt]; !ok {
726+
tagsCount[gt] = 0
727+
}
728+
tagsCount[gt]++
701729
}
702730
}
703731

704-
return count
732+
return count, tagsCount
705733
}
706734

707735
func unwrapNonNull(t ast.Type) (ast.Type, bool) {

introspection_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import (
1111
"github.com/graph-gophers/graphql-go/example/starwars"
1212
)
1313

14-
var socialSchema = graphql.MustParseSchema(social.Schema, &social.Resolver{}, graphql.UseFieldResolvers())
15-
1614
func TestSchema_ToJSON(t *testing.T) {
1715
t.Parallel()
1816

@@ -29,7 +27,7 @@ func TestSchema_ToJSON(t *testing.T) {
2927
}{
3028
{
3129
Name: "Social Schema",
32-
Args: args{Schema: socialSchema},
30+
Args: args{Schema: graphql.MustParseSchema(social.Schema, &social.Resolver{}, graphql.UseFieldResolvers())},
3331
Want: want{JSON: mustReadFile("example/social/introspect.json")},
3432
},
3533
{

0 commit comments

Comments
 (0)