diff --git a/generate/convert.go b/generate/convert.go new file mode 100644 index 0000000..ae05d49 --- /dev/null +++ b/generate/convert.go @@ -0,0 +1,356 @@ +package generate + +// This file implements the core type-generation logic of genqlient, whereby we +// traverse an operation-definition (and the schema against which it will be +// executed), and convert that into Go types. It returns data structures +// representing the types to be generated; these are defined, and converted +// into code, in types.go. +// +// The entrypoints are convertOperation, which builds the response-type for a +// query, and convertInputType, which builds the argument-types. + +import ( + "fmt" + "strings" + + "github.com/vektah/gqlparser/v2/ast" +) + +// 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) { + switch operation { + case ast.Query: + return g.schema.Query, nil + case ast.Mutation: + return g.schema.Mutation, nil + case ast.Subscription: + if !allowBrokenFeatures { + return nil, errorf(nil, "genqlient does not yet support subscriptions") + } + return g.schema.Subscription, nil + default: + return nil, errorf(nil, "unexpected operation: %v", operation) + } +} + +// convertOperation builds the response-type into which the given operation's +// result will be unmarshaled. +func (g *generator) convertOperation( + operation *ast.OperationDefinition, + 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) + } + + baseType, err := g.baseTypeForOperation(operation.Operation) + if err != nil { + return nil, errorf(operation.Position, "%v", err) + } + + goTyp, err := g.convertDefinition( + name, operation.Name, baseType, operation.Position, + operation.SelectionSet, queryOptions) + + if structType, ok := goTyp.(*goStructType); ok { + // Override the ordinary description; the GraphQL documentation for + // Query/Mutation is unlikely to be of value. + // TODO(benkraft): This is a bit awkward/fragile. + structType.Description = + fmt.Sprintf("%v is returned by %v on success.", name, operation.Name) + structType.Incomplete = false + } + + return goTyp, err +} + +var builtinTypes = map[string]string{ + // GraphQL guarantees int32 is enough, but using int seems more idiomatic + "Int": "int", + "Float": "float64", + "String": "string", + "Boolean": "bool", + "ID": "string", +} + +// typeName computes the name, in Go, that we should use for the given +// GraphQL type definition. This is dependent on its location within the query +// (see DESIGN.md for more on why we generate type-names this way), which is +// determined by the prefix argument; the nextPrefix result should be passed to +// calls to typeName on any child types. +func (g *generator) typeName(prefix string, typ *ast.Definition) (name, nextPrefix string) { + typeGoName := upperFirst(typ.Name) + if typ.Kind == ast.Enum || typ.Kind == ast.InputObject { + // If we're an enum or 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. + return typeGoName, typeGoName + } + + name = prefix + if !strings.HasSuffix(prefix, 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 += 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. So we just use the input prefix. + return name, prefix + } + + // Otherwise, the name will also be the prefix for the next type. + return name, name +} + +// convertInputType decides the Go type we will generate corresponding to an +// argument to a GraphQL operation. +func (g *generator) convertInputType( + opName string, + typ *ast.Type, + options, queryOptions *GenqlientDirective, +) (goType, error) { + // Sort of a hack: case the input type name to match the op-name. + // TODO(benkraft): this is another thing that breaks the assumption that we + // only need one of an input type, albeit in a relatively safe way. + name := matchFirst(typ.Name(), opName) + // note prefix is ignored here (see generator.typeName), as is selectionSet + // (for input types we use the whole thing)). + return g.convertType(name, "", typ, nil, options, queryOptions) +} + +// convertType decides the Go type we will generate corresponding to a +// particular GraphQL type. In this context, "type" represents the type of a +// field, and may be a list or a reference to a named type, with or without the +// "non-null" annotation. +func (g *generator) convertType( + name, namePrefix string, + typ *ast.Type, + selectionSet ast.SelectionSet, + options, queryOptions *GenqlientDirective, +) (goType, error) { + if typ.Elem != nil { + // Type is a list. + elem, err := g.convertType( + name, namePrefix, typ.Elem, selectionSet, options, queryOptions) + return &goSliceType{elem}, err + } + + // If this is a builtin type or custom scalar, just refer to it. + def := g.schema.Types[typ.Name()] + goTyp, err := g.convertDefinition( + name, namePrefix, def, typ.Position, selectionSet, queryOptions) + + if options.GetPointer() { + // Whatever we get, wrap it in a pointer. (Because of the way the + // options work, recursing here isn't as connvenient.) + // Note this does []*T or [][]*T, not e.g. *[][]T. See #16. + goTyp = &goPointerType{goTyp} + } + return goTyp, err +} + +// convertDefinition decides the Go type we will generate corresponding to a +// particular GraphQL named type. +// +// In this context, "definition" (and "named type") refer to an +// *ast.Definition, which represents the definition of a type in the GraphQL +// schema, which may be referenced by a field-type (see convertType). +func (g *generator) convertDefinition( + name, namePrefix string, + def *ast.Definition, + pos *ast.Position, + selectionSet ast.SelectionSet, + queryOptions *GenqlientDirective, +) (goType, error) { + qualifiedGoName, ok := g.Config.Scalars[def.Name] + if ok { + goRef, err := g.addRef(qualifiedGoName) + return &goOpaqueType{goRef}, err + } + goBuiltinName, ok := builtinTypes[def.Name] + if ok { + return &goOpaqueType{goBuiltinName}, nil + } + + switch def.Kind { + case ast.Object: + goType := &goStructType{ + GoName: name, + Description: def.Description, + GraphQLName: def.Name, + Fields: make([]*goStructField, len(selectionSet)), + Incomplete: true, + } + g.typeMap[name] = goType + + for i, selection := range selectionSet { + _, selectionDirective, err := g.parsePrecedingComment( + selection, selection.GetPosition()) + if err != nil { + return nil, err + } + selectionOptions := queryOptions.merge(selectionDirective) + + switch selection := selection.(type) { + case *ast.Field: + goType.Fields[i], err = g.convertField( + namePrefix, selection, selectionOptions, queryOptions) + if err != nil { + return nil, err + } + case *ast.FragmentSpread: + return nil, errorf(selection.Position, "not implemented: %T", selection) + case *ast.InlineFragment: + return nil, errorf(selection.Position, "not implemented: %T", selection) + default: + return nil, errorf(nil, "invalid selection type: %T", selection) + } + } + return goType, nil + + case ast.InputObject: + goType := &goStructType{ + GoName: name, + Description: def.Description, + GraphQLName: def.Name, + Fields: make([]*goStructField, len(def.Fields)), + } + g.typeMap[name] = goType + + for i, field := range def.Fields { + goName := upperFirst(field.Name) + // Several of the arguments don't really make sense here: + // - no field-specific options can apply, because this is + // a field in the type, not in the query (see also #14). + // - namePrefix is ignored for input types; see note in + // generator.typeName. + // TODO(benkraft): Can we refactor to avoid passing the values that + // will be ignored? We know field.Type is a scalar, enum, or input + // type. But plumbing that is a bit tricky in practice. + fieldGoType, err := g.convertType( + field.Type.Name(), "", field.Type, nil, queryOptions, queryOptions) + if err != nil { + return nil, err + } + + goType.Fields[i] = &goStructField{ + GoName: goName, + GoType: fieldGoType, + JSONName: field.Name, + Description: field.Description, + } + } + return goType, nil + + case ast.Interface, ast.Union: + if !allowBrokenFeatures { + return nil, errorf(pos, "not implemented: %v", def.Kind) + } + + implementationTypes := g.schema.GetPossibleTypes(def) + goType := &goInterfaceType{ + GoName: name, + Description: def.Description, + GraphQLName: def.Name, + Implementations: make([]*goStructType, len(implementationTypes)), + } + g.typeMap[name] = goType + + for i, implDef := range implementationTypes { + implName, implNamePrefix := g.typeName(namePrefix, implDef) + implTyp, err := g.convertDefinition( + implName, implNamePrefix, implDef, pos, selectionSet, queryOptions) + if err != nil { + return nil, err + } + + implStructTyp, ok := implTyp.(*goStructType) + if !ok { // (should never happen on a valid schema) + return nil, errorf( + pos, "interface %s had non-object implementation %s", + def.Name, implDef.Name) + } + goType.Implementations[i] = implStructTyp + } + return goType, nil + + case ast.Enum: + goType := &goEnumType{ + GoName: 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 + + case ast.Scalar: + return nil, errorf( + pos, "unknown scalar %v: please add it to genqlient.yaml", def.Name) + default: + return nil, errorf(pos, "unexpected kind: %v", def.Kind) + } +} + +// convertField converts a single GraphQL operation-field into a GraphQL type. +// +// Note that input-type fields are handled separately (inline in +// convertDefinition), because they come from the type-definition, not the +// operation. +func (g *generator) convertField( + namePrefix string, + field *ast.Field, + fieldOptions, queryOptions *GenqlientDirective, +) (*goStructField, error) { + if field.Definition == nil { + // Unclear why gqlparser hasn't already rejected this, + // but empirically it might not. + return nil, errorf( + field.Position, "undefined field %v", field.Alias) + } + + // Needs to be exported for JSON-marshaling + goName := upperFirst(field.Alias) + + typ := field.Definition.Type + fieldTypedef := g.schema.Types[typ.Name()] + + // Note we don't deduplicate suffixes 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 named based on the GraphQL alias (Go + // name), 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. + name, namePrefix := g.typeName(namePrefix+goName, fieldTypedef) + fieldGoType, err := g.convertType( + name, namePrefix, typ, field.SelectionSet, + fieldOptions, queryOptions) + if err != nil { + return nil, err + } + + return &goStructField{ + GoName: goName, + GoType: fieldGoType, + JSONName: field.Alias, + Description: field.Definition.Description, + }, nil +} diff --git a/generate/generate.go b/generate/generate.go index d3f62de..c780db6 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -1,5 +1,9 @@ package generate +// This file implements the main entrypoint and framework for the genqlient +// code-generation process. See comments in Generate for the high-level +// overview. + import ( "bytes" "encoding/json" @@ -24,7 +28,7 @@ type generator struct { // The list of operations for which to generate code. Operations []operation // The types needed for these operations. - typeMap map[string]string + typeMap map[string]goType // Imports needed for these operations, path -> alias and alias -> true imports map[string]string usedAliases map[string]bool @@ -67,7 +71,7 @@ type argument struct { func newGenerator(config *Config, schema *ast.Schema) *generator { g := generator{ Config: config, - typeMap: map[string]string{}, + typeMap: map[string]goType{}, imports: map[string]string{}, usedAliases: map[string]bool{}, templateCache: map[string]*template.Template{}, @@ -90,7 +94,7 @@ func newGenerator(config *Config, schema *ast.Schema) *generator { return &g } -func (g *generator) Types() string { +func (g *generator) Types() (string, error) { names := make([]string, 0, len(g.typeMap)) for name := range g.typeMap { names = append(names, name) @@ -102,10 +106,16 @@ func (g *generator) Types() string { sort.Strings(names) defs := make([]string, 0, len(g.typeMap)) + var builder strings.Builder for _, name := range names { - defs = append(defs, g.typeMap[name]) + builder.Reset() + err := g.typeMap[name].WriteDefinition(&builder, g) + if err != nil { + return "", err + } + defs = append(defs, builder.String()) } - return strings.Join(defs, "\n\n") + return strings.Join(defs, "\n\n"), nil } func (g *generator) getArgument( @@ -119,7 +129,7 @@ func (g *generator) getArgument( } graphQLName := arg.Variable - goType, err := g.getTypeForInputType( + goTyp, err := g.convertInputType( opName, arg.Type, directive, operationDirective) if err != nil { return argument{}, err @@ -127,7 +137,7 @@ func (g *generator) getArgument( return argument{ GraphQLName: graphQLName, GoName: lowerFirst(graphQLName), - GoType: goType, + GoType: goTyp.Reference(), IsSlice: arg.Type.Elem != nil, Options: operationDirective.merge(directive), }, nil @@ -158,7 +168,7 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error { } } - responseName, err := g.getTypeForOperation(op, directive) + responseType, err := g.convertOperation(op, directive) if err != nil { return err } @@ -185,7 +195,7 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error { // The newline just makes it format a little nicer. Body: "\n" + builder.String(), Args: args, - ResponseName: responseName, + ResponseName: responseType.Reference(), SourceFilename: sourceFilename, }) @@ -193,7 +203,14 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error { } // Generate returns a map from absolute-path filename to generated content. +// +// This is the main entrypoint to the code-generation process for callers who +// wish to manage the config-reading (ReadAndValidateConfig) and file-writing +// on their own. (Those are wired in by Main.) func Generate(config *Config) (map[string][]byte, error) { + // Step 1: Read in the schema and operations from the files defined by the + // config (and validate the operations against the schema). This is all + // defined in parse.go. schema, err := getSchema(config.Schema) if err != nil { return nil, err @@ -204,9 +221,9 @@ func Generate(config *Config) (map[string][]byte, error) { return nil, err } - // TODO: we could also allow this, and generate an empty file with just the - // package-name, if it turns out to be more convenient that way. (As-is, - // we generate a broken file, with just (unused) imports.) + // TODO(benkraft): we could also allow this, and generate an empty file + // with just the package-name, if it turns out to be more convenient that + // way. (As-is, we generate a broken file, with just (unused) imports.) if len(document.Operations) == 0 { // Hard to have a position when there are no operations :( return nil, errorf(nil, "no queries found, looked in: %v", @@ -218,6 +235,9 @@ func Generate(config *Config) (map[string][]byte, error) { "genqlient does not yet support fragments") } + // Step 2: For each operation, convert it into data structures representing + // Go types (defined in types.go). The bulk of this logic is in + // convert.go. g := newGenerator(config, schema) for _, op := range document.Operations { if err = g.addOperation(op); err != nil { @@ -225,6 +245,9 @@ func Generate(config *Config) (map[string][]byte, error) { } } + // Step 3: Glue it all together! Most of this is done inline in the + // template, but the call to g.Types() in the template calls out to + // types.go to actually generate the code for each type. var buf bytes.Buffer err = g.execute("operation.go.tmpl", &buf, g) if err != nil { diff --git a/generate/comments.go b/generate/genqlient_directive.go similarity index 100% rename from generate/comments.go rename to generate/genqlient_directive.go diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go new file mode 100644 index 0000000..28dffac --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go @@ -0,0 +1,87 @@ +package test + +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +import ( + "github.com/Khan/genqlient/graphql" + "github.com/Khan/genqlient/internal/testutil" +) + +// InterfaceNoFragmentsQueryResponse is returned by InterfaceNoFragmentsQuery on success. +type InterfaceNoFragmentsQueryResponse struct { + Root InterfaceNoFragmentsQueryRootTopic `json:"root"` +} + +// InterfaceNoFragmentsQueryRootTopic includes the requested fields of the GraphQL type Topic. +type InterfaceNoFragmentsQueryRootTopic struct { + // ID is documented in the Content interface. + Id testutil.ID `json:"id"` + Name string `json:"name"` + Children []InterfaceNoFragmentsQueryRootTopicChildrenContent `json:"children"` +} + +// InterfaceNoFragmentsQueryRootTopicChildrenArticle includes the requested fields of the GraphQL type Article. +type InterfaceNoFragmentsQueryRootTopicChildrenArticle struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// InterfaceNoFragmentsQueryRootTopicChildrenContent includes the requested fields of the GraphQL type Content. +// The GraphQL type's documentation follows. +// +// Content is implemented by various types like Article, Video, and Topic. +type InterfaceNoFragmentsQueryRootTopicChildrenContent interface { + implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() +} + +func (v *InterfaceNoFragmentsQueryRootTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +} +func (v *InterfaceNoFragmentsQueryRootTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +} +func (v *InterfaceNoFragmentsQueryRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +} + +// InterfaceNoFragmentsQueryRootTopicChildrenTopic includes the requested fields of the GraphQL type Topic. +type InterfaceNoFragmentsQueryRootTopicChildrenTopic struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// InterfaceNoFragmentsQueryRootTopicChildrenVideo includes the requested fields of the GraphQL type Video. +type InterfaceNoFragmentsQueryRootTopicChildrenVideo struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +func InterfaceNoFragmentsQuery( + client graphql.Client, +) (*InterfaceNoFragmentsQueryResponse, error) { + var retval InterfaceNoFragmentsQueryResponse + err := client.MakeRequest( + nil, + "InterfaceNoFragmentsQuery", + ` +query InterfaceNoFragmentsQuery { + root { + id + name + children { + __typename + id + name + } + } +} +`, + &retval, + nil, + ) + return &retval, err +} + diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go index d1e2af2..581d838 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go @@ -3,8 +3,6 @@ package test // Code generated by github.com/Khan/genqlient, DO NOT EDIT. import ( - "encoding/json" - "github.com/Khan/genqlient/graphql" "github.com/Khan/genqlient/internal/testutil" ) @@ -18,54 +16,7 @@ type InterfaceNestingResponse struct { type InterfaceNestingRootTopic struct { // ID is documented in the Content interface. Id testutil.ID `json:"id"` - Children []InterfaceNestingRootTopicChildrenContent `json:"-"` -} - -func (v *InterfaceNestingRootTopic) UnmarshalJSON(b []byte) error { - var firstPass struct { - *InterfaceNestingRootTopic - Children json.RawMessage `json:"children"` - } - firstPass.InterfaceNestingRootTopic = v - - err := json.Unmarshal(b, &firstPass) - if err != nil { - return err - } - - var tn struct { - TypeName string `json:"__typename"` - } - err = json.Unmarshal(firstPass.Children, &tn) - if err != nil { - return err - } - switch tn.TypeName { - - case "Article": - - v.Children = InterfaceNestingRootTopicChildrenArticle{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - case "Video": - - v.Children = InterfaceNestingRootTopicChildrenVideo{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - case "Topic": - - v.Children = InterfaceNestingRootTopicChildrenTopic{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - } - if err != nil { - return err - } - - return nil + Children []InterfaceNestingRootTopicChildrenContent `json:"children"` } // InterfaceNestingRootTopicChildrenArticle includes the requested fields of the GraphQL type Article. @@ -75,61 +26,11 @@ type InterfaceNestingRootTopicChildrenArticle struct { Parent InterfaceNestingRootTopicChildrenArticleParentTopic `json:"parent"` } -func (v InterfaceNestingRootTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { -} - // InterfaceNestingRootTopicChildrenArticleParentTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenArticleParentTopic struct { // ID is documented in the Content interface. Id testutil.ID `json:"id"` - Children []InterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent `json:"-"` -} - -func (v *InterfaceNestingRootTopicChildrenArticleParentTopic) UnmarshalJSON(b []byte) error { - var firstPass struct { - *InterfaceNestingRootTopicChildrenArticleParentTopic - Children json.RawMessage `json:"children"` - } - firstPass.InterfaceNestingRootTopicChildrenArticleParentTopic = v - - err := json.Unmarshal(b, &firstPass) - if err != nil { - return err - } - - var tn struct { - TypeName string `json:"__typename"` - } - err = json.Unmarshal(firstPass.Children, &tn) - if err != nil { - return err - } - switch tn.TypeName { - - case "Article": - - v.Children = InterfaceNestingRootTopicChildrenArticleParentTopicChildrenArticle{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - case "Video": - - v.Children = InterfaceNestingRootTopicChildrenArticleParentTopicChildrenVideo{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - case "Topic": - - v.Children = InterfaceNestingRootTopicChildrenArticleParentTopicChildrenTopic{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - } - if err != nil { - return err - } - - return nil + Children []InterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent `json:"children"` } // InterfaceNestingRootTopicChildrenArticleParentTopicChildrenArticle includes the requested fields of the GraphQL type Article. @@ -138,9 +39,6 @@ type InterfaceNestingRootTopicChildrenArticleParentTopicChildrenArticle struct { Id testutil.ID `json:"id"` } -func (v InterfaceNestingRootTopicChildrenArticleParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { -} - // InterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent includes the requested fields of the GraphQL type Content. // The GraphQL type's documentation follows. // @@ -149,24 +47,25 @@ type InterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent interfac implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() } +func (v InterfaceNestingRootTopicChildrenArticleParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { +} +func (v InterfaceNestingRootTopicChildrenArticleParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { +} +func (v InterfaceNestingRootTopicChildrenArticleParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { +} + // InterfaceNestingRootTopicChildrenArticleParentTopicChildrenTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenArticleParentTopicChildrenTopic struct { // ID is the identifier of the content. Id testutil.ID `json:"id"` } -func (v InterfaceNestingRootTopicChildrenArticleParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { -} - // InterfaceNestingRootTopicChildrenArticleParentTopicChildrenVideo includes the requested fields of the GraphQL type Video. type InterfaceNestingRootTopicChildrenArticleParentTopicChildrenVideo struct { // ID is the identifier of the content. Id testutil.ID `json:"id"` } -func (v InterfaceNestingRootTopicChildrenArticleParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenArticleParentTopicChildrenContent() { -} - // InterfaceNestingRootTopicChildrenContent includes the requested fields of the GraphQL type Content. // The GraphQL type's documentation follows. // @@ -175,6 +74,13 @@ type InterfaceNestingRootTopicChildrenContent interface { implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() } +func (v InterfaceNestingRootTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { +} +func (v InterfaceNestingRootTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { +} +func (v InterfaceNestingRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { +} + // InterfaceNestingRootTopicChildrenTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenTopic struct { // ID is the identifier of the content. @@ -182,61 +88,11 @@ type InterfaceNestingRootTopicChildrenTopic struct { Parent InterfaceNestingRootTopicChildrenTopicParentTopic `json:"parent"` } -func (v InterfaceNestingRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { -} - // InterfaceNestingRootTopicChildrenTopicParentTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenTopicParentTopic struct { // ID is documented in the Content interface. Id testutil.ID `json:"id"` - Children []InterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent `json:"-"` -} - -func (v *InterfaceNestingRootTopicChildrenTopicParentTopic) UnmarshalJSON(b []byte) error { - var firstPass struct { - *InterfaceNestingRootTopicChildrenTopicParentTopic - Children json.RawMessage `json:"children"` - } - firstPass.InterfaceNestingRootTopicChildrenTopicParentTopic = v - - err := json.Unmarshal(b, &firstPass) - if err != nil { - return err - } - - var tn struct { - TypeName string `json:"__typename"` - } - err = json.Unmarshal(firstPass.Children, &tn) - if err != nil { - return err - } - switch tn.TypeName { - - case "Article": - - v.Children = InterfaceNestingRootTopicChildrenTopicParentTopicChildrenArticle{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - case "Video": - - v.Children = InterfaceNestingRootTopicChildrenTopicParentTopicChildrenVideo{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - case "Topic": - - v.Children = InterfaceNestingRootTopicChildrenTopicParentTopicChildrenTopic{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - } - if err != nil { - return err - } - - return nil + Children []InterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent `json:"children"` } // InterfaceNestingRootTopicChildrenTopicParentTopicChildrenArticle includes the requested fields of the GraphQL type Article. @@ -245,9 +101,6 @@ type InterfaceNestingRootTopicChildrenTopicParentTopicChildrenArticle struct { Id testutil.ID `json:"id"` } -func (v InterfaceNestingRootTopicChildrenTopicParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { -} - // InterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent includes the requested fields of the GraphQL type Content. // The GraphQL type's documentation follows. // @@ -256,24 +109,25 @@ type InterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent interface implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() } +func (v InterfaceNestingRootTopicChildrenTopicParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { +} +func (v InterfaceNestingRootTopicChildrenTopicParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { +} +func (v InterfaceNestingRootTopicChildrenTopicParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { +} + // InterfaceNestingRootTopicChildrenTopicParentTopicChildrenTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenTopicParentTopicChildrenTopic struct { // ID is the identifier of the content. Id testutil.ID `json:"id"` } -func (v InterfaceNestingRootTopicChildrenTopicParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { -} - // InterfaceNestingRootTopicChildrenTopicParentTopicChildrenVideo includes the requested fields of the GraphQL type Video. type InterfaceNestingRootTopicChildrenTopicParentTopicChildrenVideo struct { // ID is the identifier of the content. Id testutil.ID `json:"id"` } -func (v InterfaceNestingRootTopicChildrenTopicParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenTopicParentTopicChildrenContent() { -} - // InterfaceNestingRootTopicChildrenVideo includes the requested fields of the GraphQL type Video. type InterfaceNestingRootTopicChildrenVideo struct { // ID is the identifier of the content. @@ -281,61 +135,11 @@ type InterfaceNestingRootTopicChildrenVideo struct { Parent InterfaceNestingRootTopicChildrenVideoParentTopic `json:"parent"` } -func (v InterfaceNestingRootTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenContent() { -} - // InterfaceNestingRootTopicChildrenVideoParentTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenVideoParentTopic struct { // ID is documented in the Content interface. Id testutil.ID `json:"id"` - Children []InterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent `json:"-"` -} - -func (v *InterfaceNestingRootTopicChildrenVideoParentTopic) UnmarshalJSON(b []byte) error { - var firstPass struct { - *InterfaceNestingRootTopicChildrenVideoParentTopic - Children json.RawMessage `json:"children"` - } - firstPass.InterfaceNestingRootTopicChildrenVideoParentTopic = v - - err := json.Unmarshal(b, &firstPass) - if err != nil { - return err - } - - var tn struct { - TypeName string `json:"__typename"` - } - err = json.Unmarshal(firstPass.Children, &tn) - if err != nil { - return err - } - switch tn.TypeName { - - case "Article": - - v.Children = InterfaceNestingRootTopicChildrenVideoParentTopicChildrenArticle{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - case "Video": - - v.Children = InterfaceNestingRootTopicChildrenVideoParentTopicChildrenVideo{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - case "Topic": - - v.Children = InterfaceNestingRootTopicChildrenVideoParentTopicChildrenTopic{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - } - if err != nil { - return err - } - - return nil + Children []InterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent `json:"children"` } // InterfaceNestingRootTopicChildrenVideoParentTopicChildrenArticle includes the requested fields of the GraphQL type Article. @@ -344,9 +148,6 @@ type InterfaceNestingRootTopicChildrenVideoParentTopicChildrenArticle struct { Id testutil.ID `json:"id"` } -func (v InterfaceNestingRootTopicChildrenVideoParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { -} - // InterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent includes the requested fields of the GraphQL type Content. // The GraphQL type's documentation follows. // @@ -355,24 +156,25 @@ type InterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent interface implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() } +func (v InterfaceNestingRootTopicChildrenVideoParentTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { +} +func (v InterfaceNestingRootTopicChildrenVideoParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { +} +func (v InterfaceNestingRootTopicChildrenVideoParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { +} + // InterfaceNestingRootTopicChildrenVideoParentTopicChildrenTopic includes the requested fields of the GraphQL type Topic. type InterfaceNestingRootTopicChildrenVideoParentTopicChildrenTopic struct { // ID is the identifier of the content. Id testutil.ID `json:"id"` } -func (v InterfaceNestingRootTopicChildrenVideoParentTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { -} - // InterfaceNestingRootTopicChildrenVideoParentTopicChildrenVideo includes the requested fields of the GraphQL type Video. type InterfaceNestingRootTopicChildrenVideoParentTopicChildrenVideo struct { // ID is the identifier of the content. Id testutil.ID `json:"id"` } -func (v InterfaceNestingRootTopicChildrenVideoParentTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNestingRootTopicChildrenVideoParentTopicChildrenContent() { -} - func InterfaceNesting( client graphql.Client, ) (*InterfaceNestingResponse, error) { diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go index e7761df..795343a 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go @@ -3,8 +3,6 @@ package test // Code generated by github.com/Khan/genqlient, DO NOT EDIT. import ( - "encoding/json" - "github.com/Khan/genqlient/graphql" "github.com/Khan/genqlient/internal/testutil" ) @@ -19,54 +17,7 @@ type InterfaceNoFragmentsQueryRootTopic struct { // ID is documented in the Content interface. Id testutil.ID `json:"id"` Name string `json:"name"` - Children []InterfaceNoFragmentsQueryRootTopicChildrenContent `json:"-"` -} - -func (v *InterfaceNoFragmentsQueryRootTopic) UnmarshalJSON(b []byte) error { - var firstPass struct { - *InterfaceNoFragmentsQueryRootTopic - Children json.RawMessage `json:"children"` - } - firstPass.InterfaceNoFragmentsQueryRootTopic = v - - err := json.Unmarshal(b, &firstPass) - if err != nil { - return err - } - - var tn struct { - TypeName string `json:"__typename"` - } - err = json.Unmarshal(firstPass.Children, &tn) - if err != nil { - return err - } - switch tn.TypeName { - - case "Article": - - v.Children = InterfaceNoFragmentsQueryRootTopicChildrenArticle{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - case "Video": - - v.Children = InterfaceNoFragmentsQueryRootTopicChildrenVideo{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - case "Topic": - - v.Children = InterfaceNoFragmentsQueryRootTopicChildrenTopic{} - err = json.Unmarshal( - firstPass.Children, &v.Children) - - } - if err != nil { - return err - } - - return nil + Children []InterfaceNoFragmentsQueryRootTopicChildrenContent `json:"children"` } // InterfaceNoFragmentsQueryRootTopicChildrenArticle includes the requested fields of the GraphQL type Article. @@ -76,9 +27,6 @@ type InterfaceNoFragmentsQueryRootTopicChildrenArticle struct { Name string `json:"name"` } -func (v InterfaceNoFragmentsQueryRootTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { -} - // InterfaceNoFragmentsQueryRootTopicChildrenContent includes the requested fields of the GraphQL type Content. // The GraphQL type's documentation follows. // @@ -87,6 +35,13 @@ type InterfaceNoFragmentsQueryRootTopicChildrenContent interface { implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() } +func (v InterfaceNoFragmentsQueryRootTopicChildrenArticle) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +} +func (v InterfaceNoFragmentsQueryRootTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +} +func (v InterfaceNoFragmentsQueryRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { +} + // InterfaceNoFragmentsQueryRootTopicChildrenTopic includes the requested fields of the GraphQL type Topic. type InterfaceNoFragmentsQueryRootTopicChildrenTopic struct { // ID is the identifier of the content. @@ -94,9 +49,6 @@ type InterfaceNoFragmentsQueryRootTopicChildrenTopic struct { Name string `json:"name"` } -func (v InterfaceNoFragmentsQueryRootTopicChildrenTopic) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { -} - // InterfaceNoFragmentsQueryRootTopicChildrenVideo includes the requested fields of the GraphQL type Video. type InterfaceNoFragmentsQueryRootTopicChildrenVideo struct { // ID is the identifier of the content. @@ -104,9 +56,6 @@ type InterfaceNoFragmentsQueryRootTopicChildrenVideo struct { Name string `json:"name"` } -func (v InterfaceNoFragmentsQueryRootTopicChildrenVideo) implementsGraphQLInterfaceInterfaceNoFragmentsQueryRootTopicChildrenContent() { -} - func InterfaceNoFragmentsQuery( client graphql.Client, ) (*InterfaceNoFragmentsQueryResponse, error) { diff --git a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go index 9acc72f..af2371f 100644 --- a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go @@ -13,9 +13,6 @@ type UnionNoFragmentsQueryRandomLeafArticle struct { Typename string `json:"__typename"` } -func (v UnionNoFragmentsQueryRandomLeafArticle) implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() { -} - // UnionNoFragmentsQueryRandomLeafLeafContent includes the requested fields of the GraphQL type LeafContent. // The GraphQL type's documentation follows. // @@ -24,14 +21,16 @@ type UnionNoFragmentsQueryRandomLeafLeafContent interface { implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() } +func (v UnionNoFragmentsQueryRandomLeafArticle) implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() { +} +func (v UnionNoFragmentsQueryRandomLeafVideo) implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() { +} + // UnionNoFragmentsQueryRandomLeafVideo includes the requested fields of the GraphQL type Video. type UnionNoFragmentsQueryRandomLeafVideo struct { Typename string `json:"__typename"` } -func (v UnionNoFragmentsQueryRandomLeafVideo) implementsGraphQLInterfaceUnionNoFragmentsQueryRandomLeafLeafContent() { -} - // UnionNoFragmentsQueryResponse is returned by UnionNoFragmentsQuery on success. type UnionNoFragmentsQueryResponse struct { RandomLeaf UnionNoFragmentsQueryRandomLeafLeafContent `json:"-"` diff --git a/generate/types.go b/generate/types.go index 4027da0..23fdf1f 100644 --- a/generate/types.go +++ b/generate/types.go @@ -1,412 +1,188 @@ package generate -// This file is the core of genqlient: it's what generates the types into which -// we will unmarshal. -// -// TODO: this file really really needs a file-comment explaining what's going -// on. probably writing that will help me figure out how to make it make more -// sense! +// This file defines the data structures from which genqlient generates types, +// and the code to write them out as actual Go code. The main entrypoint is +// goType, which represents such a type, but convert.go also constructs each +// of the implementing types, by traversing the GraphQL operation and schema. import ( "fmt" + "io" "strings" - - "github.com/vektah/gqlparser/v2/ast" ) -type typeBuilder struct { - strings.Builder - *generator +// goType represents a type for which we'll generate code. +type goType interface { + // WriteDefinition writes the code for this type into the given io.Writer. + // + // TODO(benkraft): Some of the implementations might now benefit from being + // converted to templates. + WriteDefinition(io.Writer, *generator) error + + // Reference returns the Go name of this type, e.g. []*MyStruct, and may be + // used to refer to it in Go code. + Reference() string } -func (g *generator) baseTypeForOperation(operation ast.Operation) (*ast.Definition, error) { - switch operation { - case ast.Query: - return g.schema.Query, nil - case ast.Mutation: - return g.schema.Mutation, nil - case ast.Subscription: - if !allowBrokenFeatures { - return nil, errorf(nil, "genqlient does not yet support subscriptions") - } - return g.schema.Subscription, nil - default: - return nil, errorf(nil, "unexpected operation: %v", operation) +var ( + _ goType = (*goOpaqueType)(nil) + _ goType = (*goSliceType)(nil) + _ goType = (*goPointerType)(nil) + _ goType = (*goEnumType)(nil) + _ goType = (*goStructType)(nil) + _ goType = (*goInterfaceType)(nil) +) + +type ( + // goOpaqueType represents a user-defined or builtin type, used to + // represent a GraphQL scalar. + goOpaqueType struct{ GoRef string } + // goSliceType represents the Go type []Elem, used to represent GraphQL + // list types. + goSliceType struct{ Elem goType } + // goSliceType represents the Go type *Elem, used when requested by the + // user (perhaps to handle nulls explicitly, or to avoid copying large + // structures). + goPointerType struct{ Elem goType } +) + +// 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 *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() } + +// 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 + Description string + Values []goEnumValue +} + +type goEnumValue struct { + Name string + Description string +} + +func (typ *goEnumType) WriteDefinition(w io.Writer, g *generator) error { + // All GraphQL enums have underlying type string (in the Go sense). + writeDescription(w, typ.Description) + fmt.Fprintf(w, "type %s string\n", typ.GoName) + fmt.Fprintf(w, "const (\n") + for _, val := range typ.Values { + writeDescription(w, val.Description) + fmt.Fprintf(w, "%s %s = \"%s\"\n", + typ.GoName+goConstName(val.Name), + typ.GoName, val.Name) } -} - -func (g *generator) getTypeForOperation(operation *ast.OperationDefinition, queryOptions *GenqlientDirective) (name string, err error) { - name = operation.Name + "Response" - - if def, ok := g.typeMap[name]; ok { - // TODO: check for and handle conflicts a better way - return "", errorf(operation.Position, "%s defined twice:\n%s", name, def) - } - - fields, err := selections(g, operation.SelectionSet, queryOptions) - if err != nil { - return "", err - } - - baseType, err := g.baseTypeForOperation(operation.Operation) - if err != nil { - return "", errorf(operation.Position, "%v", err) - } - - description := fmt.Sprintf("%v is returned by %v on success.", name, operation.Name) - - builder := &typeBuilder{generator: g} - err = builder.writeTypedef( - name, operation.Name, baseType, operation.Position, fields, queryOptions, description) - return name, err -} - -var builtinTypes = map[string]string{ - // GraphQL guarantees int32 is enough, but using int seems more idiomatic - "Int": "int", - "Float": "float64", - "String": "string", - "Boolean": "bool", - "ID": "string", -} - -func (g *generator) typeName(prefix string, typ *ast.Definition) (name, nextPrefix string) { - typeGoName := upperFirst(typ.Name) - if typ.Kind == ast.Enum || typ.Kind == ast.InputObject { - // If we're an enum or 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. - return typeGoName, typeGoName - } - - name = prefix - if !strings.HasSuffix(prefix, 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 += 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. So we just use the input prefix. - return name, prefix - } - - // Otherwise, the name will also be the prefix for the next type. - return name, name -} - -func (g *generator) getTypeForInputType(opName string, typ *ast.Type, options, queryOptions *GenqlientDirective) (string, error) { - // Sort of a hack: case the input type name to match the op-name. - name := matchFirst(typ.Name(), opName) - builder := &typeBuilder{generator: g} - // note prefix is ignored here (see generator.typeName) - err := builder.writeType(name, "", typ, selectionsForInputType(g, typ, queryOptions), options) - return builder.String(), err -} - -type field interface { - Alias() string - Options() (*GenqlientDirective, error) - Description() string - Type() *ast.Type - Pos() *ast.Position - SubFields() ([]field, error) -} - -type outputField struct { - *generator - queryOptions *GenqlientDirective - field *ast.Field -} - -func (s outputField) Alias() string { - // gqlparser sets Alias even if the field is not aliased, see e.g. - // https://github.com/vektah/gqlparser/v2/blob/c06d8e0d135f285e37e7f1ff397f10e049733eb3/parser/query.go#L150 - return s.field.Alias -} - -func (s outputField) Options() (*GenqlientDirective, error) { - _, directive, err := s.generator.parsePrecedingComment(s.field, s.field.Position) - if err != nil { - return nil, err - } - return s.queryOptions.merge(directive), nil -} - -func (s outputField) Description() string { - if s.field.Definition == nil { - return "" - } - return s.field.Definition.Description -} - -func (s outputField) Type() *ast.Type { - if s.field.Definition == nil { - return nil - } - return s.field.Definition.Type -} - -func (s outputField) Pos() *ast.Position { - return s.field.Position -} - -func (s outputField) SubFields() ([]field, error) { - return selections(s.generator, s.field.SelectionSet, s.queryOptions) -} - -func selections(g *generator, selectionSet ast.SelectionSet, options *GenqlientDirective) ([]field, error) { - retval := make([]field, len(selectionSet)) - for i, selection := range selectionSet { - switch selection := selection.(type) { - case *ast.Field: - retval[i] = outputField{g, options, selection} - case *ast.FragmentSpread: - return nil, errorf(selection.Position, "not implemented: %T", selection) - case *ast.InlineFragment: - return nil, errorf(selection.Position, "not implemented: %T", selection) - default: - return nil, errorf(nil, "invalid selection type: %T", selection) - } - } - return retval, nil -} - -type inputField struct { - *generator - field *ast.FieldDefinition - queryOptions *GenqlientDirective -} - -func (s inputField) Alias() string { return s.field.Name } -func (s inputField) Options() (*GenqlientDirective, error) { - _, directive, err := s.generator.parsePrecedingComment(s.field, s.field.Position) - if err != nil { - return nil, err - } - return s.queryOptions.merge(directive), nil -} -func (s inputField) Description() string { return s.field.Description } -func (s inputField) Type() *ast.Type { return s.field.Type } -func (s inputField) Pos() *ast.Position { return s.field.Position } - -func (s inputField) SubFields() ([]field, error) { - return selectionsForInputType(s.generator, s.field.Type, s.queryOptions), nil -} - -func selectionsForInputType(g *generator, typ *ast.Type, queryOptions *GenqlientDirective) []field { - def := g.schema.Types[typ.Name()] - fields := make([]field, len(def.Fields)) - for i, field := range def.Fields { - fields[i] = inputField{g, field, queryOptions} - } - return fields -} - -func (builder *typeBuilder) writeField(typeNamePrefix string, field field) error { - jsonName := field.Alias() - // We need an exportable name for JSON-marshaling. - goName := upperFirst(jsonName) - - typ := field.Type() - if typ == nil { - // Unclear why gqlparser hasn't already rejected this, - // but empirically it might not. - return errorf(field.Pos(), "undefined field %v", field.Alias()) - } - - fields, err := field.SubFields() - if err != nil { - return err - } - - options, err := field.Options() - if err != nil { - return err - } - - typedef := builder.schema.Types[typ.Name()] - - builder.writeDescription(field.Description()) - builder.WriteString(goName) - builder.WriteRune(' ') - - // Note we don't deduplicate suffixes 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 named based on the GraphQL alias (Go name), 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. - name, namePrefix := builder.typeName(typeNamePrefix+goName, typedef) - err = builder.writeType(name, namePrefix, typ, fields, options) - if err != nil { - return err - } - - if typedef.IsAbstractType() { - // abstract types are handled in our UnmarshalJSON - jsonName = "-" - } - - fmt.Fprintf(builder, " `json:\"%s\"`\n", jsonName) + fmt.Fprintf(w, ")\n") return nil } -func (builder *typeBuilder) writeType(name, namePrefix string, typ *ast.Type, fields []field, options *GenqlientDirective) 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 - for typ.Elem != nil { - // Type is a list. - builder.WriteString("[]") - typ = typ.Elem - } - if options.GetPointer() { - // Note this does []*T or [][]*T, not e.g. *[][]T. See #16. - builder.WriteString("*") - } +func (typ *goEnumType) Reference() string { return typ.GoName } - // If this is a builtin type or custom scalar, just refer to it. - def := builder.schema.Types[typ.Name()] - goName, ok := builder.Config.Scalars[def.Name] - if ok { - newName, err := builder.addRef(goName) - builder.WriteString(newName) - return err - } - goName, ok = builtinTypes[def.Name] - if ok { - builder.WriteString(goName) - return nil - } - - // Else, write the name, then generate the definition. - builder.WriteString(name) - - childBuilder := &typeBuilder{generator: builder.generator} - return childBuilder.writeTypedef(name, namePrefix, def, typ.Position, fields, options, "") +// goStructType represents a Go struct type used to represent a GraphQL object +// or input-object type. +type goStructType struct { + GoName string + Description string + GraphQLName string + Fields []*goStructField + // Incomplete is set if this type contains only certain fields of the + // corresponding GraphQL type (i.e. those selected by the operation) in + // which case we put a note in the doc-comment saying as much. + Incomplete bool } -func (builder *typeBuilder) writeTypedef( - typeName, typeNamePrefix string, - typedef *ast.Definition, - pos *ast.Position, - fields []field, - options *GenqlientDirective, //nolint:unparam // it is used! - description string, // defaults to typedef.Description -) (err error) { - defer func() { - // Whenever we're done, add the type to the type-map. - // TODO: there's got to be a better way than defer. - if err == nil { - // TODO: this should also check for conflicts (except not for enums - // and input-objects, see above) - builder.typeMap[typeName] = builder.String() - } - }() - - if description == "" { - switch typedef.Kind { - case ast.Object, ast.Interface, ast.Union: - // For types where we only have some fields, note that, along with - // the GraphQL documentation (if any). We don't want to just use - // the GraphQL documentation, since it may refer to fields we - // haven't selected, say. - // TODO: When we implement interfaces and unions more completely, - // also mention the concrete types they might be. - description = fmt.Sprintf( - "%v includes the requested fields of the GraphQL type %v.", - typeName, typedef.Name) - if typedef.Description != "" { - description = fmt.Sprintf( - "%v\nThe GraphQL type's documentation follows.\n\n%v", - description, typedef.Description) - } - default: - description = typedef.Description - } - } - builder.writeDescription(description) - - fmt.Fprintf(builder, "type %s ", typeName) - switch typedef.Kind { - case ast.Object, ast.InputObject: - builder.WriteString("struct {\n") - for _, field := range fields { - err := builder.writeField(typeNamePrefix, field) - if err != nil { - return err - } - } - builder.WriteString("}") - - // If any field is abstract, we need an UnmarshalJSON method to handle - // it. - return builder.maybeWriteUnmarshal(typeName, typeNamePrefix, fields) - - case ast.Interface, ast.Union: - if !allowBrokenFeatures { - return errorf(pos, "not implemented: %v", typedef.Kind) - } - - // First, write the interface type. - builder.WriteString("interface {\n") - implementsMethodName := fmt.Sprintf("implementsGraphQLInterface%v", typeName) - // TODO: Also write GetX() accessor methods for fields of the interface - builder.WriteString(implementsMethodName) - builder.WriteString("()\n") - builder.WriteString("}") - - // Then, write the implementations. - // TODO(benkraft): Put a doc-comment somewhere with the list. - for _, impldef := range builder.schema.GetPossibleTypes(typedef) { - name, namePrefix := builder.typeName(typeNamePrefix, impldef) - implBuilder := &typeBuilder{generator: builder.generator} - err := implBuilder.writeTypedef(name, namePrefix, impldef, pos, fields, options, "") - if err != nil { - return err - } - - builder.typeMap[name] += fmt.Sprintf( - "\nfunc (v %v) %v() {}", name, implementsMethodName) - } - - return nil - - case ast.Enum: - // All GraphQL enums have underlying type string (in the Go sense). - builder.WriteString("string\n") - builder.WriteString("const (\n") - for _, val := range typedef.EnumValues { - builder.writeDescription(val.Description) - fmt.Fprintf(builder, "%s %s = \"%s\"\n", - typeName+goConstName(val.Name), - typeName, val.Name) - } - builder.WriteString(")\n") - return nil - case ast.Scalar: - return errorf(pos, "unknown scalar %v: please add it to genqlient.yaml", typedef.Name) - default: - return errorf(pos, "unexpected kind: %v", typedef.Kind) - } +type goStructField struct { + GoName string + GoType goType + JSONName string + Description string } -func (builder *typeBuilder) writeDescription(desc string) { +func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error { + description := typ.Description + if typ.Incomplete { + description = incompleteTypeDescription(typ.GoName, typ.GraphQLName, typ.Description) + } + writeDescription(w, description) + + fmt.Fprintf(w, "type %s struct {\n", typ.GoName) + for _, field := range typ.Fields { + writeDescription(w, field.Description) + jsonName := field.JSONName + if _, ok := field.GoType.(*goInterfaceType); ok { + // abstract types are handled in our UnmarshalJSON + jsonName = "-" + } + fmt.Fprintf(w, "\t%s %s `json:\"%s\"`\n", + field.GoName, field.GoType.Reference(), jsonName) + } + fmt.Fprintf(w, "}\n") + + return g.maybeWriteUnmarshal(w, typ) +} + +func (typ *goStructType) Reference() string { return typ.GoName } + +// goInterfaceType represents a Go interface type, used to represent a GraphQL +// interface or union type. +type goInterfaceType struct { + GoName string + Description string + GraphQLName string + Implementations []*goStructType +} + +func (typ *goInterfaceType) WriteDefinition(w io.Writer, g *generator) error { + // TODO(benkraft): also mention the list of implementations. + description := incompleteTypeDescription(typ.GoName, typ.GraphQLName, typ.Description) + writeDescription(w, description) + + // Write the interface. + fmt.Fprintf(w, "type %s interface {\n", typ.GoName) + implementsMethodName := fmt.Sprintf("implementsGraphQLInterface%v", typ.GoName) + // TODO(benkraft): Also write GetX() accessor methods for fields of the interface + fmt.Fprintf(w, "\t%s()\n", implementsMethodName) + fmt.Fprintf(w, "}\n") + + // Now, write out the implementations. + for _, impl := range typ.Implementations { + fmt.Fprintf(w, "func (v %s) %s() {}\n", + impl.Reference(), implementsMethodName) + } + + return nil +} + +func (typ *goInterfaceType) Reference() string { return typ.GoName } + +func incompleteTypeDescription(goName, graphQLName, description string) string { + // For types where we only have some fields, note that, along with + // the GraphQL documentation (if any). We don't want to just use + // the GraphQL documentation, since it may refer to fields we + // haven't selected, say. + prefix := fmt.Sprintf( + "%v includes the requested fields of the GraphQL type %v.", + goName, graphQLName) + if description != "" { + return fmt.Sprintf( + "%v\nThe GraphQL type's documentation follows.\n\n%v", + prefix, description) + } + return prefix +} + +func writeDescription(w io.Writer, desc string) { if desc != "" { for _, line := range strings.Split(desc, "\n") { - builder.WriteString("// " + strings.TrimLeft(line, " \t") + "\n") + fmt.Fprintf(w, "// %s\n", strings.TrimLeft(line, " \t")) } } } diff --git a/generate/unmarshal.go b/generate/unmarshal.go index c8bc55d..d3cd13a 100644 --- a/generate/unmarshal.go +++ b/generate/unmarshal.go @@ -1,5 +1,9 @@ package generate +import "io" + +// TODO(benkraft): We could potentially get rid of these now, and do everything +// directly from the types. type templateData struct { // Go type to which the method will be added Type string @@ -19,23 +23,22 @@ type concreteType struct { GoName, GraphQLName string } -func (builder *typeBuilder) maybeWriteUnmarshal(typeName, typeNamePrefix string, fields []field) error { - data := templateData{Type: typeName} - for _, field := range fields { - typedef := builder.schema.Types[field.Type().Name()] - if typedef.IsAbstractType() { +func (g *generator) maybeWriteUnmarshal(w io.Writer, typ *goStructType) error { + data := templateData{Type: typ.GoName} + for _, field := range typ.Fields { + // TODO(benkraft): To handle list-of-interface fields, we should really + // be "unwrapping" any goSliceType/goPointerType wrappers to find the + // goInterfaceType. + if iface, ok := field.GoType.(*goInterfaceType); ok { fieldInfo := abstractField{ - GoName: upperFirst(field.Alias()), - JSONName: field.Alias(), + GoName: field.GoName, + JSONName: field.JSONName, } - for _, typedef := range builder.schema.GetPossibleTypes(typedef) { - // TODO: this is slightly fragile (it needs to match the - // similar call in writeField) - goName, _ := builder.typeName(typeNamePrefix+fieldInfo.GoName, typedef) + for _, impl := range iface.Implementations { fieldInfo.ConcreteTypes = append(fieldInfo.ConcreteTypes, concreteType{ - GoName: goName, - GraphQLName: typedef.Name, + GoName: impl.GoName, + GraphQLName: impl.GraphQLName, }) } data.Fields = append(data.Fields, fieldInfo) @@ -46,11 +49,10 @@ func (builder *typeBuilder) maybeWriteUnmarshal(typeName, typeNamePrefix string, return nil } - _, err := builder.addRef("encoding/json.Unmarshal") + _, err := g.addRef("encoding/json.Unmarshal") if err != nil { return err } - builder.WriteString("\n\n") - return builder.execute("unmarshal.go.tmpl", builder, data) + return g.execute("unmarshal.go.tmpl", w, data) } diff --git a/generate/unmarshal.go.tmpl b/generate/unmarshal.go.tmpl index 8a9638c..dafd038 100644 --- a/generate/unmarshal.go.tmpl +++ b/generate/unmarshal.go.tmpl @@ -1,3 +1,6 @@ +{{/* (the blank lines at the start are intentional, to separate + UnmarshalJSON from the function it follows) */}} + func (v *{{.Type}}) UnmarshalJSON(b []byte) error { var firstPass struct{ *{{.Type}} @@ -12,6 +15,9 @@ func (v *{{.Type}}) UnmarshalJSON(b []byte) error { return err } + {{/* TODO(benkraft): split this out into a separate helper function, + generated for each interface type, rather than generating it inline + with the struct type. */}} {{range .Fields -}} var tn struct { TypeName string `json:"__typename"` } err = {{ref "encoding/json.Unmarshal"}}(firstPass.{{.GoName}}, &tn)