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{})