Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix marshaling interfaces and union types #3211

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions _examples/federation/accounts/graph/generated.go

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

12 changes: 12 additions & 0 deletions _examples/federation/products/graph/generated.go

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

24 changes: 24 additions & 0 deletions _examples/federation/reviews/graph/generated.go

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

12 changes: 12 additions & 0 deletions _examples/selection/generated.go

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

30 changes: 30 additions & 0 deletions _examples/starwars/generated/exec.go

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

12 changes: 12 additions & 0 deletions _examples/type-system-extension/generated.go

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

3 changes: 3 additions & 0 deletions codegen/interface.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ func (ec *executionContext) _{{$interface.Name}}(ctx context.Context, sel ast.Se
return graphql.Null
{{- range $implementor := $interface.Implementors }}
case {{$implementor.Type | ref}}:
if len(graphql.CollectFields(ec.OperationContext, sel, []string{"{{$interface.Type | typeName }}", "{{$implementor.Type | typeName}}"})) == 0 {
return graphql.Empty{}
}
{{- if $implementor.CanBeNil }}
if obj == nil {
return graphql.Null
Expand Down
21 changes: 16 additions & 5 deletions codegen/templates/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ func Render(cfg Options) error {
}

roots := make([]string, 0, len(t.Templates()))
for _, template := range t.Templates() {
for _, templ := range t.Templates() {
// templates that end with _.gotpl are special files we don't want to include
if strings.HasSuffix(template.Name(), "_.gotpl") ||
if strings.HasSuffix(templ.Name(), "_.gotpl") ||
// filter out templates added with {{ template xxx }} syntax inside the template file
!strings.HasSuffix(template.Name(), ".gotpl") {
!strings.HasSuffix(templ.Name(), ".gotpl") {
continue
}

roots = append(roots, template.Name())
roots = append(roots, templ.Name())
}

// then execute all the important looking ones in order, adding them to the same file
Expand Down Expand Up @@ -220,6 +220,7 @@ func Funcs() template.FuncMap {
"render": func(filename string, tpldata any) (*bytes.Buffer, error) {
return render(resolveName(filename, 0), tpldata)
},
"typeName": typeName,
}
}

Expand Down Expand Up @@ -647,6 +648,16 @@ func resolveName(name string, skip int) string {
return filepath.Join(filepath.Dir(callerFile), name)
}

func typeName(t types.Type) string {
name := types.TypeString(t, func(*types.Package) string {
return ""
})
if name != "" && strings.HasPrefix(name, "*") {
return name[1:]
}
return name
}

func render(filename string, tpldata any) (*bytes.Buffer, error) {
t := template.New("").Funcs(Funcs())

Expand All @@ -672,7 +683,7 @@ func write(filename string, b []byte, packages *code.Packages) error {

formatted, err := imports.Prune(filename, b, packages)
if err != nil {
fmt.Fprintf(os.Stderr, "gofmt failed on %s: %s\n", filepath.Base(filename), err.Error())
_, _ = fmt.Fprintf(os.Stderr, "gofmt failed on %s: %s\n", filepath.Base(filename), err.Error())
formatted = b
}

Expand Down
26 changes: 26 additions & 0 deletions codegen/templates/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package templates
import (
"embed"
"fmt"
"go/types"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -355,3 +356,28 @@ func TestRenderFS(t *testing.T) {
// don't look at last character since it's \n on Linux and \r\n on Windows
assert.Equal(t, expectedString, actualContentsStr[:len(expectedString)])
}

func TestTypeName(t *testing.T) {
testType := types.NewNamed(
types.NewTypeName(0, types.NewPackage(
"github.com/99designs/gqlgen/codegen/templates",
"templates",
), "testType", nil),
types.NewStruct(nil, nil),
nil,
)

tests := []struct {
input types.Type
expected string
}{
{testType, "testType"},
{types.NewPointer(testType), "testType"},
{types.NewPointer(types.NewPointer(testType)), "*testType"},
}

for _, test := range tests {
result := typeName(test.input)
assert.Equal(t, test.expected, result)
}
}
25 changes: 25 additions & 0 deletions codegen/testserver/followschema/generated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ func TestUnionFragments(t *testing.T) {
resolvers.QueryResolver.ShapeUnion = func(ctx context.Context) (ShapeUnion, error) {
return &Circle{Radius: 32}, nil
}
resolvers.QueryResolver.Shapes = func(ctx context.Context) ([]Shape, error) {
return []Shape{
&Circle{Radius: 45},
&Circle{Radius: 54},
}, nil
}

srv := handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolvers}))
c := client.New(srv)
Expand Down Expand Up @@ -78,4 +84,23 @@ func TestUnionFragments(t *testing.T) {
`, &resp)
require.NotEmpty(t, resp.ShapeUnion.Radius)
})

t.Run("without circle", func(t *testing.T) {
var resp struct {
Shapes []struct {
Length, Width float64
}
}
require.Empty(t, resp.Shapes)
c.MustPost(`query {
shapes {
... on Rectangle {
length
width
}
}
}
`, &resp)
require.Empty(t, resp.Shapes)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats not compliant with the GraphQL spec. The resolver must return two empty objects (for the two circles) here. I also write the comment on the issue. Its not a but or inconvenience if the graphql server is returning empty objects in union or interface types.

Thats part of the specification. https://spec.graphql.org/October2021/#sec-Unions

and here is one quote:

interfaces must return the set of interfaces that an object implements (if none, interfaces must return the empty set).

})
}
Loading
Loading