From cc1942eb4bfc9ef691c8fd6d3f1265bc28a905a2 Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Thu, 19 Aug 2021 17:19:53 -0700 Subject: [PATCH] Add support for interfaces, part 2: list-of-interface In this commit I remove one of the limitations of our support for interfaces, from #52, by adding support for list-of-interface fields. This was surprisingly complex! The issue is that, as before, it's the containing type that has to do all the glue work -- and it's that glue work that is complicated by list-of-interface fields. All in all, it's not that much new code, and by far the hard part is just 20 lines in the UnmarshalJSON template (which come with almost twice as many lines of comments to explain them). It may be easiest to start by reading some of the generated code, and then read the template. I also added support for such fields with `pointer: true` specified, such that the type is `[][]...[]*MyInterface`, although I don't know why you would want that. This does *not* allow e.g. `*[]*[][]*MyInterface`; that would require a way to specify it (see #16) but also add some extra complexity (as we'd have to actually walk the type-unwrap chain properly, instead of just counting the number of slices and whether there's a pointer). Issue: https://github.com/Khan/genqlient/issues/8 Test plan: make check Reviewers: marksandstrom, miguel, csilvers, adam --- generate/generate_test.go | 1 - generate/template.go | 24 +- .../queries/InterfaceListField.graphql | 12 +- .../InterfaceListOfListsOfListsField.graphql | 5 + .../queries/InterfaceNoFragments.graphql | 2 + generate/testdata/queries/schema.graphql | 1 + ...ield.graphql-InterfaceListField.graphql.go | 208 ++++++++++++-- ...ld.graphql-InterfaceListField.graphql.json | 4 +- ...nterfaceListOfListsOfListsField.graphql.go | 254 ++++++++++++++++++ ...erfaceListOfListsOfListsField.graphql.json | 9 + ...esting.graphql-InterfaceNesting.graphql.go | 140 +++++++++- ...ts.graphql-InterfaceNoFragments.graphql.go | 103 ++++++- ....graphql-InterfaceNoFragments.graphql.json | 2 +- ...gments.graphql-UnionNoFragments.graphql.go | 13 +- generate/types.go | 44 ++- generate/unmarshal.go.tmpl | 64 ++++- internal/integration/generated.go | 129 ++++++++- internal/integration/integration_test.go | 30 +++ internal/integration/schema.graphql | 1 + internal/integration/server/gqlgen_exec.go | 160 ++++++++++- internal/integration/server/server.go | 8 + 21 files changed, 1142 insertions(+), 72 deletions(-) create mode 100644 generate/testdata/queries/InterfaceListOfListsOfListsField.graphql create mode 100644 generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go create mode 100644 generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.json diff --git a/generate/generate_test.go b/generate/generate_test.go index fdbc33ac..0432cff1 100644 --- a/generate/generate_test.go +++ b/generate/generate_test.go @@ -72,7 +72,6 @@ func TestGenerate(t *testing.T) { if testing.Short() { t.Skip("skipping build due to -short") } else if sourceFilename == "InterfaceNesting.graphql" || // #8 - sourceFilename == "InterfaceListField.graphql" || // #8 sourceFilename == "Omitempty.graphql" { // #43 t.Skip("TODO: enable these once they build") } diff --git a/generate/template.go b/generate/template.go index bca94153..e8887e40 100644 --- a/generate/template.go +++ b/generate/template.go @@ -4,6 +4,7 @@ import ( "io" "path/filepath" "runtime" + "strings" "text/template" ) @@ -12,13 +13,34 @@ var ( thisDir = filepath.Dir(thisFilename) ) +func repeat(n int, s string) string { + var builder strings.Builder + for i := 0; i < n; i++ { + builder.WriteString(s) + } + return builder.String() +} + +func intRange(n int) []int { + ret := make([]int, n) + for i := 0; i < n; i++ { + ret[i] = i + } + return ret +} + +func sub(x, y int) int { return x - y } + // execute executes the given template with the funcs from this generator. func (g *generator) execute(tmplRelFilename string, w io.Writer, data interface{}) error { tmpl := g.templateCache[tmplRelFilename] if tmpl == nil { absFilename := filepath.Join(thisDir, tmplRelFilename) funcMap := template.FuncMap{ - "ref": g.ref, + "ref": g.ref, + "repeat": repeat, + "intRange": intRange, + "sub": sub, } var err error tmpl, err = template.New(tmplRelFilename).Funcs(funcMap).ParseFiles(absFilename) diff --git a/generate/testdata/queries/InterfaceListField.graphql b/generate/testdata/queries/InterfaceListField.graphql index 55fd02a7..dc57f93b 100644 --- a/generate/testdata/queries/InterfaceListField.graphql +++ b/generate/testdata/queries/InterfaceListField.graphql @@ -1,4 +1,4 @@ -query InterfaceNoFragmentsQuery { +query InterfaceListField { root { id name @@ -8,4 +8,14 @@ query InterfaceNoFragmentsQuery { name } } + # @genqlient(pointer: true) + withPointer: root { + id + name + children { + __typename + id + name + } + } } diff --git a/generate/testdata/queries/InterfaceListOfListsOfListsField.graphql b/generate/testdata/queries/InterfaceListOfListsOfListsField.graphql new file mode 100644 index 00000000..69ec1d36 --- /dev/null +++ b/generate/testdata/queries/InterfaceListOfListsOfListsField.graphql @@ -0,0 +1,5 @@ +query InterfaceListOfListOfListsField { + listOfListsOfListsOfContent { __typename id name } + # @genqlient(pointer: true) + withPointer: listOfListsOfListsOfContent { __typename id name } +} diff --git a/generate/testdata/queries/InterfaceNoFragments.graphql b/generate/testdata/queries/InterfaceNoFragments.graphql index 88d0bb31..f9d552aa 100644 --- a/generate/testdata/queries/InterfaceNoFragments.graphql +++ b/generate/testdata/queries/InterfaceNoFragments.graphql @@ -1,4 +1,6 @@ query InterfaceNoFragmentsQuery { root { id name } # (make sure sibling fields work) randomItem { __typename id name } + # @genqlient(pointer: true) + withPointer: randomItem { __typename id name } } diff --git a/generate/testdata/queries/schema.graphql b/generate/testdata/queries/schema.graphql index 31f7c2ab..dc872c8d 100644 --- a/generate/testdata/queries/schema.graphql +++ b/generate/testdata/queries/schema.graphql @@ -111,6 +111,7 @@ type Query { getJunk: Junk getComplexJunk: ComplexJunk listOfListsOfLists: [[[String!]!]!]! + listOfListsOfListsOfContent: [[[Content!]!]!]! } type Mutation { diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go index 5c14d931..828fd6ef 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go @@ -10,43 +10,186 @@ import ( "github.com/Khan/genqlient/internal/testutil" ) -// InterfaceNoFragmentsQueryResponse is returned by InterfaceNoFragmentsQuery on success. -type InterfaceNoFragmentsQueryResponse struct { - Root InterfaceNoFragmentsQueryRootTopic `json:"root"` +// InterfaceListFieldResponse is returned by InterfaceListField on success. +type InterfaceListFieldResponse struct { + Root InterfaceListFieldRootTopic `json:"root"` + WithPointer *InterfaceListFieldWithPointerTopic `json:"withPointer"` } -// InterfaceNoFragmentsQueryRootTopic includes the requested fields of the GraphQL type Topic. -type InterfaceNoFragmentsQueryRootTopic struct { +// InterfaceListFieldRootTopic includes the requested fields of the GraphQL type Topic. +type InterfaceListFieldRootTopic struct { + // ID is documented in the Content interface. + Id testutil.ID `json:"id"` + Name string `json:"name"` + Children []InterfaceListFieldRootTopicChildrenContent `json:"-"` +} + +func (v *InterfaceListFieldRootTopic) UnmarshalJSON(b []byte) error { + + type InterfaceListFieldRootTopicWrapper InterfaceListFieldRootTopic + + var firstPass struct { + *InterfaceListFieldRootTopicWrapper + Children []json.RawMessage `json:"children"` + } + firstPass.InterfaceListFieldRootTopicWrapper = (*InterfaceListFieldRootTopicWrapper)(v) + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.Children + raw := firstPass.Children + *target = make( + []InterfaceListFieldRootTopicChildrenContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + err = __unmarshalInterfaceListFieldRootTopicChildrenContent( + target, raw) + if err != nil { + return err + } + } + } + return nil +} + +// InterfaceListFieldRootTopicChildrenArticle includes the requested fields of the GraphQL type Article. +type InterfaceListFieldRootTopicChildrenArticle struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// InterfaceListFieldRootTopicChildrenContent 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 InterfaceListFieldRootTopicChildrenContent interface { + implementsGraphQLInterfaceInterfaceListFieldRootTopicChildrenContent() +} + +func (v *InterfaceListFieldRootTopicChildrenArticle) implementsGraphQLInterfaceInterfaceListFieldRootTopicChildrenContent() { +} +func (v *InterfaceListFieldRootTopicChildrenVideo) implementsGraphQLInterfaceInterfaceListFieldRootTopicChildrenContent() { +} +func (v *InterfaceListFieldRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceListFieldRootTopicChildrenContent() { +} + +func __unmarshalInterfaceListFieldRootTopicChildrenContent(v *InterfaceListFieldRootTopicChildrenContent, 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(InterfaceListFieldRootTopicChildrenArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InterfaceListFieldRootTopicChildrenVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(InterfaceListFieldRootTopicChildrenTopic) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for InterfaceListFieldRootTopicChildrenContent: "%v"`, tn.TypeName) + } +} + +// InterfaceListFieldRootTopicChildrenTopic includes the requested fields of the GraphQL type Topic. +type InterfaceListFieldRootTopicChildrenTopic struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// InterfaceListFieldRootTopicChildrenVideo includes the requested fields of the GraphQL type Video. +type InterfaceListFieldRootTopicChildrenVideo struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// InterfaceListFieldWithPointerTopic includes the requested fields of the GraphQL type Topic. +type InterfaceListFieldWithPointerTopic struct { // ID is documented in the Content interface. Id testutil.ID `json:"id"` Name string `json:"name"` - Children []InterfaceNoFragmentsQueryRootTopicChildrenContent `json:"children"` + Children []InterfaceListFieldWithPointerTopicChildrenContent `json:"-"` } -// InterfaceNoFragmentsQueryRootTopicChildrenArticle includes the requested fields of the GraphQL type Article. -type InterfaceNoFragmentsQueryRootTopicChildrenArticle struct { +func (v *InterfaceListFieldWithPointerTopic) UnmarshalJSON(b []byte) error { + + type InterfaceListFieldWithPointerTopicWrapper InterfaceListFieldWithPointerTopic + + var firstPass struct { + *InterfaceListFieldWithPointerTopicWrapper + Children []json.RawMessage `json:"children"` + } + firstPass.InterfaceListFieldWithPointerTopicWrapper = (*InterfaceListFieldWithPointerTopicWrapper)(v) + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.Children + raw := firstPass.Children + *target = make( + []InterfaceListFieldWithPointerTopicChildrenContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + err = __unmarshalInterfaceListFieldWithPointerTopicChildrenContent( + target, raw) + if err != nil { + return err + } + } + } + return nil +} + +// InterfaceListFieldWithPointerTopicChildrenArticle includes the requested fields of the GraphQL type Article. +type InterfaceListFieldWithPointerTopicChildrenArticle 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. +// InterfaceListFieldWithPointerTopicChildrenContent 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 InterfaceListFieldWithPointerTopicChildrenContent interface { + implementsGraphQLInterfaceInterfaceListFieldWithPointerTopicChildrenContent() } -func (v *InterfaceNoFragmentsQueryRootTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +func (v *InterfaceListFieldWithPointerTopicChildrenArticle) implementsGraphQLInterfaceInterfaceListFieldWithPointerTopicChildrenContent() { } -func (v *InterfaceNoFragmentsQueryRootTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +func (v *InterfaceListFieldWithPointerTopicChildrenVideo) implementsGraphQLInterfaceInterfaceListFieldWithPointerTopicChildrenContent() { } -func (v *InterfaceNoFragmentsQueryRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +func (v *InterfaceListFieldWithPointerTopicChildrenTopic) implementsGraphQLInterfaceInterfaceListFieldWithPointerTopicChildrenContent() { } -func __unmarshalInterfaceNoFragmentsQueryRootTopicChildrenContent(v *InterfaceNoFragmentsQueryRootTopicChildrenContent, m json.RawMessage) error { +func __unmarshalInterfaceListFieldWithPointerTopicChildrenContent(v *InterfaceListFieldWithPointerTopicChildrenContent, m json.RawMessage) error { if string(m) == "null" { return nil } @@ -61,45 +204,45 @@ func __unmarshalInterfaceNoFragmentsQueryRootTopicChildrenContent(v *InterfaceNo switch tn.TypeName { case "Article": - *v = new(InterfaceNoFragmentsQueryRootTopicChildrenArticle) + *v = new(InterfaceListFieldWithPointerTopicChildrenArticle) return json.Unmarshal(m, *v) case "Video": - *v = new(InterfaceNoFragmentsQueryRootTopicChildrenVideo) + *v = new(InterfaceListFieldWithPointerTopicChildrenVideo) return json.Unmarshal(m, *v) case "Topic": - *v = new(InterfaceNoFragmentsQueryRootTopicChildrenTopic) + *v = new(InterfaceListFieldWithPointerTopicChildrenTopic) return json.Unmarshal(m, *v) default: return fmt.Errorf( - `Unexpected concrete type for InterfaceNoFragmentsQueryRootTopicChildrenContent: "%v"`, tn.TypeName) + `Unexpected concrete type for InterfaceListFieldWithPointerTopicChildrenContent: "%v"`, tn.TypeName) } } -// InterfaceNoFragmentsQueryRootTopicChildrenTopic includes the requested fields of the GraphQL type Topic. -type InterfaceNoFragmentsQueryRootTopicChildrenTopic struct { +// InterfaceListFieldWithPointerTopicChildrenTopic includes the requested fields of the GraphQL type Topic. +type InterfaceListFieldWithPointerTopicChildrenTopic 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 { +// InterfaceListFieldWithPointerTopicChildrenVideo includes the requested fields of the GraphQL type Video. +type InterfaceListFieldWithPointerTopicChildrenVideo struct { Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` Name string `json:"name"` } -func InterfaceNoFragmentsQuery( +func InterfaceListField( client graphql.Client, -) (*InterfaceNoFragmentsQueryResponse, error) { - var retval InterfaceNoFragmentsQueryResponse +) (*InterfaceListFieldResponse, error) { + var retval InterfaceListFieldResponse err := client.MakeRequest( nil, - "InterfaceNoFragmentsQuery", + "InterfaceListField", ` -query InterfaceNoFragmentsQuery { +query InterfaceListField { root { id name @@ -109,6 +252,15 @@ query InterfaceNoFragmentsQuery { name } } + withPointer: root { + id + name + children { + __typename + id + name + } + } } `, &retval, diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.json b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.json index f6665e9d..e2500e1e 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.json +++ b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.json @@ -1,8 +1,8 @@ { "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", + "operationName": "InterfaceListField", + "query": "\nquery InterfaceListField {\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\twithPointer: root {\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-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go new file mode 100644 index 00000000..4d3d3022 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go @@ -0,0 +1,254 @@ +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" +) + +// InterfaceListOfListOfListsFieldListOfListsOfListsOfContent 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 InterfaceListOfListOfListsFieldListOfListsOfListsOfContent interface { + implementsGraphQLInterfaceInterfaceListOfListOfListsFieldListOfListsOfListsOfContent() +} + +func (v *InterfaceListOfListOfListsFieldListOfListsOfListsOfContentArticle) implementsGraphQLInterfaceInterfaceListOfListOfListsFieldListOfListsOfListsOfContent() { +} +func (v *InterfaceListOfListOfListsFieldListOfListsOfListsOfContentVideo) implementsGraphQLInterfaceInterfaceListOfListOfListsFieldListOfListsOfListsOfContent() { +} +func (v *InterfaceListOfListOfListsFieldListOfListsOfListsOfContentTopic) implementsGraphQLInterfaceInterfaceListOfListOfListsFieldListOfListsOfListsOfContent() { +} + +func __unmarshalInterfaceListOfListOfListsFieldListOfListsOfListsOfContent(v *InterfaceListOfListOfListsFieldListOfListsOfListsOfContent, 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(InterfaceListOfListOfListsFieldListOfListsOfListsOfContentArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InterfaceListOfListOfListsFieldListOfListsOfListsOfContentVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(InterfaceListOfListOfListsFieldListOfListsOfListsOfContentTopic) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for InterfaceListOfListOfListsFieldListOfListsOfListsOfContent: "%v"`, tn.TypeName) + } +} + +// InterfaceListOfListOfListsFieldListOfListsOfListsOfContentArticle includes the requested fields of the GraphQL type Article. +type InterfaceListOfListOfListsFieldListOfListsOfListsOfContentArticle struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// InterfaceListOfListOfListsFieldListOfListsOfListsOfContentTopic includes the requested fields of the GraphQL type Topic. +type InterfaceListOfListOfListsFieldListOfListsOfListsOfContentTopic struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// InterfaceListOfListOfListsFieldListOfListsOfListsOfContentVideo includes the requested fields of the GraphQL type Video. +type InterfaceListOfListOfListsFieldListOfListsOfListsOfContentVideo struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// InterfaceListOfListOfListsFieldResponse is returned by InterfaceListOfListOfListsField on success. +type InterfaceListOfListOfListsFieldResponse struct { + ListOfListsOfListsOfContent [][][]InterfaceListOfListOfListsFieldListOfListsOfListsOfContent `json:"-"` + WithPointer [][][]*InterfaceListOfListOfListsFieldWithPointerContent `json:"-"` +} + +func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error { + + type InterfaceListOfListOfListsFieldResponseWrapper InterfaceListOfListOfListsFieldResponse + + var firstPass struct { + *InterfaceListOfListOfListsFieldResponseWrapper + ListOfListsOfListsOfContent [][][]json.RawMessage `json:"listOfListsOfListsOfContent"` + WithPointer [][][]json.RawMessage `json:"withPointer"` + } + firstPass.InterfaceListOfListOfListsFieldResponseWrapper = (*InterfaceListOfListOfListsFieldResponseWrapper)(v) + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.ListOfListsOfListsOfContent + raw := firstPass.ListOfListsOfListsOfContent + *target = make( + [][][]InterfaceListOfListOfListsFieldListOfListsOfListsOfContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + *target = make( + [][]InterfaceListOfListOfListsFieldListOfListsOfListsOfContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + *target = make( + []InterfaceListOfListOfListsFieldListOfListsOfListsOfContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + err = __unmarshalInterfaceListOfListOfListsFieldListOfListsOfListsOfContent( + target, raw) + if err != nil { + return err + } + } + } + } + } + { + target := &v.WithPointer + raw := firstPass.WithPointer + *target = make( + [][][]*InterfaceListOfListOfListsFieldWithPointerContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + *target = make( + [][]*InterfaceListOfListOfListsFieldWithPointerContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + *target = make( + []*InterfaceListOfListOfListsFieldWithPointerContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + err = __unmarshalInterfaceListOfListOfListsFieldWithPointerContent( + *target, raw) + if err != nil { + return err + } + } + } + } + } + return nil +} + +// InterfaceListOfListOfListsFieldWithPointerArticle includes the requested fields of the GraphQL type Article. +type InterfaceListOfListOfListsFieldWithPointerArticle struct { + Typename *string `json:"__typename"` + // ID is the identifier of the content. + Id *testutil.ID `json:"id"` + Name *string `json:"name"` +} + +// InterfaceListOfListOfListsFieldWithPointerContent 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 InterfaceListOfListOfListsFieldWithPointerContent interface { + implementsGraphQLInterfaceInterfaceListOfListOfListsFieldWithPointerContent() +} + +func (v *InterfaceListOfListOfListsFieldWithPointerArticle) implementsGraphQLInterfaceInterfaceListOfListOfListsFieldWithPointerContent() { +} +func (v *InterfaceListOfListOfListsFieldWithPointerVideo) implementsGraphQLInterfaceInterfaceListOfListOfListsFieldWithPointerContent() { +} +func (v *InterfaceListOfListOfListsFieldWithPointerTopic) implementsGraphQLInterfaceInterfaceListOfListOfListsFieldWithPointerContent() { +} + +func __unmarshalInterfaceListOfListOfListsFieldWithPointerContent(v *InterfaceListOfListOfListsFieldWithPointerContent, 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(InterfaceListOfListOfListsFieldWithPointerArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InterfaceListOfListOfListsFieldWithPointerVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(InterfaceListOfListOfListsFieldWithPointerTopic) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for InterfaceListOfListOfListsFieldWithPointerContent: "%v"`, tn.TypeName) + } +} + +// InterfaceListOfListOfListsFieldWithPointerTopic includes the requested fields of the GraphQL type Topic. +type InterfaceListOfListOfListsFieldWithPointerTopic struct { + Typename *string `json:"__typename"` + // ID is the identifier of the content. + Id *testutil.ID `json:"id"` + Name *string `json:"name"` +} + +// InterfaceListOfListOfListsFieldWithPointerVideo includes the requested fields of the GraphQL type Video. +type InterfaceListOfListOfListsFieldWithPointerVideo struct { + Typename *string `json:"__typename"` + // ID is the identifier of the content. + Id *testutil.ID `json:"id"` + Name *string `json:"name"` +} + +func InterfaceListOfListOfListsField( + client graphql.Client, +) (*InterfaceListOfListOfListsFieldResponse, error) { + var retval InterfaceListOfListOfListsFieldResponse + err := client.MakeRequest( + nil, + "InterfaceListOfListOfListsField", + ` +query InterfaceListOfListOfListsField { + listOfListsOfListsOfContent { + __typename + id + name + } + withPointer: listOfListsOfListsOfContent { + __typename + id + name + } +} +`, + &retval, + nil, + ) + return &retval, err +} + diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.json b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.json new file mode 100644 index 00000000..178e8721 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.json @@ -0,0 +1,9 @@ +{ + "operations": [ + { + "operationName": "InterfaceListOfListOfListsField", + "query": "\nquery InterfaceListOfListOfListsField {\n\tlistOfListsOfListsOfContent {\n\t\t__typename\n\t\tid\n\t\tname\n\t}\n\twithPointer: listOfListsOfListsOfContent {\n\t\t__typename\n\t\tid\n\t\tname\n\t}\n}\n", + "sourceLocation": "testdata/queries/InterfaceListOfListsOfListsField.graphql" + } + ] +} diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go index cb47b03d..e94a80cf 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go @@ -19,7 +19,40 @@ type InterfaceNestingResponse struct { type InterfaceNestingRootTopic struct { // ID is documented in the Content interface. Id testutil.ID `json:"id"` - Children []InterfaceNestingRootTopicChildrenContent `json:"children"` + Children []InterfaceNestingRootTopicChildrenContent `json:"-"` +} + +func (v *InterfaceNestingRootTopic) UnmarshalJSON(b []byte) error { + + type InterfaceNestingRootTopicWrapper InterfaceNestingRootTopic + + var firstPass struct { + *InterfaceNestingRootTopicWrapper + Children []json.RawMessage `json:"children"` + } + firstPass.InterfaceNestingRootTopicWrapper = (*InterfaceNestingRootTopicWrapper)(v) + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.Children + raw := firstPass.Children + *target = make( + []InterfaceNestingRootTopicChildrenContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + err = __unmarshalInterfaceNestingRootTopicChildrenContent( + target, raw) + if err != nil { + return err + } + } + } + return nil } // InterfaceNestingRootTopicChildrenArticle includes the requested fields of the GraphQL type Article. @@ -35,7 +68,40 @@ type InterfaceNestingRootTopicChildrenArticleParentTopic struct { Typename string `json:"__typename"` // ID is documented in the Content interface. Id testutil.ID `json:"id"` - Children []InterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent `json:"children"` + Children []InterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent `json:"-"` +} + +func (v *InterfaceNestingRootTopicChildrenArticleParentTopic) UnmarshalJSON(b []byte) error { + + type InterfaceNestingRootTopicChildrenArticleParentTopicWrapper InterfaceNestingRootTopicChildrenArticleParentTopic + + var firstPass struct { + *InterfaceNestingRootTopicChildrenArticleParentTopicWrapper + Children []json.RawMessage `json:"children"` + } + firstPass.InterfaceNestingRootTopicChildrenArticleParentTopicWrapper = (*InterfaceNestingRootTopicChildrenArticleParentTopicWrapper)(v) + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.Children + raw := firstPass.Children + *target = make( + []InterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + err = __unmarshalInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent( + target, raw) + if err != nil { + return err + } + } + } + return nil } // InterfaceNestingRootTopicChildrenArticleParentTopicChildrenArticle includes the requested fields of the GraphQL type Article. @@ -160,7 +226,40 @@ type InterfaceNestingRootTopicChildrenTopicParentTopic struct { Typename string `json:"__typename"` // ID is documented in the Content interface. Id testutil.ID `json:"id"` - Children []InterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent `json:"children"` + Children []InterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent `json:"-"` +} + +func (v *InterfaceNestingRootTopicChildrenTopicParentTopic) UnmarshalJSON(b []byte) error { + + type InterfaceNestingRootTopicChildrenTopicParentTopicWrapper InterfaceNestingRootTopicChildrenTopicParentTopic + + var firstPass struct { + *InterfaceNestingRootTopicChildrenTopicParentTopicWrapper + Children []json.RawMessage `json:"children"` + } + firstPass.InterfaceNestingRootTopicChildrenTopicParentTopicWrapper = (*InterfaceNestingRootTopicChildrenTopicParentTopicWrapper)(v) + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.Children + raw := firstPass.Children + *target = make( + []InterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + err = __unmarshalInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent( + target, raw) + if err != nil { + return err + } + } + } + return nil } // InterfaceNestingRootTopicChildrenTopicParentTopicChildrenArticle includes the requested fields of the GraphQL type Article. @@ -241,7 +340,40 @@ type InterfaceNestingRootTopicChildrenVideoParentTopic struct { Typename string `json:"__typename"` // ID is documented in the Content interface. Id testutil.ID `json:"id"` - Children []InterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent `json:"children"` + Children []InterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent `json:"-"` +} + +func (v *InterfaceNestingRootTopicChildrenVideoParentTopic) UnmarshalJSON(b []byte) error { + + type InterfaceNestingRootTopicChildrenVideoParentTopicWrapper InterfaceNestingRootTopicChildrenVideoParentTopic + + var firstPass struct { + *InterfaceNestingRootTopicChildrenVideoParentTopicWrapper + Children []json.RawMessage `json:"children"` + } + firstPass.InterfaceNestingRootTopicChildrenVideoParentTopicWrapper = (*InterfaceNestingRootTopicChildrenVideoParentTopicWrapper)(v) + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.Children + raw := firstPass.Children + *target = make( + []InterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + err = __unmarshalInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent( + target, raw) + if err != nil { + return err + } + } + } + return nil } // InterfaceNestingRootTopicChildrenVideoParentTopicChildrenArticle includes the requested fields of the GraphQL type Article. diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go index e182548a..73af3cb4 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go @@ -80,8 +80,9 @@ type InterfaceNoFragmentsQueryRandomItemVideo struct { // InterfaceNoFragmentsQueryResponse is returned by InterfaceNoFragmentsQuery on success. type InterfaceNoFragmentsQueryResponse struct { - Root InterfaceNoFragmentsQueryRootTopic `json:"root"` - RandomItem InterfaceNoFragmentsQueryRandomItemContent `json:"-"` + Root InterfaceNoFragmentsQueryRootTopic `json:"root"` + RandomItem InterfaceNoFragmentsQueryRandomItemContent `json:"-"` + WithPointer *InterfaceNoFragmentsQueryWithPointerContent `json:"-"` } func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { @@ -90,7 +91,8 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { var firstPass struct { *InterfaceNoFragmentsQueryResponseWrapper - RandomItem json.RawMessage `json:"randomItem"` + RandomItem json.RawMessage `json:"randomItem"` + WithPointer json.RawMessage `json:"withPointer"` } firstPass.InterfaceNoFragmentsQueryResponseWrapper = (*InterfaceNoFragmentsQueryResponseWrapper)(v) @@ -99,12 +101,24 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { return err } - err = __unmarshalInterfaceNoFragmentsQueryRandomItemContent( - &v.RandomItem, firstPass.RandomItem) - if err != nil { - return err + { + target := &v.RandomItem + raw := firstPass.RandomItem + err = __unmarshalInterfaceNoFragmentsQueryRandomItemContent( + target, raw) + if err != nil { + return err + } + } + { + target := &v.WithPointer + raw := firstPass.WithPointer + err = __unmarshalInterfaceNoFragmentsQueryWithPointerContent( + *target, raw) + if err != nil { + return err + } } - return nil } @@ -115,6 +129,74 @@ type InterfaceNoFragmentsQueryRootTopic struct { Name string `json:"name"` } +// InterfaceNoFragmentsQueryWithPointerArticle includes the requested fields of the GraphQL type Article. +type InterfaceNoFragmentsQueryWithPointerArticle struct { + Typename *string `json:"__typename"` + // ID is the identifier of the content. + Id *testutil.ID `json:"id"` + Name *string `json:"name"` +} + +// InterfaceNoFragmentsQueryWithPointerContent 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 InterfaceNoFragmentsQueryWithPointerContent interface { + implementsGraphQLInterfaceInterfaceNoFragmentsQueryWithPointerContent() +} + +func (v *InterfaceNoFragmentsQueryWithPointerArticle) implementsGraphQLInterfaceInterfaceNoFragmentsQueryWithPointerContent() { +} +func (v *InterfaceNoFragmentsQueryWithPointerVideo) implementsGraphQLInterfaceInterfaceNoFragmentsQueryWithPointerContent() { +} +func (v *InterfaceNoFragmentsQueryWithPointerTopic) implementsGraphQLInterfaceInterfaceNoFragmentsQueryWithPointerContent() { +} + +func __unmarshalInterfaceNoFragmentsQueryWithPointerContent(v *InterfaceNoFragmentsQueryWithPointerContent, 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(InterfaceNoFragmentsQueryWithPointerArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InterfaceNoFragmentsQueryWithPointerVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(InterfaceNoFragmentsQueryWithPointerTopic) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for InterfaceNoFragmentsQueryWithPointerContent: "%v"`, tn.TypeName) + } +} + +// InterfaceNoFragmentsQueryWithPointerTopic includes the requested fields of the GraphQL type Topic. +type InterfaceNoFragmentsQueryWithPointerTopic struct { + Typename *string `json:"__typename"` + // ID is the identifier of the content. + Id *testutil.ID `json:"id"` + Name *string `json:"name"` +} + +// InterfaceNoFragmentsQueryWithPointerVideo includes the requested fields of the GraphQL type Video. +type InterfaceNoFragmentsQueryWithPointerVideo struct { + Typename *string `json:"__typename"` + // ID is the identifier of the content. + Id *testutil.ID `json:"id"` + Name *string `json:"name"` +} + func InterfaceNoFragmentsQuery( client graphql.Client, ) (*InterfaceNoFragmentsQueryResponse, error) { @@ -133,6 +215,11 @@ query InterfaceNoFragmentsQuery { id name } + withPointer: randomItem { + __typename + id + name + } } `, &retval, diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.json b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.json index 48994c1d..0d1b8be1 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}\n\trandomItem {\n\t\t__typename\n\t\tid\n\t\tname\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\twithPointer: randomItem {\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 b8060dba..ac214bee 100644 --- a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go @@ -78,12 +78,15 @@ func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { return err } - err = __unmarshalUnionNoFragmentsQueryRandomLeafLeafContent( - &v.RandomLeaf, firstPass.RandomLeaf) - if err != nil { - return err + { + target := &v.RandomLeaf + raw := firstPass.RandomLeaf + err = __unmarshalUnionNoFragmentsQueryRandomLeafLeafContent( + target, raw) + if err != nil { + return err + } } - return nil } diff --git a/generate/types.go b/generate/types.go index b0f4df76..d3d8ca4e 100644 --- a/generate/types.go +++ b/generate/types.go @@ -22,6 +22,17 @@ type goType interface { // Reference returns the Go name of this type, e.g. []*MyStruct, and may be // used to refer to it in Go code. Reference() string + + // Remove slice/pointer wrappers, and return the underlying (named (or + // builtin)) type. For example, given []*MyStruct, return MyStruct. + Unwrap() goType + + // Count the number of times Unwrap() will unwrap a slice type. For + // example, given []*[]**[]MyStruct, return 3. + SliceDepth() int + + // True if Unwrap() will unwrap a pointer at least once. + IsPointer() bool } var ( @@ -106,6 +117,11 @@ type goStructField struct { Description string } +func isAbstract(typ goType) bool { + _, ok := typ.Unwrap().(*goInterfaceType) + return ok +} + func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error { description := typ.Description if typ.Incomplete { @@ -117,7 +133,7 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error { for _, field := range typ.Fields { writeDescription(w, field.Description) jsonName := field.JSONName - if _, ok := field.GoType.(*goInterfaceType); ok { + if isAbstract(field.GoType) { // abstract types are handled in our UnmarshalJSON jsonName = "-" } @@ -155,10 +171,7 @@ func (typ *goStructType) Reference() string { return typ.GoName } 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 { + if isAbstract(field.GoType) { ret = append(ret, field) } } @@ -211,6 +224,27 @@ func (typ *goInterfaceType) WriteDefinition(w io.Writer, g *generator) error { func (typ *goInterfaceType) Reference() string { return typ.GoName } +func (typ *goOpaqueType) Unwrap() goType { return typ } +func (typ *goSliceType) Unwrap() goType { return typ.Elem.Unwrap() } +func (typ *goPointerType) Unwrap() goType { return typ.Elem.Unwrap() } +func (typ *goEnumType) Unwrap() goType { return typ } +func (typ *goStructType) Unwrap() goType { return typ } +func (typ *goInterfaceType) Unwrap() goType { return typ } + +func (typ *goOpaqueType) SliceDepth() int { return 0 } +func (typ *goSliceType) SliceDepth() int { return typ.Elem.SliceDepth() + 1 } +func (typ *goPointerType) SliceDepth() int { return 0 } +func (typ *goEnumType) SliceDepth() int { return 0 } +func (typ *goStructType) SliceDepth() int { return 0 } +func (typ *goInterfaceType) SliceDepth() int { return 0 } + +func (typ *goOpaqueType) IsPointer() bool { return false } +func (typ *goSliceType) IsPointer() bool { return typ.Elem.IsPointer() } +func (typ *goPointerType) IsPointer() bool { return true } +func (typ *goEnumType) IsPointer() bool { return false } +func (typ *goStructType) IsPointer() bool { return false } +func (typ *goInterfaceType) IsPointer() bool { return false } + func incompleteTypeDescription(goName, graphQLName, description string) string { // For types where we only have some fields, note that, along with // the GraphQL documentation (if any). We don't want to just use diff --git a/generate/unmarshal.go.tmpl b/generate/unmarshal.go.tmpl index ed3eb585..b4e0086a 100644 --- a/generate/unmarshal.go.tmpl +++ b/generate/unmarshal.go.tmpl @@ -21,7 +21,7 @@ func (v *{{.GoName}}) UnmarshalJSON(b []byte) error { var firstPass struct{ *{{.GoName}}Wrapper {{range .AbstractFields -}} - {{.GoName}} {{ref "encoding/json.RawMessage"}} `json:"{{.JSONName}}"` + {{.GoName}} {{repeat .GoType.SliceDepth "[]"}}{{ref "encoding/json.RawMessage"}} `json:"{{.JSONName}}"` {{end}} } firstPass.{{.GoName}}Wrapper = (*{{.GoName}}Wrapper)(v) @@ -31,13 +31,61 @@ func (v *{{.GoName}}) UnmarshalJSON(b []byte) error { return err } - {{/* 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 + {{/* Now, for each field, call out to the unmarshal-helper. + This gets a little complicated because we may have a slice field. + So what we do is basically, for each field: + + target := &v.MyField // *[][]...[]MyType + raw := firstPass.MyField // [][]...[]json.RawMessage + + // repeat the following three lines n times; each time, inside + // the loop we have one less layer of slice on raw and target + *target = make([][]...[]MyType, len(raw)) + for i, raw := range raw { + // We need the &(*target)[i] because at each stage we want to + // keep target as a pointer. (It only really has to be a + // pointer at the innermost level, but it's easiest to be + // consistent.) + target := &(*target)[i] + + // (now we have `target *MyType` and `raw json.RawMessage`) + __unmarshalMyType(target, raw) + + } // (also n times) + + Note that if the field also uses a pointer ([][]...[]*MyType), we need + to do `*target` at the end, since the unmarshaler handles MyType, not + *MyType. (But again it's still easiest to pass around + *[][]...[]*MyType, even though in principle [][]...[]*MyType would now + work.) Of course, since MyType is an interface, I'm not sure why + you'd want a pointer anyway. + + One additional trick is we wrap everything above in a block ({ ... }), + so that the variables target and raw may take on different types for + each field we are handling, which would otherwise conflict. (We could + instead suffix the names, but that makes things much harder to read.) + */}} + {{range $field := .AbstractFields -}} + { + target := &v.{{$field.GoName}} + raw := firstPass.{{$field.GoName}} + {{range $i := intRange $field.GoType.SliceDepth -}} + *target = make( + {{repeat (sub $field.GoType.SliceDepth $i) "[]"}}{{if $field.GoType.IsPointer}}*{{end}}{{$field.GoType.Unwrap.Reference}}, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + {{end -}} + err = __unmarshal{{$field.GoType.Unwrap.Reference}}( + {{if $field.GoType.IsPointer}}*{{end}}target, raw) + if err != nil { + return err + } + {{range $i := intRange $field.GoType.SliceDepth -}} + } + {{end -}} } - {{end}} + {{end -}} + return nil } diff --git a/internal/integration/generated.go b/internal/integration/generated.go index aa50bef4..ed96b2f8 100644 --- a/internal/integration/generated.go +++ b/internal/integration/generated.go @@ -10,6 +10,94 @@ import ( "github.com/Khan/genqlient/graphql" ) +// queryWithInterfaceListFieldBeingsAnimal includes the requested fields of the GraphQL type Animal. +type queryWithInterfaceListFieldBeingsAnimal struct { + Typename string `json:"__typename"` + Id string `json:"id"` + Name string `json:"name"` +} + +// queryWithInterfaceListFieldBeingsBeing includes the requested fields of the GraphQL type Being. +type queryWithInterfaceListFieldBeingsBeing interface { + implementsGraphQLInterfacequeryWithInterfaceListFieldBeingsBeing() +} + +func (v *queryWithInterfaceListFieldBeingsUser) implementsGraphQLInterfacequeryWithInterfaceListFieldBeingsBeing() { +} +func (v *queryWithInterfaceListFieldBeingsAnimal) implementsGraphQLInterfacequeryWithInterfaceListFieldBeingsBeing() { +} + +func __unmarshalqueryWithInterfaceListFieldBeingsBeing(v *queryWithInterfaceListFieldBeingsBeing, 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(queryWithInterfaceListFieldBeingsUser) + return json.Unmarshal(m, *v) + case "Animal": + *v = new(queryWithInterfaceListFieldBeingsAnimal) + return json.Unmarshal(m, *v) + default: + return fmt.Errorf( + `Unexpected concrete type for queryWithInterfaceListFieldBeingsBeing: "%v"`, tn.TypeName) + } +} + +// queryWithInterfaceListFieldBeingsUser includes the requested fields of the GraphQL type User. +type queryWithInterfaceListFieldBeingsUser struct { + Typename string `json:"__typename"` + Id string `json:"id"` + Name string `json:"name"` +} + +// queryWithInterfaceListFieldResponse is returned by queryWithInterfaceListField on success. +type queryWithInterfaceListFieldResponse struct { + Beings []queryWithInterfaceListFieldBeingsBeing `json:"-"` +} + +func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error { + + type queryWithInterfaceListFieldResponseWrapper queryWithInterfaceListFieldResponse + + var firstPass struct { + *queryWithInterfaceListFieldResponseWrapper + Beings []json.RawMessage `json:"beings"` + } + firstPass.queryWithInterfaceListFieldResponseWrapper = (*queryWithInterfaceListFieldResponseWrapper)(v) + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.Beings + raw := firstPass.Beings + *target = make( + []queryWithInterfaceListFieldBeingsBeing, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + err = __unmarshalqueryWithInterfaceListFieldBeingsBeing( + target, raw) + if err != nil { + return err + } + } + } + return nil +} + // queryWithInterfaceNoFragmentsBeing includes the requested fields of the GraphQL type Being. type queryWithInterfaceNoFragmentsBeing interface { implementsGraphQLInterfacequeryWithInterfaceNoFragmentsBeing() @@ -87,12 +175,15 @@ func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error { return err } - err = __unmarshalqueryWithInterfaceNoFragmentsBeing( - &v.Being, firstPass.Being) - if err != nil { - return err + { + target := &v.Being + raw := firstPass.Being + err = __unmarshalqueryWithInterfaceNoFragmentsBeing( + target, raw) + if err != nil { + return err + } } - return nil } @@ -202,3 +293,31 @@ query queryWithInterfaceNoFragments ($id: ID!) { ) return &retval, err } + +func queryWithInterfaceListField( + ctx context.Context, + client graphql.Client, + ids []string, +) (*queryWithInterfaceListFieldResponse, error) { + variables := map[string]interface{}{ + "ids": ids, + } + + var retval queryWithInterfaceListFieldResponse + err := client.MakeRequest( + ctx, + "queryWithInterfaceListField", + ` +query queryWithInterfaceListField ($ids: [ID!]!) { + beings(ids: $ids) { + __typename + id + name + } +} +`, + &retval, + variables, + ) + return &retval, err +} diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go index 9f55aeb9..d057766f 100644 --- a/internal/integration/integration_test.go +++ b/internal/integration/integration_test.go @@ -99,6 +99,36 @@ func TestInterfaceNoFragments(t *testing.T) { assert.Nil(t, resp.Being) } +func TestInterfaceListField(t *testing.T) { + _ = `# @genqlient + query queryWithInterfaceListField($ids: [ID!]!) { + beings(ids: $ids) { __typename id name } + }` + + ctx := context.Background() + server := server.RunServer() + defer server.Close() + client := graphql.NewClient(server.URL, http.DefaultClient) + + resp, err := queryWithInterfaceListField(ctx, client, + []string{"1", "3", "12847394823"}) + require.NoError(t, err) + + require.Len(t, resp.Beings, 3) + + user, ok := resp.Beings[0].(*queryWithInterfaceListFieldBeingsUser) + require.Truef(t, ok, "got %T, not User", resp.Beings[0]) + assert.Equal(t, "1", user.Id) + assert.Equal(t, "Yours Truly", user.Name) + + animal, ok := resp.Beings[1].(*queryWithInterfaceListFieldBeingsAnimal) + require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) + assert.Equal(t, "3", animal.Id) + assert.Equal(t, "Fido", animal.Name) + + assert.Nil(t, resp.Beings[2]) +} + 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 2520059e..5c42e462 100644 --- a/internal/integration/schema.graphql +++ b/internal/integration/schema.graphql @@ -2,6 +2,7 @@ type Query { me: User user(id: ID!): User being(id: ID!): Being + beings(ids: [ID!]!): [Being]! } type User implements Being { diff --git a/internal/integration/server/gqlgen_exec.go b/internal/integration/server/gqlgen_exec.go index 0df4e783..d50db5ad 100644 --- a/internal/integration/server/gqlgen_exec.go +++ b/internal/integration/server/gqlgen_exec.go @@ -9,6 +9,7 @@ import ( "fmt" "strconv" "sync" + "sync/atomic" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" @@ -49,9 +50,10 @@ type ComplexityRoot struct { } Query struct { - Being func(childComplexity int, id string) int - Me func(childComplexity int) int - User func(childComplexity int, id string) int + Being func(childComplexity int, id string) int + Beings func(childComplexity int, ids []string) int + Me func(childComplexity int) int + User func(childComplexity int, id string) int } User struct { @@ -65,6 +67,7 @@ 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) + Beings(ctx context.Context, ids []string) ([]Being, error) } type executableSchema struct { @@ -122,6 +125,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.Being(childComplexity, args["id"].(string)), true + case "Query.beings": + if e.complexity.Query.Beings == nil { + break + } + + args, err := ec.field_Query_beings_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.Beings(childComplexity, args["ids"].([]string)), true + case "Query.me": if e.complexity.Query.Me == nil { break @@ -216,6 +231,7 @@ var sources = []*ast.Source{ me: User user(id: ID!): User being(id: ID!): Being + beings(ids: [ID!]!): [Being]! } type User implements Being { @@ -278,6 +294,21 @@ func (ec *executionContext) field_Query_being_args(ctx context.Context, rawArgs return args, nil } +func (ec *executionContext) field_Query_beings_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["ids"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ids")) + arg0, err = ec.unmarshalNID2ᚕstringᚄ(ctx, tmp) + if err != nil { + return nil, err + } + } + args["ids"] = 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{}{} @@ -578,6 +609,48 @@ func (ec *executionContext) _Query_being(ctx context.Context, field graphql.Coll return ec.marshalOBeing2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeing(ctx, field.Selections, res) } +func (ec *executionContext) _Query_beings(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_beings_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().Beings(rctx, args["ids"].([]string)) + }) + 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.([]Being) + fc.Result = res + return ec.marshalNBeing2ᚕgithubᚗ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 { @@ -1956,6 +2029,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr res = ec._Query_being(ctx, field) return res }) + case "beings": + 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_beings(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) case "__type": out.Values[i] = ec._Query___type(ctx, field) case "__schema": @@ -2250,6 +2337,43 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o // region ***************************** type.gotpl ***************************** +func (ec *executionContext) marshalNBeing2ᚕgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeing(ctx context.Context, sel ast.SelectionSet, v []Being) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalOBeing2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeing(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v interface{}) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) @@ -2280,6 +2404,36 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } +func (ec *executionContext) unmarshalNID2ᚕstringᚄ(ctx context.Context, v interface{}) ([]string, error) { + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} + } + } + var err error + res := make([]string, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNID2string(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalNID2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalNID2string(ctx, sel, v[i]) + } + + return ret +} + 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) diff --git a/internal/integration/server/server.go b/internal/integration/server/server.go index f24b4911..8032326f 100644 --- a/internal/integration/server/server.go +++ b/internal/integration/server/server.go @@ -55,6 +55,14 @@ func (r *queryResolver) Being(ctx context.Context, id string) (Being, error) { return beingByID(id), nil } +func (r *queryResolver) Beings(ctx context.Context, ids []string) ([]Being, error) { + ret := make([]Being, len(ids)) + for i, id := range ids { + ret[i] = beingByID(id) + } + return ret, nil +} + func RunServer() *httptest.Server { gqlgenServer := handler.New(NewExecutableSchema(Config{Resolvers: &resolver{}})) gqlgenServer.AddTransport(transport.POST{})