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