From cf7136ca65db7b67549c1904ba90242a7bece861 Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Thu, 16 Jul 2020 13:28:43 -0700 Subject: [PATCH] total rewrite to interface handling; not complete but it compiles --- README.md | 3 +- example/generated.go | 16 +-- generate/generate.go | 17 +--- generate/operation.go.tmpl | 5 +- generate/template.go | 15 +++ .../testdata/InterfaceNoFragments.graphql | 10 ++ .../testdata/InterfaceNoFragments.graphql.go | 97 +++++++++++++++++++ generate/testdata/QueryWithAlias.graphql | 2 +- generate/testdata/QueryWithAlias.graphql.go | 30 +++++- .../testdata/QueryWithDoubleAlias.graphql | 2 +- .../testdata/QueryWithDoubleAlias.graphql.go | 37 +++++-- generate/testdata/QueryWithEnums.graphql | 2 +- generate/testdata/QueryWithEnums.graphql.go | 40 ++++++-- generate/testdata/QueryWithInput.graphql | 2 +- generate/testdata/QueryWithInput.graphql.go | 52 +++++++--- generate/testdata/QueryWithSlices.graphql | 2 +- generate/testdata/QueryWithSlices.graphql.go | 43 ++++++-- generate/testdata/QueryWithStructs.graphql | 2 +- generate/testdata/QueryWithStructs.graphql.go | 45 +++++++-- generate/testdata/SimpleQuery.graphql | 2 +- generate/testdata/SimpleQuery.graphql.go | 32 +++++- generate/testdata/TypeName.graphql | 2 +- generate/testdata/TypeName.graphql.go | 37 +++++-- generate/testdata/UnionNoFragments.graphql | 5 + generate/testdata/UnionNoFragments.graphql.go | 79 +++++++++++++++ generate/testdata/UsesEnumTwice.graphql | 2 +- generate/testdata/UsesEnumTwice.graphql.go | 60 +++++++++--- generate/types.go | 89 +++++++++++------ generate/types_test.go | 54 +++-------- generate/unmarshal.go | 51 ++++++++++ generate/unmarshal.go.tmpl | 35 +++++++ generate/util.go | 9 +- generate/util_test.go | 6 +- 33 files changed, 712 insertions(+), 173 deletions(-) create mode 100644 generate/template.go create mode 100644 generate/testdata/InterfaceNoFragments.graphql create mode 100644 generate/testdata/InterfaceNoFragments.graphql.go create mode 100644 generate/testdata/UnionNoFragments.graphql create mode 100644 generate/testdata/UnionNoFragments.graphql.go create mode 100644 generate/unmarshal.go create mode 100644 generate/unmarshal.go.tmpl diff --git a/README.md b/README.md index 5db59d9..95be9fe 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ For a complete working example, see `example/`. ## Tests -`go test ./...` does some perfunctory tests. (This is run by GitHub Actions.) +`go test ./...` tests code generation. (This is run by GitHub Actions.) `make example` tests that everything wires up to a real API correctly. @@ -69,6 +69,7 @@ Config options: - proper config/arguments setup (e.g. with [viper](https://github.com/spf13/viper) Other: +- naming collisions are a mess - error-checking/validation/etc. everywhere - more tests - documentation diff --git a/example/generated.go b/example/generated.go index 0c07345..b0e0cea 100644 --- a/example/generated.go +++ b/example/generated.go @@ -8,16 +8,20 @@ import ( "github.com/Khan/genql/graphql" ) +type User struct { + MyName *string +} + +type User1 struct { + TheirName *string `json:"theirName"` +} + type getUserResponse struct { - User *struct { - TheirName *string `json:"theirName"` - } `json:"user"` + User *User1 `json:"user"` } type getViewerResponse struct { - Viewer struct { - MyName *string - } `json:"viewer"` + Viewer User `json:"viewer"` } func getViewer(ctx context.Context, client *graphql.Client) (*getViewerResponse, error) { diff --git a/generate/generate.go b/generate/generate.go index 1784779..7a08cf2 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -4,22 +4,14 @@ import ( "bytes" "fmt" "go/format" - "path/filepath" - "runtime" "sort" "strings" - "text/template" "github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/formatter" ) -// TODO: package template into the binary using one of those asset thingies -var _, thisFilename, _, _ = runtime.Caller(0) -var tmplRelFilename = "operation.go.tmpl" -var tmplAbsFilename = filepath.Join(filepath.Dir(thisFilename), tmplRelFilename) - -var tmpl = template.Must(template.ParseFiles(tmplAbsFilename)) +var fileTemplate = mustTemplate("operation.go.tmpl") // generator is the context for the codegen process (and ends up getting passed // to the template). @@ -29,8 +21,9 @@ type generator struct { // The list of operations for which to generate code. Operations []operation // The types needed for these operations. - typeMap map[string]string - schema *ast.Schema + typeMap map[string]string + ImportJSON bool + schema *ast.Schema } type operation struct { @@ -165,7 +158,7 @@ func Generate(config *Config) ([]byte, error) { } var buf bytes.Buffer - err = tmpl.Execute(&buf, g) + err = fileTemplate.Execute(&buf, g) if err != nil { return nil, fmt.Errorf("could not render template: %v", err) } diff --git a/generate/operation.go.tmpl b/generate/operation.go.tmpl index 0b3734a..13c5f03 100644 --- a/generate/operation.go.tmpl +++ b/generate/operation.go.tmpl @@ -1,9 +1,12 @@ -package {{$.Config.Package}} +package {{.Config.Package}} // Code generated by github.com/Khan/genql, DO NOT EDIT. import ( "context" + {{- if .ImportJSON -}} + "encoding/json" + {{end}} "github.com/Khan/genql/graphql" ) diff --git a/generate/template.go b/generate/template.go new file mode 100644 index 0000000..95ca779 --- /dev/null +++ b/generate/template.go @@ -0,0 +1,15 @@ +package generate + +import ( + "path/filepath" + "runtime" + "text/template" +) + +// TODO: package templates into the binary using one of those asset thingies +var _, thisFilename, _, _ = runtime.Caller(0) +var thisDir = filepath.Dir(thisFilename) + +func mustTemplate(relFilename string) *template.Template { + return template.Must(template.ParseFiles(filepath.Join(thisDir, relFilename))) +} diff --git a/generate/testdata/InterfaceNoFragments.graphql b/generate/testdata/InterfaceNoFragments.graphql new file mode 100644 index 0000000..0e8f4a0 --- /dev/null +++ b/generate/testdata/InterfaceNoFragments.graphql @@ -0,0 +1,10 @@ +query InterfaceNoFragmentsQuery { + root { + id + name + children { + id + name + } + } +} diff --git a/generate/testdata/InterfaceNoFragments.graphql.go b/generate/testdata/InterfaceNoFragments.graphql.go new file mode 100644 index 0000000..e603cfa --- /dev/null +++ b/generate/testdata/InterfaceNoFragments.graphql.go @@ -0,0 +1,97 @@ +package test + +// Code generated by github.com/Khan/genql, DO NOT EDIT. + +import ( + "context" + + "github.com/Khan/genql/graphql" +) + +type Article struct { + Id string `json:"id"` + Name string `json:"name"` +} + +func (v Article) implementsGraphQLInterfaceContent() {} + +type Content interface { + implementsGraphQLInterfaceContent() +} + +type InterfaceNoFragmentsQueryResponse struct { + Root Topic `json:"root"` +} + +type Topic struct { + Id string `json:"id"` + Name string `json:"name"` + Children []Content `json:"-"` +} + +func (v *Topic) UnmarshalJSON(b []byte) error { + var firstPass struct { + *Topic + Children json.RawMessage `json:"children"` + } + firstPass.Topic = v + + err := json.Unmarshal(b, &typenames) + if err != nil { + return err + } + + var tn struct { + TypeName string `json:"__typename"` + } + err = json.Unmarshal(firstPass.Children, &tn) + if err != nil { + return err + } + switch tn.TypeName { + + case "Article": + v.Children = Article{} + err = json.Unmarshal( + firstPass.Children, &v.Children) + + case "Video": + v.Children = Video{} + err = json.Unmarshal( + firstPass.Children, &v.Children) + + case "Topic": + v.Children = Topic{} + err = json.Unmarshal( + firstPass.Children, &v.Children) + + } + if err != nil { + return err + } + +} + +type Video struct { + Id string `json:"id"` + Name string `json:"name"` +} + +func (v Video) implementsGraphQLInterfaceContent() {} + +func InterfaceNoFragmentsQuery(client *graphql.Client) (*InterfaceNoFragmentsQueryResponse, error) { + var retval InterfaceNoFragmentsQueryResponse + err := client.MakeRequest(context.Background(), ` +query InterfaceNoFragmentsQuery { + root { + id + name + children { + id + name + } + } +} +`, &retval, nil) + return &retval, err +} diff --git a/generate/testdata/QueryWithAlias.graphql b/generate/testdata/QueryWithAlias.graphql index bd2598e..467d93b 100644 --- a/generate/testdata/QueryWithAlias.graphql +++ b/generate/testdata/QueryWithAlias.graphql @@ -1 +1 @@ -{ User: user { ID: id } } +query QueryWithAlias { User: user { ID: id } } diff --git a/generate/testdata/QueryWithAlias.graphql.go b/generate/testdata/QueryWithAlias.graphql.go index 74a9802..30bbea8 100644 --- a/generate/testdata/QueryWithAlias.graphql.go +++ b/generate/testdata/QueryWithAlias.graphql.go @@ -1,5 +1,29 @@ -type Response struct { - User *struct { - ID string +package test + +// Code generated by github.com/Khan/genql, DO NOT EDIT. + +import ( + "context" + + "github.com/Khan/genql/graphql" +) + +type QueryWithAliasResponse struct { + User *User +} + +type User struct { + ID string +} + +func QueryWithAlias(client *graphql.Client) (*QueryWithAliasResponse, error) { + var retval QueryWithAliasResponse + err := client.MakeRequest(context.Background(), ` +query QueryWithAlias { + User: user { + ID: id } } +`, &retval, nil) + return &retval, err +} diff --git a/generate/testdata/QueryWithDoubleAlias.graphql b/generate/testdata/QueryWithDoubleAlias.graphql index 9a2c1b7..d7eb850 100644 --- a/generate/testdata/QueryWithDoubleAlias.graphql +++ b/generate/testdata/QueryWithDoubleAlias.graphql @@ -1,4 +1,4 @@ -{ +query QueryWithDoubleAlias { user { ID: id AlsoID: id diff --git a/generate/testdata/QueryWithDoubleAlias.graphql.go b/generate/testdata/QueryWithDoubleAlias.graphql.go index 6a40da8..4805917 100644 --- a/generate/testdata/QueryWithDoubleAlias.graphql.go +++ b/generate/testdata/QueryWithDoubleAlias.graphql.go @@ -1,6 +1,31 @@ -type Response struct { - User *struct { - ID string - AlsoID string - } `json:"user"` -} \ No newline at end of file +package test + +// Code generated by github.com/Khan/genql, DO NOT EDIT. + +import ( + "context" + + "github.com/Khan/genql/graphql" +) + +type QueryWithDoubleAliasResponse struct { + User *User `json:"user"` +} + +type User struct { + ID string + AlsoID string +} + +func QueryWithDoubleAlias(client *graphql.Client) (*QueryWithDoubleAliasResponse, error) { + var retval QueryWithDoubleAliasResponse + err := client.MakeRequest(context.Background(), ` +query QueryWithDoubleAlias { + user { + ID: id + AlsoID: id + } +} +`, &retval, nil) + return &retval, err +} diff --git a/generate/testdata/QueryWithEnums.graphql b/generate/testdata/QueryWithEnums.graphql index 34aea40..eb55551 100644 --- a/generate/testdata/QueryWithEnums.graphql +++ b/generate/testdata/QueryWithEnums.graphql @@ -1,4 +1,4 @@ -{ +query QueryWithEnums { user { roles } diff --git a/generate/testdata/QueryWithEnums.graphql.go b/generate/testdata/QueryWithEnums.graphql.go index 04446c9..2e98251 100644 --- a/generate/testdata/QueryWithEnums.graphql.go +++ b/generate/testdata/QueryWithEnums.graphql.go @@ -1,12 +1,36 @@ -type Response struct { - User *struct { - Roles []role `json:"roles"` - } `json:"user"` +package test + +// Code generated by github.com/Khan/genql, DO NOT EDIT. + +import ( + "context" + + "github.com/Khan/genql/graphql" +) + +type QueryWithEnumsResponse struct { + User *User `json:"user"` } -type role string +type Role string const ( - studentRole role = "STUDENT" - teacherRole role = "TEACHER" -) \ No newline at end of file + StudentRole Role = "STUDENT" + TeacherRole Role = "TEACHER" +) + +type User struct { + Roles []Role `json:"roles"` +} + +func QueryWithEnums(client *graphql.Client) (*QueryWithEnumsResponse, error) { + var retval QueryWithEnumsResponse + err := client.MakeRequest(context.Background(), ` +query QueryWithEnums { + user { + roles + } +} +`, &retval, nil) + return &retval, err +} diff --git a/generate/testdata/QueryWithInput.graphql b/generate/testdata/QueryWithInput.graphql index 59febff..dbc26f2 100644 --- a/generate/testdata/QueryWithInput.graphql +++ b/generate/testdata/QueryWithInput.graphql @@ -1,4 +1,4 @@ -query ($query: UserQueryInput) { +query QueryWithInput($query: UserQueryInput) { user(query: $query) { id } diff --git a/generate/testdata/QueryWithInput.graphql.go b/generate/testdata/QueryWithInput.graphql.go index b609231..64dd84a 100644 --- a/generate/testdata/QueryWithInput.graphql.go +++ b/generate/testdata/QueryWithInput.graphql.go @@ -1,19 +1,47 @@ -type Response struct { - User *struct { - Id string `json:"id"` - } `json:"user"` -} +package test -type role string +// Code generated by github.com/Khan/genql, DO NOT EDIT. -const ( - studentRole role = "STUDENT" - teacherRole role = "TEACHER" +import ( + "context" + + "github.com/Khan/genql/graphql" ) -type userQueryInput struct { +type QueryWithInputResponse struct { + User *User `json:"user"` +} + +type Role string + +const ( + StudentRole Role = "STUDENT" + TeacherRole Role = "TEACHER" +) + +type User struct { + Id string `json:"id"` +} + +type UserQueryInput struct { Email *string `json:"email"` Name *string `json:"name"` Id *string `json:"id"` - Role *role `json:"role"` -} \ No newline at end of file + Role *Role `json:"role"` +} + +func QueryWithInput(client *graphql.Client, query *UserQueryInput) (*QueryWithInputResponse, error) { + variables := map[string]interface{}{ + "query": query, + } + + var retval QueryWithInputResponse + err := client.MakeRequest(context.Background(), ` +query QueryWithInput ($query: UserQueryInput) { + user(query: $query) { + id + } +} +`, &retval, variables) + return &retval, err +} diff --git a/generate/testdata/QueryWithSlices.graphql b/generate/testdata/QueryWithSlices.graphql index 3640724..31d1b3d 100644 --- a/generate/testdata/QueryWithSlices.graphql +++ b/generate/testdata/QueryWithSlices.graphql @@ -1,4 +1,4 @@ -{ +query QueryWithSlices { user { emails emailsOrNull diff --git a/generate/testdata/QueryWithSlices.graphql.go b/generate/testdata/QueryWithSlices.graphql.go index 23b4ffc..8392836 100644 --- a/generate/testdata/QueryWithSlices.graphql.go +++ b/generate/testdata/QueryWithSlices.graphql.go @@ -1,8 +1,35 @@ -type Response struct { - User *struct { - Emails []string `json:"emails"` - EmailsOrNull []string `json:"emailsOrNull"` - EmailsWithNulls []*string `json:"emailsWithNulls"` - EmailsWithNullsOrNull []*string `json:"emailsWithNullsOrNull"` - } `json:"user"` -} \ No newline at end of file +package test + +// Code generated by github.com/Khan/genql, DO NOT EDIT. + +import ( + "context" + + "github.com/Khan/genql/graphql" +) + +type QueryWithSlicesResponse struct { + User *User `json:"user"` +} + +type User struct { + Emails []string `json:"emails"` + EmailsOrNull []string `json:"emailsOrNull"` + EmailsWithNulls []*string `json:"emailsWithNulls"` + EmailsWithNullsOrNull []*string `json:"emailsWithNullsOrNull"` +} + +func QueryWithSlices(client *graphql.Client) (*QueryWithSlicesResponse, error) { + var retval QueryWithSlicesResponse + err := client.MakeRequest(context.Background(), ` +query QueryWithSlices { + user { + emails + emailsOrNull + emailsWithNulls + emailsWithNullsOrNull + } +} +`, &retval, nil) + return &retval, err +} diff --git a/generate/testdata/QueryWithStructs.graphql b/generate/testdata/QueryWithStructs.graphql index 5850964..eab61cd 100644 --- a/generate/testdata/QueryWithStructs.graphql +++ b/generate/testdata/QueryWithStructs.graphql @@ -1,4 +1,4 @@ -{ +query QueryWithStructs { user { authMethods { provider diff --git a/generate/testdata/QueryWithStructs.graphql.go b/generate/testdata/QueryWithStructs.graphql.go index 4460c3c..4416899 100644 --- a/generate/testdata/QueryWithStructs.graphql.go +++ b/generate/testdata/QueryWithStructs.graphql.go @@ -1,8 +1,37 @@ -type Response struct { - User *struct { - AuthMethods []struct { - Provider *string `json:"provider"` - Email *string `json:"email"` - } `json:"authMethods"` - } `json:"user"` -} \ No newline at end of file +package test + +// Code generated by github.com/Khan/genql, DO NOT EDIT. + +import ( + "context" + + "github.com/Khan/genql/graphql" +) + +type AuthMethod struct { + Provider *string `json:"provider"` + Email *string `json:"email"` +} + +type QueryWithStructsResponse struct { + User *User `json:"user"` +} + +type User struct { + AuthMethods []AuthMethod `json:"authMethods"` +} + +func QueryWithStructs(client *graphql.Client) (*QueryWithStructsResponse, error) { + var retval QueryWithStructsResponse + err := client.MakeRequest(context.Background(), ` +query QueryWithStructs { + user { + authMethods { + provider + email + } + } +} +`, &retval, nil) + return &retval, err +} diff --git a/generate/testdata/SimpleQuery.graphql b/generate/testdata/SimpleQuery.graphql index 2e67e3f..1bee8aa 100644 --- a/generate/testdata/SimpleQuery.graphql +++ b/generate/testdata/SimpleQuery.graphql @@ -1 +1 @@ -{ user { id } } \ No newline at end of file +query SimpleQuery { user { id } } diff --git a/generate/testdata/SimpleQuery.graphql.go b/generate/testdata/SimpleQuery.graphql.go index e974001..b317e30 100644 --- a/generate/testdata/SimpleQuery.graphql.go +++ b/generate/testdata/SimpleQuery.graphql.go @@ -1,5 +1,29 @@ -type Response struct { - User *struct { - Id string `json:"id"` - } `json:"user"` +package test + +// Code generated by github.com/Khan/genql, DO NOT EDIT. + +import ( + "context" + + "github.com/Khan/genql/graphql" +) + +type SimpleQueryResponse struct { + User *User `json:"user"` +} + +type User struct { + Id string `json:"id"` +} + +func SimpleQuery(client *graphql.Client) (*SimpleQueryResponse, error) { + var retval SimpleQueryResponse + err := client.MakeRequest(context.Background(), ` +query SimpleQuery { + user { + id + } +} +`, &retval, nil) + return &retval, err } diff --git a/generate/testdata/TypeName.graphql b/generate/testdata/TypeName.graphql index 1cde75c..af8675f 100644 --- a/generate/testdata/TypeName.graphql +++ b/generate/testdata/TypeName.graphql @@ -1,4 +1,4 @@ -{ +query TypeNameQuery { user { __typename id diff --git a/generate/testdata/TypeName.graphql.go b/generate/testdata/TypeName.graphql.go index 2ac33e8..cb46f30 100644 --- a/generate/testdata/TypeName.graphql.go +++ b/generate/testdata/TypeName.graphql.go @@ -1,6 +1,31 @@ -type Response struct { - User *struct { - Typename *string `json:"__typename"` - Id string `json:"id"` - } `json:"user"` -} \ No newline at end of file +package test + +// Code generated by github.com/Khan/genql, DO NOT EDIT. + +import ( + "context" + + "github.com/Khan/genql/graphql" +) + +type TypeNameQueryResponse struct { + User *User `json:"user"` +} + +type User struct { + Typename *string `json:"__typename"` + Id string `json:"id"` +} + +func TypeNameQuery(client *graphql.Client) (*TypeNameQueryResponse, error) { + var retval TypeNameQueryResponse + err := client.MakeRequest(context.Background(), ` +query TypeNameQuery { + user { + __typename + id + } +} +`, &retval, nil) + return &retval, err +} diff --git a/generate/testdata/UnionNoFragments.graphql b/generate/testdata/UnionNoFragments.graphql new file mode 100644 index 0000000..49ae12e --- /dev/null +++ b/generate/testdata/UnionNoFragments.graphql @@ -0,0 +1,5 @@ +query UnionNoFragmentsQuery { + randomLeaf { + __typename + } +} diff --git a/generate/testdata/UnionNoFragments.graphql.go b/generate/testdata/UnionNoFragments.graphql.go new file mode 100644 index 0000000..ed78c33 --- /dev/null +++ b/generate/testdata/UnionNoFragments.graphql.go @@ -0,0 +1,79 @@ +package test + +// Code generated by github.com/Khan/genql, DO NOT EDIT. + +import ( + "context" + + "github.com/Khan/genql/graphql" +) + +type Article struct { + Typename *string `json:"__typename"` +} + +func (v Article) implementsGraphQLInterfaceLeafContent() {} + +type LeafContent interface { + implementsGraphQLInterfaceLeafContent() +} + +type UnionNoFragmentsQueryResponse struct { + RandomLeaf LeafContent `json:"-"` +} + +func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { + var firstPass struct { + *UnionNoFragmentsQueryResponse + RandomLeaf json.RawMessage `json:"randomLeaf"` + } + firstPass.UnionNoFragmentsQueryResponse = v + + err := json.Unmarshal(b, &typenames) + 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 = Article{} + err = json.Unmarshal( + firstPass.RandomLeaf, &v.RandomLeaf) + + case "Video": + v.RandomLeaf = Video{} + err = json.Unmarshal( + firstPass.RandomLeaf, &v.RandomLeaf) + + } + if err != nil { + return err + } + +} + +type Video struct { + Typename *string `json:"__typename"` +} + +func (v Video) implementsGraphQLInterfaceLeafContent() {} + +func UnionNoFragmentsQuery(client *graphql.Client) (*UnionNoFragmentsQueryResponse, error) { + var retval UnionNoFragmentsQueryResponse + err := client.MakeRequest(context.Background(), ` +query UnionNoFragmentsQuery { + randomLeaf { + __typename + } +} +`, &retval, nil) + return &retval, err +} diff --git a/generate/testdata/UsesEnumTwice.graphql b/generate/testdata/UsesEnumTwice.graphql index 2350cd8..4904d1b 100644 --- a/generate/testdata/UsesEnumTwice.graphql +++ b/generate/testdata/UsesEnumTwice.graphql @@ -1,4 +1,4 @@ -{ +query UsesEnumTwiceQuery { Me: user { roles } OtherUser: user { roles } } diff --git a/generate/testdata/UsesEnumTwice.graphql.go b/generate/testdata/UsesEnumTwice.graphql.go index a24dbbd..3652cd8 100644 --- a/generate/testdata/UsesEnumTwice.graphql.go +++ b/generate/testdata/UsesEnumTwice.graphql.go @@ -1,15 +1,51 @@ -type Response struct { - Me *struct { - Roles []role `json:"roles"` - } - OtherUser *struct { - Roles []role `json:"roles"` - } -} +package test -type role string +// Code generated by github.com/Khan/genql, DO NOT EDIT. + +import ( + "context" + + "github.com/Khan/genql/graphql" +) + +type Role string const ( - studentRole role = "STUDENT" - teacherRole role = "TEACHER" -) \ No newline at end of file + StudentRole Role = "STUDENT" + TeacherRole Role = "TEACHER" +) + +type Role1 string + +const ( + StudentRole1 Role1 = "STUDENT" + TeacherRole1 Role1 = "TEACHER" +) + +type User struct { + Roles []Role `json:"roles"` +} + +type User1 struct { + Roles []Role1 `json:"roles"` +} + +type UsesEnumTwiceQueryResponse struct { + Me *User + OtherUser *User1 +} + +func UsesEnumTwiceQuery(client *graphql.Client) (*UsesEnumTwiceQueryResponse, error) { + var retval UsesEnumTwiceQueryResponse + err := client.MakeRequest(context.Background(), ` +query UsesEnumTwiceQuery { + Me: user { + roles + } + OtherUser: user { + roles + } +} +`, &retval, nil) + return &retval, err +} diff --git a/generate/types.go b/generate/types.go index 486f95c..5aa752c 100644 --- a/generate/types.go +++ b/generate/types.go @@ -2,6 +2,7 @@ package generate import ( "fmt" + "strconv" "strings" "github.com/vektah/gqlparser/ast" @@ -54,11 +55,16 @@ func (g *generator) addTypeForDefinition(nameOverride string, typ *ast.Definitio name = nameOverride } else { // TODO: casing should be configurable - name = lowerFirst(typ.Name) + name = upperFirst(typ.Name) } - if _, ok := g.typeMap[name]; ok { - return name, nil + // TODO: in some cases we can deduplicate, do that + // TODO: nicer naming scheme + i := 0 + origName := name + for g.typeMap[name] != "" { + i++ + name = origName + strconv.Itoa(i) } builder := &typeBuilder{typeName: name, generator: g} @@ -73,22 +79,27 @@ func (g *generator) addTypeForDefinition(nameOverride string, typ *ast.Definitio } func (g *generator) getTypeForInputType(typ *ast.Type) (string, error) { - builder := &typeBuilder{typeName: lowerFirst(typ.Name()), generator: g} - err := builder.writeType(typ, selectionsForType(g, typ), false) + builder := &typeBuilder{typeName: upperFirst(typ.Name()), generator: g} + err := builder.writeType(typ, selectionsForType(g, typ)) return builder.String(), err } +// TODO: this is really "field" now, rename it type selection interface { Alias() string - Name() string Type() *ast.Type SelectionSet() ([]selection, error) } type field struct{ field *ast.Field } -func (s field) Alias() string { return s.field.Alias } -func (s field) Name() string { return s.field.Name } +func (s field) Alias() string { + if s.field.Alias != "" { + return s.field.Alias + } + // TODO: is this case needed? tests don't seem to get here. + return s.field.Name +} func (s field) Type() *ast.Type { if s.field.Definition == nil { @@ -122,7 +133,6 @@ type inputField struct { } func (s inputField) Alias() string { return s.field.Name } -func (s inputField) Name() string { return s.field.Name } func (s inputField) Type() *ast.Type { return s.field.Type } func (s inputField) SelectionSet() ([]selection, error) { @@ -139,13 +149,7 @@ func selectionsForType(g *generator, typ *ast.Type) []selection { } func (builder *typeBuilder) writeField(selection selection) error { - var jsonName string - if selection.Alias() != "" { - jsonName = selection.Alias() - } else { - // TODO: is this case needed? tests don't seem to get here. - jsonName = selection.Name() - } + jsonName := selection.Alias() // We need an exportable name for JSON-marshaling. goName := upperFirst(jsonName) @@ -156,7 +160,7 @@ func (builder *typeBuilder) writeField(selection selection) error { if typ == nil { // Unclear why gqlparser hasn't already rejected this, // but empirically it might not. - return fmt.Errorf("undefined field %v", selection.Name()) + return fmt.Errorf("undefined field %v", selection.Alias()) } selectionSet, err := selection.SelectionSet() @@ -164,12 +168,15 @@ func (builder *typeBuilder) writeField(selection selection) error { return err } - err = builder.writeType(typ, selectionSet, true) + err = builder.writeType(typ, selectionSet) if err != nil { return err } - if jsonName != goName { + if builder.schema.Types[typ.Name()].IsAbstractType() { + // abstract types are handled in our UnmarshalJSON + builder.WriteString(" `json:\"-\"`") + } else if jsonName != goName { fmt.Fprintf(builder, " `json:\"%s\"`", jsonName) } builder.WriteRune('\n') @@ -184,7 +191,7 @@ var builtinTypes = map[string]string{ "ID": "string", // TODO: named type for IDs? } -func (builder *typeBuilder) writeType(typ *ast.Type, selectionSet []selection, inline bool) error { +func (builder *typeBuilder) writeType(typ *ast.Type, selectionSet []selection) error { // gqlgen does slightly different things here since it defines names for // all the intermediate types, but its implementation may be useful to crib // from: @@ -200,16 +207,6 @@ func (builder *typeBuilder) writeType(typ *ast.Type, selectionSet []selection, i } def := builder.schema.Types[typ.Name()] - // TODO: set inline = false for nested types - switch def.Kind { - case ast.Scalar, ast.Enum, ast.Union, ast.Interface: - inline = false - } - - if inline { - return builder.writeTypedef(def, selectionSet) - } - // Writes a typedef elsewhere (if not already defined) name, err := builder.addTypeForDefinition("", def, selectionSet) if err != nil { @@ -231,7 +228,35 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, selectionSet [ } } builder.WriteString("}") + + // If any field is abstract, we need an UnmarshalJSON method to handle + // it. + return builder.maybeWriteUnmarshal(selectionSet) + + case ast.Interface, ast.Union: + // First, write the interface type. + builder.WriteString("interface {\n") + implementsMethodName := fmt.Sprintf("implementsGraphQLInterface%v", builder.typeName) + // TODO: Also write GetX() accessor methods for fields of the interface + builder.WriteString(implementsMethodName) + builder.WriteString("()\n") + builder.WriteString("}") + + // Then, write the implementations. + // TODO(benkraft): Put a doc-comment somewhere with the list. + for _, impldef := range builder.schema.GetPossibleTypes(typedef) { + name, err := builder.addTypeForDefinition("", impldef, selectionSet) + if err != nil { + return err + } + + // HACK HACK HACK + builder.typeMap[name] += fmt.Sprintf( + "\nfunc (v %v) %v() {}", name, implementsMethodName) + } + return nil + case ast.Enum: // All GraphQL enums have underlying type string (in the Go sense). builder.WriteString("string\n") @@ -244,8 +269,8 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, selectionSet [ } builder.WriteString(")\n") return nil - case ast.Scalar, ast.Union, ast.Interface: - // TODO(benkraft): Handle custom scalars, unions, and interfaces. + case ast.Scalar: + // TODO(benkraft): Handle custom scalars. return fmt.Errorf("not implemented: %v", typedef.Kind) default: return fmt.Errorf("unexpected kind: %v", typedef.Kind) diff --git a/generate/types_test.go b/generate/types_test.go index bcff5d9..8dde7ad 100644 --- a/generate/types_test.go +++ b/generate/types_test.go @@ -55,8 +55,6 @@ func TestTypeForOperation(t *testing.T) { t.Fatal(err) } - schemaText := readFile(t, "schema.graphql", false) - for _, file := range files { graphqlFilename := file.Name() if graphqlFilename == "schema.graphql" || !strings.HasSuffix(graphqlFilename, ".graphql") { @@ -65,45 +63,25 @@ func TestTypeForOperation(t *testing.T) { goFilename := graphqlFilename + ".go" t.Run(graphqlFilename, func(t *testing.T) { - expectedGoType, err := gofmt(readFile(t, goFilename, update)) + expectedGoCode, err := gofmt(readFile(t, goFilename, update)) if err != nil { t.Fatal(err) } - schema, graphqlError := gqlparser.LoadSchema( - &ast.Source{Name: "test schema", Input: schemaText}) - if graphqlError != nil { - t.Fatal(graphqlError) - } - - queryDoc, graphqlListError := gqlparser.LoadQuery( - schema, readFile(t, graphqlFilename, false)) - if graphqlListError != nil { - t.Fatal(graphqlListError) - } - - if len(queryDoc.Operations) != 1 { - t.Fatalf("got %v operations, want 1", len(queryDoc.Operations)) - } - - g := newGenerator(&Config{Package: "test_package"}, schema) - err = g.addOperation(queryDoc.Operations[0]) + goCode, err := Generate(&Config{ + Schema: filepath.Join("testdata", "schema.graphql"), + Queries: filepath.Join("testdata", graphqlFilename), + Package: "test", + }) if err != nil { - t.Error(err) + t.Fatal(err) } - // gofmt before comparing. - goType, err := gofmt(g.Types()) - if err != nil { - t.Error(err) - } - - if goType != expectedGoType { - t.Errorf("got:\n%v\nwant:\n%v\n", goType, expectedGoType) + if string(goCode) != expectedGoCode { + t.Errorf("got:\n%v\nwant:\n%v\n", string(goCode), expectedGoCode) if update { t.Log("Updating testdata dir to match") - err = ioutil.WriteFile( - filepath.Join(dataDir, goFilename), []byte(goType), 0644) + err = ioutil.WriteFile(filepath.Join(dataDir, goFilename), goCode, 0644) if err != nil { t.Errorf("Unable to update testdata dir: %v", err) } @@ -138,18 +116,18 @@ func TestTypeForInputType(t *testing.T) { }, { `DefinedType`, `UserQueryInput`, - `*userQueryInput`, + `*UserQueryInput`, []string{ - `type role string + `type Role string const ( - studentRole role = "STUDENT" - teacherRole role = "TEACHER" + StudentRole Role = "STUDENT" + TeacherRole Role = "TEACHER" )`, - `type userQueryInput struct { + `type UserQueryInput struct { Email *string ` + "`json:\"email\"`" + ` Name *string ` + "`json:\"name\"`" + ` Id *string ` + "`json:\"id\"`" + ` - Role *role ` + "`json:\"role\"`" + ` + Role *Role ` + "`json:\"role\"`" + ` }`, }, }} diff --git a/generate/unmarshal.go b/generate/unmarshal.go new file mode 100644 index 0000000..a955e97 --- /dev/null +++ b/generate/unmarshal.go @@ -0,0 +1,51 @@ +package generate + +var unmarshalTemplate = mustTemplate("unmarshal.go.tmpl") + +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 (builder *typeBuilder) maybeWriteUnmarshal(fields []selection) error { + data := templateData{Type: builder.typeName} + for _, field := range fields { + typedef := builder.schema.Types[field.Type().Name()] + if typedef.IsAbstractType() { + fieldInfo := abstractField{ + GoName: upperFirst(field.Alias()), + JSONName: field.Alias(), + } + for _, typedef := range builder.schema.GetPossibleTypes(typedef) { + fieldInfo.ConcreteTypes = append(fieldInfo.ConcreteTypes, + concreteType{ + // TODO: lies! We might have added "1" or something. + GoName: upperFirst(typedef.Name), + GraphQLName: typedef.Name, + }) + } + data.Fields = append(data.Fields, fieldInfo) + } + } + + if len(data.Fields) == 0 { + return nil + } + + builder.WriteString("\n\n") + return unmarshalTemplate.Execute(builder, data) +} diff --git a/generate/unmarshal.go.tmpl b/generate/unmarshal.go.tmpl new file mode 100644 index 0000000..99f0592 --- /dev/null +++ b/generate/unmarshal.go.tmpl @@ -0,0 +1,35 @@ +func (v *{{.Type}}) UnmarshalJSON(b []byte) error { + var firstPass struct{ + *{{.Type}} + {{range .Fields -}} + {{.GoName}} json.RawMessage `json:"{{.JSONName}}"` + {{end}} + } + firstPass.{{.Type}} = v + + err := json.Unmarshal(b, &typenames) + if err != nil { + return err + } + + {{range .Fields -}} + var tn struct { TypeName string `json:"__typename"` } + err = json.Unmarshal(firstPass.{{.GoName}}, &tn) + if err != nil { + return err + } + switch tn.TypeName { + {{with $field := .}} + {{range $field.ConcreteTypes}} + case "{{.GraphQLName}}": + v.{{$field.GoName}} = {{.GoName}}{} + err = json.Unmarshal( + firstPass.{{$field.GoName}}, &v.{{$field.GoName}}) + {{end}} + {{end}} + } + if err != nil { + return err + } + {{end}} +} diff --git a/generate/util.go b/generate/util.go index 6368b85..ed74581 100644 --- a/generate/util.go +++ b/generate/util.go @@ -29,14 +29,15 @@ func upperFirst(s string) string { } func goConstName(s string) string { + if strings.TrimLeft(s, "_") == "" { + return s + } var prev rune return strings.Map(func(r rune) rune { var ret rune - if prev == 0 && r == '_' { - return '_' // still treat next char as first - } else if r == '_' { + if r == '_' { ret = -1 - } else if prev == '_' { + } else if prev == '_' || prev == 0 { ret = unicode.ToUpper(r) } else { ret = unicode.ToLower(r) diff --git a/generate/util_test.go b/generate/util_test.go index 886638b..886b729 100644 --- a/generate/util_test.go +++ b/generate/util_test.go @@ -55,10 +55,10 @@ func TestUpperFirst(t *testing.T) { func TestGoConstName(t *testing.T) { tests := []test{ {"Empty", "", ""}, - {"AllCaps", "ASDF", "asdf"}, - {"AllCapsWithUnderscore", "ASDF_GH", "asdfGh"}, + {"AllCaps", "ASDF", "Asdf"}, + {"AllCapsWithUnderscore", "ASDF_GH", "AsdfGh"}, {"JustUnderscore", "_", "_"}, - {"LeadingUnderscore", "_ASDF_GH", "_asdfGh"}, + {"LeadingUnderscore", "_ASDF_GH", "AsdfGh"}, } testStringFunc(t, goConstName, tests)