From cbde0ea97359831026ecc22d2e69adc2c3cd22ad Mon Sep 17 00:00:00 2001 From: Luke Cawood Date: Mon, 10 Sep 2018 18:17:44 +1000 Subject: [PATCH 1/3] Generate typed interfaces for gql interfaces & unions --- Gopkg.lock | 18 +++++++++--------- Gopkg.toml | 4 ++++ codegen/model.go | 1 + codegen/models_build.go | 5 +++-- codegen/object.go | 1 + codegen/object_build.go | 4 ++++ codegen/templates/data.go | 2 +- codegen/templates/models.gotpl | 9 ++++++++- example/selection/models_gen.go | 8 +++++++- example/starwars/model.go | 6 ++++++ example/starwars/models_gen.go | 10 ++++++++-- 11 files changed, 52 insertions(+), 16 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 7399021ad65..5e5d741300b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -26,7 +26,7 @@ version = "v3.3.2" [[projects]] - digest = "1:78907d832e27dbfc6e3fdfc52bd2e5e2e05c1d0e3789d4825b824489fbeab233" + digest = "1:f3df613325a793ffb3d0ce7644a3bb6f62db45ac744dafe20172fe999c61cdbf" name = "github.com/gogo/protobuf" packages = [ "io", @@ -99,7 +99,7 @@ version = "v1.0.0" [[projects]] - digest = "1:27af6024faa3c28426a698b8c653be0fd908bc96e25b7d76f2192eb342427db6" + digest = "1:450b7623b185031f3a456801155c8320209f75d0d4c4e633c6b1e59d44d6e392" name = "github.com/opentracing/opentracing-go" packages = [ ".", @@ -143,7 +143,7 @@ revision = "ffb13db8def02f545acc58bd288ec6057c2bbfb9" [[projects]] - digest = "1:73697231b93fb74a73ebd8384b68b9a60c57ea6b13c56d2425414566a72c8e6d" + digest = "1:7e8d267900c7fa7f35129a2a37596e38ed0f11ca746d6d9ba727980ee138f9f6" name = "github.com/stretchr/testify" packages = [ "assert", @@ -170,8 +170,8 @@ revision = "314ac81052eedc03ac0a79bdc89d05a49a2a5814" [[projects]] - branch = "master" - digest = "1:3e2c3d0b8cb780a09467884bebc032c831ae273d1d993b8f55fa61084e37278c" + branch = "add-schema-implements" + digest = "1:cb4a3b347499d5cca82cef6daa1fd1932f027a7cab651689c16af85300463eb2" name = "github.com/vektah/gqlparser" packages = [ ".", @@ -183,7 +183,7 @@ "validator/rules", ] pruneopts = "UT" - revision = "91abf84889ffb5ab4537b0ae1b1670109a9fbc28" + revision = "bd86bdb57ce80cd71b3b7f8707bb7e09b3fb2fab" [[projects]] branch = "master" @@ -195,7 +195,7 @@ [[projects]] branch = "master" - digest = "1:77fe642412bfed48743e2b75163e3ab5c430cfe22dd488788647b89b28794635" + digest = "1:3cbc05413b8aac22b1f6d4350ed696b5a83a8515a4136db8f1ec3a0aee3d76e1" name = "golang.org/x/tools" packages = [ "go/ast/astutil", @@ -216,7 +216,7 @@ [[projects]] branch = "master" - digest = "1:7ddb3a7b35cc853fe0db36a1b2473bdff03f28add7d28e4725e692603111266e" + digest = "1:741ebea9214cc226789d3003baeca9b169e04b5b336fb1a3b2c16e75bd296bb5" name = "sourcegraph.com/sourcegraph/appdash" packages = [ ".", @@ -232,7 +232,7 @@ [[projects]] branch = "master" - digest = "1:be108b48d79c3b3c345811a57a47ee87fdbe895beb4bb56239da71d4943e5be7" + digest = "1:8e0a2957fe342f22d70a543c3fcdf390f7627419c3d82d87ab4fd715a9ef5716" name = "sourcegraph.com/sourcegraph/appdash-data" packages = ["."] pruneopts = "UT" diff --git a/Gopkg.toml b/Gopkg.toml index b12c5375ffe..91960cf4e47 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -12,6 +12,10 @@ required = ["github.com/vektah/dataloaden"] branch = "master" name = "golang.org/x/tools" +[[constraint]] + branch = "add-schema-implements" + name = "github.com/vektah/gqlparser" + [prune] go-tests = true unused-packages = true diff --git a/codegen/model.go b/codegen/model.go index 5ba50337e27..bcdc8703a6c 100644 --- a/codegen/model.go +++ b/codegen/model.go @@ -4,6 +4,7 @@ type Model struct { *NamedType Description string Fields []ModelField + Implements []*NamedType } type ModelField struct { diff --git a/codegen/models_build.go b/codegen/models_build.go index 9f98a07dd5d..bf0c9c9b8f1 100644 --- a/codegen/models_build.go +++ b/codegen/models_build.go @@ -54,8 +54,9 @@ func (cfg *Config) buildModels(types NamedTypes, prog *loader.Program, imports * func (cfg *Config) obj2Model(obj *Object) Model { model := Model{ - NamedType: obj.NamedType, - Fields: []ModelField{}, + NamedType: obj.NamedType, + Implements: obj.Implements, + Fields: []ModelField{}, } model.GoType = ucFirst(obj.GQLType) diff --git a/codegen/object.go b/codegen/object.go index d9f610f4591..e704aef6bd6 100644 --- a/codegen/object.go +++ b/codegen/object.go @@ -24,6 +24,7 @@ type Object struct { Fields []Field Satisfies []string + Implements []*NamedType ResolverInterface *Ref Root bool DisableConcurrency bool diff --git a/codegen/object_build.go b/codegen/object_build.go index ee2b2f1c2b9..9c9e548e41e 100644 --- a/codegen/object_build.go +++ b/codegen/object_build.go @@ -104,6 +104,10 @@ func (cfg *Config) buildObject(types NamedTypes, typ *ast.Definition, imports *I obj.Satisfies = append(obj.Satisfies, typ.Interfaces...) + for _, intf := range cfg.schema.GetImplements(typ) { + obj.Implements = append(obj.Implements, types[intf.Name]) + } + for _, field := range typ.Fields { if typ == cfg.schema.Query && field.Name == "__type" { obj.Fields = append(obj.Fields, Field{ diff --git a/codegen/templates/data.go b/codegen/templates/data.go index 8362848427a..9c039ba0b8f 100644 --- a/codegen/templates/data.go +++ b/codegen/templates/data.go @@ -6,7 +6,7 @@ var data = map[string]string{ "generated.gotpl": "// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\n// NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface.\nfunc NewExecutableSchema(cfg Config) graphql.ExecutableSchema {\n\treturn &executableSchema{\n\t\tresolvers: cfg.Resolvers,\n\t\tdirectives: cfg.Directives,\n\t\tcomplexity: cfg.Complexity,\n\t}\n}\n\ntype Config struct {\n\tResolvers ResolverRoot\n\tDirectives DirectiveRoot\n\tComplexity ComplexityRoot\n}\n\ntype ResolverRoot interface {\n{{- range $object := .Objects -}}\n\t{{ if $object.HasResolvers -}}\n\t\t{{$object.GQLType}}() {{$object.GQLType}}Resolver\n\t{{ end }}\n{{- end }}\n}\n\ntype DirectiveRoot struct {\n{{ range $directive := .Directives }}\n\t{{ $directive.Declaration }}\n{{ end }}\n}\n\ntype ComplexityRoot struct {\n{{ range $object := .Objects }}\n\t{{ if not $object.IsReserved -}}\n\t\t{{ $object.GQLType|toCamel }} struct {\n\t\t{{ range $field := $object.Fields -}}\n\t\t\t{{ if not $field.IsReserved -}}\n\t\t\t\t{{ $field.GQLName|toCamel }} {{ $field.ComplexitySignature }}\n\t\t\t{{ end }}\n\t\t{{- end }}\n\t\t}\n\t{{- end }}\n{{ end }}\n}\n\n{{ range $object := .Objects -}}\n\t{{ if $object.HasResolvers }}\n\t\ttype {{$object.GQLType}}Resolver interface {\n\t\t{{ range $field := $object.Fields -}}\n\t\t\t{{ $field.ShortResolverDeclaration }}\n\t\t{{ end }}\n\t\t}\n\t{{- end }}\n{{- end }}\n\n{{ range $object := .Objects -}}\n\t{{ range $field := $object.Fields -}}\n\t\t{{ if $field.Args }}\n\t\t\tfunc {{ $field.ArgsFunc }}(rawArgs map[string]interface{}) (map[string]interface{}, error) {\n\t\t\t{{ template \"args.gotpl\" $field.Args }}\n\t\t\t}\n\t\t{{ end }}\n\t{{ end }}\n{{- end }}\n\n{{ range $directive := .Directives }}\n\t{{ if $directive.Args }}\n\t\tfunc {{ $directive.ArgsFunc }}(rawArgs map[string]interface{}) (map[string]interface{}, error) {\n\t\t{{ template \"args.gotpl\" $directive.Args }}\n\t\t}\n\t{{ end }}\n{{ end }}\n\ntype executableSchema struct {\n\tresolvers ResolverRoot\n\tdirectives DirectiveRoot\n\tcomplexity ComplexityRoot\n}\n\nfunc (e *executableSchema) Schema() *ast.Schema {\n\treturn parsedSchema\n}\n\nfunc (e *executableSchema) Complexity(typeName, field string, childComplexity int, rawArgs map[string]interface{}) (int, bool) {\n\tswitch typeName + \".\" + field {\n\t{{ range $object := .Objects }}\n\t\t{{ if not $object.IsReserved }}\n\t\t\t{{ range $field := $object.Fields }}\n\t\t\t\t{{ if not $field.IsReserved }}\n\t\t\t\t\tcase \"{{$object.GQLType}}.{{$field.GQLName}}\":\n\t\t\t\t\t\tif e.complexity.{{$object.GQLType|toCamel}}.{{$field.GQLName|toCamel}} == nil {\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\t{{ if $field.Args }}\n\t\t\t\t\t\t\targs, err := {{ $field.ArgsFunc }}(rawArgs)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn 0, false\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t{{ end }}\n\t\t\t\t\t\treturn e.complexity.{{$object.GQLType|toCamel}}.{{$field.GQLName|toCamel}}(childComplexity{{if $field.Args}}, {{$field.ComplexityArgs}} {{end}}), true\n\t\t\t\t{{ end }}\n\t\t\t{{ end }}\n\t\t{{ end }}\n\t{{ end }}\n\t}\n\treturn 0, false\n}\n\nfunc (e *executableSchema) Query(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {\n\t{{- if .QueryRoot }}\n\t\tec := executionContext{graphql.GetRequestContext(ctx), e}\n\n\t\tbuf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {\n\t\t\tdata := ec._{{.QueryRoot.GQLType}}(ctx, op.SelectionSet)\n\t\t\tvar buf bytes.Buffer\n\t\t\tdata.MarshalGQL(&buf)\n\t\t\treturn buf.Bytes()\n\t\t})\n\n\t\treturn &graphql.Response{\n\t\t\tData: buf,\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn graphql.ErrorResponse(ctx, \"queries are not supported\")\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Mutation(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {\n\t{{- if .MutationRoot }}\n\t\tec := executionContext{graphql.GetRequestContext(ctx), e}\n\n\t\tbuf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {\n\t\t\tdata := ec._{{.MutationRoot.GQLType}}(ctx, op.SelectionSet)\n\t\t\tvar buf bytes.Buffer\n\t\t\tdata.MarshalGQL(&buf)\n\t\t\treturn buf.Bytes()\n\t\t})\n\n\t\treturn &graphql.Response{\n\t\t\tData: buf,\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn graphql.ErrorResponse(ctx, \"mutations are not supported\")\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Subscription(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response {\n\t{{- if .SubscriptionRoot }}\n\t\tec := executionContext{graphql.GetRequestContext(ctx), e}\n\n\t\tnext := ec._{{.SubscriptionRoot.GQLType}}(ctx, op.SelectionSet)\n\t\tif ec.Errors != nil {\n\t\t\treturn graphql.OneShot(&graphql.Response{Data: []byte(\"null\"), Errors: ec.Errors})\n\t\t}\n\n\t\tvar buf bytes.Buffer\n\t\treturn func() *graphql.Response {\n\t\t\tbuf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {\n\t\t\t\tbuf.Reset()\n\t\t\t\tdata := next()\n\n\t\t\t\tif data == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tdata.MarshalGQL(&buf)\n\t\t\t\treturn buf.Bytes()\n\t\t\t})\n\n\t\t\treturn &graphql.Response{\n\t\t\t\tData: buf,\n\t\t\t\tErrors: ec.Errors,\n\t\t\t}\n\t\t}\n\t{{- else }}\n\t\treturn graphql.OneShot(graphql.ErrorResponse(ctx, \"subscriptions are not supported\"))\n\t{{- end }}\n}\n\ntype executionContext struct {\n\t*graphql.RequestContext\n\t*executableSchema\n}\n\n{{- range $object := .Objects }}\n\t{{ template \"object.gotpl\" $object }}\n\n\t{{- range $field := $object.Fields }}\n\t\t{{ template \"field.gotpl\" $field }}\n\t{{ end }}\n{{- end}}\n\n{{- range $interface := .Interfaces }}\n\t{{ template \"interface.gotpl\" $interface }}\n{{- end }}\n\n{{- range $input := .Inputs }}\n\t{{ template \"input.gotpl\" $input }}\n{{- end }}\n\nfunc (ec *executionContext) FieldMiddleware(ctx context.Context, obj interface{}, next graphql.Resolver) (ret interface{}) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tec.Error(ctx, ec.Recover(ctx, r))\n\t\t\tret = nil\n\t\t}\n\t}()\n\t{{- if .Directives }}\n\trctx := graphql.GetResolverContext(ctx)\n\tfor _, d := range rctx.Field.Definition.Directives {\n\t\tswitch d.Name {\n\t\t{{- range $directive := .Directives }}\n\t\tcase \"{{$directive.Name}}\":\n\t\t\tif ec.directives.{{$directive.Name|ucFirst}} != nil {\n\t\t\t\t{{- if $directive.Args }}\n\t\t\t\t\trawArgs := d.ArgumentMap(ec.Variables)\n\t\t\t\t\targs, err := {{ $directive.ArgsFunc }}(rawArgs)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tec.Error(ctx, err)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t{{- end }}\n\t\t\t\tn := next\n\t\t\t\tnext = func(ctx context.Context) (interface{}, error) {\n\t\t\t\t\treturn ec.directives.{{$directive.Name|ucFirst}}({{$directive.CallArgs}})\n\t\t\t\t}\n\t\t\t}\n\t\t{{- end }}\n\t\t}\n\t}\n\t{{- end }}\n\tres, err := ec.ResolverMiddleware(ctx, next)\n\tif err != nil {\n\t\tec.Error(ctx, err)\n\t\treturn nil\n\t}\n\treturn res\n}\n\nfunc (ec *executionContext) introspectSchema() *introspection.Schema {\n\treturn introspection.WrapSchema(parsedSchema)\n}\n\nfunc (ec *executionContext) introspectType(name string) *introspection.Type {\n\treturn introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name])\n}\n\nvar parsedSchema = gqlparser.MustLoadSchema(\n\t&ast.Source{Name: {{.SchemaFilename|quote}}, Input: {{.SchemaRaw|rawQuote}}},\n)\n", "input.gotpl": "\t{{- if .IsMarshaled }}\n\tfunc Unmarshal{{ .GQLType }}(v interface{}) ({{.FullName}}, error) {\n\t\tvar it {{.FullName}}\n\t\tvar asMap = v.(map[string]interface{})\n\t\t{{ range $field := .Fields}}\n\t\t\t{{- if $field.Default}}\n\t\t\t\tif _, present := asMap[{{$field.GQLName|quote}}] ; !present {\n\t\t\t\t\tasMap[{{$field.GQLName|quote}}] = {{ $field.Default | dump }}\n\t\t\t\t}\n\t\t\t{{- end}}\n\t\t{{- end }}\n\n\t\tfor k, v := range asMap {\n\t\t\tswitch k {\n\t\t\t{{- range $field := .Fields }}\n\t\t\tcase {{$field.GQLName|quote}}:\n\t\t\t\tvar err error\n\t\t\t\t{{ $field.Unmarshal (print \"it.\" $field.GoFieldName) \"v\" }}\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn it, err\n\t\t\t\t}\n\t\t\t{{- end }}\n\t\t\t}\n\t\t}\n\n\t\treturn it, nil\n\t}\n\t{{- end }}\n", "interface.gotpl": "{{- $interface := . }}\n\nfunc (ec *executionContext) _{{$interface.GQLType}}(ctx context.Context, sel ast.SelectionSet, obj *{{$interface.FullName}}) graphql.Marshaler {\n\tswitch obj := (*obj).(type) {\n\tcase nil:\n\t\treturn graphql.Null\n\t{{- range $implementor := $interface.Implementors }}\n\t\t{{- if $implementor.ValueReceiver }}\n\t\t\tcase {{$implementor.FullName}}:\n\t\t\t\treturn ec._{{$implementor.GQLType}}(ctx, sel, &obj)\n\t\t{{- end}}\n\t\tcase *{{$implementor.FullName}}:\n\t\t\treturn ec._{{$implementor.GQLType}}(ctx, sel, obj)\n\t{{- end }}\n\tdefault:\n\t\tpanic(fmt.Errorf(\"unexpected type %T\", obj))\n\t}\n}\n", - "models.gotpl": "// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\n{{ range $model := .Models }}\n\t{{with .Description}} {{.|prefixLines \"// \"}} {{end}}\n\t{{- if .IsInterface }}\n\t\ttype {{.GoType}} interface {}\n\t{{- else }}\n\t\ttype {{.GoType}} struct {\n\t\t\t{{- range $field := .Fields }}\n\t\t\t\t{{- with .Description}}\n\t\t\t\t\t{{.|prefixLines \"// \"}}\n\t\t\t\t{{- end}}\n\t\t\t\t{{- if $field.GoFieldName }}\n\t\t\t\t\t{{ $field.GoFieldName }} {{$field.Signature}} `json:\"{{$field.GQLName}}\"`\n\t\t\t\t{{- else }}\n\t\t\t\t\t{{ $field.GoFKName }} {{$field.GoFKType}}\n\t\t\t\t{{- end }}\n\t\t\t{{- end }}\n\t\t}\n\t{{- end }}\n{{- end}}\n\n{{ range $enum := .Enums }}\n\t{{with .Description}}{{.|prefixLines \"// \"}} {{end}}\n\ttype {{.GoType}} string\n\tconst (\n\t{{- range $value := .Values}}\n\t\t{{- with .Description}}\n\t\t\t{{.|prefixLines \"// \"}}\n\t\t{{- end}}\n\t\t{{$enum.GoType}}{{ .Name|toCamel }} {{$enum.GoType}} = {{.Name|quote}}\n\t{{- end }}\n\t)\n\n\tfunc (e {{.GoType}}) IsValid() bool {\n\t\tswitch e {\n\t\tcase {{ range $index, $element := .Values}}{{if $index}},{{end}}{{ $enum.GoType }}{{ $element.Name|toCamel }}{{end}}:\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tfunc (e {{.GoType}}) String() string {\n\t\treturn string(e)\n\t}\n\n\tfunc (e *{{.GoType}}) UnmarshalGQL(v interface{}) error {\n\t\tstr, ok := v.(string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"enums must be strings\")\n\t\t}\n\n\t\t*e = {{.GoType}}(str)\n\t\tif !e.IsValid() {\n\t\t\treturn fmt.Errorf(\"%s is not a valid {{.GQLType}}\", str)\n\t\t}\n\t\treturn nil\n\t}\n\n\tfunc (e {{.GoType}}) MarshalGQL(w io.Writer) {\n\t\tfmt.Fprint(w, strconv.Quote(e.String()))\n\t}\n\n{{- end }}\n", + "models.gotpl": "// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\n{{ range $model := .Models }}\n\t{{with .Description}} {{.|prefixLines \"// \"}} {{end}}\n\t{{- if .IsInterface }}\n\t\ttype {{.GoType}} interface {\n\t\t\tIs{{.GoType}}()\n\t\t}\n\t{{- else }}\n\t\ttype {{.GoType}} struct {\n\t\t\t{{- range $field := .Fields }}\n\t\t\t\t{{- with .Description}}\n\t\t\t\t\t{{.|prefixLines \"// \"}}\n\t\t\t\t{{- end}}\n\t\t\t\t{{- if $field.GoFieldName }}\n\t\t\t\t\t{{ $field.GoFieldName }} {{$field.Signature}} `json:\"{{$field.GQLName}}\"`\n\t\t\t\t{{- else }}\n\t\t\t\t\t{{ $field.GoFKName }} {{$field.GoFKType}}\n\t\t\t\t{{- end }}\n\t\t\t{{- end }}\n\t\t}\n\n\t\t{{- range $iface := .Implements }}\n\t\t\tfunc ({{$model.GoType}}) Is{{$iface.GoType}}() {}\n\t\t{{- end }}\n\n\t{{- end }}\n{{- end}}\n\n{{ range $enum := .Enums }}\n\t{{with .Description}}{{.|prefixLines \"// \"}} {{end}}\n\ttype {{.GoType}} string\n\tconst (\n\t{{- range $value := .Values}}\n\t\t{{- with .Description}}\n\t\t\t{{.|prefixLines \"// \"}}\n\t\t{{- end}}\n\t\t{{$enum.GoType}}{{ .Name|toCamel }} {{$enum.GoType}} = {{.Name|quote}}\n\t{{- end }}\n\t)\n\n\tfunc (e {{.GoType}}) IsValid() bool {\n\t\tswitch e {\n\t\tcase {{ range $index, $element := .Values}}{{if $index}},{{end}}{{ $enum.GoType }}{{ $element.Name|toCamel }}{{end}}:\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tfunc (e {{.GoType}}) String() string {\n\t\treturn string(e)\n\t}\n\n\tfunc (e *{{.GoType}}) UnmarshalGQL(v interface{}) error {\n\t\tstr, ok := v.(string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"enums must be strings\")\n\t\t}\n\n\t\t*e = {{.GoType}}(str)\n\t\tif !e.IsValid() {\n\t\t\treturn fmt.Errorf(\"%s is not a valid {{.GQLType}}\", str)\n\t\t}\n\t\treturn nil\n\t}\n\n\tfunc (e {{.GoType}}) MarshalGQL(w io.Writer) {\n\t\tfmt.Fprint(w, strconv.Quote(e.String()))\n\t}\n\n{{- end }}\n", "object.gotpl": "{{ $object := . }}\n\nvar {{ $object.GQLType|lcFirst}}Implementors = {{$object.Implementors}}\n\n// nolint: gocyclo, errcheck, gas, goconst\n{{- if .Stream }}\nfunc (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel ast.SelectionSet) func() graphql.Marshaler {\n\tfields := graphql.CollectFields(ctx, sel, {{$object.GQLType|lcFirst}}Implementors)\n\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{\n\t\tObject: {{$object.GQLType|quote}},\n\t})\n\tif len(fields) != 1 {\n\t\tec.Errorf(ctx, \"must subscribe to exactly one stream\")\n\t\treturn nil\n\t}\n\n\tswitch fields[0].Name {\n\t{{- range $field := $object.Fields }}\n\tcase \"{{$field.GQLName}}\":\n\t\treturn ec._{{$object.GQLType}}_{{$field.GQLName}}(ctx, fields[0])\n\t{{- end }}\n\tdefault:\n\t\tpanic(\"unknown field \" + strconv.Quote(fields[0].Name))\n\t}\n}\n{{- else }}\nfunc (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel ast.SelectionSet{{if not $object.Root}}, obj *{{$object.FullName}} {{end}}) graphql.Marshaler {\n\tfields := graphql.CollectFields(ctx, sel, {{$object.GQLType|lcFirst}}Implementors)\n\t{{if $object.Root}}\n\t\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{\n\t\t\tObject: {{$object.GQLType|quote}},\n\t\t})\n\t{{end}}\n\n\t{{if $object.IsConcurrent}} var wg sync.WaitGroup {{end}}\n\tout := graphql.NewOrderedMap(len(fields))\n\tinvalid := false\n\tfor i, field := range fields {\n\t\tout.Keys[i] = field.Alias\n\n\t\tswitch field.Name {\n\t\tcase \"__typename\":\n\t\t\tout.Values[i] = graphql.MarshalString({{$object.GQLType|quote}})\n\t\t{{- range $field := $object.Fields }}\n\t\tcase \"{{$field.GQLName}}\":\n\t\t\t{{- if $field.IsConcurrent }}\n\t\t\t\twg.Add(1)\n\t\t\t\tgo func(i int, field graphql.CollectedField) {\n\t\t\t{{- end }}\n\t\t\t\tout.Values[i] = ec._{{$object.GQLType}}_{{$field.GQLName}}(ctx, field{{if not $object.Root}}, obj{{end}})\n\t\t\t\t{{- if $field.ASTType.NonNull }}\n\t\t\t\t\tif out.Values[i] == graphql.Null {\n\t\t\t\t\t\tinvalid = true\n\t\t\t\t\t}\n\t\t\t\t{{- end }}\n\t\t\t{{- if $field.IsConcurrent }}\n\t\t\t\t\twg.Done()\n\t\t\t\t}(i, field)\n\t\t\t{{- end }}\n\t\t{{- end }}\n\t\tdefault:\n\t\t\tpanic(\"unknown field \" + strconv.Quote(field.Name))\n\t\t}\n\t}\n\t{{if $object.IsConcurrent}} wg.Wait() {{end}}\n\tif invalid { return graphql.Null }\n\treturn out\n}\n{{- end }}\n", "resolver.gotpl": "//go:generate gorunpkg github.com/99designs/gqlgen\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\ntype {{.ResolverType}} struct {}\n\n{{ range $object := .Objects -}}\n\t{{- if $object.HasResolvers -}}\n\t\tfunc (r *{{$.ResolverType}}) {{$object.GQLType}}() {{ $object.ResolverInterface.FullName }} {\n\t\t\treturn &{{lcFirst $object.GQLType}}Resolver{r}\n\t\t}\n\t{{ end -}}\n{{ end }}\n\n{{ range $object := .Objects -}}\n\t{{- if $object.HasResolvers -}}\n\t\ttype {{lcFirst $object.GQLType}}Resolver struct { *Resolver }\n\n\t\t{{ range $field := $object.Fields -}}\n\t\t\t{{- if $field.IsResolver -}}\n\t\t\tfunc (r *{{lcFirst $object.GQLType}}Resolver) {{ $field.ShortResolverDeclaration }} {\n\t\t\t\tpanic(\"not implemented\")\n\t\t\t}\n\t\t\t{{ end -}}\n\t\t{{ end -}}\n\t{{ end -}}\n{{ end }}\n", "server.gotpl": "package main\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\nconst defaultPort = \"8080\"\n\nfunc main() {\n\tport := os.Getenv(\"PORT\")\n\tif port == \"\" {\n\t\tport = defaultPort\n\t}\n\n\thttp.Handle(\"/\", handler.Playground(\"GraphQL playground\", \"/query\"))\n\thttp.Handle(\"/query\", handler.GraphQL({{.ExecPackageName}}.NewExecutableSchema({{.ExecPackageName}}.Config{Resolvers: &{{.ResolverPackageName}}.Resolver{}})))\n\n\tlog.Printf(\"connect to http://localhost:%s/ for GraphQL playground\", port)\n\tlog.Fatal(http.ListenAndServe(\":\" + port, nil))\n}\n", diff --git a/codegen/templates/models.gotpl b/codegen/templates/models.gotpl index 7427d71d7a4..4777397e7b6 100644 --- a/codegen/templates/models.gotpl +++ b/codegen/templates/models.gotpl @@ -11,7 +11,9 @@ import ( {{ range $model := .Models }} {{with .Description}} {{.|prefixLines "// "}} {{end}} {{- if .IsInterface }} - type {{.GoType}} interface {} + type {{.GoType}} interface { + Is{{.GoType}}() + } {{- else }} type {{.GoType}} struct { {{- range $field := .Fields }} @@ -25,6 +27,11 @@ import ( {{- end }} {{- end }} } + + {{- range $iface := .Implements }} + func ({{$model.GoType}}) Is{{$iface.GoType}}() {} + {{- end }} + {{- end }} {{- end}} diff --git a/example/selection/models_gen.go b/example/selection/models_gen.go index ec05898826d..a94b368c070 100644 --- a/example/selection/models_gen.go +++ b/example/selection/models_gen.go @@ -6,7 +6,9 @@ import ( time "time" ) -type Event interface{} +type Event interface { + IsEvent() +} type Like struct { Reaction string `json:"reaction"` @@ -15,9 +17,13 @@ type Like struct { Collected []string `json:"collected"` } +func (Like) IsEvent() {} + type Post struct { Message string `json:"message"` Sent time.Time `json:"sent"` Selection []string `json:"selection"` Collected []string `json:"collected"` } + +func (Post) IsEvent() {} diff --git a/example/starwars/model.go b/example/starwars/model.go index 557f59a8472..4ff05e0bfd9 100644 --- a/example/starwars/model.go +++ b/example/starwars/model.go @@ -34,6 +34,9 @@ func (h *Human) Height(unit LengthUnit) float64 { } } +func (Human) IsCharacter() {} +func (Human) IsSearchResult() {} + type Review struct { Stars int Commentary *string @@ -45,6 +48,9 @@ type Droid struct { PrimaryFunction string } +func (Droid) IsCharacter() {} +func (Droid) IsSearchResult() {} + func (r *Resolver) resolveFriendConnection(ctx context.Context, ids []string, first *int, after *string) (FriendsConnection, error) { from := 0 if after != nil { diff --git a/example/starwars/models_gen.go b/example/starwars/models_gen.go index b34acb08797..4d0d65df4ef 100644 --- a/example/starwars/models_gen.go +++ b/example/starwars/models_gen.go @@ -8,7 +8,9 @@ import ( strconv "strconv" ) -type Character interface{} +type Character interface { + IsCharacter() +} type FriendsEdge struct { Cursor string `json:"cursor"` @@ -21,7 +23,9 @@ type PageInfo struct { HasNextPage bool `json:"hasNextPage"` } -type SearchResult interface{} +type SearchResult interface { + IsSearchResult() +} type Starship struct { ID string `json:"id"` @@ -30,6 +34,8 @@ type Starship struct { History [][]int `json:"history"` } +func (Starship) IsSearchResult() {} + type Episode string const ( From b8af0c811747c48190126e2d2b4006e718362756 Mon Sep 17 00:00:00 2001 From: Luke Cawood Date: Tue, 2 Oct 2018 11:51:17 +1000 Subject: [PATCH 2/3] Use types.Implements to check if an interface implementor accepts value recievers --- codegen/interface_build.go | 19 +------------------ codegen/testserver/interfaces.go | 7 +++++++ 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/codegen/interface_build.go b/codegen/interface_build.go index 9f4a4ff4d5c..92052ba6b99 100644 --- a/codegen/interface_build.go +++ b/codegen/interface_build.go @@ -1,9 +1,7 @@ package codegen import ( - "fmt" "go/types" - "os" "sort" "github.com/vektah/gqlparser/ast" @@ -51,20 +49,5 @@ func (cfg *Config) isValueReceiver(intf *NamedType, implementor *NamedType, prog return true } - for i := 0; i < interfaceType.NumMethods(); i++ { - intfMethod := interfaceType.Method(i) - - implMethod := findMethod(implementorType, intfMethod.Name()) - if implMethod == nil { - fmt.Fprintf(os.Stderr, "missing method %s on %s\n", intfMethod.Name(), implementor.GoType) - return false - } - - sig := implMethod.Type().(*types.Signature) - if _, isPtr := sig.Recv().Type().(*types.Pointer); isPtr { - return false - } - } - - return true + return types.Implements(implementorType, interfaceType) } diff --git a/codegen/testserver/interfaces.go b/codegen/testserver/interfaces.go index a1500ebf4a7..18488d8153f 100644 --- a/codegen/testserver/interfaces.go +++ b/codegen/testserver/interfaces.go @@ -4,10 +4,12 @@ import "math" type Shape interface { Area() float64 + isShape() } type ShapeUnion interface { Area() float64 + isShapeUnion() } type Circle struct { @@ -18,6 +20,9 @@ func (c *Circle) Area() float64 { return c.Radius * math.Pi * math.Pi } +func (c *Circle) isShapeUnion() {} +func (c *Circle) isShape() {} + type Rectangle struct { Length, Width float64 } @@ -25,3 +30,5 @@ type Rectangle struct { func (r *Rectangle) Area() float64 { return r.Length * r.Width } +func (r *Rectangle) isShapeUnion() {} +func (r *Rectangle) isShape() {} From d3e27553742d7559996fb9cfb310e87c5774fea4 Mon Sep 17 00:00:00 2001 From: Luke Cawood Date: Tue, 2 Oct 2018 11:54:01 +1000 Subject: [PATCH 3/3] Bump gqlparser to latest master --- Gopkg.lock | 6 +++--- Gopkg.toml | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 5e5d741300b..34890156d5d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -170,8 +170,8 @@ revision = "314ac81052eedc03ac0a79bdc89d05a49a2a5814" [[projects]] - branch = "add-schema-implements" - digest = "1:cb4a3b347499d5cca82cef6daa1fd1932f027a7cab651689c16af85300463eb2" + branch = "master" + digest = "1:7fa8a2105f0b0853395a85d3dd8ef1475c44919374c6496dba7b987586edaced" name = "github.com/vektah/gqlparser" packages = [ ".", @@ -183,7 +183,7 @@ "validator/rules", ] pruneopts = "UT" - revision = "bd86bdb57ce80cd71b3b7f8707bb7e09b3fb2fab" + revision = "f119686bf1d4d1a68c7ed6afe35f183625443c41" [[projects]] branch = "master" diff --git a/Gopkg.toml b/Gopkg.toml index 91960cf4e47..b12c5375ffe 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -12,10 +12,6 @@ required = ["github.com/vektah/dataloaden"] branch = "master" name = "golang.org/x/tools" -[[constraint]] - branch = "add-schema-implements" - name = "github.com/vektah/gqlparser" - [prune] go-tests = true unused-packages = true