diff --git a/validator/schema.go b/validator/schema.go index 3d3d76be..3242822f 100644 --- a/validator/schema.go +++ b/validator/schema.go @@ -195,12 +195,8 @@ func validateDefinition(schema *Schema, def *Definition) *gqlerror.Error { } for _, intf := range def.Interfaces { - intDef := schema.Types[intf] - if intDef == nil { - return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(intf)) - } - if intDef.Kind != Interface { - return gqlerror.ErrorPosf(def.Position, "%s is a non interface type %s.", strconv.Quote(intf), intDef.Kind) + if err := validateImplements(schema, def, intf); err != nil { + return err } } @@ -302,6 +298,84 @@ func validateDirectives(schema *Schema, dirs DirectiveList, currentDirective *Di return nil } +func validateImplements(schema *Schema, def *Definition, intfName string) *gqlerror.Error { + // see validation rules at the bottom of + // https://facebook.github.io/graphql/June2018/#sec-Objects + intf := schema.Types[intfName] + if intf == nil { + return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(intfName)) + } + if intf.Kind != Interface { + return gqlerror.ErrorPosf(def.Position, "%s is a non interface type %s.", strconv.Quote(intfName), intf.Kind) + } + for _, requiredField := range intf.Fields { + foundField := def.Fields.ForName(requiredField.Name) + if foundField == nil { + return gqlerror.ErrorPosf(def.Position, + `For %s to implement %s it must have a field called %s.`, + def.Name, intf.Name, requiredField.Name, + ) + } + + if !isCovariant(schema, requiredField.Type, foundField.Type) { + return gqlerror.ErrorPosf(foundField.Position, + `For %s to implement %s the field %s must have type %s.`, + def.Name, intf.Name, requiredField.Name, requiredField.Type.String(), + ) + } + + for _, requiredArg := range requiredField.Arguments { + foundArg := foundField.Arguments.ForName(requiredArg.Name) + if foundArg == nil { + return gqlerror.ErrorPosf(foundField.Position, + `For %s to implement %s the field %s must have the same arguments but it is missing %s.`, + def.Name, intf.Name, requiredField.Name, requiredArg.Name, + ) + } + + if !requiredArg.Type.IsCompatible(foundArg.Type) { + return gqlerror.ErrorPosf(foundArg.Position, + `For %s to implement %s the field %s must have the same arguments but %s has the wrong type.`, + def.Name, intf.Name, requiredField.Name, requiredArg.Name, + ) + } + } + for _, foundArgs := range foundField.Arguments { + if requiredField.Arguments.ForName(foundArgs.Name) == nil && foundArgs.Type.NonNull && foundArgs.DefaultValue == nil { + return gqlerror.ErrorPosf(foundArgs.Position, + `For %s to implement %s any additional arguments on %s must be optional or have a default value but %s is required.`, + def.Name, intf.Name, foundField.Name, foundArgs.Name, + ) + } + } + } + return nil +} + +func isCovariant(schema *Schema, required *Type, actual *Type) bool { + if required.NonNull && !actual.NonNull { + return false + } + + if required.NamedType != "" { + if required.NamedType == actual.NamedType { + return true + } + for _, pt := range schema.PossibleTypes[required.NamedType] { + if pt.Name == actual.NamedType { + return true + } + } + return false + } + + if required.Elem != nil && actual.Elem == nil { + return false + } + + return isCovariant(schema, required.Elem, actual.Elem) +} + func validateName(pos *Position, name string) *gqlerror.Error { if strings.HasPrefix(name, "__") { return gqlerror.ErrorPosf(pos, `Name "%s" must not begin with "__", which is reserved by GraphQL introspection.`, name) diff --git a/validator/schema_test.yml b/validator/schema_test.yml index a9571dcd..b1d85c44 100644 --- a/validator/schema_test.yml +++ b/validator/schema_test.yml @@ -175,6 +175,106 @@ interfaces: message: 'INTERFACE field must be one of SCALAR, OBJECT, INTERFACE, UNION, ENUM.' locations: [{line: 8, column: 3}] + - name: must have all fields from interface + input: | + type Bar implements BarInterface { + someField: Int! + } + + interface BarInterface { + id: ID! + } + error: + message: 'For Bar to implement BarInterface it must have a field called id.' + locations: [{line: 1, column: 6}] + + - name: must have same type of fields + input: | + type Bar implements BarInterface { + id: Int! + } + + interface BarInterface { + id: ID! + } + error: + message: 'For Bar to implement BarInterface the field id must have type ID!.' + locations: [{line: 2, column: 5}] + + - name: must have all required arguments + input: | + type Bar implements BarInterface { + id: ID! + } + + interface BarInterface { + id(ff: Int!): ID! + } + error: + message: 'For Bar to implement BarInterface the field id must have the same arguments but it is missing ff.' + locations: [{line: 2, column: 5}] + + - name: must have same argument types + input: | + type Bar implements BarInterface { + id(ff: ID!): ID! + } + + interface BarInterface { + id(ff: Int!): ID! + } + error: + message: 'For Bar to implement BarInterface the field id must have the same arguments but ff has the wrong type.' + locations: [{line: 2, column: 8}] + + - name: may defined additional nullable arguments + input: | + type Bar implements BarInterface { + id(opt: Int): ID! + } + + interface BarInterface { + id: ID! + } + + - name: may defined additional required arguments with defaults + input: | + type Bar implements BarInterface { + id(opt: Int! = 1): ID! + } + + interface BarInterface { + id: ID! + } + + - name: must not define additional required arguments without defaults + input: | + type Bar implements BarInterface { + id(opt: Int!): ID! + } + + interface BarInterface { + id: ID! + } + error: + message: 'For Bar to implement BarInterface any additional arguments on id must be optional or have a default value but opt is required.' + locations: [{line: 2, column: 8}] + + - name: can have covariant argument types + input: | + union U = A|B + + type A { name: String } + type B { name: String } + + type Bar implements BarInterface { + f: A! + } + + interface BarInterface { + f: U! + } + inputs: - name: must define one or more input fields input: |