From a52e55632f219186c4327cc1f38917134ef3ed19 Mon Sep 17 00:00:00 2001 From: Craig Silverstein Date: Tue, 5 Oct 2021 08:49:50 -0700 Subject: [PATCH] Allow creating aliases for builtin types, using `typename`. (#133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: This lets you write code like: ``` query x { # @genqlient(typename: "MyString") someStringField } ``` and genqlient will do ``` typename MyString string type x struct { someStringField MyString } ``` This was not difficult to implement, though it required introducing a new identifier type. The main difficulty I had was weird test failures, that it turns out was due to the tests putting a bunch of fields on the same line, so that the genqlient directive on the previous line applied to all of them, accidentally. This became a problem when `typename` suddenly started being respected for builtin types! I fixed it by just spreading out the queries a bit. Fixes #130 ## Test plan: make check Author: csilvers Reviewers: dnerdy, StevenACoffman, benjaminjkraft Required Reviewers: Approved By: StevenACoffman Checks: ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Lint, ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Lint Pull Request URL: https://github.com/Khan/genqlient/pull/133 --- docs/CHANGELOG.md | 2 + docs/genqlient.yaml | 12 +-- docs/genqlient_directive.graphql | 24 ++++++ generate/convert.go | 13 ++- .../errors/ConflictingTypeNames.graphql | 9 ++- generate/testdata/queries/TypeNames.graphql | 16 +++- ...ate-TypeNames.graphql-TypeNames.graphql.go | 16 ++-- generate/types.go | 79 ++++++++++++------- 8 files changed, 123 insertions(+), 48 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a6581e48..33bdfaf8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -26,6 +26,8 @@ When releasing a new version: - genqlient now generates getter methods for all fields, even those which do not implement a genqlient-generated interface; this can be useful for callers who wish to define their own interface and have several unrelated genqlient types which have the same fields implement it. - genqlient config now accepts either a single or multiple schema files for the `schema` field. +- The `typename` option can now be used on basic types (string, int, etc) as well as structs; this can be useful to have genqlient define new types like `type Language string` and use that type for specified fields. + ### Bug fixes: - In certain very rare cases involving duplicate fields in fragment spreads, genqlient would generate code that failed to compile due to duplicate methods not getting promoted; genqlient now generates correct types. (See #126 for a more complete description.) diff --git a/docs/genqlient.yaml b/docs/genqlient.yaml index 1173f9d1..3cd37991 100644 --- a/docs/genqlient.yaml +++ b/docs/genqlient.yaml @@ -76,11 +76,13 @@ client_getter: "github.com/you/yourpkg.GetClient" # A map from GraphQL type name to Go fully-qualified type name to override # the Go type genqlient will use for this GraphQL type. # -# This is primarily used for custom scalars, or to map builtin scalars to -# a nonstandard type. By default, builtin scalars are mapped to the -# obvious Go types (String and ID to string, Int to int, Float to float64, -# and Boolean to bool), but this setting will extend or override those -# mappings. +# This is primarily used for custom scalars, or to map builtin scalars +# to a nonstandard type that is defined elsewhere. By default, +# builtin scalars are mapped to the obvious Go types (String and ID to +# string, Int to int, Float to float64, and Boolean to bool), but this +# setting will extend or override those mappings. (See also +# @genqlient(typename: ...), which can be used to map builtin scalars +# to a nonstandard type that genqlient defines for you.) # # genqlient does not validate these types in any way; they must define # whatever logic is needed (MarshalJSON/UnmarshalJSON or JSON tags) to diff --git a/docs/genqlient_directive.graphql b/docs/genqlient_directive.graphql index fa23f0b3..57e8b2fc 100644 --- a/docs/genqlient_directive.graphql +++ b/docs/genqlient_directive.graphql @@ -164,6 +164,10 @@ directive genqlient( # you can do that, as long as its UnmarshalJSON method can accept a list # of datetimes.) # + # Note that the type you bind to must be defined elsewhere in your code. + # If you want genqlient to create the type definition, use "typename" + # instead. + # # See bindings in genqlient.yaml for more details; this is effectively to a # local version of that global setting and should be used with similar care. # If set to "-", overrides any such global setting and uses a @@ -189,6 +193,26 @@ directive genqlient( # } # instead of its usual, more verbose type names. # + # You may also use "typename" on basic types, and Go will create a + # type definition for that basic type. For instance: + # query MyQuery { + # user { + # # @genqlient(typename: "NameType") + # name + # } + # } + # will cause gnqlient to generate: + # type Resp struct { + # User User + # } + # type NameType string + # type User struct { + # Name NameType + # } + # (Compare this to @genqlient(bind: "path/to/pkg.NameType"), which does + # something similar but depends on "NameType" being defined in some + # other package, rather than having genqlient define it for you.) + # # With great power comes great responsibility: when using typename you'll # need to avoid comments; genqlient will complain if you use the same # type-name in multiple places unless they request the exact same fields, or diff --git a/generate/convert.go b/generate/convert.go index 28046e1f..3841e281 100644 --- a/generate/convert.go +++ b/generate/convert.go @@ -289,7 +289,7 @@ func (g *generator) convertDefinition( }, err } goBuiltinName, ok := builtinTypes[def.Name] - if ok { + if ok && options.TypeName == "" { return &goOpaqueType{GoRef: goBuiltinName, GraphQLName: def.Name}, nil } @@ -479,6 +479,17 @@ func (g *generator) convertDefinition( return g.addType(goType, goType.GoName, pos) case ast.Scalar: + if builtinTypes[def.Name] != "" { + // In this case, the user asked for a custom Go type-name + // for a built-in type, e.g. `type MyString string`. + goType := &goTypenameForBuiltinType{ + GoTypeName: name, + GoBuiltinName: builtinTypes[def.Name], + GraphQLName: def.Name, + } + return g.addType(goType, goType.GoTypeName, pos) + } + // (If you had an entry in bindings, we would have returned it above.) return nil, errorf( pos, `unknown scalar %v: please add it to "bindings" in genqlient.yaml`, def.Name) diff --git a/generate/testdata/errors/ConflictingTypeNames.graphql b/generate/testdata/errors/ConflictingTypeNames.graphql index 09f0ab99..13bb5bbe 100644 --- a/generate/testdata/errors/ConflictingTypeNames.graphql +++ b/generate/testdata/errors/ConflictingTypeNames.graphql @@ -1,6 +1,11 @@ query ConflictingTypeNames { # @genqlient(typename: "T") - f { g } + f { + g + } # @genqlient(typename: "T") - otherF: f { g h } + otherF: f { + g + h + } } diff --git a/generate/testdata/queries/TypeNames.graphql b/generate/testdata/queries/TypeNames.graphql index 7c7ce550..e8f75f72 100644 --- a/generate/testdata/queries/TypeNames.graphql +++ b/generate/testdata/queries/TypeNames.graphql @@ -1,10 +1,20 @@ # @genqlient(typename: "Resp") query TypeNames { # @genqlient(typename: "User") - user { id name } + user { + id + name + } # @genqlient(typename: "Item") - randomItem { id name } + randomItem { + id + # @genqlient(typename: "NameType") + name + } # (ok to reuse the name as long as they match) # @genqlient(typename: "User") - users { id name } + users { + id + name + } } diff --git a/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go index baa4bb9a..c30cf92d 100644 --- a/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go @@ -29,7 +29,7 @@ type Item interface { // ID is the identifier of the content. GetId() testutil.ID // GetName returns the interface-field "name" from its implementation. - GetName() string + GetName() NameType } func (v *ItemArticle) implementsGraphQLInterfaceItem() {} @@ -109,7 +109,7 @@ type ItemArticle struct { Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` - Name string `json:"name"` + Name NameType `json:"name"` } // GetTypename returns ItemArticle.Typename, and is useful for accessing the field via an interface. @@ -119,14 +119,14 @@ func (v *ItemArticle) GetTypename() string { return v.Typename } func (v *ItemArticle) GetId() testutil.ID { return v.Id } // GetName returns ItemArticle.Name, and is useful for accessing the field via an interface. -func (v *ItemArticle) GetName() string { return v.Name } +func (v *ItemArticle) GetName() NameType { return v.Name } // ItemTopic includes the requested fields of the GraphQL type Topic. type ItemTopic struct { Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` - Name string `json:"name"` + Name NameType `json:"name"` } // GetTypename returns ItemTopic.Typename, and is useful for accessing the field via an interface. @@ -136,14 +136,14 @@ func (v *ItemTopic) GetTypename() string { return v.Typename } func (v *ItemTopic) GetId() testutil.ID { return v.Id } // GetName returns ItemTopic.Name, and is useful for accessing the field via an interface. -func (v *ItemTopic) GetName() string { return v.Name } +func (v *ItemTopic) GetName() NameType { return v.Name } // ItemVideo includes the requested fields of the GraphQL type Video. type ItemVideo struct { Typename string `json:"__typename"` // ID is the identifier of the content. Id testutil.ID `json:"id"` - Name string `json:"name"` + Name NameType `json:"name"` } // GetTypename returns ItemVideo.Typename, and is useful for accessing the field via an interface. @@ -153,7 +153,9 @@ func (v *ItemVideo) GetTypename() string { return v.Typename } func (v *ItemVideo) GetId() testutil.ID { return v.Id } // GetName returns ItemVideo.Name, and is useful for accessing the field via an interface. -func (v *ItemVideo) GetName() string { return v.Name } +func (v *ItemVideo) GetName() NameType { return v.Name } + +type NameType string // Resp is returned by TypeNames on success. type Resp struct { diff --git a/generate/types.go b/generate/types.go index c1df5b36..ff1cb569 100644 --- a/generate/types.go +++ b/generate/types.go @@ -65,6 +65,14 @@ type ( GraphQLName string Marshaler, Unmarshaler string } + // goTypenameForBuiltinType represents a builtin type that was + // given a different name due to a `typename` directive. We + // create a type like `type MyString string` for it. + goTypenameForBuiltinType struct { + GoTypeName string + GoBuiltinName string + GraphQLName string + } // goSliceType represents the Go type []Elem, used to represent GraphQL // list types. goSliceType struct{ Elem goType } @@ -75,21 +83,29 @@ type ( ) // Opaque types are defined by the user; pointers and slices need no definition -func (typ *goOpaqueType) WriteDefinition(io.Writer, *generator) error { return nil } +func (typ *goOpaqueType) WriteDefinition(io.Writer, *generator) error { return nil } + +func (typ *goTypenameForBuiltinType) WriteDefinition(w io.Writer, g *generator) error { + fmt.Fprintf(w, "type %s %s", typ.GoTypeName, typ.GoBuiltinName) + return nil +} func (typ *goSliceType) WriteDefinition(io.Writer, *generator) error { return nil } func (typ *goPointerType) WriteDefinition(io.Writer, *generator) error { return nil } -func (typ *goOpaqueType) Reference() string { return typ.GoRef } -func (typ *goSliceType) Reference() string { return "[]" + typ.Elem.Reference() } -func (typ *goPointerType) Reference() string { return "*" + typ.Elem.Reference() } +func (typ *goOpaqueType) Reference() string { return typ.GoRef } +func (typ *goTypenameForBuiltinType) Reference() string { return typ.GoTypeName } +func (typ *goSliceType) Reference() string { return "[]" + typ.Elem.Reference() } +func (typ *goPointerType) Reference() string { return "*" + typ.Elem.Reference() } -func (typ *goOpaqueType) SelectionSet() ast.SelectionSet { return nil } -func (typ *goSliceType) SelectionSet() ast.SelectionSet { return typ.Elem.SelectionSet() } -func (typ *goPointerType) SelectionSet() ast.SelectionSet { return typ.Elem.SelectionSet() } +func (typ *goOpaqueType) SelectionSet() ast.SelectionSet { return nil } +func (typ *goTypenameForBuiltinType) SelectionSet() ast.SelectionSet { return nil } +func (typ *goSliceType) SelectionSet() ast.SelectionSet { return typ.Elem.SelectionSet() } +func (typ *goPointerType) SelectionSet() ast.SelectionSet { return typ.Elem.SelectionSet() } -func (typ *goOpaqueType) GraphQLTypeName() string { return typ.GraphQLName } -func (typ *goSliceType) GraphQLTypeName() string { return typ.Elem.GraphQLTypeName() } -func (typ *goPointerType) GraphQLTypeName() string { return typ.Elem.GraphQLTypeName() } +func (typ *goOpaqueType) GraphQLTypeName() string { return typ.GraphQLName } +func (typ *goTypenameForBuiltinType) GraphQLTypeName() string { return typ.GraphQLName } +func (typ *goSliceType) GraphQLTypeName() string { return typ.Elem.GraphQLTypeName() } +func (typ *goPointerType) GraphQLTypeName() string { return typ.Elem.GraphQLTypeName() } // goEnumType represents a Go named-string type used to represent a GraphQL // enum. In this case, we generate both the type (`type T string`) and also a @@ -502,26 +518,29 @@ func (typ *goInterfaceType) Reference() string { return typ.GoName func (typ *goInterfaceType) SelectionSet() ast.SelectionSet { return typ.Selection } func (typ *goInterfaceType) GraphQLTypeName() string { return typ.GraphQLName } -func (typ *goOpaqueType) Unwrap() goType { return typ } -func (typ *goSliceType) Unwrap() goType { return typ.Elem.Unwrap() } -func (typ *goPointerType) Unwrap() goType { return typ.Elem.Unwrap() } -func (typ *goEnumType) Unwrap() goType { return typ } -func (typ *goStructType) Unwrap() goType { return typ } -func (typ *goInterfaceType) Unwrap() goType { return typ } - -func (typ *goOpaqueType) SliceDepth() int { return 0 } -func (typ *goSliceType) SliceDepth() int { return typ.Elem.SliceDepth() + 1 } -func (typ *goPointerType) SliceDepth() int { return 0 } -func (typ *goEnumType) SliceDepth() int { return 0 } -func (typ *goStructType) SliceDepth() int { return 0 } -func (typ *goInterfaceType) SliceDepth() int { return 0 } - -func (typ *goOpaqueType) IsPointer() bool { return false } -func (typ *goSliceType) IsPointer() bool { return typ.Elem.IsPointer() } -func (typ *goPointerType) IsPointer() bool { return true } -func (typ *goEnumType) IsPointer() bool { return false } -func (typ *goStructType) IsPointer() bool { return false } -func (typ *goInterfaceType) IsPointer() bool { return false } +func (typ *goOpaqueType) Unwrap() goType { return typ } +func (typ *goTypenameForBuiltinType) Unwrap() goType { return typ } +func (typ *goSliceType) Unwrap() goType { return typ.Elem.Unwrap() } +func (typ *goPointerType) Unwrap() goType { return typ.Elem.Unwrap() } +func (typ *goEnumType) Unwrap() goType { return typ } +func (typ *goStructType) Unwrap() goType { return typ } +func (typ *goInterfaceType) Unwrap() goType { return typ } + +func (typ *goOpaqueType) SliceDepth() int { return 0 } +func (typ *goTypenameForBuiltinType) SliceDepth() int { return 0 } +func (typ *goSliceType) SliceDepth() int { return typ.Elem.SliceDepth() + 1 } +func (typ *goPointerType) SliceDepth() int { return 0 } +func (typ *goEnumType) SliceDepth() int { return 0 } +func (typ *goStructType) SliceDepth() int { return 0 } +func (typ *goInterfaceType) SliceDepth() int { return 0 } + +func (typ *goOpaqueType) IsPointer() bool { return false } +func (typ *goTypenameForBuiltinType) IsPointer() bool { return false } +func (typ *goSliceType) IsPointer() bool { return typ.Elem.IsPointer() } +func (typ *goPointerType) IsPointer() bool { return true } +func (typ *goEnumType) IsPointer() bool { return false } +func (typ *goStructType) IsPointer() bool { return false } +func (typ *goInterfaceType) IsPointer() bool { return false } func writeDescription(w io.Writer, desc string) { if desc != "" {