-
Notifications
You must be signed in to change notification settings - Fork 491
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
Add support for interfaces implementing other interfaces #436
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,6 +48,7 @@ type Schema struct { | |
unions []*Union | ||
enums []*Enum | ||
extensions []*Extension | ||
interfaces []*Interface | ||
} | ||
|
||
// Resolve a named type in the schema by its name. | ||
|
@@ -101,11 +102,13 @@ type Object struct { | |
// | ||
// http://facebook.github.io/graphql/draft/#sec-Interfaces | ||
type Interface struct { | ||
Name string | ||
PossibleTypes []*Object | ||
Fields FieldList // NOTE: the spec refers to this as `FieldsDefinition`. | ||
Desc string | ||
Directives common.DirectiveList | ||
Name string | ||
PossibleTypes []*Object | ||
Fields FieldList // NOTE: the spec refers to this as `FieldsDefinition`. | ||
Desc string | ||
Directives common.DirectiveList | ||
Interfaces []*Interface // NOTE: http://spec.graphql.org/draft/#sec-Interfaces.Interfaces-Implementing-Interfaces | ||
interfaceNames []string | ||
} | ||
|
||
// Union types represent objects that could be one of a list of GraphQL object types, but provides no | ||
|
@@ -340,6 +343,52 @@ func (s *Schema) Parse(schemaString string, useStringDescriptions bool) error { | |
} | ||
} | ||
|
||
for _, iface := range s.interfaces { | ||
iface.Interfaces = make([]*Interface, len(iface.interfaceNames)) | ||
|
||
for i, intfName := range iface.interfaceNames { | ||
t, ok := s.Types[intfName] | ||
if !ok { | ||
return errors.Errorf("interface %q not found", intfName) | ||
} | ||
intf, ok := t.(*Interface) | ||
if !ok { | ||
return errors.Errorf("type %q is not an interface", intfName) | ||
} | ||
for _, f := range intf.Fields.Names() { | ||
if iface.Fields.Get(f) == nil { | ||
return errors.Errorf("interface %q expects field %q but %q does not provide it", intfName, f, iface.Name) | ||
} | ||
} | ||
|
||
iface.Interfaces[i] = intf | ||
} | ||
} | ||
|
||
for _, iface := range s.interfaces { | ||
if iface == nil { | ||
continue | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this ever be true? Can we have a nil interface? |
||
expectedNames := interfaceDependancies(*iface) | ||
actualNames := iface.interfaceNames | ||
|
||
expMap := make(map[string]int) | ||
actMap := make(map[string]int) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you, please, remove the |
||
|
||
for _, name := range expectedNames { | ||
expMap[name]++ | ||
} | ||
for _, name := range actualNames { | ||
actMap[name]++ | ||
} | ||
|
||
for name, id := range expMap { | ||
if actMap[name] != id { | ||
return errors.Errorf("interface %q must explicitly implement transitive interface %q", iface.Name, name) | ||
} | ||
} | ||
} | ||
|
||
for _, union := range s.unions { | ||
if err := resolveDirectives(s, union.Directives, "UNION"); err != nil { | ||
return err | ||
|
@@ -557,6 +606,7 @@ func parseSchema(s *Schema, l *common.Lexer) { | |
iface := parseInterfaceDef(l) | ||
iface.Desc = desc | ||
s.Types[iface.Name] = iface | ||
s.interfaces = append(s.interfaces, iface) | ||
|
||
case "union": | ||
union := parseUnionDef(l) | ||
|
@@ -634,6 +684,18 @@ func parseObjectDef(l *common.Lexer) *Object { | |
func parseInterfaceDef(l *common.Lexer) *Interface { | ||
i := &Interface{Name: l.ConsumeIdent()} | ||
|
||
if l.Peek() == scanner.Ident { | ||
l.ConsumeKeyword("implements") | ||
|
||
for l.Peek() != '{' && l.Peek() != '@' { | ||
if l.Peek() == '&' { | ||
l.ConsumeToken('&') | ||
} | ||
|
||
i.interfaceNames = append(i.interfaceNames, l.ConsumeIdent()) | ||
} | ||
} | ||
|
||
i.Directives = common.ParseDirectives(l) | ||
l.ConsumeToken('{') | ||
i.Fields = parseFieldsDef(l) | ||
|
@@ -770,3 +832,30 @@ func parseFieldsDef(l *common.Lexer) FieldList { | |
} | ||
return fields | ||
} | ||
|
||
// interfaceDependancies searches through the tree of interface relations | ||
// to build a slice of all the names that should be implemented on the | ||
// given interface | ||
// | ||
// TODO: Would be more efficient not to search every interface multiple | ||
// times. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my comment above how to handle that. |
||
// | ||
// NOTE: https://spec.graphql.org/draft/#sel-FAHbhBHCAACGB35P | ||
func interfaceDependancies(iface Interface) []string { | ||
var search func(requiredNames []string, iface Interface) []string | ||
|
||
search = func(requiredNames []string, iface Interface) []string { | ||
for _, f := range iface.Interfaces { | ||
if f == nil { | ||
continue | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this happen? Is it possible that one of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, good point, ifaces should never be nil in the current implementation. Just used to checking pointer types, is there a reason all the types are using pointers or can I change There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what the reason is since I haven't written this code but my guess is that the author didn't want to copy structs by value for performance reasons. On the other hand, interfaces are not supposed to be huge. But we never know how people use it. So let's leave it consistent with all other types. I'd prefer if both the |
||
if len(f.Interfaces) > 0 { | ||
requiredNames = append(requiredNames, search(requiredNames, *f)...) | ||
} | ||
requiredNames = append(requiredNames, f.interfaceNames...) | ||
} | ||
return requiredNames | ||
} | ||
|
||
return search([]string{}, iface) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to avoid checking interfaces multiple times, you might define a variable of type
map[string][]string
above thisfor
statement that contains all the dependencies for each interface. Then you will check every interface exactly once if it is not in the map already.However, if I understand correctly, you will need to sort all the interfaces by
len(interfaceNames)
ascending. This way interfaces that implement multiple other interfaces will make use of that map efficiently. This map will be used only once at runtime and then it will be garbage collected.