diff --git a/docs/FAQ.md b/docs/FAQ.md index 4fe8260..b918132 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -66,6 +66,18 @@ if errors.As(err, &errList) { } ``` +### … use custom scalars? + +Just tell genqlient via the `bindings` option in `genqlient.yaml`: + +```yaml +bindings: + DateTime: + type: time.Time +``` + +Make sure the given type has whatever logic is needed to convert to/from JSON (e.g. `MarshalJSON`/`UnmarshalJSON` or JSON tags). See the [`genqlient.yaml` documentation](genqlient.yaml) for the full syntax. + ### … require 32-bit integers? The GraphQL spec officially defines the `Int` type to be a [signed 32-bit integer](https://spec.graphql.org/draft/#sec-Int). GraphQL clients and servers vary wildly in their enforcement of this; for example: @@ -205,6 +217,129 @@ type GetBooksFavoriteBook struct { Keep in mind that if you later want to add fragments to your selection, you won't be able to use `struct` anymore; when you remove it you may need to update your code to replace `.Title` with `.GetTitle()` and so on. +### … shared types between different parts of the query? + +Suppose you have a query which requests several different fields each of the same GraphQL type, e.g. `User` (or `[User]`): + +```graphql +query GetMonopolyPlayers { + game { + winner { id name } + banker { id name } + spectators { id name } + } +} +``` + +This will produce a Go type like: +```go +type GetMonopolyPlayersGame struct { + Winner GetMonopolyPlayersGameWinnerUser + Banker GetMonopolyPlayersGameBankerUser + Spectators []GetMonopolyPlayersGameSpectatorsUser +} + +type GetMonopolyPlayersGameWinnerUser struct { + Id string + Name string +} + +// (others similarly) +``` + +But maybe you wanted to be able to pass all those users to a shared function (defined in your code), say `FormatUser(user ???) string`. That's no good; you need to put three different types as the `???`. genqlient has two ways to deal with this. + +One option -- the GraphQL Way, perhaps -- is to use fragments. You'd write your query like: + +```graphql +fragment MonopolyUser on User { + id + name +} + +query GetMonopolyPlayers { + game { + winner { ...MonopolyUser } + banker { ...MonopolyUser } + spectators { ...MonopolyUser } + } +} +``` + +genqlient will notice this, and generate a type corresponding to the fragment; `GetMonopolyPlayersGame` will look as before, but each of the field types will have a shared embed: + +```go +type MonopolyUser struct { + Id string + Name string +} + +type GetMonopolyPlayersGameWinnerUser struct { + MonopolyUser +} + +// (others similarly) +``` + +Thus you can have `FormatUser` accept a `MonopolyUser`, and pass it `game.Winner.MonopolyUser`, `game.Spectators[i].MonopolyUser`, etc. This is convenient if you may later want to add other fields to some of the queries, because you can still do + +```graphql +fragment MonopolyUser on User { + id + name +} + +query GetMonopolyPlayers { + game { + winner { + winCount + ...MonopolyUser + } + banker { + bankerRating + ...MonopolyUser + } + spectators { ...MonopolyUser } + } +} +``` + +and you can even spread the fragment into interface types. It also avoids having to list the fields several times. + +Alternately, if you always want exactly the same fields, you can use the simpler but more restrictive genqlient option `typename`: + +```graphql +query GetMonopolyPlayers { + game { + # @genqlient(typename: "User") + winner { id name } + # @genqlient(typename: "User") + banker { id name } + # @genqlient(typename: "User") + spectators { id name } + } +} +``` + +This will tell genqlient to use the same types for each field: + +```go +type GetMonopolyPlayersGame struct { + Winner User + Banker User + Spectators []User +} + +type User struct { + Id string + Name string +} +``` + +In this case, genqlient will validate that each type given the name `User` has the exact same fields; see the [full documentation](genqlient_directive.graphql) for details. + +Note that it's also possible to use the `bindings` option (see [`genqlient.yaml` documentation](genqlient.yaml)) for a similar purpose, but this is not recommended as it typically requires more work for less gain. + ### … documentation on the output types? For any GraphQL types or fields with documentation in the GraphQL schema, genqlient automatically includes that documentation in the generated code's GoDoc. To add additional information to genqlient entrypoints, you can put comments in the GraphQL source: @@ -258,6 +393,21 @@ type User = GetFamilyNamesUser type ChildUser = GetFamilyNamesUserChildrenUser ``` +Alternately, you can use the `typename` option: if you query +```graphql +query GetFamilyNames { + # @genqlient(typename: "User") + user { + name + # @genqlient(typename: "ChildUser") + children { + name + } + } +} +``` +genqlient will instead generate types with the given names. (You'll need to avoid conflicts; see the [full documentation](genqlient_directive.graphql) for details.) + ### … my editor/IDE plugin not know about the code genqlient just generated? If your tools are backed by [gopls](https://github.com/golang/tools/blob/master/gopls/README.md) (which is most of them), they simply don't know it was updated. In most cases, keeping the generated file (typically `generated.go`) open in the background, and reloading it after each run of `genqlient`, will do the trick. diff --git a/docs/genqlient_directive.graphql b/docs/genqlient_directive.graphql index 7dca682..5a845c1 100644 --- a/docs/genqlient_directive.graphql +++ b/docs/genqlient_directive.graphql @@ -91,6 +91,38 @@ directive genqlient( # genqlient-generated type. bind: String + # If set, the type of this field will have the given name in Go. + # + # For example, given the following query: + # # @genqlient(typename: "MyResp") + # query MyQuery { + # # @genqlient(typename: "User") + # user { + # id + # } + # } + # genqlient will generate + # type Resp struct { + # User User + # } + # type User struct { + # Id string + # } + # instead of its usual, more verbose type names. + # + # 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 + # if your type-name conflicts with an autogenerated one (again, unless they + # request the exact same fields). They must even have the fields in the + # same order. Fragments are often easier to use (see the discussion of + # code-sharing in FAQ.md). + # + # Note that unlike most directives, if applied to the entire operation, + # typename affects the overall response type, rather than being propagated + # down to all child fields (which would cause conflicts). + typename: String + ) on # genqlient directives can go almost anywhere, although some options are only # applicable in certain locations as described above. diff --git a/generate/convert.go b/generate/convert.go index 6e39bac..609c993 100644 --- a/generate/convert.go +++ b/generate/convert.go @@ -15,6 +15,61 @@ import ( "github.com/vektah/gqlparser/v2/ast" ) +// getType returns the existing type in g.typeMap with the given name, if any, +// and an error if such type is incompatible with this one. +// +// This is useful as an early-out and a safety-check when generating types; if +// the type has already been generated we can skip generating it again. (This +// is necessary to handle recursive input types, and an optimization in other +// cases.) +func (g *generator) getType( + goName, graphQLName string, + selectionSet ast.SelectionSet, + pos *ast.Position, +) (goType, error) { + typ, ok := g.typeMap[goName] + if !ok { + return nil, nil + } + + if typ.GraphQLTypeName() != graphQLName { + return typ, errorf( + pos, "conflicting definition for %s; this can indicate either "+ + "a genqlient internal error, a conflict between user-specified "+ + "type-names, or some very tricksy GraphQL field/type names: "+ + "expected GraphQL type %s, got %s", + goName, typ.GraphQLTypeName(), graphQLName) + } + + expectedSelectionSet := typ.SelectionSet() + if err := selectionsMatch(pos, selectionSet, expectedSelectionSet); err != nil { + return typ, errorf( + pos, "conflicting definition for %s; this can indicate either "+ + "a genqlient internal error, a conflict between user-specified "+ + "type-names, or some very tricksy GraphQL field/type names: %v", + goName, err) + } + + return typ, nil +} + +// addType inserts the type into g.typeMap, checking for conflicts. +// +// The conflict-checking is as described in getType. Note we have to do it +// here again, even if the caller has already called getType, because the +// caller in between may have generated new types, which potentially creates +// new conflicts. +// +// Returns an already-existing type if found, and otherwise the given type. +func (g *generator) addType(typ goType, goName string, pos *ast.Position) (goType, error) { + otherTyp, err := g.getType(goName, typ.GraphQLTypeName(), typ.SelectionSet(), pos) + if otherTyp != nil || err != nil { + return otherTyp, err + } + g.typeMap[goName] = typ + return typ, nil +} + // baseTypeForOperation returns the definition of the GraphQL type to which the // root of the operation corresponds, e.g. the "Query" or "Mutation" type. func (g *generator) baseTypeForOperation(operation ast.Operation) (*ast.Definition, error) { @@ -40,9 +95,10 @@ func (g *generator) convertOperation( queryOptions *genqlientDirective, ) (goType, error) { name := operation.Name + "Response" - - if def, ok := g.typeMap[name]; ok { - return nil, errorf(operation.Position, "%s defined twice:\n%s", name, def) + namePrefix := newPrefixList(operation.Name) + if queryOptions.TypeName != "" { + name = queryOptions.TypeName + namePrefix = newPrefixList(queryOptions.TypeName) } baseType, err := g.baseTypeForOperation(operation.Operation) @@ -54,7 +110,7 @@ func (g *generator) convertOperation( // thing, because we want to do a few things differently, and because we // know we have an object type, so we can include only that case. fields, err := g.convertSelectionSet( - newPrefixList(operation.Name), operation.SelectionSet, baseType, queryOptions) + namePrefix, operation.SelectionSet, baseType, queryOptions) if err != nil { return nil, err } @@ -67,11 +123,11 @@ func (g *generator) convertOperation( GraphQLName: baseType.Name, // omit the GraphQL description for baseType; it's uninteresting. }, - Fields: fields, + Fields: fields, + Selection: operation.SelectionSet, } - g.typeMap[name] = goType - return goType, nil + return g.addType(goType, goType.GoName, operation.Position) } var builtinTypes = map[string]string{ @@ -110,7 +166,7 @@ func (g *generator) convertType( localBinding := options.Bind if localBinding != "" && localBinding != "-" { goRef, err := g.addRef(localBinding) - return &goOpaqueType{goRef}, err + return &goOpaqueType{goRef, typ.Name()}, err } if typ.Elem != nil { @@ -162,11 +218,46 @@ func (g *generator) convertDefinition( } } goRef, err := g.addRef(globalBinding.Type) - return &goOpaqueType{goRef}, err + return &goOpaqueType{goRef, def.Name}, err } goBuiltinName, ok := builtinTypes[def.Name] if ok { - return &goOpaqueType{goBuiltinName}, nil + return &goOpaqueType{goBuiltinName, def.Name}, nil + } + + // Determine the name to use for this type. + var name string + if options.TypeName != "" { + // If the user specified a name, use it! + name = options.TypeName + if namePrefix.head == name && namePrefix.tail == nil { + // Special case: if this name is also the only component of the + // name-prefix, append the type-name anyway. This happens when you + // assign a type name to an interface type, and we are generating + // one of its implementations. + name = makeLongTypeName(namePrefix, def.Name) + } + // (But the prefix is shared.) + namePrefix = newPrefixList(options.TypeName) + } else if def.Kind == ast.InputObject || def.Kind == ast.Enum { + // If we're an input-object or enum, there is only one type we will + // ever possibly generate for this type, so we don't need any of the + // qualifiers. This is especially helpful because the caller is very + // likely to need to reference these types in their code. + name = upperFirst(def.Name) + // (namePrefix is ignored in this case.) + } else { + // Else, construct a name using the usual algorithm (see names.go). + name = makeTypeName(namePrefix, def.Name) + } + + // If we already generated the type, we can skip it as long as it matches + // (and must fail if it doesn't). (This can happen for input/enum types, + // types of fields of interfaces, when options.TypeName is set, or, of + // course, on invalid configuration or internal error.) + existing, err := g.getType(name, def.Name, selectionSet, pos) + if existing != nil || err != nil { + return existing, err } desc := descriptionInfo{ @@ -184,8 +275,6 @@ func (g *generator) convertDefinition( } switch kind { case ast.Object: - name := makeTypeName(namePrefix, def.Name) - fields, err := g.convertSelectionSet( namePrefix, selectionSet, def, queryOptions) if err != nil { @@ -195,25 +284,24 @@ func (g *generator) convertDefinition( goType := &goStructType{ GoName: name, Fields: fields, + Selection: selectionSet, descriptionInfo: desc, } - g.typeMap[name] = goType - return goType, nil + return g.addType(goType, goType.GoName, pos) case ast.InputObject: - // If we're an input-object, there is only one type we will ever - // possibly generate for this type, so we don't need any of the - // qualifiers. This is especially helpful because the caller is very - // likely to need to reference these types in their code. - name := upperFirst(def.Name) - goType := &goStructType{ GoName: name, Fields: make([]*goStructField, len(def.Fields)), descriptionInfo: desc, IsInput: true, } - g.typeMap[name] = goType + // To handle recursive types, we need to add the type to the type-map + // *before* converting its fields. + _, err := g.addType(goType, goType.GoName, pos) + if err != nil { + return nil, err + } for i, field := range def.Fields { goName := upperFirst(field.Name) @@ -242,8 +330,6 @@ func (g *generator) convertDefinition( return goType, nil case ast.Interface, ast.Union: - name := makeTypeName(namePrefix, def.Name) - sharedFields, err := g.convertSelectionSet( namePrefix, selectionSet, def, queryOptions) if err != nil { @@ -255,9 +341,9 @@ func (g *generator) convertDefinition( GoName: name, SharedFields: sharedFields, Implementations: make([]*goStructType, len(implementationTypes)), + Selection: selectionSet, descriptionInfo: desc, } - g.typeMap[name] = goType for i, implDef := range implementationTypes { // TODO(benkraft): In principle we should skip generating a Go @@ -279,24 +365,19 @@ func (g *generator) convertDefinition( } goType.Implementations[i] = implStructTyp } - return goType, nil + return g.addType(goType, goType.GoName, pos) case ast.Enum: - // Like with InputObject, there's only one type we will ever generate - // for an enum. - name := upperFirst(def.Name) - goType := &goEnumType{ GoName: name, + GraphQLName: def.Name, Description: def.Description, Values: make([]goEnumValue, len(def.EnumValues)), } - g.typeMap[name] = goType - for i, val := range def.EnumValues { goType.Values[i] = goEnumValue{Name: val.Name, Description: val.Description} } - return goType, nil + return g.addType(goType, goType.GoName, pos) case ast.Scalar: // (If you had an entry in bindings, we would have returned it above.) @@ -559,6 +640,7 @@ func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goTy goType := &goStructType{ GoName: fragment.Name, Fields: fields, + Selection: fragment.SelectionSet, descriptionInfo: desc, } g.typeMap[fragment.Name] = goType @@ -569,6 +651,7 @@ func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goTy GoName: fragment.Name, SharedFields: fields, Implementations: make([]*goStructType, len(implementationTypes)), + Selection: fragment.SelectionSet, descriptionInfo: desc, } g.typeMap[fragment.Name] = goType @@ -586,6 +669,7 @@ func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goTy implTyp := &goStructType{ GoName: fragment.Name + upperFirst(implDef.Name), Fields: implFields, + Selection: fragment.SelectionSet, descriptionInfo: implDesc, } goType.Implementations[i] = implTyp diff --git a/generate/genqlient_directive.go b/generate/genqlient_directive.go index f40e3dd..ca00aad 100644 --- a/generate/genqlient_directive.go +++ b/generate/genqlient_directive.go @@ -16,6 +16,7 @@ type genqlientDirective struct { Pointer *bool Struct *bool Bind string + TypeName string } func (dir *genqlientDirective) GetOmitempty() bool { return dir.Omitempty != nil && *dir.Omitempty } @@ -68,6 +69,8 @@ func fromGraphQL(dir *ast.Directive, pos *ast.Position) (*genqlientDirective, er err = setBool(&retval.Struct, arg.Value) case "bind": err = setString(&retval.Bind, arg.Value) + case "typename": + err = setString(&retval.TypeName, arg.Value) default: return nil, errorf(pos, "unknown argument %v for @genqlient", arg.Name) } @@ -157,6 +160,10 @@ func validateStructOption( return nil } +// merge joins the directive applied to this node (the argument) and the one +// applied to the entire operation (the receiver) and returns a new +// directive-object representing the options to apply to this node (where in +// general we take the node's option, then the operation's, then the default). func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirective { retval := *dir if other.Omitempty != nil { @@ -171,6 +178,10 @@ func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirect if other.Bind != "" { retval.Bind = other.Bind } + // For typename, the local directive always wins: when specified on the query + // options typename applies to the response-struct, not to all parts of the + // query. + retval.TypeName = other.TypeName return &retval } diff --git a/generate/names.go b/generate/names.go index e5173c9..29b7bf2 100644 --- a/generate/names.go +++ b/generate/names.go @@ -166,3 +166,13 @@ func nextPrefix(prefix *prefixList, field *ast.Field) *prefixList { func makeTypeName(prefix *prefixList, typeName string) string { return joinPrefixList(typeNameParts(prefix, typeName)) } + +// Like makeTypeName, but append typeName unconditionally. +// +// This is used for when you specify a type-name for a field of interface +// type; we use YourName for the interface, but need to do YourNameImplName for +// the implementations. +func makeLongTypeName(prefix *prefixList, typeName string) string { + typeName = upperFirst(typeName) + return joinPrefixList(&prefixList{typeName, prefix}) +} diff --git a/generate/testdata/errors/ConflictingTypeNames.go b/generate/testdata/errors/ConflictingTypeNames.go new file mode 100644 index 0000000..1b9a787 --- /dev/null +++ b/generate/testdata/errors/ConflictingTypeNames.go @@ -0,0 +1,10 @@ +package errors + +_ = `# @genqlient +query ConflictingTypeNames { + # @genqlient(typename: "T") + f { g } + # @genqlient(typename: "T") + otherF: f { g h } +} +` diff --git a/generate/testdata/errors/ConflictingTypeNames.graphql b/generate/testdata/errors/ConflictingTypeNames.graphql new file mode 100644 index 0000000..09f0ab9 --- /dev/null +++ b/generate/testdata/errors/ConflictingTypeNames.graphql @@ -0,0 +1,6 @@ +query ConflictingTypeNames { + # @genqlient(typename: "T") + f { g } + # @genqlient(typename: "T") + otherF: f { g h } +} diff --git a/generate/testdata/errors/ConflictingTypeNames.schema.graphql b/generate/testdata/errors/ConflictingTypeNames.schema.graphql new file mode 100644 index 0000000..b2b5e22 --- /dev/null +++ b/generate/testdata/errors/ConflictingTypeNames.schema.graphql @@ -0,0 +1,8 @@ +type Query { + f: T +} + +type T { + g: String! + h: String! +} diff --git a/generate/testdata/queries/Recursion.graphql b/generate/testdata/queries/Recursion.graphql new file mode 100644 index 0000000..2856c9c --- /dev/null +++ b/generate/testdata/queries/Recursion.graphql @@ -0,0 +1,6 @@ +query Recursion($input: RecursiveInput!) { + recur(input: $input) { + # (sadly, or happily, GraphQL doesn't let us recur infinitely here) + rec { rec { rec { id } } } + } +} diff --git a/generate/testdata/queries/TypeNames.graphql b/generate/testdata/queries/TypeNames.graphql new file mode 100644 index 0000000..7c7ce55 --- /dev/null +++ b/generate/testdata/queries/TypeNames.graphql @@ -0,0 +1,10 @@ +# @genqlient(typename: "Resp") +query TypeNames { + # @genqlient(typename: "User") + user { id name } + # @genqlient(typename: "Item") + randomItem { id name } + # (ok to reuse the name as long as they match) + # @genqlient(typename: "User") + users { id name } +} diff --git a/generate/testdata/queries/schema.graphql b/generate/testdata/queries/schema.graphql index d3c940c..3392352 100644 --- a/generate/testdata/queries/schema.graphql +++ b/generate/testdata/queries/schema.graphql @@ -131,6 +131,15 @@ type Topic implements Content { schoolGrade: String } +input RecursiveInput { + rec: [RecursiveInput] +} + +type Recursive { + id: ID! + rec: Recursive +} + """Query's description is probably ignored by almost all callers.""" type Query { """user looks up a user by some stuff. @@ -140,7 +149,7 @@ type Query { """ user(query: UserQueryInput): User - users(query: [UserQueryInput]): User + users(query: [UserQueryInput]): [User] """usersWithRole looks a user up by role.""" usersWithRole(role: Role!): [User!]! @@ -153,6 +162,7 @@ type Query { getComplexJunk: ComplexJunk listOfListsOfLists: [[[String!]!]!]! listOfListsOfListsOfContent: [[[Content!]!]!]! + recur(input: RecursiveInput!): Recursive } type Mutation { diff --git a/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go b/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go index cb81b5e..4a904f0 100644 --- a/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go @@ -15,10 +15,10 @@ type OmitEmptyQueryResponse struct { // // See UserQueryInput for what stuff is supported. // If query is null, returns the current user. - User OmitEmptyQueryUser `json:"user"` - Users OmitEmptyQueryUsersUser `json:"users"` - MaybeConvert time.Time `json:"maybeConvert"` - Convert2 time.Time `json:"convert2"` + User OmitEmptyQueryUser `json:"user"` + Users []OmitEmptyQueryUsersUser `json:"users"` + MaybeConvert time.Time `json:"maybeConvert"` + Convert2 time.Time `json:"convert2"` } // OmitEmptyQueryUser includes the requested fields of the GraphQL type User. diff --git a/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.go b/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.go new file mode 100644 index 0000000..02e2852 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.go @@ -0,0 +1,71 @@ +package test + +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +import ( + "github.com/Khan/genqlient/graphql" + "github.com/Khan/genqlient/internal/testutil" +) + +// RecursionRecurRecursive includes the requested fields of the GraphQL type Recursive. +type RecursionRecurRecursive struct { + Rec RecursionRecurRecursiveRecRecursive `json:"rec"` +} + +// RecursionRecurRecursiveRecRecursive includes the requested fields of the GraphQL type Recursive. +type RecursionRecurRecursiveRecRecursive struct { + Rec RecursionRecurRecursiveRecRecursiveRecRecursive `json:"rec"` +} + +// RecursionRecurRecursiveRecRecursiveRecRecursive includes the requested fields of the GraphQL type Recursive. +type RecursionRecurRecursiveRecRecursiveRecRecursive struct { + Rec RecursionRecurRecursiveRecRecursiveRecRecursiveRecRecursive `json:"rec"` +} + +// RecursionRecurRecursiveRecRecursiveRecRecursiveRecRecursive includes the requested fields of the GraphQL type Recursive. +type RecursionRecurRecursiveRecRecursiveRecRecursiveRecRecursive struct { + Id testutil.ID `json:"id"` +} + +// RecursionResponse is returned by Recursion on success. +type RecursionResponse struct { + Recur RecursionRecurRecursive `json:"recur"` +} + +type RecursiveInput struct { + Rec []RecursiveInput `json:"rec"` +} + +func Recursion( + client graphql.Client, + input RecursiveInput, +) (*RecursionResponse, error) { + variables := map[string]interface{}{ + "input": input, + } + + var err error + + var retval RecursionResponse + err = client.MakeRequest( + nil, + "Recursion", + ` +query Recursion ($input: RecursiveInput!) { + recur(input: $input) { + rec { + rec { + rec { + id + } + } + } + } +} +`, + &retval, + variables, + ) + return &retval, err +} + diff --git a/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.json b/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.json new file mode 100644 index 0000000..44fb4fb --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.json @@ -0,0 +1,9 @@ +{ + "operations": [ + { + "operationName": "Recursion", + "query": "\nquery Recursion ($input: RecursiveInput!) {\n\trecur(input: $input) {\n\t\trec {\n\t\t\trec {\n\t\t\t\trec {\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/Recursion.graphql" + } + ] +} diff --git a/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go new file mode 100644 index 0000000..4445b53 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go @@ -0,0 +1,205 @@ +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" +) + +// Item includes the requested fields of the GraphQL interface Content. +// +// Item is implemented by the following types: +// ItemArticle +// ItemVideo +// ItemTopic +// The GraphQL type's documentation follows. +// +// Content is implemented by various types like Article, Video, and Topic. +type Item interface { + implementsGraphQLInterfaceItem() + // GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values). + GetTypename() string + // GetId returns the interface-field "id" from its implementation. + // The GraphQL interface field's documentation follows. + // + // ID is the identifier of the content. + GetId() testutil.ID + // GetName returns the interface-field "name" from its implementation. + GetName() string +} + +func (v *ItemArticle) implementsGraphQLInterfaceItem() {} + +// GetTypename is a part of, and documented with, the interface Item. +func (v *ItemArticle) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface Item. +func (v *ItemArticle) GetId() testutil.ID { return v.Id } + +// GetName is a part of, and documented with, the interface Item. +func (v *ItemArticle) GetName() string { return v.Name } + +func (v *ItemVideo) implementsGraphQLInterfaceItem() {} + +// GetTypename is a part of, and documented with, the interface Item. +func (v *ItemVideo) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface Item. +func (v *ItemVideo) GetId() testutil.ID { return v.Id } + +// GetName is a part of, and documented with, the interface Item. +func (v *ItemVideo) GetName() string { return v.Name } + +func (v *ItemTopic) implementsGraphQLInterfaceItem() {} + +// GetTypename is a part of, and documented with, the interface Item. +func (v *ItemTopic) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface Item. +func (v *ItemTopic) GetId() testutil.ID { return v.Id } + +// GetName is a part of, and documented with, the interface Item. +func (v *ItemTopic) GetName() string { return v.Name } + +func __unmarshalItem(v *Item, 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(ItemArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(ItemVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(ItemTopic) + return json.Unmarshal(m, *v) + case "": + return fmt.Errorf( + "Response was missing Content.__typename") + default: + return fmt.Errorf( + `Unexpected concrete type for Item: "%v"`, tn.TypeName) + } +} + +// ItemArticle includes the requested fields of the GraphQL type Article. +type ItemArticle struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"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"` +} + +// 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"` +} + +// Resp is returned by TypeNames on success. +type Resp struct { + // user looks up a user by some stuff. + // + // See UserQueryInput for what stuff is supported. + // If query is null, returns the current user. + User User `json:"user"` + RandomItem Item `json:"-"` + Users []User `json:"users"` +} + +func (v *Resp) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *Resp + RandomItem json.RawMessage `json:"randomItem"` + graphql.NoUnmarshalJSON + } + firstPass.Resp = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.RandomItem + raw := firstPass.RandomItem + err = __unmarshalItem( + target, raw) + if err != nil { + return fmt.Errorf( + "Unable to unmarshal Resp.RandomItem: %w", err) + } + } + return nil +} + +// User includes the requested fields of the GraphQL type User. +// The GraphQL type's documentation follows. +// +// A User is a user! +type User struct { + // id is the user's ID. + // + // It is stable, unique, and opaque, like all good IDs. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +func TypeNames( + client graphql.Client, +) (*Resp, error) { + var err error + + var retval Resp + err = client.MakeRequest( + nil, + "TypeNames", + ` +query TypeNames { + user { + id + name + } + randomItem { + __typename + id + name + } + users { + id + name + } +} +`, + &retval, + nil, + ) + return &retval, err +} + diff --git a/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.json b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.json new file mode 100644 index 0000000..3d8e0f3 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.json @@ -0,0 +1,9 @@ +{ + "operations": [ + { + "operationName": "TypeNames", + "query": "\nquery TypeNames {\n\tuser {\n\t\tid\n\t\tname\n\t}\n\trandomItem {\n\t\t__typename\n\t\tid\n\t\tname\n\t}\n\tusers {\n\t\tid\n\t\tname\n\t}\n}\n", + "sourceLocation": "testdata/queries/TypeNames.graphql" + } + ] +} diff --git a/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-go b/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-go new file mode 100644 index 0000000..ff46213 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-go @@ -0,0 +1 @@ +invalid Go file testdata/errors/ConflictingTypeNames.go: testdata/errors/ConflictingTypeNames.go:3:1: expected declaration, found _ diff --git a/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-graphql b/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-graphql new file mode 100644 index 0000000..ca5d185 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-graphql @@ -0,0 +1 @@ +testdata/errors/ConflictingTypeNames.schema.graphql:2: conflicting definition for T; this can indicate either a genqlient internal error, a conflict between user-specified type-names, or some very tricksy GraphQL field/type names: expected 2 fields, got 1 diff --git a/generate/types.go b/generate/types.go index d62ee1d..653c0be 100644 --- a/generate/types.go +++ b/generate/types.go @@ -9,6 +9,8 @@ import ( "fmt" "io" "strings" + + "github.com/vektah/gqlparser/v2/ast" ) // goType represents a type for which we'll generate code. @@ -23,6 +25,16 @@ type goType interface { // used to refer to it in Go code. Reference() string + // GraphQLTypeName returns the name of the GraphQL type to which this Go type + // corresponds. + GraphQLTypeName() string + + // SelectionSet returns the selection-set of the GraphQL field from which + // this type was generated, or nil if none is applicable (for GraphQL + // scalar, enum, and input types, as well as any opaque + // (non-genqlient-generated) type since those are validated upon creation). + SelectionSet() ast.SelectionSet + // Remove slice/pointer wrappers, and return the underlying (named (or // builtin)) type. For example, given []*MyStruct, return MyStruct. Unwrap() goType @@ -48,7 +60,10 @@ var ( type ( // goOpaqueType represents a user-defined or builtin type, often used to // represent a GraphQL scalar. (See Config.Bindings for more context.) - goOpaqueType struct{ GoRef string } + goOpaqueType struct { + GoRef string + GraphQLName string + } // goSliceType represents the Go type []Elem, used to represent GraphQL // list types. goSliceType struct{ Elem goType } @@ -67,11 +82,20 @@ 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) 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() } + // 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 // list of consts representing the values. type goEnumType struct { GoName string + GraphQLName string Description string Values []goEnumValue } @@ -96,14 +120,17 @@ func (typ *goEnumType) WriteDefinition(w io.Writer, g *generator) error { return nil } -func (typ *goEnumType) Reference() string { return typ.GoName } +func (typ *goEnumType) Reference() string { return typ.GoName } +func (typ *goEnumType) SelectionSet() ast.SelectionSet { return nil } +func (typ *goEnumType) GraphQLTypeName() string { return typ.GraphQLName } // goStructType represents a Go struct type used to represent a GraphQL object // or input-object type. type goStructType struct { - GoName string - Fields []*goStructField - IsInput bool + GoName string + Fields []*goStructField + IsInput bool + Selection ast.SelectionSet descriptionInfo } @@ -185,7 +212,9 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error { return g.execute("unmarshal.go.tmpl", w, typ) } -func (typ *goStructType) Reference() string { return typ.GoName } +func (typ *goStructType) Reference() string { return typ.GoName } +func (typ *goStructType) SelectionSet() ast.SelectionSet { return typ.Selection } +func (typ *goStructType) GraphQLTypeName() string { return typ.GraphQLName } // goInterfaceType represents a Go interface type, used to represent a GraphQL // interface or union type. @@ -195,6 +224,7 @@ type goInterfaceType struct { // we'll generate getter methods for each. SharedFields []*goStructField Implementations []*goStructType + Selection ast.SelectionSet descriptionInfo } @@ -272,7 +302,9 @@ func (typ *goInterfaceType) WriteDefinition(w io.Writer, g *generator) error { return g.execute("unmarshal_helper.go.tmpl", w, typ) } -func (typ *goInterfaceType) Reference() string { return typ.GoName } +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() } diff --git a/generate/binding_validation.go b/generate/validation.go similarity index 93% rename from generate/binding_validation.go rename to generate/validation.go index 9f5f8e2..048996a 100644 --- a/generate/binding_validation.go +++ b/generate/validation.go @@ -1,7 +1,8 @@ package generate -// This file is responsible for doing the validation for type-bindings, if they -// are so configured (see TypeBinding). +// This file contains helpers to do various bits of validation in the process +// of converting types to Go, notably, for cases where we need to check that +// two types match. import ( "fmt" @@ -17,7 +18,7 @@ import ( // order, and fragment-structure. It does not recurse into named fragments, it // only checks that their names match. // -// TODO(benkraft): Should we check arguments/directives? +// If both selection-sets are nil/empty, they compare equal. func selectionsMatch( pos *ast.Position, expectedSelectionSet, actualSelectionSet ast.SelectionSet,