From 81e957c6ebd419dc17a986bf74e3d448929c3126 Mon Sep 17 00:00:00 2001 From: David Ackroyd Date: Mon, 28 Oct 2019 19:31:32 +1100 Subject: [PATCH] Clarify errors for mismatching input implementation Producing clearer error messages when field input arguments are implemented by code: * Which does not match the schema e.g. missing field; or * Function missing struct wrapper for field arguments --- graphql_test.go | 173 +++++++++++++++++++++++++ internal/exec/packer/packer.go | 4 +- internal/exec/resolvable/resolvable.go | 2 +- 3 files changed, 176 insertions(+), 3 deletions(-) diff --git a/graphql_test.go b/graphql_test.go index ba4a3ebb..7fe0eb63 100644 --- a/graphql_test.go +++ b/graphql_test.go @@ -2696,6 +2696,179 @@ func TestInput(t *testing.T) { }) } +type inputArgumentsHello struct {} + +type inputArgumentsScalarMismatch1 struct{} + +type inputArgumentsScalarMismatch2 struct{} + +type inputArgumentsObjectMismatch1 struct{} + +type inputArgumentsObjectMismatch2 struct{} + +type inputArgumentsObjectMismatch3 struct{} + +type helloInput struct { + Name string +} + +type helloInputMismatch struct { + World string +} + +func (r *inputArgumentsHello) Hello(args struct { Input *helloInput }) string { + return "Hello " + args.Input.Name + "!" +} + +func (r *inputArgumentsScalarMismatch1) Hello(name string) string { + return "Hello " + name + "!" +} + +func (r *inputArgumentsScalarMismatch2) Hello(args struct { World string }) string { + return "Hello " + args.World + "!" +} + +func (r *inputArgumentsObjectMismatch1) Hello(in helloInput) string { + return "Hello " + in.Name + "!" +} + +func (r *inputArgumentsObjectMismatch2) Hello(args struct { Input *helloInputMismatch }) string { + return "Hello " + args.Input.World + "!" +} + +func (r *inputArgumentsObjectMismatch3) Hello(args struct { Input *struct { Thing string } }) string { + return "Hello " + args.Input.Thing + "!" +} + +func TestInputArguments_failSchemaParsing(t *testing.T) { + type args struct { + Resolver interface{} + Schema string + } + type want struct { + Error string + } + testTable := map[string]struct { + Args args + Want want + }{ + "Non-input type used with field arguments": { + Args: args{ + Resolver: &inputArgumentsHello{}, + Schema: ` + schema { + query: Query + } + type Query { + hello(input: HelloInput): String! + } + type HelloInput { + name: String + } + `, + }, + Want: want{Error: "field \"Input\": type of kind OBJECT can not be used as input\n\tused by (*graphql_test.inputArgumentsHello).Hello"}, + }, + "Missing Args Wrapper for scalar input": { + Args: args{ + Resolver: &inputArgumentsScalarMismatch1{}, + Schema: ` + schema { + query: Query + } + type Query { + hello(name: String): String! + } + input HelloInput { + name: String + } + `, + }, + Want: want{Error: "expected struct or pointer to struct, got string (hint: missing `args struct { ... }` wrapper for field arguments?)\n\tused by (*graphql_test.inputArgumentsScalarMismatch1).Hello"}, + }, + "Mismatching field name for scalar input": { + Args: args{ + Resolver: &inputArgumentsScalarMismatch2{}, + Schema: ` + schema { + query: Query + } + type Query { + hello(name: String): String! + } + `, + }, + Want: want{Error: "struct { World string } does not define field \"name\" (hint: missing `args struct { ... }` wrapper for field arguments, or missing field on input struct)\n\tused by (*graphql_test.inputArgumentsScalarMismatch2).Hello"}, + }, + "Missing Args Wrapper for Input type": { + Args: args{ + Resolver: &inputArgumentsObjectMismatch1{}, + Schema: ` + schema { + query: Query + } + type Query { + hello(input: HelloInput): String! + } + input HelloInput { + name: String + } + `, + }, + Want: want{Error: "graphql_test.helloInput does not define field \"input\" (hint: missing `args struct { ... }` wrapper for field arguments, or missing field on input struct)\n\tused by (*graphql_test.inputArgumentsObjectMismatch1).Hello"}, + }, + "Input struct missing field": { + Args: args{ + Resolver: &inputArgumentsObjectMismatch2{}, + Schema: ` + schema { + query: Query + } + type Query { + hello(input: HelloInput): String! + } + input HelloInput { + name: String + } + `, + }, + Want: want{Error: "field \"Input\": *graphql_test.helloInputMismatch does not define field \"name\" (hint: missing `args struct { ... }` wrapper for field arguments, or missing field on input struct)\n\tused by (*graphql_test.inputArgumentsObjectMismatch2).Hello"}, + }, + "Inline Input struct missing field": { + Args: args{ + Resolver: &inputArgumentsObjectMismatch3{}, + Schema: ` + schema { + query: Query + } + type Query { + hello(input: HelloInput): String! + } + input HelloInput { + name: String + } + `, + }, + Want: want{Error: "field \"Input\": *struct { Thing string } does not define field \"name\" (hint: missing `args struct { ... }` wrapper for field arguments, or missing field on input struct)\n\tused by (*graphql_test.inputArgumentsObjectMismatch3).Hello"}, + }, + } + + for name, tt := range testTable { + tt := tt + t.Run(name, func(t *testing.T) { + t.Parallel() + + _, err := graphql.ParseSchema(tt.Args.Schema, tt.Args.Resolver) + if err == nil || err.Error() != tt.Want.Error { + t.Log("Schema parsing error mismatch") + t.Logf("got: %s", err) + t.Logf("exp: %s", tt.Want.Error) + t.Fail() + } + }) + } +} + func TestComposedFragments(t *testing.T) { gqltesting.RunTests(t, []*gqltesting.Test{ { diff --git a/internal/exec/packer/packer.go b/internal/exec/packer/packer.go index 22706bcd..fca88da3 100644 --- a/internal/exec/packer/packer.go +++ b/internal/exec/packer/packer.go @@ -160,7 +160,7 @@ func (b *Builder) MakeStructPacker(values common.InputValueList, typ reflect.Typ usePtr = true } if structType.Kind() != reflect.Struct { - return nil, fmt.Errorf("expected struct or pointer to struct, got %s", typ) + return nil, fmt.Errorf("expected struct or pointer to struct, got %s (hint: missing `args struct { ... }` wrapper for field arguments?)", typ) } var fields []*structPackerField @@ -172,7 +172,7 @@ func (b *Builder) MakeStructPacker(values common.InputValueList, typ reflect.Typ sf, ok := structType.FieldByNameFunc(fx) if !ok { - return nil, fmt.Errorf("missing argument %q", v.Name) + return nil, fmt.Errorf("%s does not define field %q (hint: missing `args struct { ... }` wrapper for field arguments, or missing field on input struct)", typ, v.Name.Name) } if sf.PkgPath != "" { return nil, fmt.Errorf("field %q must be exported", sf.Name) diff --git a/internal/exec/resolvable/resolvable.go b/internal/exec/resolvable/resolvable.go index 1b248e69..9d9fe6e8 100644 --- a/internal/exec/resolvable/resolvable.go +++ b/internal/exec/resolvable/resolvable.go @@ -255,7 +255,7 @@ func (b *execBuilder) makeObjectExec(typeName string, fields schema.FieldList, p } fe, err := b.makeFieldExec(typeName, f, m, sf, methodIndex, fieldIndex, methodHasReceiver) if err != nil { - return nil, fmt.Errorf("%s\n\treturned by (%s).%s", err, resolverType, m.Name) + return nil, fmt.Errorf("%s\n\tused by (%s).%s", err, resolverType, m.Name) } Fields[f.Name] = fe }