Add support for specifying type-names, and conflict-detection (#94)
## Summary: In this commit I add two related features to genqlient: conflict-detection to avoid generating two distinct types with the same name, and an option to specify the type-name genqlient should use for some type. The conflict-detection was pretty simple once I realized I had already written all the code to do it in #70. There was a bunch of wiring, since we now need to keep track of the GraphQL type/selection-set that each type corresponds to, but it was pretty straightforward. This allows us to: - detect and reject if you have really sneaky type-names (there are some examples documented in `names.go`) - more clearly crash if genqlient accidentally generates two conflicting types, and - avoid stack-overflow when handing recursive (input) types (although sadly the poor support for options on input types (#14) makes them difficult to use in many cases; you really need to be able to set `pointer: true`) And with that all set up, the type-naming was also easy! (It doesn't have to get into the core of the type-generator, just plug in where we choose names. The desire for conflict detection was the main reason I hadn't set it up already.) Note that the existing limitation of #70 that the fields have to be in exactly the same order remains (and is now documented as #93); it's not deeply hard to fix but it's surprisingly much work. Issue: https://github.com/Khan/genqlient/issues/60 Issue: https://github.com/Khan/genqlient/issues/12 ## Test plan: make check Author: benjaminjkraft Reviewers: StevenACoffman, jvoll, benjaminjkraft, aberkan, csilvers, dnerdy, mahtabsabet, MiguelCastillo Required Reviewers: Approved By: StevenACoffman, jvoll 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/94
This commit is contained in:
+116
-32
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package errors
|
||||
|
||||
_ = `# @genqlient
|
||||
query ConflictingTypeNames {
|
||||
# @genqlient(typename: "T")
|
||||
f { g }
|
||||
# @genqlient(typename: "T")
|
||||
otherF: f { g h }
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,6 @@
|
||||
query ConflictingTypeNames {
|
||||
# @genqlient(typename: "T")
|
||||
f { g }
|
||||
# @genqlient(typename: "T")
|
||||
otherF: f { g h }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
type Query {
|
||||
f: T
|
||||
}
|
||||
|
||||
type T {
|
||||
g: String!
|
||||
h: String!
|
||||
}
|
||||
+6
@@ -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 } } }
|
||||
}
|
||||
}
|
||||
+10
@@ -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 }
|
||||
}
|
||||
+11
-1
@@ -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 {
|
||||
|
||||
+4
-4
@@ -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.
|
||||
|
||||
+71
@@ -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
|
||||
}
|
||||
|
||||
+9
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+205
@@ -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
|
||||
}
|
||||
|
||||
+9
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
invalid Go file testdata/errors/ConflictingTypeNames.go: testdata/errors/ConflictingTypeNames.go:3:1: expected declaration, found _
|
||||
@@ -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
|
||||
+39
-7
@@ -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() }
|
||||
|
||||
@@ -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,
|
||||
Reference in New Issue
Block a user