From 1e265e6c1757d827966d2fc6468972c44a6241ad Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Tue, 17 Aug 2021 17:11:02 -0700 Subject: [PATCH] Add support for interfaces, part 1: the simplest cases In this commit I begin the journey to add the long-awaited support for interfaces (part of #8). Well, it's not the beginning: I already had some half-written broken code around. But it's the first fully functional support, and especially, the first *tested* support; it's probably best to review the nontrivially-changed code as if it were new. Conceptually, the code so far is pretty simple: we generate an interface type, and the implementations. (That code is in fact mostly unchanged.) The complexity comes in because encoding/json doesn't know how to unmarshal that. So we have to add an UnmarshalJSON method, which actually has to be on the types with interface-type fields, that knows how. I factored it into two methods, such that that UnmarshalJSON method is just glue, and then there's a separate function, corresponding to each interface-type, that actually does all the work. (If only one could just write it as an actual method!) The method uses the same trick suggested to me by a few others in another context to deserialize all but one field, then handle that field specially, which is discussed in the code. This still has some limitations, which will be lifted in future commits: - it doesn't allow for list-of-interface fields - it requires that you manually ask for `__typename` - it doesn't support fragments, i.e. you can only query for interface fields, not concrete-type-specific ones But it works, even in integration tests, which is progress! As a part of this, I added a proper config option for the "allow broken features" flag, since I need to be able to set it from the integration tests which are in a separate package (and actually shell out via `go generate`). I also renamed what was to be the first case (InterfaceNoFragments), and replaced it with a further-simplified version (avoiding list-of-interface fields. [1] https://github.com/benjaminjkraft/notes/blob/master/go-json-interfaces.md Issue: https://github.com/Khan/genqlient/issues/8 Test plan: make tesc --- generate/config.go | 7 + generate/generate.go | 5 +- generate/generate_test.go | 14 +- generate/testdata/errors/MissingTypeName.go | 5 + .../testdata/errors/MissingTypeName.graphql | 1 + .../errors/MissingTypeName.schema.graphql | 11 + .../queries/InterfaceListField.graphql | 11 + .../testdata/queries/InterfaceNesting.graphql | 3 + .../queries/InterfaceNoFragments.graphql | 10 +- generate/testdata/queries/schema.graphql | 1 + ...ield.graphql-InterfaceListField.graphql.go | 32 ++ ...ld.graphql-InterfaceListField.graphql.json | 9 + ...esting.graphql-InterfaceNesting.graphql.go | 161 +++++++- ...ting.graphql-InterfaceNesting.graphql.json | 2 +- ...ts.graphql-InterfaceNoFragments.graphql.go | 118 ++++-- ....graphql-InterfaceNoFragments.graphql.json | 2 +- ...gments.graphql-UnionNoFragments.graphql.go | 62 +-- .../TestGenerateErrors-MissingTypeName.go | 1 + ...TestGenerateErrors-MissingTypeName.graphql | 1 + generate/traverse.go | 17 +- generate/types.go | 54 ++- generate/unmarshal.go | 58 --- generate/unmarshal.go.tmpl | 56 +-- generate/unmarshal_helper.go.tmpl | 28 ++ internal/integration/generated.go | 120 ++++++ internal/integration/genqlient.yaml | 1 + internal/integration/integration_test.go | 43 +++ internal/integration/schema.graphql | 20 +- internal/integration/server/gqlgen_exec.go | 357 +++++++++++++++++- internal/integration/server/gqlgen_models.go | 62 +++ internal/integration/server/server.go | 23 ++ 31 files changed, 1109 insertions(+), 186 deletions(-) create mode 100644 generate/testdata/errors/MissingTypeName.go create mode 100644 generate/testdata/errors/MissingTypeName.graphql create mode 100644 generate/testdata/errors/MissingTypeName.schema.graphql create mode 100644 generate/testdata/queries/InterfaceListField.graphql create mode 100644 generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.json create mode 100644 generate/testdata/snapshots/TestGenerateErrors-MissingTypeName.go create mode 100644 generate/testdata/snapshots/TestGenerateErrors-MissingTypeName.graphql delete mode 100644 generate/unmarshal.go create mode 100644 generate/unmarshal_helper.go.tmpl diff --git a/generate/config.go b/generate/config.go index 364891ad..f4632b68 100644 --- a/generate/config.go +++ b/generate/config.go @@ -73,6 +73,13 @@ type Config struct { // and UnmarshalJSON methods, or otherwise be convertible to JSON. Scalars map[string]string `yaml:"scalars"` + // Set to true to use features that aren't fully ready to use. + // + // This is primarily intended for genqlient's own tests. These features + // are likely BROKEN and come with NO EXPECTATION OF COMPATIBBILITY. Use + // them at your own risk! + AllowBrokenFeatures bool `yaml:"allow_broken_features"` + // Set automatically to the filename of the config file itself. configFilename string } diff --git a/generate/generate.go b/generate/generate.go index 60d94d32..19f9a656 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -17,9 +17,6 @@ import ( "golang.org/x/tools/imports" ) -// Set to true to test features that aren't yet really ready. -var allowBrokenFeatures = false - // generator is the context for the codegen process (and ends up getting passed // to the template). type generator struct { @@ -230,7 +227,7 @@ func Generate(config *Config) (map[string][]byte, error) { strings.Join(config.Operations, ", ")) } - if len(document.Fragments) > 0 && !allowBrokenFeatures { + if len(document.Fragments) > 0 && !config.AllowBrokenFeatures { return nil, errorf(document.Fragments[0].Position, "genqlient does not yet support fragments") } diff --git a/generate/generate_test.go b/generate/generate_test.go index 5dd8fb3a..fdbc33ac 100644 --- a/generate/generate_test.go +++ b/generate/generate_test.go @@ -30,9 +30,6 @@ const ( // update the snapshots. Make sure to check that the output is sensible; the // snapshots don't even get compiled! func TestGenerate(t *testing.T) { - // we can test parts of features even if they're not done yet! - allowBrokenFeatures = true - files, err := ioutil.ReadDir(dataDir) if err != nil { t.Fatal(err) @@ -59,6 +56,7 @@ func TestGenerate(t *testing.T) { "Junk": "interface{}", "ComplexJunk": "[]map[string]*[]*map[string]interface{}", }, + AllowBrokenFeatures: true, }) if err != nil { t.Fatal(err) @@ -73,9 +71,9 @@ func TestGenerate(t *testing.T) { t.Run("Build", func(t *testing.T) { if testing.Short() { t.Skip("skipping build due to -short") - } else if sourceFilename == "InterfaceNesting.graphql" || - sourceFilename == "InterfaceNoFragments.graphql" || - sourceFilename == "Omitempty.graphql" { + } else if sourceFilename == "InterfaceNesting.graphql" || // #8 + sourceFilename == "InterfaceListField.graphql" || // #8 + sourceFilename == "Omitempty.graphql" { // #43 t.Skip("TODO: enable these once they build") } @@ -109,9 +107,6 @@ func TestGenerate(t *testing.T) { } func TestGenerateErrors(t *testing.T) { - // we can test parts of features even if they're not done yet! - allowBrokenFeatures = true - files, err := ioutil.ReadDir(errorsDir) if err != nil { t.Fatal(err) @@ -138,6 +133,7 @@ func TestGenerateErrors(t *testing.T) { "ValidScalar": "string", "InvalidScalar": "bogus", }, + AllowBrokenFeatures: true, }) if err == nil { t.Fatal("expected an error") diff --git a/generate/testdata/errors/MissingTypeName.go b/generate/testdata/errors/MissingTypeName.go new file mode 100644 index 00000000..12d9eb3c --- /dev/null +++ b/generate/testdata/errors/MissingTypeName.go @@ -0,0 +1,5 @@ +package errors + +const _ = `# @genqlient + query MyQuery { i { f } } +` diff --git a/generate/testdata/errors/MissingTypeName.graphql b/generate/testdata/errors/MissingTypeName.graphql new file mode 100644 index 00000000..c6b95aa6 --- /dev/null +++ b/generate/testdata/errors/MissingTypeName.graphql @@ -0,0 +1 @@ +query MyQuery { i { f } } diff --git a/generate/testdata/errors/MissingTypeName.schema.graphql b/generate/testdata/errors/MissingTypeName.schema.graphql new file mode 100644 index 00000000..c990ba3d --- /dev/null +++ b/generate/testdata/errors/MissingTypeName.schema.graphql @@ -0,0 +1,11 @@ +type Query { + i: I +} + +type T implements I { + f: String! +} + +interface I { + f: String! +} diff --git a/generate/testdata/queries/InterfaceListField.graphql b/generate/testdata/queries/InterfaceListField.graphql new file mode 100644 index 00000000..55fd02a7 --- /dev/null +++ b/generate/testdata/queries/InterfaceListField.graphql @@ -0,0 +1,11 @@ +query InterfaceNoFragmentsQuery { + root { + id + name + children { + __typename + id + name + } + } +} diff --git a/generate/testdata/queries/InterfaceNesting.graphql b/generate/testdata/queries/InterfaceNesting.graphql index be0151d0..d3618ac7 100644 --- a/generate/testdata/queries/InterfaceNesting.graphql +++ b/generate/testdata/queries/InterfaceNesting.graphql @@ -2,10 +2,13 @@ query InterfaceNesting { root { id children { + __typename id parent { + __typename id children { + __typename id } } diff --git a/generate/testdata/queries/InterfaceNoFragments.graphql b/generate/testdata/queries/InterfaceNoFragments.graphql index 0e8f4a00..88d0bb31 100644 --- a/generate/testdata/queries/InterfaceNoFragments.graphql +++ b/generate/testdata/queries/InterfaceNoFragments.graphql @@ -1,10 +1,4 @@ query InterfaceNoFragmentsQuery { - root { - id - name - children { - id - name - } - } + root { id name } # (make sure sibling fields work) + randomItem { __typename id name } } diff --git a/generate/testdata/queries/schema.graphql b/generate/testdata/queries/schema.graphql index 377bed6b..31f7c2ab 100644 --- a/generate/testdata/queries/schema.graphql +++ b/generate/testdata/queries/schema.graphql @@ -104,6 +104,7 @@ type Query { """usersWithRole looks a user up by role.""" usersWithRole(role: Role!): [User!]! root: Topic! + randomItem: Content! randomLeaf: LeafContent! convert(dt: DateTime!, tz: String): DateTime! maybeConvert(dt: DateTime, tz: String): DateTime diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go index 28dffaca..5c14d931 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go @@ -3,6 +3,9 @@ package test // Code generated by github.com/Khan/genqlient, DO NOT EDIT. import ( + "encoding/json" + "fmt" + "github.com/Khan/genqlient/graphql" "github.com/Khan/genqlient/internal/testutil" ) @@ -43,6 +46,35 @@ func (v *InterfaceNoFragmentsQueryRootTopicChildrenVideo) implementsGraphQLInter func (v *InterfaceNoFragmentsQueryRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { } +func __unmarshalInterfaceNoFragmentsQueryRootTopicChildrenContent(v *InterfaceNoFragmentsQueryRootTopicChildrenContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(InterfaceNoFragmentsQueryRootTopicChildrenArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InterfaceNoFragmentsQueryRootTopicChildrenVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(InterfaceNoFragmentsQueryRootTopicChildrenTopic) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for InterfaceNoFragmentsQueryRootTopicChildrenContent: "%v"`, tn.TypeName) + } +} + // InterfaceNoFragmentsQueryRootTopicChildrenTopic includes the requested fields of the GraphQL type Topic. type InterfaceNoFragmentsQueryRootTopicChildrenTopic struct { Typename string `json:"__typename"` diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.json b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.json new file mode 100644 index 00000000..f6665e9d --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.json @@ -0,0 +1,9 @@ +{ + "operations": [ + { + "operationName": "InterfaceNoFragmentsQuery", + "query": "\nquery InterfaceNoFragmentsQuery {\n\troot {\n\t\tid\n\t\tname\n\t\tchildren {\n\t\t\t__typename\n\t\t\tid\n\t\t\tname\n\t\t}\n\t}\n}\n", + "sourceLocation": "testdata/queries/InterfaceListField.graphql" + } + ] +} diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go index 581d838a..cb47b03d 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go @@ -3,6 +3,9 @@ package test // Code generated by github.com/Khan/genqlient, DO NOT EDIT. import ( + "encoding/json" + "fmt" + "github.com/Khan/genqlient/graphql" "github.com/Khan/genqlient/internal/testutil" ) @@ -21,6 +24,7 @@ type InterfaceNestingRootTopic struct { // InterfaceNestingRootTopicChildrenArticle includes the requested fields of the GraphQL type Article. type InterfaceNestingRootTopicChildrenArticle struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` Parent InterfaceNestingRootTopicChildrenArticleParentTopic `json:"parent"` @@ -28,6 +32,7 @@ type InterfaceNestingRootTopicChildrenArticle struct { // InterfaceNestingRootTopicChildrenArticleParentTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenArticleParentTopic struct { + Typename string `json:"__typename"` // ID is documented in the Content interface. Id testutil.ID `json:"id"` Children []InterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent `json:"children"` @@ -35,6 +40,7 @@ type InterfaceNestingRootTopicChildrenArticleParentTopic struct { // InterfaceNestingRootTopicChildrenArticleParentTopicChildrenArticle includes the requested fields of the GraphQL type Article. type InterfaceNestingRootTopicChildrenArticleParentTopicChildrenArticle struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` } @@ -47,21 +53,52 @@ type InterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent interfac implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() } -func (v InterfaceNestingRootTopicChildrenArticleParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { +func (v *InterfaceNestingRootTopicChildrenArticleParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { +} +func (v *InterfaceNestingRootTopicChildrenArticleParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { } -func (v InterfaceNestingRootTopicChildrenArticleParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { +func (v *InterfaceNestingRootTopicChildrenArticleParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { } -func (v InterfaceNestingRootTopicChildrenArticleParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { + +func __unmarshalInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent(v *InterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(InterfaceNestingRootTopicChildrenArticleParentTopicChildrenArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InterfaceNestingRootTopicChildrenArticleParentTopicChildrenVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(InterfaceNestingRootTopicChildrenArticleParentTopicChildrenTopic) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for InterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent: "%v"`, tn.TypeName) + } } // InterfaceNestingRootTopicChildrenArticleParentTopicChildrenTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenArticleParentTopicChildrenTopic struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` } // InterfaceNestingRootTopicChildrenArticleParentTopicChildrenVideo includes the requested fields of the GraphQL type Video. type InterfaceNestingRootTopicChildrenArticleParentTopicChildrenVideo struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` } @@ -74,15 +111,45 @@ type InterfaceNestingRootTopicChildrenContent interface { implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() } -func (v InterfaceNestingRootTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { +func (v *InterfaceNestingRootTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { } -func (v InterfaceNestingRootTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { +func (v *InterfaceNestingRootTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { } -func (v InterfaceNestingRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { +func (v *InterfaceNestingRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { +} + +func __unmarshalInterfaceNestingRootTopicChildrenContent(v *InterfaceNestingRootTopicChildrenContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(InterfaceNestingRootTopicChildrenArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InterfaceNestingRootTopicChildrenVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(InterfaceNestingRootTopicChildrenTopic) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for InterfaceNestingRootTopicChildrenContent: "%v"`, tn.TypeName) + } } // InterfaceNestingRootTopicChildrenTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenTopic struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` Parent InterfaceNestingRootTopicChildrenTopicParentTopic `json:"parent"` @@ -90,6 +157,7 @@ type InterfaceNestingRootTopicChildrenTopic struct { // InterfaceNestingRootTopicChildrenTopicParentTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenTopicParentTopic struct { + Typename string `json:"__typename"` // ID is documented in the Content interface. Id testutil.ID `json:"id"` Children []InterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent `json:"children"` @@ -97,6 +165,7 @@ type InterfaceNestingRootTopicChildrenTopicParentTopic struct { // InterfaceNestingRootTopicChildrenTopicParentTopicChildrenArticle includes the requested fields of the GraphQL type Article. type InterfaceNestingRootTopicChildrenTopicParentTopicChildrenArticle struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` } @@ -109,27 +178,59 @@ type InterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent interface implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() } -func (v InterfaceNestingRootTopicChildrenTopicParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { +func (v *InterfaceNestingRootTopicChildrenTopicParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { } -func (v InterfaceNestingRootTopicChildrenTopicParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { +func (v *InterfaceNestingRootTopicChildrenTopicParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { } -func (v InterfaceNestingRootTopicChildrenTopicParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { +func (v *InterfaceNestingRootTopicChildrenTopicParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { +} + +func __unmarshalInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent(v *InterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(InterfaceNestingRootTopicChildrenTopicParentTopicChildrenArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InterfaceNestingRootTopicChildrenTopicParentTopicChildrenVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(InterfaceNestingRootTopicChildrenTopicParentTopicChildrenTopic) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for InterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent: "%v"`, tn.TypeName) + } } // InterfaceNestingRootTopicChildrenTopicParentTopicChildrenTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenTopicParentTopicChildrenTopic struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` } // InterfaceNestingRootTopicChildrenTopicParentTopicChildrenVideo includes the requested fields of the GraphQL type Video. type InterfaceNestingRootTopicChildrenTopicParentTopicChildrenVideo struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` } // InterfaceNestingRootTopicChildrenVideo includes the requested fields of the GraphQL type Video. type InterfaceNestingRootTopicChildrenVideo struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` Parent InterfaceNestingRootTopicChildrenVideoParentTopic `json:"parent"` @@ -137,6 +238,7 @@ type InterfaceNestingRootTopicChildrenVideo struct { // InterfaceNestingRootTopicChildrenVideoParentTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenVideoParentTopic struct { + Typename string `json:"__typename"` // ID is documented in the Content interface. Id testutil.ID `json:"id"` Children []InterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent `json:"children"` @@ -144,6 +246,7 @@ type InterfaceNestingRootTopicChildrenVideoParentTopic struct { // InterfaceNestingRootTopicChildrenVideoParentTopicChildrenArticle includes the requested fields of the GraphQL type Article. type InterfaceNestingRootTopicChildrenVideoParentTopicChildrenArticle struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` } @@ -156,21 +259,52 @@ type InterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent interface implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() } -func (v InterfaceNestingRootTopicChildrenVideoParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { +func (v *InterfaceNestingRootTopicChildrenVideoParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { } -func (v InterfaceNestingRootTopicChildrenVideoParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { +func (v *InterfaceNestingRootTopicChildrenVideoParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { } -func (v InterfaceNestingRootTopicChildrenVideoParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { +func (v *InterfaceNestingRootTopicChildrenVideoParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { +} + +func __unmarshalInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent(v *InterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(InterfaceNestingRootTopicChildrenVideoParentTopicChildrenArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InterfaceNestingRootTopicChildrenVideoParentTopicChildrenVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(InterfaceNestingRootTopicChildrenVideoParentTopicChildrenTopic) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for InterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent: "%v"`, tn.TypeName) + } } // InterfaceNestingRootTopicChildrenVideoParentTopicChildrenTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenVideoParentTopicChildrenTopic struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` } // InterfaceNestingRootTopicChildrenVideoParentTopicChildrenVideo includes the requested fields of the GraphQL type Video. type InterfaceNestingRootTopicChildrenVideoParentTopicChildrenVideo struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` } @@ -187,10 +321,13 @@ query InterfaceNesting { root { id children { + __typename id parent { + __typename id children { + __typename id } } diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.json b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.json index 03766ca6..aa620137 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.json +++ b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.json @@ -2,7 +2,7 @@ "operations": [ { "operationName": "InterfaceNesting", - "query": "\nquery InterfaceNesting {\n\troot {\n\t\tid\n\t\tchildren {\n\t\t\tid\n\t\t\tparent {\n\t\t\t\tid\n\t\t\t\tchildren {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n", + "query": "\nquery InterfaceNesting {\n\troot {\n\t\tid\n\t\tchildren {\n\t\t\t__typename\n\t\t\tid\n\t\t\tparent {\n\t\t\t\t__typename\n\t\t\t\tid\n\t\t\t\tchildren {\n\t\t\t\t\t__typename\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n", "sourceLocation": "testdata/queries/InterfaceNesting.graphql" } ] diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go index 795343a8..e182548a 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go @@ -3,59 +3,118 @@ package test // Code generated by github.com/Khan/genqlient, DO NOT EDIT. import ( + "encoding/json" + "fmt" + "github.com/Khan/genqlient/graphql" "github.com/Khan/genqlient/internal/testutil" ) -// InterfaceNoFragmentsQueryResponse is returned by InterfaceNoFragmentsQuery on success. -type InterfaceNoFragmentsQueryResponse struct { - Root InterfaceNoFragmentsQueryRootTopic `json:"root"` -} - -// InterfaceNoFragmentsQueryRootTopic includes the requested fields of the GraphQL type Topic. -type InterfaceNoFragmentsQueryRootTopic struct { - // ID is documented in the Content interface. - Id testutil.ID `json:"id"` - Name string `json:"name"` - Children []InterfaceNoFragmentsQueryRootTopicChildrenContent `json:"children"` -} - -// InterfaceNoFragmentsQueryRootTopicChildrenArticle includes the requested fields of the GraphQL type Article. -type InterfaceNoFragmentsQueryRootTopicChildrenArticle struct { +// InterfaceNoFragmentsQueryRandomItemArticle includes the requested fields of the GraphQL type Article. +type InterfaceNoFragmentsQueryRandomItemArticle struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` Name string `json:"name"` } -// InterfaceNoFragmentsQueryRootTopicChildrenContent includes the requested fields of the GraphQL type Content. +// InterfaceNoFragmentsQueryRandomItemContent includes the requested fields of the GraphQL type Content. // The GraphQL type's documentation follows. // // Content is implemented by various types like Article, Video, and Topic. -type InterfaceNoFragmentsQueryRootTopicChildrenContent interface { - implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() +type InterfaceNoFragmentsQueryRandomItemContent interface { + implementsGraphQLInterfaceInterfaceNoFragmentsQueryRandomItemContent() } -func (v InterfaceNoFragmentsQueryRootTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +func (v *InterfaceNoFragmentsQueryRandomItemArticle) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRandomItemContent() { } -func (v InterfaceNoFragmentsQueryRootTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +func (v *InterfaceNoFragmentsQueryRandomItemVideo) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRandomItemContent() { } -func (v InterfaceNoFragmentsQueryRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +func (v *InterfaceNoFragmentsQueryRandomItemTopic) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRandomItemContent() { } -// InterfaceNoFragmentsQueryRootTopicChildrenTopic includes the requested fields of the GraphQL type Topic. -type InterfaceNoFragmentsQueryRootTopicChildrenTopic struct { +func __unmarshalInterfaceNoFragmentsQueryRandomItemContent(v *InterfaceNoFragmentsQueryRandomItemContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(InterfaceNoFragmentsQueryRandomItemArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InterfaceNoFragmentsQueryRandomItemVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(InterfaceNoFragmentsQueryRandomItemTopic) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for InterfaceNoFragmentsQueryRandomItemContent: "%v"`, tn.TypeName) + } +} + +// InterfaceNoFragmentsQueryRandomItemTopic includes the requested fields of the GraphQL type Topic. +type InterfaceNoFragmentsQueryRandomItemTopic struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` Name string `json:"name"` } -// InterfaceNoFragmentsQueryRootTopicChildrenVideo includes the requested fields of the GraphQL type Video. -type InterfaceNoFragmentsQueryRootTopicChildrenVideo struct { +// InterfaceNoFragmentsQueryRandomItemVideo includes the requested fields of the GraphQL type Video. +type InterfaceNoFragmentsQueryRandomItemVideo struct { + Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` Name string `json:"name"` } +// InterfaceNoFragmentsQueryResponse is returned by InterfaceNoFragmentsQuery on success. +type InterfaceNoFragmentsQueryResponse struct { + Root InterfaceNoFragmentsQueryRootTopic `json:"root"` + RandomItem InterfaceNoFragmentsQueryRandomItemContent `json:"-"` +} + +func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { + + type InterfaceNoFragmentsQueryResponseWrapper InterfaceNoFragmentsQueryResponse + + var firstPass struct { + *InterfaceNoFragmentsQueryResponseWrapper + RandomItem json.RawMessage `json:"randomItem"` + } + firstPass.InterfaceNoFragmentsQueryResponseWrapper = (*InterfaceNoFragmentsQueryResponseWrapper)(v) + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = __unmarshalInterfaceNoFragmentsQueryRandomItemContent( + &v.RandomItem, firstPass.RandomItem) + if err != nil { + return err + } + + return nil +} + +// InterfaceNoFragmentsQueryRootTopic includes the requested fields of the GraphQL type Topic. +type InterfaceNoFragmentsQueryRootTopic struct { + // ID is documented in the Content interface. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + func InterfaceNoFragmentsQuery( client graphql.Client, ) (*InterfaceNoFragmentsQueryResponse, error) { @@ -68,10 +127,11 @@ query InterfaceNoFragmentsQuery { root { id name - children { - id - name - } + } + randomItem { + __typename + id + name } } `, diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.json b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.json index e5390dbd..48994c1d 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.json +++ b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.json @@ -2,7 +2,7 @@ "operations": [ { "operationName": "InterfaceNoFragmentsQuery", - "query": "\nquery InterfaceNoFragmentsQuery {\n\troot {\n\t\tid\n\t\tname\n\t\tchildren {\n\t\t\tid\n\t\t\tname\n\t\t}\n\t}\n}\n", + "query": "\nquery InterfaceNoFragmentsQuery {\n\troot {\n\t\tid\n\t\tname\n\t}\n\trandomItem {\n\t\t__typename\n\t\tid\n\t\tname\n\t}\n}\n", "sourceLocation": "testdata/queries/InterfaceNoFragments.graphql" } ] diff --git a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go index af2371f0..b8060dba 100644 --- a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go @@ -4,6 +4,7 @@ package test import ( "encoding/json" + "fmt" "github.com/Khan/genqlient/graphql" ) @@ -21,9 +22,35 @@ type UnionNoFragmentsQueryRandomLeafLeafContent interface { implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() } -func (v UnionNoFragmentsQueryRandomLeafArticle) implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() { +func (v *UnionNoFragmentsQueryRandomLeafArticle) implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() { } -func (v UnionNoFragmentsQueryRandomLeafVideo) implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() { +func (v *UnionNoFragmentsQueryRandomLeafVideo) implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() { +} + +func __unmarshalUnionNoFragmentsQueryRandomLeafLeafContent(v *UnionNoFragmentsQueryRandomLeafLeafContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(UnionNoFragmentsQueryRandomLeafArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(UnionNoFragmentsQueryRandomLeafVideo) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for UnionNoFragmentsQueryRandomLeafLeafContent: "%v"`, tn.TypeName) + } } // UnionNoFragmentsQueryRandomLeafVideo includes the requested fields of the GraphQL type Video. @@ -37,39 +64,22 @@ type UnionNoFragmentsQueryResponse struct { } func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { + + type UnionNoFragmentsQueryResponseWrapper UnionNoFragmentsQueryResponse + var firstPass struct { - *UnionNoFragmentsQueryResponse + *UnionNoFragmentsQueryResponseWrapper RandomLeaf json.RawMessage `json:"randomLeaf"` } - firstPass.UnionNoFragmentsQueryResponse = v + firstPass.UnionNoFragmentsQueryResponseWrapper = (*UnionNoFragmentsQueryResponseWrapper)(v) err := json.Unmarshal(b, &firstPass) if err != nil { return err } - var tn struct { - TypeName string `json:"__typename"` - } - err = json.Unmarshal(firstPass.RandomLeaf, &tn) - if err != nil { - return err - } - switch tn.TypeName { - - case "Article": - - v.RandomLeaf = UnionNoFragmentsQueryRandomLeafArticle{} - err = json.Unmarshal( - firstPass.RandomLeaf, &v.RandomLeaf) - - case "Video": - - v.RandomLeaf = UnionNoFragmentsQueryRandomLeafVideo{} - err = json.Unmarshal( - firstPass.RandomLeaf, &v.RandomLeaf) - - } + err = __unmarshalUnionNoFragmentsQueryRandomLeafLeafContent( + &v.RandomLeaf, firstPass.RandomLeaf) if err != nil { return err } diff --git a/generate/testdata/snapshots/TestGenerateErrors-MissingTypeName.go b/generate/testdata/snapshots/TestGenerateErrors-MissingTypeName.go new file mode 100644 index 00000000..55834ad0 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateErrors-MissingTypeName.go @@ -0,0 +1 @@ +testdata/errors/MissingTypeName.schema.graphql:2: union/interface type I must request __typename diff --git a/generate/testdata/snapshots/TestGenerateErrors-MissingTypeName.graphql b/generate/testdata/snapshots/TestGenerateErrors-MissingTypeName.graphql new file mode 100644 index 00000000..55834ad0 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateErrors-MissingTypeName.graphql @@ -0,0 +1 @@ +testdata/errors/MissingTypeName.schema.graphql:2: union/interface type I must request __typename diff --git a/generate/traverse.go b/generate/traverse.go index 5369638f..b1c6cbfe 100644 --- a/generate/traverse.go +++ b/generate/traverse.go @@ -25,7 +25,7 @@ func (g *generator) baseTypeForOperation(operation ast.Operation) (*ast.Definiti case ast.Mutation: return g.schema.Mutation, nil case ast.Subscription: - if !allowBrokenFeatures { + if !g.Config.AllowBrokenFeatures { return nil, errorf(nil, "genqlient does not yet support subscriptions") } return g.schema.Subscription, nil @@ -256,10 +256,23 @@ func (g *generator) convertDefinition( return goType, nil case ast.Interface, ast.Union: - if !allowBrokenFeatures { + if !g.Config.AllowBrokenFeatures { return nil, errorf(pos, "not implemented: %v", def.Kind) } + // We need to request __typename so we know which concrete type to use. + hasTypename := false + for _, selection := range selectionSet { + field, ok := selection.(*ast.Field) + if ok && field.Name == "__typename" { + hasTypename = true + } + } + if !hasTypename { + // TODO(benkraft): Instead, modify the query to add __typename. + return nil, errorf(pos, "union/interface type %s must request __typename", def.Name) + } + implementationTypes := g.schema.GetPossibleTypes(def) goType := &goInterfaceType{ GoName: name, diff --git a/generate/types.go b/generate/types.go index a773d448..2c2146a6 100644 --- a/generate/types.go +++ b/generate/types.go @@ -126,11 +126,45 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error { } fmt.Fprintf(w, "}\n") - return g.maybeWriteUnmarshal(w, typ) + // Now, if needed, write the unmarshaler. + // + // Specifically, in order to unmarshal interface values, we need to add an + // UnmarshalJSON method to each type which has an interface-typed *field* + // (not the interface type itself -- we can't add methods to that). + // But we put most of the logic in a per-interface-type helper function, + // written along with the interface type; the UnmarshalJSON method is just + // the boilerplate. + if len(typ.AbstractFields()) == 0 { + return nil + } + + // TODO(benkraft): Avoid having to enumerate these in advance; just let the + // template add them directly. + _, err := g.addRef("encoding/json.Unmarshal") + if err != nil { + return err + } + + return g.execute("unmarshal.go.tmpl", w, typ) } func (typ *goStructType) Reference() string { return typ.GoName } +// AbstractFields returns all the fields which are abstract types (i.e. GraphQL +// unions and interfaces; equivalently, types represented by interfaces in Go). +func (typ *goStructType) AbstractFields() []*goStructField { + var ret []*goStructField + for _, field := range typ.Fields { + // TODO(benkraft): To handle list-of-interface fields, we should really + // be "unwrapping" any goSliceType/goPointerType wrappers to find the + // goInterfaceType. + if _, ok := field.GoType.(*goInterfaceType); ok { + ret = append(ret, field) + } + } + return ret +} + // goInterfaceType represents a Go interface type, used to represent a GraphQL // interface or union type. type goInterfaceType struct { @@ -154,11 +188,25 @@ func (typ *goInterfaceType) WriteDefinition(w io.Writer, g *generator) error { // Now, write out the implementations. for _, impl := range typ.Implementations { - fmt.Fprintf(w, "func (v %s) %s() {}\n", + fmt.Fprintf(w, "func (v *%s) %s() {}\n", impl.Reference(), implementsMethodName) } - return nil + // Finally, write the unmarshal-helper, which will be called by struct + // fields referencing this type (see goStructType.WriteDefinition). + // + // TODO(benkraft): Avoid having to enumerate these refs in advance; just + // let the template add them directly. + _, err := g.addRef("encoding/json.Unmarshal") + if err != nil { + return err + } + _, err = g.addRef("fmt.Errorf") + if err != nil { + return err + } + + return g.execute("unmarshal_helper.go.tmpl", w, typ) } func (typ *goInterfaceType) Reference() string { return typ.GoName } diff --git a/generate/unmarshal.go b/generate/unmarshal.go deleted file mode 100644 index d3cd13a8..00000000 --- a/generate/unmarshal.go +++ /dev/null @@ -1,58 +0,0 @@ -package generate - -import "io" - -// TODO(benkraft): We could potentially get rid of these now, and do everything -// directly from the types. -type templateData struct { - // Go type to which the method will be added - Type string - // Abstract fields of the type, which need special handling. - Fields []abstractField -} - -type abstractField struct { - // Name of the field, in Go and JSON - GoName, JSONName string - // Concrete types the field might take. - ConcreteTypes []concreteType -} - -type concreteType struct { - // Name of the type, in Go and GraphQL - GoName, GraphQLName string -} - -func (g *generator) maybeWriteUnmarshal(w io.Writer, typ *goStructType) error { - data := templateData{Type: typ.GoName} - for _, field := range typ.Fields { - // TODO(benkraft): To handle list-of-interface fields, we should really - // be "unwrapping" any goSliceType/goPointerType wrappers to find the - // goInterfaceType. - if iface, ok := field.GoType.(*goInterfaceType); ok { - fieldInfo := abstractField{ - GoName: field.GoName, - JSONName: field.JSONName, - } - for _, impl := range iface.Implementations { - fieldInfo.ConcreteTypes = append(fieldInfo.ConcreteTypes, - concreteType{ - GoName: impl.GoName, - GraphQLName: impl.GraphQLName, - }) - } - data.Fields = append(data.Fields, fieldInfo) - } - } - - if len(data.Fields) == 0 { - return nil - } - - _, err := g.addRef("encoding/json.Unmarshal") - if err != nil { - return err - } - - return g.execute("unmarshal.go.tmpl", w, data) -} diff --git a/generate/unmarshal.go.tmpl b/generate/unmarshal.go.tmpl index dafd0380..ed3eb585 100644 --- a/generate/unmarshal.go.tmpl +++ b/generate/unmarshal.go.tmpl @@ -1,40 +1,40 @@ {{/* (the blank lines at the start are intentional, to separate UnmarshalJSON from the function it follows) */}} -func (v *{{.Type}}) UnmarshalJSON(b []byte) error { - var firstPass struct{ - *{{.Type}} - {{range .Fields -}} - {{.GoName}} {{ref "encoding/json.RawMessage"}} `json:"{{.JSONName}}"` - {{end}} - } - firstPass.{{.Type}} = v +func (v *{{.GoName}}) UnmarshalJSON(b []byte) error { + {{/* We want to specially handle the abstract fields (.AbstractFields), + but unmarshal everything else normally. To handle abstract fields, + first we unmarshal them into a json.RawMessage, and then handle those + further, below. For the rest, we just want to call json.Unmarshal. + But if we do that naively on a value of type `.Type`, it will call + this function again, and recurse infinitely. So we make a wrapper + type -- with a different name, thus different methods, but the same + fields, and unmarshal into that. For more on why this is so + difficult, see + https://github.com/benjaminjkraft/notes/blob/master/go-json-interfaces.md + TODO(benkraft)): Ensure `{{.Type}}Wrapper` won't collide with any + other type we need. (For the most part it being locally-scoped saves + us; it's not clear if this can be a problem in practice.) + */}} + type {{.GoName}}Wrapper {{.GoName}} - err := {{ref "encoding/json.Unmarshal"}}(b, &firstPass) - if err != nil { - return err + var firstPass struct{ + *{{.GoName}}Wrapper + {{range .AbstractFields -}} + {{.GoName}} {{ref "encoding/json.RawMessage"}} `json:"{{.JSONName}}"` + {{end}} } + firstPass.{{.GoName}}Wrapper = (*{{.GoName}}Wrapper)(v) - {{/* TODO(benkraft): split this out into a separate helper function, - generated for each interface type, rather than generating it inline - with the struct type. */}} - {{range .Fields -}} - var tn struct { TypeName string `json:"__typename"` } - err = {{ref "encoding/json.Unmarshal"}}(firstPass.{{.GoName}}, &tn) + err := {{ref "encoding/json.Unmarshal"}}(b, &firstPass) if err != nil { return err } - switch tn.TypeName { - {{with $field := .}} - {{range $field.ConcreteTypes}} - case "{{.GraphQLName}}": - {{/* TODO: handle repeated fields! */}} - v.{{$field.GoName}} = {{.GoName}}{} - err = {{ref "encoding/json.Unmarshal"}}( - firstPass.{{$field.GoName}}, &v.{{$field.GoName}}) - {{end}} - {{end}} - } + + {{/* Now, for each field, call out to the unmarshal-helper. */}} + {{range .AbstractFields -}} + err = __unmarshal{{.GoType.Reference}}( + &v.{{.GoName}}, firstPass.{{.GoName}}) if err != nil { return err } diff --git a/generate/unmarshal_helper.go.tmpl b/generate/unmarshal_helper.go.tmpl new file mode 100644 index 00000000..89163755 --- /dev/null +++ b/generate/unmarshal_helper.go.tmpl @@ -0,0 +1,28 @@ +{{/* (the blank lines at the start are intentional, to separate + the helper from the function it follows) */}} + +func __unmarshal{{.GoName}}(v *{{.GoName}}, m {{ref "encoding/json.RawMessage"}}) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := {{ref "encoding/json.Unmarshal"}}(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + {{range .Implementations -}} + case "{{.GraphQLName}}": + {{/* TODO: handle repeated fields! */ -}} + *v = new({{.GoName}}) + return {{ref "encoding/json.Unmarshal"}}(m, *v) + {{end -}} + default: + return {{ref "fmt.Errorf"}}( + `Unexpected concrete type for {{.GoName}}: "%v"`, tn.TypeName) + } +} diff --git a/internal/integration/generated.go b/internal/integration/generated.go index ea382be4..aa50bef4 100644 --- a/internal/integration/generated.go +++ b/internal/integration/generated.go @@ -4,10 +4,98 @@ package integration import ( "context" + "encoding/json" + "fmt" "github.com/Khan/genqlient/graphql" ) +// queryWithInterfaceNoFragmentsBeing includes the requested fields of the GraphQL type Being. +type queryWithInterfaceNoFragmentsBeing interface { + implementsGraphQLInterfacequeryWithInterfaceNoFragmentsBeing() +} + +func (v *queryWithInterfaceNoFragmentsBeingUser) implementsGraphQLInterfacequeryWithInterfaceNoFragmentsBeing() { +} +func (v *queryWithInterfaceNoFragmentsBeingAnimal) implementsGraphQLInterfacequeryWithInterfaceNoFragmentsBeing() { +} + +func __unmarshalqueryWithInterfaceNoFragmentsBeing(v *queryWithInterfaceNoFragmentsBeing, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "User": + *v = new(queryWithInterfaceNoFragmentsBeingUser) + return json.Unmarshal(m, *v) + case "Animal": + *v = new(queryWithInterfaceNoFragmentsBeingAnimal) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for queryWithInterfaceNoFragmentsBeing: "%v"`, tn.TypeName) + } +} + +// queryWithInterfaceNoFragmentsBeingAnimal includes the requested fields of the GraphQL type Animal. +type queryWithInterfaceNoFragmentsBeingAnimal struct { + Typename string `json:"__typename"` + Id string `json:"id"` + Name string `json:"name"` +} + +// queryWithInterfaceNoFragmentsBeingUser includes the requested fields of the GraphQL type User. +type queryWithInterfaceNoFragmentsBeingUser struct { + Typename string `json:"__typename"` + Id string `json:"id"` + Name string `json:"name"` +} + +// queryWithInterfaceNoFragmentsMeUser includes the requested fields of the GraphQL type User. +type queryWithInterfaceNoFragmentsMeUser struct { + Id string `json:"id"` + Name string `json:"name"` +} + +// queryWithInterfaceNoFragmentsResponse is returned by queryWithInterfaceNoFragments on success. +type queryWithInterfaceNoFragmentsResponse struct { + Being queryWithInterfaceNoFragmentsBeing `json:"-"` + Me queryWithInterfaceNoFragmentsMeUser `json:"me"` +} + +func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error { + + type queryWithInterfaceNoFragmentsResponseWrapper queryWithInterfaceNoFragmentsResponse + + var firstPass struct { + *queryWithInterfaceNoFragmentsResponseWrapper + Being json.RawMessage `json:"being"` + } + firstPass.queryWithInterfaceNoFragmentsResponseWrapper = (*queryWithInterfaceNoFragmentsResponseWrapper)(v) + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = __unmarshalqueryWithInterfaceNoFragmentsBeing( + &v.Being, firstPass.Being) + if err != nil { + return err + } + + return nil +} + // queryWithVariablesResponse is returned by queryWithVariables on success. type queryWithVariablesResponse struct { User queryWithVariablesUser `json:"user"` @@ -82,3 +170,35 @@ query queryWithVariables ($id: ID!) { ) return &retval, err } + +func queryWithInterfaceNoFragments( + ctx context.Context, + client graphql.Client, + id string, +) (*queryWithInterfaceNoFragmentsResponse, error) { + variables := map[string]interface{}{ + "id": id, + } + + var retval queryWithInterfaceNoFragmentsResponse + err := client.MakeRequest( + ctx, + "queryWithInterfaceNoFragments", + ` +query queryWithInterfaceNoFragments ($id: ID!) { + being(id: $id) { + __typename + id + name + } + me { + id + name + } +} +`, + &retval, + variables, + ) + return &retval, err +} diff --git a/internal/integration/genqlient.yaml b/internal/integration/genqlient.yaml index fe4491ca..be422146 100644 --- a/internal/integration/genqlient.yaml +++ b/internal/integration/genqlient.yaml @@ -2,3 +2,4 @@ schema: schema.graphql operations: - "*_test.go" generated: generated.go +allow_broken_features: true diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go index 4ce18170..9f55aeb9 100644 --- a/internal/integration/integration_test.go +++ b/internal/integration/integration_test.go @@ -56,6 +56,49 @@ func TestVariables(t *testing.T) { assert.Zero(t, resp.User) } +func TestInterfaceNoFragments(t *testing.T) { + _ = `# @genqlient + query queryWithInterfaceNoFragments($id: ID!) { + being(id: $id) { __typename id name } + me { id name } + }` + + ctx := context.Background() + server := server.RunServer() + defer server.Close() + client := graphql.NewClient(server.URL, http.DefaultClient) + + resp, err := queryWithInterfaceNoFragments(ctx, client, "1") + require.NoError(t, err) + + assert.Equal(t, "1", resp.Me.Id) + assert.Equal(t, "Yours Truly", resp.Me.Name) + + user, ok := resp.Being.(*queryWithInterfaceNoFragmentsBeingUser) + require.Truef(t, ok, "got %T, not User", resp.Being) + assert.Equal(t, "1", user.Id) + assert.Equal(t, "Yours Truly", user.Name) + + resp, err = queryWithInterfaceNoFragments(ctx, client, "3") + require.NoError(t, err) + + assert.Equal(t, "1", resp.Me.Id) + assert.Equal(t, "Yours Truly", resp.Me.Name) + + animal, ok := resp.Being.(*queryWithInterfaceNoFragmentsBeingAnimal) + require.Truef(t, ok, "got %T, not Animal", resp.Being) + assert.Equal(t, "3", animal.Id) + assert.Equal(t, "Fido", animal.Name) + + resp, err = queryWithInterfaceNoFragments(ctx, client, "4757233945723") + require.NoError(t, err) + + assert.Equal(t, "1", resp.Me.Id) + assert.Equal(t, "Yours Truly", resp.Me.Name) + + assert.Nil(t, resp.Being) +} + func TestGeneratedCode(t *testing.T) { // TODO(benkraft): Check that gqlgen is up to date too. In practice that's // less likely to be a problem, since it should only change if you update diff --git a/internal/integration/schema.graphql b/internal/integration/schema.graphql index 813d1372..2520059e 100644 --- a/internal/integration/schema.graphql +++ b/internal/integration/schema.graphql @@ -1,10 +1,28 @@ type Query { me: User user(id: ID!): User + being(id: ID!): Being } -type User { +type User implements Being { id: ID! name: String! luckyNumber: Int } + +type Animal implements Being { + id: ID! + name: String! + species: Species! + owner: Being +} + +enum Species { + DOG + COELACANTH +} + +interface Being { + id: ID! + name: String! +} diff --git a/internal/integration/server/gqlgen_exec.go b/internal/integration/server/gqlgen_exec.go index 4643dd9e..0df4e783 100644 --- a/internal/integration/server/gqlgen_exec.go +++ b/internal/integration/server/gqlgen_exec.go @@ -6,6 +6,7 @@ import ( "bytes" "context" "errors" + "fmt" "strconv" "sync" @@ -40,9 +41,17 @@ type DirectiveRoot struct { } type ComplexityRoot struct { + Animal struct { + ID func(childComplexity int) int + Name func(childComplexity int) int + Owner func(childComplexity int) int + Species func(childComplexity int) int + } + Query struct { - Me func(childComplexity int) int - User func(childComplexity int, id string) int + Being func(childComplexity int, id string) int + Me func(childComplexity int) int + User func(childComplexity int, id string) int } User struct { @@ -55,6 +64,7 @@ type ComplexityRoot struct { type QueryResolver interface { Me(ctx context.Context) (*User, error) User(ctx context.Context, id string) (*User, error) + Being(ctx context.Context, id string) (Being, error) } type executableSchema struct { @@ -72,6 +82,46 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { + case "Animal.id": + if e.complexity.Animal.ID == nil { + break + } + + return e.complexity.Animal.ID(childComplexity), true + + case "Animal.name": + if e.complexity.Animal.Name == nil { + break + } + + return e.complexity.Animal.Name(childComplexity), true + + case "Animal.owner": + if e.complexity.Animal.Owner == nil { + break + } + + return e.complexity.Animal.Owner(childComplexity), true + + case "Animal.species": + if e.complexity.Animal.Species == nil { + break + } + + return e.complexity.Animal.Species(childComplexity), true + + case "Query.being": + if e.complexity.Query.Being == nil { + break + } + + args, err := ec.field_Query_being_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.Being(childComplexity, args["id"].(string)), true + case "Query.me": if e.complexity.Query.Me == nil { break @@ -165,13 +215,31 @@ var sources = []*ast.Source{ {Name: "../schema.graphql", Input: `type Query { me: User user(id: ID!): User + being(id: ID!): Being } -type User { +type User implements Being { id: ID! name: String! luckyNumber: Int } + +type Animal implements Being { + id: ID! + name: String! + species: Species! + owner: Being +} + +enum Species { + DOG + COELACANTH +} + +interface Being { + id: ID! + name: String! +} `, BuiltIn: false}, } var parsedSchema = gqlparser.MustLoadSchema(sources...) @@ -195,6 +263,21 @@ func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs return args, nil } +func (ec *executionContext) field_Query_being_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNID2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_user_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -248,6 +331,143 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** +func (ec *executionContext) _Animal_id(ctx context.Context, field graphql.CollectedField, obj *Animal) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Animal", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _Animal_name(ctx context.Context, field graphql.CollectedField, obj *Animal) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Animal", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _Animal_species(ctx context.Context, field graphql.CollectedField, obj *Animal) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Animal", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Species, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(Species) + fc.Result = res + return ec.marshalNSpecies2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐSpecies(ctx, field.Selections, res) +} + +func (ec *executionContext) _Animal_owner(ctx context.Context, field graphql.CollectedField, obj *Animal) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Animal", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Owner, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(Being) + fc.Result = res + return ec.marshalOBeing2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeing(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_me(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -319,6 +539,45 @@ func (ec *executionContext) _Query_user(ctx context.Context, field graphql.Colle return ec.marshalOUser2ᚖgithub.comᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUser(ctx, field.Selections, res) } +func (ec *executionContext) _Query_being(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Query_being_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Being(rctx, args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(Being) + fc.Result = res + return ec.marshalOBeing2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeing(ctx, field.Selections, res) +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -1583,10 +1842,72 @@ func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.Co // region ************************** interface.gotpl *************************** +func (ec *executionContext) _Being(ctx context.Context, sel ast.SelectionSet, obj Being) graphql.Marshaler { + switch obj := (obj).(type) { + case nil: + return graphql.Null + case User: + return ec._User(ctx, sel, &obj) + case *User: + if obj == nil { + return graphql.Null + } + return ec._User(ctx, sel, obj) + case Animal: + return ec._Animal(ctx, sel, &obj) + case *Animal: + if obj == nil { + return graphql.Null + } + return ec._Animal(ctx, sel, obj) + default: + panic(fmt.Errorf("unexpected type %T", obj)) + } +} + // endregion ************************** interface.gotpl *************************** // region **************************** object.gotpl **************************** +var animalImplementors = []string{"Animal", "Being"} + +func (ec *executionContext) _Animal(ctx context.Context, sel ast.SelectionSet, obj *Animal) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, animalImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Animal") + case "id": + out.Values[i] = ec._Animal_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "name": + out.Values[i] = ec._Animal_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "species": + out.Values[i] = ec._Animal_species(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "owner": + out.Values[i] = ec._Animal_owner(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var queryImplementors = []string{"Query"} func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -1624,6 +1945,17 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr res = ec._Query_user(ctx, field) return res }) + case "being": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_being(ctx, field) + return res + }) case "__type": out.Values[i] = ec._Query___type(ctx, field) case "__schema": @@ -1639,7 +1971,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return out } -var userImplementors = []string{"User"} +var userImplementors = []string{"User", "Being"} func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *User) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, userImplementors) @@ -1948,6 +2280,16 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } +func (ec *executionContext) unmarshalNSpecies2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐSpecies(ctx context.Context, v interface{}) (Species, error) { + var res Species + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNSpecies2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐSpecies(ctx context.Context, sel ast.SelectionSet, v Species) graphql.Marshaler { + return v +} + func (ec *executionContext) unmarshalNString2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) @@ -2192,6 +2534,13 @@ func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel a return res } +func (ec *executionContext) marshalOBeing2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeing(ctx context.Context, sel ast.SelectionSet, v Being) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Being(ctx, sel, v) +} + func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v interface{}) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/internal/integration/server/gqlgen_models.go b/internal/integration/server/gqlgen_models.go index daf92714..a3318fe4 100644 --- a/internal/integration/server/gqlgen_models.go +++ b/internal/integration/server/gqlgen_models.go @@ -2,8 +2,70 @@ package server +import ( + "fmt" + "io" + "strconv" +) + +type Being interface { + IsBeing() +} + +type Animal struct { + ID string `json:"id"` + Name string `json:"name"` + Species Species `json:"species"` + Owner Being `json:"owner"` +} + +func (Animal) IsBeing() {} + type User struct { ID string `json:"id"` Name string `json:"name"` LuckyNumber *int `json:"luckyNumber"` } + +func (User) IsBeing() {} + +type Species string + +const ( + SpeciesDog Species = "DOG" + SpeciesCoelacanth Species = "COELACANTH" +) + +var AllSpecies = []Species{ + SpeciesDog, + SpeciesCoelacanth, +} + +func (e Species) IsValid() bool { + switch e { + case SpeciesDog, SpeciesCoelacanth: + return true + } + return false +} + +func (e Species) String() string { + return string(e) +} + +func (e *Species) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = Species(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid Species", str) + } + return nil +} + +func (e Species) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} diff --git a/internal/integration/server/server.go b/internal/integration/server/server.go index ada6131b..f24b4911 100644 --- a/internal/integration/server/server.go +++ b/internal/integration/server/server.go @@ -15,6 +15,11 @@ var users = []*User{ {ID: "2", Name: "Raven", LuckyNumber: intptr(-1)}, } +var animals = []*Animal{ + {ID: "3", Name: "Fido", Species: SpeciesDog, Owner: userByID("0")}, + {ID: "4", Name: "Old One", Species: SpeciesCoelacanth, Owner: nil}, +} + func userByID(id string) *User { for _, user := range users { if id == user.ID { @@ -24,6 +29,20 @@ func userByID(id string) *User { return nil } +func beingByID(id string) Being { + for _, user := range users { + if id == user.ID { + return user + } + } + for _, animal := range animals { + if id == animal.ID { + return animal + } + } + return nil +} + func (r *queryResolver) Me(ctx context.Context) (*User, error) { return userByID("1"), nil } @@ -32,6 +51,10 @@ func (r *queryResolver) User(ctx context.Context, id string) (*User, error) { return userByID(id), nil } +func (r *queryResolver) Being(ctx context.Context, id string) (Being, error) { + return beingByID(id), nil +} + func RunServer() *httptest.Server { gqlgenServer := handler.New(NewExecutableSchema(Config{Resolvers: &resolver{}})) gqlgenServer.AddTransport(transport.POST{})