total rewrite to interface handling; not complete but it compiles

This commit is contained in:
Ben Kraft
2020-07-16 13:28:43 -07:00
parent af4a765a32
commit cf7136ca65
33 changed files with 712 additions and 173 deletions
+2 -1
View File
@@ -51,7 +51,7 @@ For a complete working example, see `example/`.
## Tests ## Tests
`go test ./...` does some perfunctory tests. (This is run by GitHub Actions.) `go test ./...` tests code generation. (This is run by GitHub Actions.)
`make example` tests that everything wires up to a real API correctly. `make example` tests that everything wires up to a real API correctly.
@@ -69,6 +69,7 @@ Config options:
- proper config/arguments setup (e.g. with [viper](https://github.com/spf13/viper) - proper config/arguments setup (e.g. with [viper](https://github.com/spf13/viper)
Other: Other:
- naming collisions are a mess
- error-checking/validation/etc. everywhere - error-checking/validation/etc. everywhere
- more tests - more tests
- documentation - documentation
+10 -6
View File
@@ -8,16 +8,20 @@ import (
"github.com/Khan/genql/graphql" "github.com/Khan/genql/graphql"
) )
type User struct {
MyName *string
}
type User1 struct {
TheirName *string `json:"theirName"`
}
type getUserResponse struct { type getUserResponse struct {
User *struct { User *User1 `json:"user"`
TheirName *string `json:"theirName"`
} `json:"user"`
} }
type getViewerResponse struct { type getViewerResponse struct {
Viewer struct { Viewer User `json:"viewer"`
MyName *string
} `json:"viewer"`
} }
func getViewer(ctx context.Context, client *graphql.Client) (*getViewerResponse, error) { func getViewer(ctx context.Context, client *graphql.Client) (*getViewerResponse, error) {
+5 -12
View File
@@ -4,22 +4,14 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"go/format" "go/format"
"path/filepath"
"runtime"
"sort" "sort"
"strings" "strings"
"text/template"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/ast"
"github.com/vektah/gqlparser/formatter" "github.com/vektah/gqlparser/formatter"
) )
// TODO: package template into the binary using one of those asset thingies var fileTemplate = mustTemplate("operation.go.tmpl")
var _, thisFilename, _, _ = runtime.Caller(0)
var tmplRelFilename = "operation.go.tmpl"
var tmplAbsFilename = filepath.Join(filepath.Dir(thisFilename), tmplRelFilename)
var tmpl = template.Must(template.ParseFiles(tmplAbsFilename))
// generator is the context for the codegen process (and ends up getting passed // generator is the context for the codegen process (and ends up getting passed
// to the template). // to the template).
@@ -29,8 +21,9 @@ type generator struct {
// The list of operations for which to generate code. // The list of operations for which to generate code.
Operations []operation Operations []operation
// The types needed for these operations. // The types needed for these operations.
typeMap map[string]string typeMap map[string]string
schema *ast.Schema ImportJSON bool
schema *ast.Schema
} }
type operation struct { type operation struct {
@@ -165,7 +158,7 @@ func Generate(config *Config) ([]byte, error) {
} }
var buf bytes.Buffer var buf bytes.Buffer
err = tmpl.Execute(&buf, g) err = fileTemplate.Execute(&buf, g)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not render template: %v", err) return nil, fmt.Errorf("could not render template: %v", err)
} }
+4 -1
View File
@@ -1,9 +1,12 @@
package {{$.Config.Package}} package {{.Config.Package}}
// Code generated by github.com/Khan/genql, DO NOT EDIT. // Code generated by github.com/Khan/genql, DO NOT EDIT.
import ( import (
"context" "context"
{{- if .ImportJSON -}}
"encoding/json"
{{end}}
"github.com/Khan/genql/graphql" "github.com/Khan/genql/graphql"
) )
+15
View File
@@ -0,0 +1,15 @@
package generate
import (
"path/filepath"
"runtime"
"text/template"
)
// TODO: package templates into the binary using one of those asset thingies
var _, thisFilename, _, _ = runtime.Caller(0)
var thisDir = filepath.Dir(thisFilename)
func mustTemplate(relFilename string) *template.Template {
return template.Must(template.ParseFiles(filepath.Join(thisDir, relFilename)))
}
+10
View File
@@ -0,0 +1,10 @@
query InterfaceNoFragmentsQuery {
root {
id
name
children {
id
name
}
}
}
+97
View File
@@ -0,0 +1,97 @@
package test
// Code generated by github.com/Khan/genql, DO NOT EDIT.
import (
"context"
"github.com/Khan/genql/graphql"
)
type Article struct {
Id string `json:"id"`
Name string `json:"name"`
}
func (v Article) implementsGraphQLInterfaceContent() {}
type Content interface {
implementsGraphQLInterfaceContent()
}
type InterfaceNoFragmentsQueryResponse struct {
Root Topic `json:"root"`
}
type Topic struct {
Id string `json:"id"`
Name string `json:"name"`
Children []Content `json:"-"`
}
func (v *Topic) UnmarshalJSON(b []byte) error {
var firstPass struct {
*Topic
Children json.RawMessage `json:"children"`
}
firstPass.Topic = v
err := json.Unmarshal(b, &typenames)
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 = Article{}
err = json.Unmarshal(
firstPass.Children, &v.Children)
case "Video":
v.Children = Video{}
err = json.Unmarshal(
firstPass.Children, &v.Children)
case "Topic":
v.Children = Topic{}
err = json.Unmarshal(
firstPass.Children, &v.Children)
}
if err != nil {
return err
}
}
type Video struct {
Id string `json:"id"`
Name string `json:"name"`
}
func (v Video) implementsGraphQLInterfaceContent() {}
func InterfaceNoFragmentsQuery(client *graphql.Client) (*InterfaceNoFragmentsQueryResponse, error) {
var retval InterfaceNoFragmentsQueryResponse
err := client.MakeRequest(context.Background(), `
query InterfaceNoFragmentsQuery {
root {
id
name
children {
id
name
}
}
}
`, &retval, nil)
return &retval, err
}
+1 -1
View File
@@ -1 +1 @@
{ User: user { ID: id } } query QueryWithAlias { User: user { ID: id } }
+27 -3
View File
@@ -1,5 +1,29 @@
type Response struct { package test
User *struct {
ID string // Code generated by github.com/Khan/genql, DO NOT EDIT.
import (
"context"
"github.com/Khan/genql/graphql"
)
type QueryWithAliasResponse struct {
User *User
}
type User struct {
ID string
}
func QueryWithAlias(client *graphql.Client) (*QueryWithAliasResponse, error) {
var retval QueryWithAliasResponse
err := client.MakeRequest(context.Background(), `
query QueryWithAlias {
User: user {
ID: id
} }
} }
`, &retval, nil)
return &retval, err
}
+1 -1
View File
@@ -1,4 +1,4 @@
{ query QueryWithDoubleAlias {
user { user {
ID: id ID: id
AlsoID: id AlsoID: id
+31 -6
View File
@@ -1,6 +1,31 @@
type Response struct { package test
User *struct {
ID string // Code generated by github.com/Khan/genql, DO NOT EDIT.
AlsoID string
} `json:"user"` import (
} "context"
"github.com/Khan/genql/graphql"
)
type QueryWithDoubleAliasResponse struct {
User *User `json:"user"`
}
type User struct {
ID string
AlsoID string
}
func QueryWithDoubleAlias(client *graphql.Client) (*QueryWithDoubleAliasResponse, error) {
var retval QueryWithDoubleAliasResponse
err := client.MakeRequest(context.Background(), `
query QueryWithDoubleAlias {
user {
ID: id
AlsoID: id
}
}
`, &retval, nil)
return &retval, err
}
+1 -1
View File
@@ -1,4 +1,4 @@
{ query QueryWithEnums {
user { user {
roles roles
} }
+32 -8
View File
@@ -1,12 +1,36 @@
type Response struct { package test
User *struct {
Roles []role `json:"roles"` // Code generated by github.com/Khan/genql, DO NOT EDIT.
} `json:"user"`
import (
"context"
"github.com/Khan/genql/graphql"
)
type QueryWithEnumsResponse struct {
User *User `json:"user"`
} }
type role string type Role string
const ( const (
studentRole role = "STUDENT" StudentRole Role = "STUDENT"
teacherRole role = "TEACHER" TeacherRole Role = "TEACHER"
) )
type User struct {
Roles []Role `json:"roles"`
}
func QueryWithEnums(client *graphql.Client) (*QueryWithEnumsResponse, error) {
var retval QueryWithEnumsResponse
err := client.MakeRequest(context.Background(), `
query QueryWithEnums {
user {
roles
}
}
`, &retval, nil)
return &retval, err
}
+1 -1
View File
@@ -1,4 +1,4 @@
query ($query: UserQueryInput) { query QueryWithInput($query: UserQueryInput) {
user(query: $query) { user(query: $query) {
id id
} }
+40 -12
View File
@@ -1,19 +1,47 @@
type Response struct { package test
User *struct {
Id string `json:"id"`
} `json:"user"`
}
type role string // Code generated by github.com/Khan/genql, DO NOT EDIT.
const ( import (
studentRole role = "STUDENT" "context"
teacherRole role = "TEACHER"
"github.com/Khan/genql/graphql"
) )
type userQueryInput struct { type QueryWithInputResponse struct {
User *User `json:"user"`
}
type Role string
const (
StudentRole Role = "STUDENT"
TeacherRole Role = "TEACHER"
)
type User struct {
Id string `json:"id"`
}
type UserQueryInput struct {
Email *string `json:"email"` Email *string `json:"email"`
Name *string `json:"name"` Name *string `json:"name"`
Id *string `json:"id"` Id *string `json:"id"`
Role *role `json:"role"` Role *Role `json:"role"`
} }
func QueryWithInput(client *graphql.Client, query *UserQueryInput) (*QueryWithInputResponse, error) {
variables := map[string]interface{}{
"query": query,
}
var retval QueryWithInputResponse
err := client.MakeRequest(context.Background(), `
query QueryWithInput ($query: UserQueryInput) {
user(query: $query) {
id
}
}
`, &retval, variables)
return &retval, err
}
+1 -1
View File
@@ -1,4 +1,4 @@
{ query QueryWithSlices {
user { user {
emails emails
emailsOrNull emailsOrNull
+35 -8
View File
@@ -1,8 +1,35 @@
type Response struct { package test
User *struct {
Emails []string `json:"emails"` // Code generated by github.com/Khan/genql, DO NOT EDIT.
EmailsOrNull []string `json:"emailsOrNull"`
EmailsWithNulls []*string `json:"emailsWithNulls"` import (
EmailsWithNullsOrNull []*string `json:"emailsWithNullsOrNull"` "context"
} `json:"user"`
} "github.com/Khan/genql/graphql"
)
type QueryWithSlicesResponse struct {
User *User `json:"user"`
}
type User struct {
Emails []string `json:"emails"`
EmailsOrNull []string `json:"emailsOrNull"`
EmailsWithNulls []*string `json:"emailsWithNulls"`
EmailsWithNullsOrNull []*string `json:"emailsWithNullsOrNull"`
}
func QueryWithSlices(client *graphql.Client) (*QueryWithSlicesResponse, error) {
var retval QueryWithSlicesResponse
err := client.MakeRequest(context.Background(), `
query QueryWithSlices {
user {
emails
emailsOrNull
emailsWithNulls
emailsWithNullsOrNull
}
}
`, &retval, nil)
return &retval, err
}
+1 -1
View File
@@ -1,4 +1,4 @@
{ query QueryWithStructs {
user { user {
authMethods { authMethods {
provider provider
+37 -8
View File
@@ -1,8 +1,37 @@
type Response struct { package test
User *struct {
AuthMethods []struct { // Code generated by github.com/Khan/genql, DO NOT EDIT.
Provider *string `json:"provider"`
Email *string `json:"email"` import (
} `json:"authMethods"` "context"
} `json:"user"`
} "github.com/Khan/genql/graphql"
)
type AuthMethod struct {
Provider *string `json:"provider"`
Email *string `json:"email"`
}
type QueryWithStructsResponse struct {
User *User `json:"user"`
}
type User struct {
AuthMethods []AuthMethod `json:"authMethods"`
}
func QueryWithStructs(client *graphql.Client) (*QueryWithStructsResponse, error) {
var retval QueryWithStructsResponse
err := client.MakeRequest(context.Background(), `
query QueryWithStructs {
user {
authMethods {
provider
email
}
}
}
`, &retval, nil)
return &retval, err
}
+1 -1
View File
@@ -1 +1 @@
{ user { id } } query SimpleQuery { user { id } }
+28 -4
View File
@@ -1,5 +1,29 @@
type Response struct { package test
User *struct {
Id string `json:"id"` // Code generated by github.com/Khan/genql, DO NOT EDIT.
} `json:"user"`
import (
"context"
"github.com/Khan/genql/graphql"
)
type SimpleQueryResponse struct {
User *User `json:"user"`
}
type User struct {
Id string `json:"id"`
}
func SimpleQuery(client *graphql.Client) (*SimpleQueryResponse, error) {
var retval SimpleQueryResponse
err := client.MakeRequest(context.Background(), `
query SimpleQuery {
user {
id
}
}
`, &retval, nil)
return &retval, err
} }
+1 -1
View File
@@ -1,4 +1,4 @@
{ query TypeNameQuery {
user { user {
__typename __typename
id id
+31 -6
View File
@@ -1,6 +1,31 @@
type Response struct { package test
User *struct {
Typename *string `json:"__typename"` // Code generated by github.com/Khan/genql, DO NOT EDIT.
Id string `json:"id"`
} `json:"user"` import (
} "context"
"github.com/Khan/genql/graphql"
)
type TypeNameQueryResponse struct {
User *User `json:"user"`
}
type User struct {
Typename *string `json:"__typename"`
Id string `json:"id"`
}
func TypeNameQuery(client *graphql.Client) (*TypeNameQueryResponse, error) {
var retval TypeNameQueryResponse
err := client.MakeRequest(context.Background(), `
query TypeNameQuery {
user {
__typename
id
}
}
`, &retval, nil)
return &retval, err
}
+5
View File
@@ -0,0 +1,5 @@
query UnionNoFragmentsQuery {
randomLeaf {
__typename
}
}
+79
View File
@@ -0,0 +1,79 @@
package test
// Code generated by github.com/Khan/genql, DO NOT EDIT.
import (
"context"
"github.com/Khan/genql/graphql"
)
type Article struct {
Typename *string `json:"__typename"`
}
func (v Article) implementsGraphQLInterfaceLeafContent() {}
type LeafContent interface {
implementsGraphQLInterfaceLeafContent()
}
type UnionNoFragmentsQueryResponse struct {
RandomLeaf LeafContent `json:"-"`
}
func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
var firstPass struct {
*UnionNoFragmentsQueryResponse
RandomLeaf json.RawMessage `json:"randomLeaf"`
}
firstPass.UnionNoFragmentsQueryResponse = v
err := json.Unmarshal(b, &typenames)
if err != nil {
return err
}
var tn struct {
TypeName string `json:"__typename"`
}
err = json.Unmarshal(firstPass.RandomLeaf, &tn)
if err != nil {
return err
}
switch tn.TypeName {
case "Article":
v.RandomLeaf = Article{}
err = json.Unmarshal(
firstPass.RandomLeaf, &v.RandomLeaf)
case "Video":
v.RandomLeaf = Video{}
err = json.Unmarshal(
firstPass.RandomLeaf, &v.RandomLeaf)
}
if err != nil {
return err
}
}
type Video struct {
Typename *string `json:"__typename"`
}
func (v Video) implementsGraphQLInterfaceLeafContent() {}
func UnionNoFragmentsQuery(client *graphql.Client) (*UnionNoFragmentsQueryResponse, error) {
var retval UnionNoFragmentsQueryResponse
err := client.MakeRequest(context.Background(), `
query UnionNoFragmentsQuery {
randomLeaf {
__typename
}
}
`, &retval, nil)
return &retval, err
}
+1 -1
View File
@@ -1,4 +1,4 @@
{ query UsesEnumTwiceQuery {
Me: user { roles } Me: user { roles }
OtherUser: user { roles } OtherUser: user { roles }
} }
+48 -12
View File
@@ -1,15 +1,51 @@
type Response struct { package test
Me *struct {
Roles []role `json:"roles"`
}
OtherUser *struct {
Roles []role `json:"roles"`
}
}
type role string // Code generated by github.com/Khan/genql, DO NOT EDIT.
import (
"context"
"github.com/Khan/genql/graphql"
)
type Role string
const ( const (
studentRole role = "STUDENT" StudentRole Role = "STUDENT"
teacherRole role = "TEACHER" TeacherRole Role = "TEACHER"
) )
type Role1 string
const (
StudentRole1 Role1 = "STUDENT"
TeacherRole1 Role1 = "TEACHER"
)
type User struct {
Roles []Role `json:"roles"`
}
type User1 struct {
Roles []Role1 `json:"roles"`
}
type UsesEnumTwiceQueryResponse struct {
Me *User
OtherUser *User1
}
func UsesEnumTwiceQuery(client *graphql.Client) (*UsesEnumTwiceQueryResponse, error) {
var retval UsesEnumTwiceQueryResponse
err := client.MakeRequest(context.Background(), `
query UsesEnumTwiceQuery {
Me: user {
roles
}
OtherUser: user {
roles
}
}
`, &retval, nil)
return &retval, err
}
+57 -32
View File
@@ -2,6 +2,7 @@ package generate
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/ast"
@@ -54,11 +55,16 @@ func (g *generator) addTypeForDefinition(nameOverride string, typ *ast.Definitio
name = nameOverride name = nameOverride
} else { } else {
// TODO: casing should be configurable // TODO: casing should be configurable
name = lowerFirst(typ.Name) name = upperFirst(typ.Name)
} }
if _, ok := g.typeMap[name]; ok { // TODO: in some cases we can deduplicate, do that
return name, nil // TODO: nicer naming scheme
i := 0
origName := name
for g.typeMap[name] != "" {
i++
name = origName + strconv.Itoa(i)
} }
builder := &typeBuilder{typeName: name, generator: g} builder := &typeBuilder{typeName: name, generator: g}
@@ -73,22 +79,27 @@ func (g *generator) addTypeForDefinition(nameOverride string, typ *ast.Definitio
} }
func (g *generator) getTypeForInputType(typ *ast.Type) (string, error) { func (g *generator) getTypeForInputType(typ *ast.Type) (string, error) {
builder := &typeBuilder{typeName: lowerFirst(typ.Name()), generator: g} builder := &typeBuilder{typeName: upperFirst(typ.Name()), generator: g}
err := builder.writeType(typ, selectionsForType(g, typ), false) err := builder.writeType(typ, selectionsForType(g, typ))
return builder.String(), err return builder.String(), err
} }
// TODO: this is really "field" now, rename it
type selection interface { type selection interface {
Alias() string Alias() string
Name() string
Type() *ast.Type Type() *ast.Type
SelectionSet() ([]selection, error) SelectionSet() ([]selection, error)
} }
type field struct{ field *ast.Field } type field struct{ field *ast.Field }
func (s field) Alias() string { return s.field.Alias } func (s field) Alias() string {
func (s field) Name() string { return s.field.Name } if s.field.Alias != "" {
return s.field.Alias
}
// TODO: is this case needed? tests don't seem to get here.
return s.field.Name
}
func (s field) Type() *ast.Type { func (s field) Type() *ast.Type {
if s.field.Definition == nil { if s.field.Definition == nil {
@@ -122,7 +133,6 @@ type inputField struct {
} }
func (s inputField) Alias() string { return s.field.Name } func (s inputField) Alias() string { return s.field.Name }
func (s inputField) Name() string { return s.field.Name }
func (s inputField) Type() *ast.Type { return s.field.Type } func (s inputField) Type() *ast.Type { return s.field.Type }
func (s inputField) SelectionSet() ([]selection, error) { func (s inputField) SelectionSet() ([]selection, error) {
@@ -139,13 +149,7 @@ func selectionsForType(g *generator, typ *ast.Type) []selection {
} }
func (builder *typeBuilder) writeField(selection selection) error { func (builder *typeBuilder) writeField(selection selection) error {
var jsonName string jsonName := selection.Alias()
if selection.Alias() != "" {
jsonName = selection.Alias()
} else {
// TODO: is this case needed? tests don't seem to get here.
jsonName = selection.Name()
}
// We need an exportable name for JSON-marshaling. // We need an exportable name for JSON-marshaling.
goName := upperFirst(jsonName) goName := upperFirst(jsonName)
@@ -156,7 +160,7 @@ func (builder *typeBuilder) writeField(selection selection) error {
if typ == nil { if typ == nil {
// Unclear why gqlparser hasn't already rejected this, // Unclear why gqlparser hasn't already rejected this,
// but empirically it might not. // but empirically it might not.
return fmt.Errorf("undefined field %v", selection.Name()) return fmt.Errorf("undefined field %v", selection.Alias())
} }
selectionSet, err := selection.SelectionSet() selectionSet, err := selection.SelectionSet()
@@ -164,12 +168,15 @@ func (builder *typeBuilder) writeField(selection selection) error {
return err return err
} }
err = builder.writeType(typ, selectionSet, true) err = builder.writeType(typ, selectionSet)
if err != nil { if err != nil {
return err return err
} }
if jsonName != goName { if builder.schema.Types[typ.Name()].IsAbstractType() {
// abstract types are handled in our UnmarshalJSON
builder.WriteString(" `json:\"-\"`")
} else if jsonName != goName {
fmt.Fprintf(builder, " `json:\"%s\"`", jsonName) fmt.Fprintf(builder, " `json:\"%s\"`", jsonName)
} }
builder.WriteRune('\n') builder.WriteRune('\n')
@@ -184,7 +191,7 @@ var builtinTypes = map[string]string{
"ID": "string", // TODO: named type for IDs? "ID": "string", // TODO: named type for IDs?
} }
func (builder *typeBuilder) writeType(typ *ast.Type, selectionSet []selection, inline bool) error { func (builder *typeBuilder) writeType(typ *ast.Type, selectionSet []selection) error {
// gqlgen does slightly different things here since it defines names for // gqlgen does slightly different things here since it defines names for
// all the intermediate types, but its implementation may be useful to crib // all the intermediate types, but its implementation may be useful to crib
// from: // from:
@@ -200,16 +207,6 @@ func (builder *typeBuilder) writeType(typ *ast.Type, selectionSet []selection, i
} }
def := builder.schema.Types[typ.Name()] def := builder.schema.Types[typ.Name()]
// TODO: set inline = false for nested types
switch def.Kind {
case ast.Scalar, ast.Enum, ast.Union, ast.Interface:
inline = false
}
if inline {
return builder.writeTypedef(def, selectionSet)
}
// Writes a typedef elsewhere (if not already defined) // Writes a typedef elsewhere (if not already defined)
name, err := builder.addTypeForDefinition("", def, selectionSet) name, err := builder.addTypeForDefinition("", def, selectionSet)
if err != nil { if err != nil {
@@ -231,7 +228,35 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, selectionSet [
} }
} }
builder.WriteString("}") builder.WriteString("}")
// If any field is abstract, we need an UnmarshalJSON method to handle
// it.
return builder.maybeWriteUnmarshal(selectionSet)
case ast.Interface, ast.Union:
// First, write the interface type.
builder.WriteString("interface {\n")
implementsMethodName := fmt.Sprintf("implementsGraphQLInterface%v", builder.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, err := builder.addTypeForDefinition("", impldef, selectionSet)
if err != nil {
return err
}
// HACK HACK HACK
builder.typeMap[name] += fmt.Sprintf(
"\nfunc (v %v) %v() {}", name, implementsMethodName)
}
return nil return nil
case ast.Enum: case ast.Enum:
// All GraphQL enums have underlying type string (in the Go sense). // All GraphQL enums have underlying type string (in the Go sense).
builder.WriteString("string\n") builder.WriteString("string\n")
@@ -244,8 +269,8 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, selectionSet [
} }
builder.WriteString(")\n") builder.WriteString(")\n")
return nil return nil
case ast.Scalar, ast.Union, ast.Interface: case ast.Scalar:
// TODO(benkraft): Handle custom scalars, unions, and interfaces. // TODO(benkraft): Handle custom scalars.
return fmt.Errorf("not implemented: %v", typedef.Kind) return fmt.Errorf("not implemented: %v", typedef.Kind)
default: default:
return fmt.Errorf("unexpected kind: %v", typedef.Kind) return fmt.Errorf("unexpected kind: %v", typedef.Kind)
+16 -38
View File
@@ -55,8 +55,6 @@ func TestTypeForOperation(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
schemaText := readFile(t, "schema.graphql", false)
for _, file := range files { for _, file := range files {
graphqlFilename := file.Name() graphqlFilename := file.Name()
if graphqlFilename == "schema.graphql" || !strings.HasSuffix(graphqlFilename, ".graphql") { if graphqlFilename == "schema.graphql" || !strings.HasSuffix(graphqlFilename, ".graphql") {
@@ -65,45 +63,25 @@ func TestTypeForOperation(t *testing.T) {
goFilename := graphqlFilename + ".go" goFilename := graphqlFilename + ".go"
t.Run(graphqlFilename, func(t *testing.T) { t.Run(graphqlFilename, func(t *testing.T) {
expectedGoType, err := gofmt(readFile(t, goFilename, update)) expectedGoCode, err := gofmt(readFile(t, goFilename, update))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
schema, graphqlError := gqlparser.LoadSchema( goCode, err := Generate(&Config{
&ast.Source{Name: "test schema", Input: schemaText}) Schema: filepath.Join("testdata", "schema.graphql"),
if graphqlError != nil { Queries: filepath.Join("testdata", graphqlFilename),
t.Fatal(graphqlError) Package: "test",
} })
queryDoc, graphqlListError := gqlparser.LoadQuery(
schema, readFile(t, graphqlFilename, false))
if graphqlListError != nil {
t.Fatal(graphqlListError)
}
if len(queryDoc.Operations) != 1 {
t.Fatalf("got %v operations, want 1", len(queryDoc.Operations))
}
g := newGenerator(&Config{Package: "test_package"}, schema)
err = g.addOperation(queryDoc.Operations[0])
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
} }
// gofmt before comparing. if string(goCode) != expectedGoCode {
goType, err := gofmt(g.Types()) t.Errorf("got:\n%v\nwant:\n%v\n", string(goCode), expectedGoCode)
if err != nil {
t.Error(err)
}
if goType != expectedGoType {
t.Errorf("got:\n%v\nwant:\n%v\n", goType, expectedGoType)
if update { if update {
t.Log("Updating testdata dir to match") t.Log("Updating testdata dir to match")
err = ioutil.WriteFile( err = ioutil.WriteFile(filepath.Join(dataDir, goFilename), goCode, 0644)
filepath.Join(dataDir, goFilename), []byte(goType), 0644)
if err != nil { if err != nil {
t.Errorf("Unable to update testdata dir: %v", err) t.Errorf("Unable to update testdata dir: %v", err)
} }
@@ -138,18 +116,18 @@ func TestTypeForInputType(t *testing.T) {
}, { }, {
`DefinedType`, `DefinedType`,
`UserQueryInput`, `UserQueryInput`,
`*userQueryInput`, `*UserQueryInput`,
[]string{ []string{
`type role string `type Role string
const ( const (
studentRole role = "STUDENT" StudentRole Role = "STUDENT"
teacherRole role = "TEACHER" TeacherRole Role = "TEACHER"
)`, )`,
`type userQueryInput struct { `type UserQueryInput struct {
Email *string ` + "`json:\"email\"`" + ` Email *string ` + "`json:\"email\"`" + `
Name *string ` + "`json:\"name\"`" + ` Name *string ` + "`json:\"name\"`" + `
Id *string ` + "`json:\"id\"`" + ` Id *string ` + "`json:\"id\"`" + `
Role *role ` + "`json:\"role\"`" + ` Role *Role ` + "`json:\"role\"`" + `
}`, }`,
}, },
}} }}
+51
View File
@@ -0,0 +1,51 @@
package generate
var unmarshalTemplate = mustTemplate("unmarshal.go.tmpl")
type templateData struct {
// Go type to which the method will be added
Type string
// Abstract fields of the type, which need special handling.
Fields []abstractField
}
type abstractField struct {
// Name of the field, in Go and JSON
GoName, JSONName string
// Concrete types the field might take.
ConcreteTypes []concreteType
}
type concreteType struct {
// Name of the type, in Go and GraphQL
GoName, GraphQLName string
}
func (builder *typeBuilder) maybeWriteUnmarshal(fields []selection) error {
data := templateData{Type: builder.typeName}
for _, field := range fields {
typedef := builder.schema.Types[field.Type().Name()]
if typedef.IsAbstractType() {
fieldInfo := abstractField{
GoName: upperFirst(field.Alias()),
JSONName: field.Alias(),
}
for _, typedef := range builder.schema.GetPossibleTypes(typedef) {
fieldInfo.ConcreteTypes = append(fieldInfo.ConcreteTypes,
concreteType{
// TODO: lies! We might have added "1" or something.
GoName: upperFirst(typedef.Name),
GraphQLName: typedef.Name,
})
}
data.Fields = append(data.Fields, fieldInfo)
}
}
if len(data.Fields) == 0 {
return nil
}
builder.WriteString("\n\n")
return unmarshalTemplate.Execute(builder, data)
}
+35
View File
@@ -0,0 +1,35 @@
func (v *{{.Type}}) UnmarshalJSON(b []byte) error {
var firstPass struct{
*{{.Type}}
{{range .Fields -}}
{{.GoName}} json.RawMessage `json:"{{.JSONName}}"`
{{end}}
}
firstPass.{{.Type}} = v
err := json.Unmarshal(b, &typenames)
if err != nil {
return err
}
{{range .Fields -}}
var tn struct { TypeName string `json:"__typename"` }
err = json.Unmarshal(firstPass.{{.GoName}}, &tn)
if err != nil {
return err
}
switch tn.TypeName {
{{with $field := .}}
{{range $field.ConcreteTypes}}
case "{{.GraphQLName}}":
v.{{$field.GoName}} = {{.GoName}}{}
err = json.Unmarshal(
firstPass.{{$field.GoName}}, &v.{{$field.GoName}})
{{end}}
{{end}}
}
if err != nil {
return err
}
{{end}}
}
+5 -4
View File
@@ -29,14 +29,15 @@ func upperFirst(s string) string {
} }
func goConstName(s string) string { func goConstName(s string) string {
if strings.TrimLeft(s, "_") == "" {
return s
}
var prev rune var prev rune
return strings.Map(func(r rune) rune { return strings.Map(func(r rune) rune {
var ret rune var ret rune
if prev == 0 && r == '_' { if r == '_' {
return '_' // still treat next char as first
} else if r == '_' {
ret = -1 ret = -1
} else if prev == '_' { } else if prev == '_' || prev == 0 {
ret = unicode.ToUpper(r) ret = unicode.ToUpper(r)
} else { } else {
ret = unicode.ToLower(r) ret = unicode.ToLower(r)
+3 -3
View File
@@ -55,10 +55,10 @@ func TestUpperFirst(t *testing.T) {
func TestGoConstName(t *testing.T) { func TestGoConstName(t *testing.T) {
tests := []test{ tests := []test{
{"Empty", "", ""}, {"Empty", "", ""},
{"AllCaps", "ASDF", "asdf"}, {"AllCaps", "ASDF", "Asdf"},
{"AllCapsWithUnderscore", "ASDF_GH", "asdfGh"}, {"AllCapsWithUnderscore", "ASDF_GH", "AsdfGh"},
{"JustUnderscore", "_", "_"}, {"JustUnderscore", "_", "_"},
{"LeadingUnderscore", "_ASDF_GH", "_asdfGh"}, {"LeadingUnderscore", "_ASDF_GH", "AsdfGh"},
} }
testStringFunc(t, goConstName, tests) testStringFunc(t, goConstName, tests)