From fed38e4f55d3f3d5166de17e17eaac8266079481 Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Fri, 19 Mar 2021 18:32:33 -0700 Subject: [PATCH] redo type naming --- README.md | 7 +- example/generated.go | 22 ++--- generate/generate_test.go | 10 +- generate/testdata/InputObject.graphql.go | 28 +++--- .../testdata/InterfaceNoFragments.graphql.go | 64 +++++++----- generate/testdata/ListInput.graphql.go | 4 +- generate/testdata/QueryWithAlias.graphql.go | 4 +- .../testdata/QueryWithDoubleAlias.graphql.go | 4 +- generate/testdata/QueryWithEnums.graphql.go | 16 +-- generate/testdata/QueryWithSlices.graphql.go | 4 +- generate/testdata/QueryWithStructs.graphql.go | 18 ++-- generate/testdata/SimpleInput.graphql.go | 4 +- generate/testdata/SimpleQuery.graphql.go | 4 +- generate/testdata/TypeName.graphql.go | 4 +- generate/testdata/UnionNoFragments.graphql.go | 33 ++++--- generate/testdata/UsesEnumTwice.graphql.go | 40 ++++---- generate/types.go | 98 ++++++++++++------- generate/unmarshal.go | 5 +- generate/unmarshal.go.tmpl | 4 +- generate/util.go | 1 + 20 files changed, 210 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index 6a41569..6128596 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,9 @@ For a complete working example, see `example/`. `go test ./...` tests code generation. (This is run by GitHub Actions.) -`make example` tests that everything wires up to a real API correctly. +Most of the tests are snapshot-based; they use the schema, queries, and snapshots in `generate/testdata`. The schema is in `schema.graphql`; the queries are in `TestName.graphql`. The test by default asserts that the output of the generator matches the snapshot `TestName.graphql.go`. To update the snapshots, run with `UPDATE_SNAPSHOTS=1`; it will fail the tests and print the diffs, but regenerate the snapshots. Make sure to check that the output is sensible! + +`make example` rebuilds the example, and tests that everything wires up to a real API correctly. TODO(benkraft): Figure out how to get GitHub Actions to run the example -- it needs a token. @@ -67,7 +69,6 @@ See [DESIGN.md](DESIGN.md) for documentation of major design decisions in this l (+) denotes things we further need before recommending anyone else use this in prod Generated code: -- (*) update type naming for new scheme - (*) remove pointers for optionality (or put behind flag) - redo support for interfaces, unions, fragments (see DESIGN) - (optional) collapsing -- should be able to have `mutation { myMutation { error { code } } }` just return `(code string, err error)` @@ -77,6 +78,7 @@ Config options: - (+) fix up context/client wiring (see DESIGN) - get schema via HTTP (perhaps even via GraphQL introspection) - send hash rather than full query +- whether names should be exported Other: - (*) error-checking/validation/etc. everywhere @@ -84,3 +86,4 @@ Other: - (+) more tests - (+) documentation - custom scalar types +- allow mapping a custom type to a particular val (if you want to use a named type for some string, say) diff --git a/example/generated.go b/example/generated.go index b0e0cea..6c4ffa4 100644 --- a/example/generated.go +++ b/example/generated.go @@ -8,24 +8,24 @@ import ( "github.com/Khan/genql/graphql" ) -type User struct { - MyName *string +type GetUserResponse struct { + User *GetUserUser `json:"user"` } -type User1 struct { +type GetUserUser struct { TheirName *string `json:"theirName"` } -type getUserResponse struct { - User *User1 `json:"user"` +type GetViewerResponse struct { + Viewer GetViewerViewerUser `json:"viewer"` } -type getViewerResponse struct { - Viewer User `json:"viewer"` +type GetViewerViewerUser struct { + MyName *string } -func getViewer(ctx context.Context, client *graphql.Client) (*getViewerResponse, error) { - var retval getViewerResponse +func getViewer(ctx context.Context, client *graphql.Client) (*GetViewerResponse, error) { + var retval GetViewerResponse err := client.MakeRequest(ctx, ` query getViewer { viewer { @@ -37,12 +37,12 @@ query getViewer { } // getUser gets the given user's name from their username. -func getUser(ctx context.Context, client *graphql.Client, login string) (*getUserResponse, error) { +func getUser(ctx context.Context, client *graphql.Client, login string) (*GetUserResponse, error) { variables := map[string]interface{}{ "Login": login, } - var retval getUserResponse + var retval GetUserResponse err := client.MakeRequest(ctx, ` query getUser ($Login: String!) { user(login: $Login) { diff --git a/generate/generate_test.go b/generate/generate_test.go index 6d28746..685dffd 100644 --- a/generate/generate_test.go +++ b/generate/generate_test.go @@ -35,12 +35,6 @@ func gofmt(src string) (string, error) { } func TestGenerate(t *testing.T) { - // This test uses the schema, queries, and expected-output in ./testdata. - // The schema is in schema.graphql. The queries are in TestName.graphql; - // the test asserts that the output of the generator for that query is - // matches TestName.graphql.go. To update the expected output, run the - // tests with UPDATE_SNAPSHOTS=1 (they will still fail, but also do the - // update, so you can see which updates were made). update := (os.Getenv("UPDATE_SNAPSHOTS") == "1") files, err := ioutil.ReadDir(dataDir) @@ -74,12 +68,14 @@ func TestGenerate(t *testing.T) { 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), goCode, 0644) + err = ioutil.WriteFile(filepath.Join(dataDir, goFilename), goCode, 0o644) if err != nil { t.Errorf("Unable to update testdata dir: %v", err) } } } + + // TODO(benkraft): Also check that the code at least builds! }) } } diff --git a/generate/testdata/InputObject.graphql.go b/generate/testdata/InputObject.graphql.go index be88014..bb24d83 100644 --- a/generate/testdata/InputObject.graphql.go +++ b/generate/testdata/InputObject.graphql.go @@ -9,28 +9,28 @@ import ( ) type InputObjectQueryResponse struct { - User *User `json:"user"` + User *InputObjectQueryUser `json:"user"` } -type Role string - -const ( - StudentRole Role = "STUDENT" - TeacherRole Role = "TEACHER" -) - -type User struct { +type InputObjectQueryUser struct { Id string `json:"id"` } type UserQueryInput struct { - Email *string `json:"email"` - Name *string `json:"name"` - Id *string `json:"id"` - Role *Role `json:"role"` - Names []*string `json:"names"` + Email *string `json:"email"` + Name *string `json:"name"` + Id *string `json:"id"` + Role *UserQueryInputRole `json:"role"` + Names []*string `json:"names"` } +type UserQueryInputRole string + +const ( + UserQueryInputRoleStudent UserQueryInputRole = "STUDENT" + UserQueryInputRoleTeacher UserQueryInputRole = "TEACHER" +) + func InputObjectQuery(client *graphql.Client, query *UserQueryInput) (*InputObjectQueryResponse, error) { variables := map[string]interface{}{ "query": query, diff --git a/generate/testdata/InterfaceNoFragments.graphql.go b/generate/testdata/InterfaceNoFragments.graphql.go index e603cfa..5315af4 100644 --- a/generate/testdata/InterfaceNoFragments.graphql.go +++ b/generate/testdata/InterfaceNoFragments.graphql.go @@ -8,35 +8,24 @@ import ( "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"` + Root InterfaceNoFragmentsQueryRootTopic `json:"root"` } -type Topic struct { - Id string `json:"id"` - Name string `json:"name"` - Children []Content `json:"-"` +type InterfaceNoFragmentsQueryRootTopic struct { + Id string `json:"id"` + Name string `json:"name"` + Children []InterfaceNoFragmentsQueryRootTopicChildrenContent `json:"-"` } -func (v *Topic) UnmarshalJSON(b []byte) error { +func (v *InterfaceNoFragmentsQueryRootTopic) UnmarshalJSON(b []byte) error { var firstPass struct { - *Topic + *InterfaceNoFragmentsQueryRootTopic Children json.RawMessage `json:"children"` } - firstPass.Topic = v + firstPass.InterfaceNoFragmentsQueryRootTopic = v - err := json.Unmarshal(b, &typenames) + err := json.Unmarshal(b, &firstPass) if err != nil { return err } @@ -51,17 +40,20 @@ func (v *Topic) UnmarshalJSON(b []byte) error { switch tn.TypeName { case "Article": - v.Children = Article{} + + v.Children = InterfaceNoFragmentsQueryRootTopicChildrenArticle{} err = json.Unmarshal( firstPass.Children, &v.Children) case "Video": - v.Children = Video{} + + v.Children = InterfaceNoFragmentsQueryRootTopicChildrenVideo{} err = json.Unmarshal( firstPass.Children, &v.Children) case "Topic": - v.Children = Topic{} + + v.Children = InterfaceNoFragmentsQueryRootTopicChildrenTopic{} err = json.Unmarshal( firstPass.Children, &v.Children) @@ -70,14 +62,36 @@ func (v *Topic) UnmarshalJSON(b []byte) error { return err } + return nil } -type Video struct { +type InterfaceNoFragmentsQueryRootTopicChildrenArticle struct { Id string `json:"id"` Name string `json:"name"` } -func (v Video) implementsGraphQLInterfaceContent() {} +func (v InterfaceNoFragmentsQueryRootTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +} + +type InterfaceNoFragmentsQueryRootTopicChildrenContent interface { + implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() +} + +type InterfaceNoFragmentsQueryRootTopicChildrenTopic struct { + Id string `json:"id"` + Name string `json:"name"` +} + +func (v InterfaceNoFragmentsQueryRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +} + +type InterfaceNoFragmentsQueryRootTopicChildrenVideo struct { + Id string `json:"id"` + Name string `json:"name"` +} + +func (v InterfaceNoFragmentsQueryRootTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +} func InterfaceNoFragmentsQuery(client *graphql.Client) (*InterfaceNoFragmentsQueryResponse, error) { var retval InterfaceNoFragmentsQueryResponse diff --git a/generate/testdata/ListInput.graphql.go b/generate/testdata/ListInput.graphql.go index c13c2be..408142a 100644 --- a/generate/testdata/ListInput.graphql.go +++ b/generate/testdata/ListInput.graphql.go @@ -9,10 +9,10 @@ import ( ) type ListInputQueryResponse struct { - User *User `json:"user"` + User *ListInputQueryUser `json:"user"` } -type User struct { +type ListInputQueryUser struct { Id string `json:"id"` } diff --git a/generate/testdata/QueryWithAlias.graphql.go b/generate/testdata/QueryWithAlias.graphql.go index 30bbea8..9c1db5a 100644 --- a/generate/testdata/QueryWithAlias.graphql.go +++ b/generate/testdata/QueryWithAlias.graphql.go @@ -9,10 +9,10 @@ import ( ) type QueryWithAliasResponse struct { - User *User + User *QueryWithAliasUser } -type User struct { +type QueryWithAliasUser struct { ID string } diff --git a/generate/testdata/QueryWithDoubleAlias.graphql.go b/generate/testdata/QueryWithDoubleAlias.graphql.go index 4805917..d1d84d9 100644 --- a/generate/testdata/QueryWithDoubleAlias.graphql.go +++ b/generate/testdata/QueryWithDoubleAlias.graphql.go @@ -9,10 +9,10 @@ import ( ) type QueryWithDoubleAliasResponse struct { - User *User `json:"user"` + User *QueryWithDoubleAliasUser `json:"user"` } -type User struct { +type QueryWithDoubleAliasUser struct { ID string AlsoID string } diff --git a/generate/testdata/QueryWithEnums.graphql.go b/generate/testdata/QueryWithEnums.graphql.go index 2e98251..751170a 100644 --- a/generate/testdata/QueryWithEnums.graphql.go +++ b/generate/testdata/QueryWithEnums.graphql.go @@ -9,20 +9,20 @@ import ( ) type QueryWithEnumsResponse struct { - User *User `json:"user"` + User *QueryWithEnumsUser `json:"user"` } -type Role string +type QueryWithEnumsUser struct { + Roles []QueryWithEnumsUserRolesRole `json:"roles"` +} + +type QueryWithEnumsUserRolesRole string const ( - StudentRole Role = "STUDENT" - TeacherRole Role = "TEACHER" + QueryWithEnumsUserRolesRoleStudent QueryWithEnumsUserRolesRole = "STUDENT" + QueryWithEnumsUserRolesRoleTeacher QueryWithEnumsUserRolesRole = "TEACHER" ) -type User struct { - Roles []Role `json:"roles"` -} - func QueryWithEnums(client *graphql.Client) (*QueryWithEnumsResponse, error) { var retval QueryWithEnumsResponse err := client.MakeRequest(context.Background(), ` diff --git a/generate/testdata/QueryWithSlices.graphql.go b/generate/testdata/QueryWithSlices.graphql.go index 8392836..60a4043 100644 --- a/generate/testdata/QueryWithSlices.graphql.go +++ b/generate/testdata/QueryWithSlices.graphql.go @@ -9,10 +9,10 @@ import ( ) type QueryWithSlicesResponse struct { - User *User `json:"user"` + User *QueryWithSlicesUser `json:"user"` } -type User struct { +type QueryWithSlicesUser struct { Emails []string `json:"emails"` EmailsOrNull []string `json:"emailsOrNull"` EmailsWithNulls []*string `json:"emailsWithNulls"` diff --git a/generate/testdata/QueryWithStructs.graphql.go b/generate/testdata/QueryWithStructs.graphql.go index 4416899..3ed4278 100644 --- a/generate/testdata/QueryWithStructs.graphql.go +++ b/generate/testdata/QueryWithStructs.graphql.go @@ -8,19 +8,19 @@ import ( "github.com/Khan/genql/graphql" ) -type AuthMethod struct { +type QueryWithStructsResponse struct { + User *QueryWithStructsUser `json:"user"` +} + +type QueryWithStructsUser struct { + AuthMethods []QueryWithStructsUserAuthMethodsAuthMethod `json:"authMethods"` +} + +type QueryWithStructsUserAuthMethodsAuthMethod 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(), ` diff --git a/generate/testdata/SimpleInput.graphql.go b/generate/testdata/SimpleInput.graphql.go index 59345b5..d4c916f 100644 --- a/generate/testdata/SimpleInput.graphql.go +++ b/generate/testdata/SimpleInput.graphql.go @@ -9,10 +9,10 @@ import ( ) type SimpleInputQueryResponse struct { - User *User `json:"user"` + User *SimpleInputQueryUser `json:"user"` } -type User struct { +type SimpleInputQueryUser struct { Id string `json:"id"` } diff --git a/generate/testdata/SimpleQuery.graphql.go b/generate/testdata/SimpleQuery.graphql.go index b317e30..575a3ae 100644 --- a/generate/testdata/SimpleQuery.graphql.go +++ b/generate/testdata/SimpleQuery.graphql.go @@ -9,10 +9,10 @@ import ( ) type SimpleQueryResponse struct { - User *User `json:"user"` + User *SimpleQueryUser `json:"user"` } -type User struct { +type SimpleQueryUser struct { Id string `json:"id"` } diff --git a/generate/testdata/TypeName.graphql.go b/generate/testdata/TypeName.graphql.go index cb46f30..1f4cd42 100644 --- a/generate/testdata/TypeName.graphql.go +++ b/generate/testdata/TypeName.graphql.go @@ -9,10 +9,10 @@ import ( ) type TypeNameQueryResponse struct { - User *User `json:"user"` + User *TypeNameQueryUser `json:"user"` } -type User struct { +type TypeNameQueryUser struct { Typename *string `json:"__typename"` Id string `json:"id"` } diff --git a/generate/testdata/UnionNoFragments.graphql.go b/generate/testdata/UnionNoFragments.graphql.go index ed78c33..bd731ba 100644 --- a/generate/testdata/UnionNoFragments.graphql.go +++ b/generate/testdata/UnionNoFragments.graphql.go @@ -8,18 +8,26 @@ import ( "github.com/Khan/genql/graphql" ) -type Article struct { +type UnionNoFragmentsQueryRandomLeafArticle struct { Typename *string `json:"__typename"` } -func (v Article) implementsGraphQLInterfaceLeafContent() {} +func (v UnionNoFragmentsQueryRandomLeafArticle) implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() { +} -type LeafContent interface { - implementsGraphQLInterfaceLeafContent() +type UnionNoFragmentsQueryRandomLeafLeafContent interface { + implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() +} + +type UnionNoFragmentsQueryRandomLeafVideo struct { + Typename *string `json:"__typename"` +} + +func (v UnionNoFragmentsQueryRandomLeafVideo) implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() { } type UnionNoFragmentsQueryResponse struct { - RandomLeaf LeafContent `json:"-"` + RandomLeaf UnionNoFragmentsQueryRandomLeafLeafContent `json:"-"` } func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { @@ -29,7 +37,7 @@ func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { } firstPass.UnionNoFragmentsQueryResponse = v - err := json.Unmarshal(b, &typenames) + err := json.Unmarshal(b, &firstPass) if err != nil { return err } @@ -44,12 +52,14 @@ func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { switch tn.TypeName { case "Article": - v.RandomLeaf = Article{} + + v.RandomLeaf = UnionNoFragmentsQueryRandomLeafArticle{} err = json.Unmarshal( firstPass.RandomLeaf, &v.RandomLeaf) case "Video": - v.RandomLeaf = Video{} + + v.RandomLeaf = UnionNoFragmentsQueryRandomLeafVideo{} err = json.Unmarshal( firstPass.RandomLeaf, &v.RandomLeaf) @@ -58,14 +68,9 @@ func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { return err } + return nil } -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(), ` diff --git a/generate/testdata/UsesEnumTwice.graphql.go b/generate/testdata/UsesEnumTwice.graphql.go index 3652cd8..396ca13 100644 --- a/generate/testdata/UsesEnumTwice.graphql.go +++ b/generate/testdata/UsesEnumTwice.graphql.go @@ -8,31 +8,31 @@ import ( "github.com/Khan/genql/graphql" ) -type Role string - -const ( - StudentRole Role = "STUDENT" - TeacherRole Role = "TEACHER" -) - -type Role1 string - -const ( - StudentRole1 Role1 = "STUDENT" - TeacherRole1 Role1 = "TEACHER" -) - -type User struct { - Roles []Role `json:"roles"` +type UsesEnumTwiceQueryMeUser struct { + Roles []UsesEnumTwiceQueryMeUserRolesRole `json:"roles"` } -type User1 struct { - Roles []Role1 `json:"roles"` +type UsesEnumTwiceQueryMeUserRolesRole string + +const ( + UsesEnumTwiceQueryMeUserRolesRoleStudent UsesEnumTwiceQueryMeUserRolesRole = "STUDENT" + UsesEnumTwiceQueryMeUserRolesRoleTeacher UsesEnumTwiceQueryMeUserRolesRole = "TEACHER" +) + +type UsesEnumTwiceQueryOtherUser struct { + Roles []UsesEnumTwiceQueryOtherUserRolesRole `json:"roles"` } +type UsesEnumTwiceQueryOtherUserRolesRole string + +const ( + UsesEnumTwiceQueryOtherUserRolesRoleStudent UsesEnumTwiceQueryOtherUserRolesRole = "STUDENT" + UsesEnumTwiceQueryOtherUserRolesRoleTeacher UsesEnumTwiceQueryOtherUserRolesRole = "TEACHER" +) + type UsesEnumTwiceQueryResponse struct { - Me *User - OtherUser *User1 + Me *UsesEnumTwiceQueryMeUser + OtherUser *UsesEnumTwiceQueryOtherUser } func UsesEnumTwiceQuery(client *graphql.Client) (*UsesEnumTwiceQueryResponse, error) { diff --git a/generate/types.go b/generate/types.go index 0e743ae..5c830bc 100644 --- a/generate/types.go +++ b/generate/types.go @@ -2,14 +2,14 @@ package generate import ( "fmt" - "strconv" "strings" "github.com/vektah/gqlparser/ast" ) type typeBuilder struct { - typeName string + typeName string + typeNamePrefix string strings.Builder *generator } @@ -29,7 +29,8 @@ func (g *generator) baseTypeForOperation(operation ast.Operation) *ast.Definitio func (g *generator) getTypeForOperation(operation *ast.OperationDefinition) (name string, err error) { // TODO: configure ResponseName format - name = operation.Name + "Response" + namePrefix := upperFirst(operation.Name) + name = namePrefix + "Response" if def, ok := g.typeMap[name]; ok { // TODO: check for and handle conflicts a better way @@ -42,45 +43,68 @@ func (g *generator) getTypeForOperation(operation *ast.OperationDefinition) (nam } return g.addTypeForDefinition( - name, g.baseTypeForOperation(operation.Operation), fields) + namePrefix, name, g.baseTypeForOperation(operation.Operation), fields) } -func (g *generator) addTypeForDefinition(nameOverride string, typ *ast.Definition, fields []field) (name string, err error) { +var builtinTypes = map[string]string{ + "Int": "int", // TODO: technically int32 is always enough, use that? + "Float": "float64", + "String": "string", + "Boolean": "bool", + "ID": "string", // TODO: named type for IDs? +} + +func (g *generator) addTypeForDefinition(namePrefix, nameOverride string, typ *ast.Definition, fields []field) (name string, err error) { + // If this is a builtin type, just refer to it. goName, ok := builtinTypes[typ.Name] if ok { return goName, nil } if nameOverride != "" { + // if we have an explicit name, the passed-in prefix is what we + // propagate forward name = nameOverride } else { - // TODO: casing should be configurable - name = upperFirst(typ.Name) + typeGoName := upperFirst(typ.Name) + if strings.HasSuffix(namePrefix, typeGoName) { + // If the field and type names are the same, we can avoid the + // duplication. (We include the field name in case there are + // multiple fields with the same type, and the type name because + // that's the actual name (the rest are really qualifiers); but if + // they are the same then including it once suffices for both + // purposes.) + name = namePrefix + } else { + name = namePrefix + typeGoName + } + + if typ.Kind != ast.Interface && typ.Kind != ast.Union { + // for interface/union types, we do not add the type name to the + // name prefix; we want to have QueryFieldType rather than + // QueryFieldInterfaceType. Otherwise, the name will also be the + // prefix for the next type. + namePrefix = name + } + } - // 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} + // Otherwise, build the type, put that in the type-map, and return its + // name. + builder := &typeBuilder{typeName: name, typeNamePrefix: namePrefix, generator: g} fmt.Fprintf(builder, "type %s ", name) err = builder.writeTypedef(typ, fields) if err != nil { return "", err } - g.typeMap[name] = builder.String() return name, nil } func (g *generator) getTypeForInputType(typ *ast.Type) (string, error) { - builder := &typeBuilder{typeName: upperFirst(typ.Name()), generator: g} - err := builder.writeType(typ, selectionsForType(g, typ)) + typeName := upperFirst(typ.Name()) + builder := &typeBuilder{typeName: typeName, typeNamePrefix: typeName, generator: g} + err := builder.writeType("", typ, selectionsForType(g, typ)) return builder.String(), err } @@ -167,7 +191,17 @@ func (builder *typeBuilder) writeField(field field) error { return err } - err = builder.writeType(typ, fields) + err = builder.writeType( + // Note we don't deduplicate here -- if our prefix is GetUser and the + // field name is User, we do GetUserUser. This is important because if + // you have a field called user on a type called User we need + // `query q { user { user { id } } }` to generate two types, QUser and + // QUserUser. + // Note also this is the alias, not the field-name, because if we have + // `query q { a: f { b }, c: f { d } }` we need separate types for a + // and c, even though they are the same type in GraphQL, because they + // have different fields. + builder.typeNamePrefix+upperFirst(field.Alias()), typ, fields) if err != nil { return err } @@ -182,20 +216,10 @@ func (builder *typeBuilder) writeField(field field) error { return nil } -var builtinTypes = map[string]string{ - "Int": "int", // TODO: technically int32 is always enough, use that? - "Float": "float64", - "String": "string", - "Boolean": "bool", - "ID": "string", // TODO: named type for IDs? -} - -func (builder *typeBuilder) writeType(typ *ast.Type, fields []field) 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: +func (builder *typeBuilder) writeType(namePrefix string, typ *ast.Type, fields []field) error { + // gqlgen does slightly different things here, but its implementation may + // be useful to crib from: // https://github.com/99designs/gqlgen/blob/master/plugin/modelgen/models.go#L113 - // TODO: or maybe we should do that? if typ.Elem != nil { // Type is a list. builder.WriteString("[]") @@ -207,7 +231,7 @@ func (builder *typeBuilder) writeType(typ *ast.Type, fields []field) error { def := builder.schema.Types[typ.Name()] // Writes a typedef elsewhere (if not already defined) - name, err := builder.addTypeForDefinition("", def, fields) + name, err := builder.addTypeForDefinition(namePrefix, "", def, fields) if err != nil { return err } @@ -244,7 +268,7 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, fields []field // 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, fields) + name, err := builder.addTypeForDefinition(builder.typeNamePrefix, "", impldef, fields) if err != nil { return err } @@ -263,7 +287,7 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, fields []field for _, val := range typedef.EnumValues { // TODO: casing should be configurable fmt.Fprintf(builder, "%s %s = \"%s\"\n", - goConstName(val.Name+"_"+builder.typeName), + builder.typeNamePrefix+goConstName(val.Name), builder.typeName, val.Name) } builder.WriteString(")\n") diff --git a/generate/unmarshal.go b/generate/unmarshal.go index 32a28e4..5356426 100644 --- a/generate/unmarshal.go +++ b/generate/unmarshal.go @@ -33,8 +33,9 @@ func (builder *typeBuilder) maybeWriteUnmarshal(fields []field) error { 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), + // TODO: this is quite fragile (and maybe wrong if the + // field name + type name are the same) + GoName: builder.typeNamePrefix + fieldInfo.GoName + upperFirst(typedef.Name), GraphQLName: typedef.Name, }) } diff --git a/generate/unmarshal.go.tmpl b/generate/unmarshal.go.tmpl index 99f0592..bc7cea5 100644 --- a/generate/unmarshal.go.tmpl +++ b/generate/unmarshal.go.tmpl @@ -7,7 +7,7 @@ func (v *{{.Type}}) UnmarshalJSON(b []byte) error { } firstPass.{{.Type}} = v - err := json.Unmarshal(b, &typenames) + err := json.Unmarshal(b, &firstPass) if err != nil { return err } @@ -22,6 +22,7 @@ func (v *{{.Type}}) UnmarshalJSON(b []byte) error { {{with $field := .}} {{range $field.ConcreteTypes}} case "{{.GraphQLName}}": + {{/* TODO: handle repeated fields! */}} v.{{$field.GoName}} = {{.GoName}}{} err = json.Unmarshal( firstPass.{{$field.GoName}}, &v.{{$field.GoName}}) @@ -32,4 +33,5 @@ func (v *{{.Type}}) UnmarshalJSON(b []byte) error { return err } {{end}} + return nil } diff --git a/generate/util.go b/generate/util.go index ed74581..a08f75d 100644 --- a/generate/util.go +++ b/generate/util.go @@ -25,6 +25,7 @@ func lowerFirst(s string) string { } func upperFirst(s string) string { + // TODO: initialisms return changeFirst(strings.TrimLeft(s, "_"), unicode.ToUpper) }