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
`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.
@@ -69,6 +69,7 @@ Config options:
- proper config/arguments setup (e.g. with [viper](https://github.com/spf13/viper)
Other:
- naming collisions are a mess
- error-checking/validation/etc. everywhere
- more tests
- documentation
+10 -6
View File
@@ -8,16 +8,20 @@ import (
"github.com/Khan/genql/graphql"
)
type User struct {
MyName *string
}
type User1 struct {
TheirName *string `json:"theirName"`
}
type getUserResponse struct {
User *struct {
TheirName *string `json:"theirName"`
} `json:"user"`
User *User1 `json:"user"`
}
type getViewerResponse struct {
Viewer struct {
MyName *string
} `json:"viewer"`
Viewer User `json:"viewer"`
}
func getViewer(ctx context.Context, client *graphql.Client) (*getViewerResponse, error) {
+5 -12
View File
@@ -4,22 +4,14 @@ import (
"bytes"
"fmt"
"go/format"
"path/filepath"
"runtime"
"sort"
"strings"
"text/template"
"github.com/vektah/gqlparser/ast"
"github.com/vektah/gqlparser/formatter"
)
// TODO: package template into the binary using one of those asset thingies
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))
var fileTemplate = mustTemplate("operation.go.tmpl")
// generator is the context for the codegen process (and ends up getting passed
// to the template).
@@ -29,8 +21,9 @@ type generator struct {
// The list of operations for which to generate code.
Operations []operation
// The types needed for these operations.
typeMap map[string]string
schema *ast.Schema
typeMap map[string]string
ImportJSON bool
schema *ast.Schema
}
type operation struct {
@@ -165,7 +158,7 @@ func Generate(config *Config) ([]byte, error) {
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, g)
err = fileTemplate.Execute(&buf, g)
if err != nil {
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.
import (
"context"
{{- if .ImportJSON -}}
"encoding/json"
{{end}}
"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 {
User *struct {
ID string
package test
// 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 {
ID: id
AlsoID: id
+31 -6
View File
@@ -1,6 +1,31 @@
type Response struct {
User *struct {
ID string
AlsoID string
} `json:"user"`
}
package test
// Code generated by github.com/Khan/genql, DO NOT EDIT.
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 {
roles
}
+32 -8
View File
@@ -1,12 +1,36 @@
type Response struct {
User *struct {
Roles []role `json:"roles"`
} `json:"user"`
package test
// Code generated by github.com/Khan/genql, DO NOT EDIT.
import (
"context"
"github.com/Khan/genql/graphql"
)
type QueryWithEnumsResponse struct {
User *User `json:"user"`
}
type role string
type Role string
const (
studentRole role = "STUDENT"
teacherRole role = "TEACHER"
)
StudentRole Role = "STUDENT"
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) {
id
}
+40 -12
View File
@@ -1,19 +1,47 @@
type Response struct {
User *struct {
Id string `json:"id"`
} `json:"user"`
}
package test
type role string
// Code generated by github.com/Khan/genql, DO NOT EDIT.
const (
studentRole role = "STUDENT"
teacherRole role = "TEACHER"
import (
"context"
"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"`
Name *string `json:"name"`
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 {
emails
emailsOrNull
+35 -8
View File
@@ -1,8 +1,35 @@
type Response struct {
User *struct {
Emails []string `json:"emails"`
EmailsOrNull []string `json:"emailsOrNull"`
EmailsWithNulls []*string `json:"emailsWithNulls"`
EmailsWithNullsOrNull []*string `json:"emailsWithNullsOrNull"`
} `json:"user"`
}
package test
// Code generated by github.com/Khan/genql, DO NOT EDIT.
import (
"context"
"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 {
authMethods {
provider
+37 -8
View File
@@ -1,8 +1,37 @@
type Response struct {
User *struct {
AuthMethods []struct {
Provider *string `json:"provider"`
Email *string `json:"email"`
} `json:"authMethods"`
} `json:"user"`
}
package test
// Code generated by github.com/Khan/genql, DO NOT EDIT.
import (
"context"
"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 {
User *struct {
Id string `json:"id"`
} `json:"user"`
package test
// Code generated by github.com/Khan/genql, DO NOT EDIT.
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 {
__typename
id
+31 -6
View File
@@ -1,6 +1,31 @@
type Response struct {
User *struct {
Typename *string `json:"__typename"`
Id string `json:"id"`
} `json:"user"`
}
package test
// Code generated by github.com/Khan/genql, DO NOT EDIT.
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 }
OtherUser: user { roles }
}
+48 -12
View File
@@ -1,15 +1,51 @@
type Response struct {
Me *struct {
Roles []role `json:"roles"`
}
OtherUser *struct {
Roles []role `json:"roles"`
}
}
package test
type role string
// Code generated by github.com/Khan/genql, DO NOT EDIT.
import (
"context"
"github.com/Khan/genql/graphql"
)
type Role string
const (
studentRole role = "STUDENT"
teacherRole role = "TEACHER"
)
StudentRole Role = "STUDENT"
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 (
"fmt"
"strconv"
"strings"
"github.com/vektah/gqlparser/ast"
@@ -54,11 +55,16 @@ func (g *generator) addTypeForDefinition(nameOverride string, typ *ast.Definitio
name = nameOverride
} else {
// TODO: casing should be configurable
name = lowerFirst(typ.Name)
name = upperFirst(typ.Name)
}
if _, ok := g.typeMap[name]; ok {
return name, nil
// TODO: in some cases we can deduplicate, do that
// TODO: nicer naming scheme
i := 0
origName := name
for g.typeMap[name] != "" {
i++
name = origName + strconv.Itoa(i)
}
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) {
builder := &typeBuilder{typeName: lowerFirst(typ.Name()), generator: g}
err := builder.writeType(typ, selectionsForType(g, typ), false)
builder := &typeBuilder{typeName: upperFirst(typ.Name()), generator: g}
err := builder.writeType(typ, selectionsForType(g, typ))
return builder.String(), err
}
// TODO: this is really "field" now, rename it
type selection interface {
Alias() string
Name() string
Type() *ast.Type
SelectionSet() ([]selection, error)
}
type field struct{ field *ast.Field }
func (s field) Alias() string { return s.field.Alias }
func (s field) Name() string { return s.field.Name }
func (s field) Alias() string {
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 {
if s.field.Definition == nil {
@@ -122,7 +133,6 @@ type inputField struct {
}
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) SelectionSet() ([]selection, error) {
@@ -139,13 +149,7 @@ func selectionsForType(g *generator, typ *ast.Type) []selection {
}
func (builder *typeBuilder) writeField(selection selection) error {
var jsonName string
if selection.Alias() != "" {
jsonName = selection.Alias()
} else {
// TODO: is this case needed? tests don't seem to get here.
jsonName = selection.Name()
}
jsonName := selection.Alias()
// We need an exportable name for JSON-marshaling.
goName := upperFirst(jsonName)
@@ -156,7 +160,7 @@ func (builder *typeBuilder) writeField(selection selection) error {
if typ == nil {
// Unclear why gqlparser hasn't already rejected this,
// 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()
@@ -164,12 +168,15 @@ func (builder *typeBuilder) writeField(selection selection) error {
return err
}
err = builder.writeType(typ, selectionSet, true)
err = builder.writeType(typ, selectionSet)
if err != nil {
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)
}
builder.WriteRune('\n')
@@ -184,7 +191,7 @@ var builtinTypes = map[string]string{
"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
// all the intermediate types, but its implementation may be useful to crib
// from:
@@ -200,16 +207,6 @@ func (builder *typeBuilder) writeType(typ *ast.Type, selectionSet []selection, i
}
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)
name, err := builder.addTypeForDefinition("", def, selectionSet)
if err != nil {
@@ -231,7 +228,35 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, selectionSet [
}
}
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
case ast.Enum:
// All GraphQL enums have underlying type string (in the Go sense).
builder.WriteString("string\n")
@@ -244,8 +269,8 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, selectionSet [
}
builder.WriteString(")\n")
return nil
case ast.Scalar, ast.Union, ast.Interface:
// TODO(benkraft): Handle custom scalars, unions, and interfaces.
case ast.Scalar:
// TODO(benkraft): Handle custom scalars.
return fmt.Errorf("not implemented: %v", typedef.Kind)
default:
return fmt.Errorf("unexpected kind: %v", typedef.Kind)
+16 -38
View File
@@ -55,8 +55,6 @@ func TestTypeForOperation(t *testing.T) {
t.Fatal(err)
}
schemaText := readFile(t, "schema.graphql", false)
for _, file := range files {
graphqlFilename := file.Name()
if graphqlFilename == "schema.graphql" || !strings.HasSuffix(graphqlFilename, ".graphql") {
@@ -65,45 +63,25 @@ func TestTypeForOperation(t *testing.T) {
goFilename := graphqlFilename + ".go"
t.Run(graphqlFilename, func(t *testing.T) {
expectedGoType, err := gofmt(readFile(t, goFilename, update))
expectedGoCode, err := gofmt(readFile(t, goFilename, update))
if err != nil {
t.Fatal(err)
}
schema, graphqlError := gqlparser.LoadSchema(
&ast.Source{Name: "test schema", Input: schemaText})
if graphqlError != nil {
t.Fatal(graphqlError)
}
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])
goCode, err := Generate(&Config{
Schema: filepath.Join("testdata", "schema.graphql"),
Queries: filepath.Join("testdata", graphqlFilename),
Package: "test",
})
if err != nil {
t.Error(err)
t.Fatal(err)
}
// gofmt before comparing.
goType, err := gofmt(g.Types())
if err != nil {
t.Error(err)
}
if goType != expectedGoType {
t.Errorf("got:\n%v\nwant:\n%v\n", goType, expectedGoType)
if string(goCode) != expectedGoCode {
t.Errorf("got:\n%v\nwant:\n%v\n", string(goCode), expectedGoCode)
if update {
t.Log("Updating testdata dir to match")
err = ioutil.WriteFile(
filepath.Join(dataDir, goFilename), []byte(goType), 0644)
err = ioutil.WriteFile(filepath.Join(dataDir, goFilename), goCode, 0644)
if err != nil {
t.Errorf("Unable to update testdata dir: %v", err)
}
@@ -138,18 +116,18 @@ func TestTypeForInputType(t *testing.T) {
}, {
`DefinedType`,
`UserQueryInput`,
`*userQueryInput`,
`*UserQueryInput`,
[]string{
`type role string
`type Role string
const (
studentRole role = "STUDENT"
teacherRole role = "TEACHER"
StudentRole Role = "STUDENT"
TeacherRole Role = "TEACHER"
)`,
`type userQueryInput struct {
`type UserQueryInput struct {
Email *string ` + "`json:\"email\"`" + `
Name *string ` + "`json:\"name\"`" + `
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 {
if strings.TrimLeft(s, "_") == "" {
return s
}
var prev rune
return strings.Map(func(r rune) rune {
var ret rune
if prev == 0 && r == '_' {
return '_' // still treat next char as first
} else if r == '_' {
if r == '_' {
ret = -1
} else if prev == '_' {
} else if prev == '_' || prev == 0 {
ret = unicode.ToUpper(r)
} else {
ret = unicode.ToLower(r)
+3 -3
View File
@@ -55,10 +55,10 @@ func TestUpperFirst(t *testing.T) {
func TestGoConstName(t *testing.T) {
tests := []test{
{"Empty", "", ""},
{"AllCaps", "ASDF", "asdf"},
{"AllCapsWithUnderscore", "ASDF_GH", "asdfGh"},
{"AllCaps", "ASDF", "Asdf"},
{"AllCapsWithUnderscore", "ASDF_GH", "AsdfGh"},
{"JustUnderscore", "_", "_"},
{"LeadingUnderscore", "_ASDF_GH", "_asdfGh"},
{"LeadingUnderscore", "_ASDF_GH", "AsdfGh"},
}
testStringFunc(t, goConstName, tests)