Skip to content

Commit 21d1871

Browse files
authored
Support "Interfaces Implementing Interfaces" (#471)
Interface implementing interfaces support https://spec.graphql.org/draft/#sec-Interfaces.Interfaces-Implementing-Interfaces
1 parent 417fcd2 commit 21d1871

File tree

4 files changed

+225
-1
lines changed

4 files changed

+225
-1
lines changed

graphql_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4246,3 +4246,54 @@ func TestQueryVariablesValidation(t *testing.T) {
42464246
}},
42474247
}})
42484248
}
4249+
4250+
type interfaceImplementingInterfaceResolver struct{}
4251+
type interfaceImplementingInterfaceExample struct {
4252+
A string
4253+
B string
4254+
C bool
4255+
}
4256+
4257+
func (r *interfaceImplementingInterfaceResolver) Hey() *interfaceImplementingInterfaceExample {
4258+
return &interfaceImplementingInterfaceExample{
4259+
A: "testing",
4260+
B: "test",
4261+
C: true,
4262+
}
4263+
}
4264+
4265+
func TestInterfaceImplementingInterface(t *testing.T) {
4266+
gqltesting.RunTests(t, []*gqltesting.Test{{
4267+
Schema: graphql.MustParseSchema(`
4268+
interface A {
4269+
a: String!
4270+
}
4271+
interface B implements A {
4272+
a: String!
4273+
b: String!
4274+
}
4275+
interface C implements B & A {
4276+
a: String!
4277+
b: String!
4278+
c: Boolean!
4279+
}
4280+
type ABC implements C {
4281+
a: String!
4282+
b: String!
4283+
c: Boolean!
4284+
}
4285+
type Query {
4286+
hey: ABC
4287+
}`, &interfaceImplementingInterfaceResolver{}, graphql.UseFieldResolvers(), graphql.UseFieldResolvers()),
4288+
Query: `query {hey { a b c }}`,
4289+
ExpectedResult: `
4290+
{
4291+
"hey": {
4292+
"a": "testing",
4293+
"b": "test",
4294+
"c": true
4295+
}
4296+
}
4297+
`,
4298+
}})
4299+
}

internal/schema/schema.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,33 @@ func Parse(s *types.Schema, schemaString string, useStringDescriptions bool) err
7676
s.EntryPoints[key] = t
7777
}
7878

