Skip to content

Commit

Permalink
Merge pull request #1020 from 99designs/handle-interfaces-implementin…
Browse files Browse the repository at this point in the history
…g-interfaces

Handle interfaces that implement interfaces
  • Loading branch information
vektah committed Feb 9, 2020
2 parents ab8d62b + b7a58a1 commit 30e2375
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 34 deletions.
6 changes: 3 additions & 3 deletions codegen/config/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func (t *TypeReference) IsPtr() bool {
}

func (t *TypeReference) IsNilable() bool {
return isNilable(t.GO)
return IsNilable(t.GO)
}

func (t *TypeReference) IsSlice() bool {
Expand Down Expand Up @@ -403,14 +403,14 @@ func (b *Binder) CopyModifiersFromAst(t *ast.Type, base types.Type) types.Type {
_, isInterface = named.Underlying().(*types.Interface)
}

if !isInterface && !isNilable(base) && !t.NonNull {
if !isInterface && !IsNilable(base) && !t.NonNull {
return types.NewPointer(base)
}

return base
}

func isNilable(t types.Type) bool {
func IsNilable(t types.Type) bool {
if namedType, isNamed := t.(*types.Named); isNamed {
t = namedType.Underlying()
}
Expand Down
5 changes: 4 additions & 1 deletion codegen/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ func BuildData(cfg *config.Config) (*Data, error) {
s.Inputs = append(s.Inputs, input)

case ast.Union, ast.Interface:
s.Interfaces[schemaType.Name] = b.buildInterface(schemaType)
s.Interfaces[schemaType.Name], err = b.buildInterface(schemaType)
if err != nil {
return nil, errors.Wrap(err, "unable to bind to interface")
}
}
}

Expand Down
67 changes: 46 additions & 21 deletions codegen/interface.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package codegen

import (
"fmt"
"go/types"

"github.com/pkg/errors"
"github.com/vektah/gqlparser/ast"

"github.com/99designs/gqlgen/codegen/config"
)

type Interface struct {
Expand All @@ -16,11 +20,11 @@ type Interface struct {
type InterfaceImplementor struct {
*ast.Definition

Interface *Interface
Type types.Type
Type types.Type
TakeRef bool
}

func (b *builder) buildInterface(typ *ast.Definition) *Interface {
func (b *builder) buildInterface(typ *ast.Definition) (*Interface, error) {
obj, err := b.Binder.DefaultUserObject(typ.Name)
if err != nil {
panic(err)
Expand All @@ -32,32 +36,53 @@ func (b *builder) buildInterface(typ *ast.Definition) *Interface {
InTypemap: b.Config.Models.UserDefined(typ.Name),
}

interfaceType, err := findGoInterface(i.Type)
if interfaceType == nil || err != nil {
return nil, fmt.Errorf("%s is not an interface", i.Type)
}

for _, implementor := range b.Schema.GetPossibleTypes(typ) {
obj, err := b.Binder.DefaultUserObject(implementor.Name)
if err != nil {
panic(err)
return nil, fmt.Errorf("%s has no backing go type", implementor.Name)
}

i.Implementors = append(i.Implementors, InterfaceImplementor{
Definition: implementor,
Type: obj,
Interface: i,
})
}
implementorType, err := findGoNamedType(obj)
if err != nil {
return nil, errors.Wrapf(err, "can not find backing go type %s", obj.String())
} else if implementorType == nil {
return nil, fmt.Errorf("can not find backing go type %s", obj.String())
}

return i
}
anyValid := false

func (i *InterfaceImplementor) ValueReceiver() bool {
interfaceType, err := findGoInterface(i.Interface.Type)
if interfaceType == nil || err != nil {
return true
}
// first check if the value receiver can be nil, eg can we type switch on case Thing:
if types.Implements(implementorType, interfaceType) {
i.Implementors = append(i.Implementors, InterfaceImplementor{
Definition: implementor,
Type: obj,
TakeRef: !types.IsInterface(obj),
})
anyValid = true
}

// then check if the pointer receiver can be nil, eg can we type switch on case *Thing:
if types.Implements(types.NewPointer(implementorType), interfaceType) {
i.Implementors = append(i.Implementors, InterfaceImplementor{
Definition: implementor,
Type: types.NewPointer(obj),
})
anyValid = true
}

implementorType, err := findGoNamedType(i.Type)
if implementorType == nil || err != nil {
return true
if !anyValid {
return nil, fmt.Errorf("%s does not satisfy the interface %s", implementorType.String(), i.Type.String())
}
}

return types.Implements(implementorType, interfaceType)
return i, nil
}

func (i *InterfaceImplementor) CanBeNil() bool {
return config.IsNilable(i.Type)
}
16 changes: 7 additions & 9 deletions codegen/interface.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ func (ec *executionContext) _{{$interface.Name}}(ctx context.Context, sel ast.Se
case nil:
return graphql.Null
{{- range $implementor := $interface.Implementors }}
{{- if $implementor.ValueReceiver }}
case {{$implementor.Type | ref}}:
return ec._{{$implementor.Name}}(ctx, sel, &obj)
{{- end}}
case *{{$implementor.Type | ref}}:
if obj == nil {
return graphql.Null
}
return ec._{{$implementor.Name}}(ctx, sel, obj)
case {{$implementor.Type | ref}}:
{{- if $implementor.CanBeNil }}
if obj == nil {
return graphql.Null
}
{{- end }}
return ec._{{$implementor.Name}}(ctx, sel, {{ if $implementor.TakeRef }}&{{ end }}obj)
{{- end }}
default:
panic(fmt.Errorf("unexpected type %T", obj))
Expand Down
124 changes: 124 additions & 0 deletions codegen/testserver/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions codegen/testserver/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ func (n *ConcreteNodeA) Child() (Node, error) {
return n.child, nil
}

// Implements the Node interface with another interface
type ConcreteNodeInterface interface {
Node
ID() string
}

type ConcreteNodeInterfaceImplementor struct{}

func (c ConcreteNodeInterfaceImplementor) ID() string {
return "CNII"
}

func (c ConcreteNodeInterfaceImplementor) Child() (Node, error) {
return &ConcreteNodeA{
ID: "Child",
Name: "child",
}, nil
}

type BackedByInterface interface {
ThisShouldBind() string
ThisShouldBindWithError() (string, error)
Expand Down
6 changes: 6 additions & 0 deletions codegen/testserver/interfaces.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,9 @@ type ConcreteNodeA implements Node {
child: Node!
name: String!
}

""" Implements the Node interface with another interface """
type ConcreteNodeInterface implements Node {
id: ID!
child: Node!
}
21 changes: 21 additions & 0 deletions codegen/testserver/interfaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,25 @@ func TestInterfaces(t *testing.T) {
err := c.Post(`{ notAnInterface { id, thisShouldBind, thisShouldBindWithError } }`, &resp)
require.EqualError(t, err, `[{"message":"boom","path":["notAnInterface","thisShouldBindWithError"]}]`)
})

t.Run("interfaces can implement other interfaces", func(t *testing.T) {
resolvers := &Stub{}
resolvers.QueryResolver.Node = func(ctx context.Context) (node Node, err error) {
return ConcreteNodeInterfaceImplementor{}, nil
}

c := client.New(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolvers})))

var resp struct {
Node struct {
ID string
Child struct {
ID string
}
}
}
c.MustPost(`{ node { id, child { id } } }`, &resp)
require.Equal(t, "CNII", resp.Node.ID)
require.Equal(t, "Child", resp.Node.Child.ID)
})
}

0 comments on commit 30e2375

Please sign in to comment.