diff --git a/internal/schema/schema.go b/internal/schema/schema.go index e1c77032..fa4ac4d4 100644 --- a/internal/schema/schema.go +++ b/internal/schema/schema.go @@ -511,10 +511,21 @@ func parseDirectiveDef(l *common.Lexer) *types.DirectiveDefinition { l.ConsumeToken(')') } - l.ConsumeKeyword("on") + switch x := l.ConsumeIdent(); x { + case "on": + // no-op; Go doesn't fallthrough by default + case "repeatable": + d.Repeatable = true + l.ConsumeKeyword("on") + default: + l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "on" or "repeatable"`, x)) + } for { loc := l.ConsumeIdent() + if _, ok := legalDirectiveLocationNames[loc]; !ok { + l.SyntaxError(fmt.Sprintf("%q is not a legal directive location (options: %v)", loc, legalDirectiveLocationNames)) + } d.Locations = append(d.Locations, loc) if l.Peek() != '|' { break @@ -584,3 +595,25 @@ func parseFieldsDef(l *common.Lexer) types.FieldsDefinition { } return fields } + +var legalDirectiveLocationNames = map[string]struct{}{ + "SCHEMA": {}, + "SCALAR": {}, + "OBJECT": {}, + "FIELD_DEFINITION": {}, + "ARGUMENT_DEFINITION": {}, + "INTERFACE": {}, + "UNION": {}, + "ENUM": {}, + "ENUM_VALUE": {}, + "INPUT_OBJECT": {}, + "INPUT_FIELD_DEFINITION": {}, + "QUERY": {}, + "MUTATION": {}, + "SUBSCRIPTION": {}, + "FIELD": {}, + "FRAGMENT_DEFINITION": {}, + "FRAGMENT_SPREAD": {}, + "INLINE_FRAGMENT": {}, + "VARIABLE_DEFINITION": {}, +} diff --git a/internal/schema/schema_test.go b/internal/schema/schema_test.go index 9c4cb4cb..726546b1 100644 --- a/internal/schema/schema_test.go +++ b/internal/schema/schema_test.go @@ -2,6 +2,7 @@ package schema_test import ( "fmt" + "strings" "testing" "github.com/graph-gophers/graphql-go/internal/schema" @@ -813,6 +814,7 @@ Second line of the description. | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + directive @repeatabledirective repeatable on SCALAR interface NamedEntity @directive { name: String } @@ -834,6 +836,8 @@ Second line of the description. } union Union @uniondirective = Photo | Person + + scalar Mass @repeatabledirective @repeatabledirective `, validateSchema: func(s *types.Schema) error { namedEntityDirectives := s.Types["NamedEntity"].(*types.InterfaceTypeDefinition).Directives @@ -864,6 +868,55 @@ Second line of the description. if len(unionDirectives) != 1 || unionDirectives[0].Name.Name != "uniondirective" { return fmt.Errorf("missing directive on Union union, expected @uniondirective but got %v", unionDirectives) } + + massDirectives := s.Types["Mass"].(*types.ScalarTypeDefinition).Directives + if len(massDirectives) != 2 || massDirectives[0].Name.Name != "repeatabledirective" || massDirectives[1].Name.Name != "repeatabledirective" { + return fmt.Errorf("missing directive on Repeatable scalar, expected @repeatabledirective @repeatabledirective but got %v", massDirectives) + } + return nil + }, + }, + { + name: "Sets Directive.Repeatable if `repeatable` keyword is given", + sdl: ` + directive @nonrepeatabledirective on SCALAR + directive @repeatabledirective repeatable on SCALAR + `, + validateSchema: func(s *types.Schema) error { + if dir := s.Directives["nonrepeatabledirective"]; dir.Repeatable { + return fmt.Errorf("did not expect directive to be repeatable: %v", dir) + } + if dir := s.Directives["repeatabledirective"]; !dir.Repeatable { + return fmt.Errorf("expected directive to be repeatable: %v", dir) + } + return nil + }, + }, + { + name: "Directive definition does not allow double-`repeatable`", + sdl: ` + directive @mydirective repeatable repeatable SCALAR + scalar MyScalar @mydirective + `, + validateError: func(err error) error { + msg := `graphql: syntax error: unexpected "repeatable", expecting "on" (line 2, column 38)` + if err == nil || err.Error() != msg { + return fmt.Errorf("expected error %q, but got %q", msg, err) + } + return nil + }, + }, + { + name: "Directive definition does not allow double-`on` instead of `repeatable on`", + sdl: ` + directive @mydirective on on SCALAR + scalar MyScalar @mydirective + `, + validateError: func(err error) error { + prefix := `graphql: syntax error: "on" is not a legal directive location` + if err == nil || !strings.HasPrefix(err.Error(), prefix) { + return fmt.Errorf("expected error starting with %q, but got %q", prefix, err) + } return nil }, }, diff --git a/types/directive.go b/types/directive.go index 0f8a4b99..7b62d51e 100644 --- a/types/directive.go +++ b/types/directive.go @@ -14,11 +14,12 @@ type Directive struct { // // http://spec.graphql.org/draft/#sec-Type-System.Directives type DirectiveDefinition struct { - Name string - Desc string - Locations []string - Arguments ArgumentsDefinition - Loc errors.Location + Name string + Desc string + Repeatable bool + Locations []string + Arguments ArgumentsDefinition + Loc errors.Location } type DirectiveList []*Directive