79+
// Interface types need validation: https://spec.graphql.org/draft/#sec-Interfaces.Interfaces-Implementing-Interfaces
80+
for _, typeDef := range s.Types {
81+
switch t := typeDef.(type) {
82+
case *types.InterfaceTypeDefinition:
83+
for i, implements := range t.Interfaces {
84+
typ, ok := s.Types[implements.Name]
85+
if !ok {
86+
return errors.Errorf("interface %q not found", implements)
87+
}
88+
inteface, ok := typ.(*types.InterfaceTypeDefinition)
89+
if !ok {
90+
return errors.Errorf("type %q is not an interface", inteface)
91+
}
92+
93+
for _, f := range inteface.Fields.Names() {
94+
if t.Fields.Get(f) == nil {
95+
return errors.Errorf("interface %q expects field %q but %q does not provide it", inteface.Name, f, t.Name)
96+
}
97+
}
98+
99+
t.Interfaces[i] = inteface
100+
}
101+
default:
102+
continue
103+
}
104+
}
105+
79106
for _, obj := range s.Objects {
80107
obj.Interfaces = make([]*types.InterfaceTypeDefinition, len(obj.InterfaceNames))
81108
if err := resolveDirectives(s, obj.Directives, "OBJECT"); err != nil {
@@ -406,6 +433,16 @@ func parseObjectDef(l *common.Lexer) *types.ObjectTypeDefinition {
406433
func parseInterfaceDef(l *common.Lexer) *types.InterfaceTypeDefinition {
407434
i := &types.InterfaceTypeDefinition{Loc: l.Location(), Name: l.ConsumeIdent()}
408435

436+
if l.Peek() == scanner.Ident {
437+
l.ConsumeKeyword("implements")
438+
i.Interfaces = append(i.Interfaces, &types.InterfaceTypeDefinition{Name: l.ConsumeIdent()})
439+
440+
for l.Peek() == '&' {
441+
l.ConsumeToken('&')
442+
i.Interfaces = append(i.Interfaces, &types.InterfaceTypeDefinition{Name: l.ConsumeIdent()})
443+
}
444+
}
445+
409446
i.Directives = common.ParseDirectives(l)
410447

411448
l.ConsumeToken('{')

internal/schema/schema_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,3 +873,137 @@ Second line of the description.
873873
})
874874
}
875875
}
876+
877+
func TestInterfaceImplementsInterface(t *testing.T) {
878+
for _, tt := range []struct {
879+
name string
880+
sdl string
881+
useStringDescriptions bool
882+
validateError func(err error) error
883+
validateSchema func(s *types.Schema) error
884+
}{
885+
{
886+
name: "Parses interface implementing other interface",
887+
sdl: `
888+
interface Foo {
889+
field: String!
890+
}
891+
interface Bar implements Foo {
892+
field: String!
893+
}
894+
`,
895+
validateSchema: func(s *types.Schema) error {
896+
const implementedInterfaceName = "Bar"
897+
typ, ok := s.Types[implementedInterfaceName].(*types.InterfaceTypeDefinition)
898+
if !ok {
899+
return fmt.Errorf("interface %q not found", implementedInterfaceName)
900+
}
901+
if len(typ.Fields) != 1 {
902+
return fmt.Errorf("invalid number of fields: want %d, have %d", 1, len(typ.Fields))
903+
}
904+
const fieldName = "field"
905+
906+
if typ.Fields[0].Name != fieldName {
907+
return fmt.Errorf("field %q not found", fieldName)
908+
}
909+
910+
if len(typ.Interfaces) != 1 {
911+
return fmt.Errorf("invalid number of implementing interfaces found on %q: want %d, have %d", implementedInterfaceName, 1, len(typ.Interfaces))
912+
}
913+
914+
const implementingInterfaceName = "Foo"
915+
if typ.Interfaces[0].Name != implementingInterfaceName {
916+
return fmt.Errorf("interface %q not found", implementingInterfaceName)
917+
}
918+
919+
return nil
920+
},
921+
},
922+
{
923+
name: "Parses interface transitively implementing an interface that implements an interface",
924+
sdl: `
925+
interface Foo {
926+
field: String!
927+
}
928+
interface Bar implements Foo {
929+
field: String!
930+
}
931+
interface Baz implements Bar & Foo {
932+
field: String!
933+
}
934+
`,
935+
validateSchema: func(s *types.Schema) error {
936+
const implementedInterfaceName = "Baz"
937+
typ, ok := s.Types[implementedInterfaceName].(*types.InterfaceTypeDefinition)
938+
if !ok {
939+
return fmt.Errorf("interface %q not found", implementedInterfaceName)
940+
}
941+
if len(typ.Fields) != 1 {
942+
return fmt.Errorf("invalid number of fields: want %d, have %d", 1, len(typ.Fields))
943+
}
944+
const fieldName = "field"
945+
946+
if typ.Fields[0].Name != fieldName {
947+
return fmt.Errorf("field %q not found", fieldName)
948+
}
949+
950+
if len(typ.Interfaces) != 2 {
951+
return fmt.Errorf("invalid number of implementing interfaces found on %q: want %d, have %d", implementedInterfaceName, 2, len(typ.Interfaces))
952+
}
953+
954+
const firstImplementingInterfaceName = "Bar"
955+
if typ.Interfaces[0].Name != firstImplementingInterfaceName {
956+
return fmt.Errorf("first interface %q not found", firstImplementingInterfaceName)
957+
}
958+
959+
const secondImplementingInterfaceName = "Foo"
960+
if typ.Interfaces[1].Name != secondImplementingInterfaceName {
961+
return fmt.Errorf("second interface %q not found", secondImplementingInterfaceName)
962+
}
963+
964+
return nil
965+
},
966+
},
967+
{
968+
name: "Transitively implemented interfaces must also be defined on an implementing type or interface",
969+
sdl: `
970+
interface A {
971+
message: String!
972+
}
973+
interface B implements A {
974+
message: String!
975+
name: String!
976+
}
977+
interface C implements B {
978+
message: String!
979+
name: String!
980+
hug: Boolean!
981+
}
982+
`,
983+
validateError: func(err error) error {
984+
msg := `graphql: interface "C" must explicitly implement transitive interface "A"`
985+
if err == nil || err.Error() != msg {
986+
return fmt.Errorf("expected error %q, but got %q", msg, err)
987+
}
988+
return nil
989+
},
990+
},
991+
} {
992+
t.Run(tt.name, func(t *testing.T) {
993+
s, err := schema.ParseSchema(tt.sdl, tt.useStringDescriptions)
994+
if err != nil {
995+
if tt.validateError == nil {
996+
t.Fatal(err)
997+
}
998+
if err := tt.validateError(err); err != nil {
999+
t.Fatal(err)
1000+
}
1001+
}
1002+
if tt.validateSchema != nil {
1003+
if err := tt.validateSchema(s); err != nil {
1004+
t.Fatal(err)
1005+
}
1006+
}
1007+
})
1008+
}
1009+
}

types/interface.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package types
22

33
import "github.com/graph-gophers/graphql-go/errors"
44

5-
// InterfaceTypeDefinition represents a list of named fields and their arguments.
5+
// InterfaceTypeDefinition recusrively defines list of named fields with their arguments via the
6+
// implementation chain of interfaces.
67
//
78
// GraphQL objects can then implement these interfaces which requires that the object type will
89
// define all fields defined by those interfaces.
@@ -15,6 +16,7 @@ type InterfaceTypeDefinition struct {
1516
Desc string
1617
Directives DirectiveList
1718
Loc errors.Location
19+
Interfaces []*InterfaceTypeDefinition
1820
}
1921

2022
func (*InterfaceTypeDefinition) Kind() string { return "INTERFACE" }

0 commit comments

Comments
 (0)