weixin_39617470
weixin_39617470
2021-01-12 18:47

Cannot map Go interface type to schema interface/union type

I am hitting an issue where I cannot get the gqlgen generated code to build when I map a schema interface type to a Go interface.

Below is an example of what I'm talking about. Full code and detailed instructions to reproduce are at https://github.com/ereyes01/gqlgen-interface-bug

Suppose I have this schema:

graphql
schema {
    query: Query
}

type Query {
    shapes(): [Shape]
}

interface Shape {
    area(): Float
}

type Circle implements Shape {
    radius: Float
    area() : Float
}

type Rectangle implements Shape {
    length: Float
    width: Float
    area(): Float
}

And suppose I have the following corresponding Go types:

go
package shapes

import "math"

type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

func (c *Circle) Area() float64 {
    return c.Radius * math.Pi * math.Pi
}

type Rectangle struct {
    Length, Width float64
}

func (r *Rectangle) Area() float64 {
    return r.Length * r.Width
}

So let's map our types as follows:

json
{
    "Shape": "github.com/ereyes01/gqlgen-interface-bug/shapes.Shape",
    "Circle": "github.com/ereyes01/gqlgen-interface-bug/shapes.Circle",
    "Rectangle": "github.com/ereyes01/gqlgen-interface-bug/shapes.Rectangle"
}

NOTE: ^^ This uses the path to my repository given above as if I'd obtained it via go get.

So from this we can now create our generated.go as follows:

$ gqlgen -out generated.go -package shapes -typemap types.json schema.graphql

... and that works fine. We now implement the needed resolver:

go
package shapes

import "context"

type ShapeResolver struct{}

func (r *ShapeResolver) Query_shapes(ctx context.Context) ([]Shape, error) {
    return []Shape{
        &Circle{Radius: 10.0},
        &Rectangle{Length: 1.0, Width: 10.0},
        &Rectangle{Length: 10.0, Width: 10.0},
    }, nil
}

... And I confirmed that the code generated for Shape.area() looks correct- it just calls the Area() Go method on the Go type.

So now let's say that inside shapes/server we place a simple Playground server:

go
package main

import (
    "log"
    "net/http"

    "github.com/ereyes01/gqlgen-interface-bug/shapes"
    "github.com/vektah/gqlgen/handler"
)

func main() {
    http.Handle("/", handler.Playground("Shapes", "/query"))
    http.Handle("/query", handler.GraphQL(shapes.MakeExecutableSchema(new(shapes.ShapeResolver))))

    log.Fatal(http.ListenAndServe(":9090", nil))
}

If I try to build that via go install I get a compile error in the generated code:


$ go install github.com/ereyes01/gqlgen-interface-bug/shapes/server
# github.com/ereyes01/gqlgen-interface-bug/shapes
shapes/generated.go:711:2: impossible type switch case: *obj (type Shape) cannot have dynamic type Circle (Area method has pointer receiver)
shapes/generated.go:716:2: impossible type switch case: *obj (type Shape) cannot have dynamic type Rectangle (Area method has pointer receiver)

If we go have a look at that part of the generated code, we have:

go
func (ec *executionContext) _Shape(sel []query.Selection, obj *Shape) graphql.Marshaler {
    switch obj := (*obj).(type) {
    case nil:
        return graphql.Null
    case Circle:
        return ec._Circle(sel, &obj)

    case *Circle:
        return ec._Circle(sel, obj)
    case Rectangle:
        return ec._Rectangle(sel, &obj)

    case *Rectangle:
        return ec._Rectangle(sel, obj)
    default:
        panic(fmt.Errorf("unexpected type %T", obj))
    }
}

Of course, the case Circle and case Rectangle are not valid when type-switching on the Shape type, as those types do not implement the Shape interface (the pointers to those types do).

I could work around this by commenting out the case Circle and case Rectangle sections, and then everything works fine.

I investigated why this code was generated, and I found that in the template for the interface type, it just blindly writes out a case for both the Go type and its pointer:


{{- $interface := . }}

func (ec *executionContext) _{{$interface.GQLType}}(sel []query.Selection, obj *{{$interface.FullName}}) graphql.Marshaler {
    switch obj := (*obj).(type) {
    case nil:
        return graphql.Null
    {{- range $implementor := $interface.Implementors }}
    case {{$implementor.FullName}}:
        return ec._{{$implementor.GQLType}}(sel, &obj)

    case *{{$implementor.FullName}}:
        return ec._{{$implementor.GQLType}}(sel, obj)

    {{- end }}
    default:
        panic(fmt.Errorf("unexpected type %T", obj))
    }
}

This generated will only ever work if:

  • The matching Go interface type is the empty interface (interface{})
  • Both the type and its pointer implement the matching Go interface

... which doesn't sound right if you're going to allow the interface type to be matched to your own type.

I'm not immediately sure what the best way to fix this might be. One way might be to look into whether the Go type system can tell you which are all the implementing types of an interface (I haven't really investigated that rabbit hole).

Another approach might be to allow us to tell you the implementing types in the types.json file. Of course, that will further complicate the format of that JSON file, but it seems otherwise robust at first glance.

Thanks for taking a look.

该提问来源于开源项目:99designs/gqlgen

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答

4条回答

  • weixin_39617470 weixin_39617470 4月前

    That did it, thanks again!

    点赞 评论 复制链接分享
  • weixin_39824223 weixin_39824223 4月前

    Of course, the case Circle and case Rectangle are not valid when type-switching on the Shape type, as those types do not implement the Shape interface (the pointers to those types do).

    in this case they aren't, but if they accepted non pointer receivers, eg:

    
    func (c Circle) Area() float64 {
        return c.Radius * math.Pi * math.Pi
    }
    

    then it would be valid to switch on case Circle:

    I'm not immediately sure what the best way to fix this might be. One way might be to look into whether the Go type system can tell you which are all the implementing types of an interface (I haven't really investigated that rabbit hole).

    I think this has to be the answer.

    Another approach might be to allow us to tell you the implementing types in the types.json file. Of course, that will further complicate the format of that JSON file, but it seems otherwise robust at first glance.

    You already know the implementing types, the schema maps them out for you.

    What probably needs to be added is a bindInterface pass after buildInterface.

    eg for Objects

    And the interface implementor struct probably needs a custom type to carry this extra info into the template.

    If you want to continue your deep dive, a PR it would be appreciated. Otherwise I'll take a look when I get a chance.

    点赞 评论 复制链接分享
  • weixin_39617470 weixin_39617470 4月前

    Thanks , really appreciate the fix! This was still a few items deep in my todo list...

    However, while the issue seems to have been fixed for the graphql interface type, it seems to still persist for the union type. I have added a union type mapped to a Go interface in my sample repo that reproduces this problem with the latest code: https://github.com/ereyes01/gqlgen-interface-bug/commit/ad9648ba20358cb1a248bb98f5bff8f08394d93d

    Let me know if you will reopen this, or if you need a new issue. Thanks!

    点赞 评论 复制链接分享
  • weixin_39824223 weixin_39824223 4月前

    Should be fixed in https://github.com/vektah/gqlgen/commit/15b3af2d5a6ed7240d09fb4c21fd905c8c316aa7

    点赞 评论 复制链接分享

相关推荐