From 967346e0d604408795155ff9b8d9327018785392 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 7 Mar 2016 16:40:12 +0800 Subject: [PATCH 01/69] Added useful comments for `graphql.Params` fields --- graphql.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/graphql.go b/graphql.go index db6b86ab..047535b4 100644 --- a/graphql.go +++ b/graphql.go @@ -8,11 +8,24 @@ import ( ) type Params struct { - Schema Schema - RequestString string - RootObject map[string]interface{} + // The GraphQL type system to use when validating and executing a query. + Schema Schema + + // A GraphQL language formatted string representing the requested operation. + RequestString string + + // The value provided as the first argument to resolver functions on the top + // level type (e.g. the query object type). + RootObject map[string]interface{} + + // A mapping of variable name to runtime value to use for all variables + // defined in the requestString. VariableValues map[string]interface{} - OperationName string + + // The name of the operation to use if requestString contains multiple + // possible operations. Can be omitted if requestString contains only + // one operation. + OperationName string // Context may be provided to pass application-specific per-request // information to resolve functions. From c21493fdabc1bcb8e488050684bde3e0bd847f3d Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 7 Mar 2016 16:40:37 +0800 Subject: [PATCH 02/69] Minor gofmt --- executor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/executor.go b/executor.go index fb4e14c1..d84ecbc5 100644 --- a/executor.go +++ b/executor.go @@ -397,10 +397,10 @@ func doesFragmentConditionMatch(eCtx *ExecutionContext, fragment ast.Node, ttype if conditionalType == ttype { return true } - if conditionalType.Name() == ttype.Name() { + if conditionalType.Name() == ttype.Name() { return true } - + if conditionalType, ok := conditionalType.(Abstract); ok { return conditionalType.IsPossibleType(ttype) } From bfa307ca0b692045006468cbc6cff2a22ec25a84 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 7 Mar 2016 17:04:21 +0800 Subject: [PATCH 03/69] Ported changes for `gqlerrors` - Include originalError in GraphQL error to preserve any additional info - Fixes #251 - https://github.com/graphql/graphql-js/commit/3d27953a4653ca8ea5aa18edab105ca374dc0b39 - Remove remaining references to u2028 and u2029 - https://github.com/graphql/graphql-js/commit/5ee1edc9022e796c4c70da70703bd8093354a444 --- gqlerrors/error.go | 28 +++++++++++++++------------- gqlerrors/located.go | 8 ++++++++ gqlerrors/syntax.go | 3 ++- language/lexer/lexer_test.go | 18 ------------------ located.go | 5 +++++ rules.go | 1 + values.go | 3 +++ 7 files changed, 34 insertions(+), 32 deletions(-) diff --git a/gqlerrors/error.go b/gqlerrors/error.go index c32fff3c..1809870a 100644 --- a/gqlerrors/error.go +++ b/gqlerrors/error.go @@ -9,12 +9,13 @@ import ( ) type Error struct { - Message string - Stack string - Nodes []ast.Node - Source *source.Source - Positions []int - Locations []location.SourceLocation + Message string + Stack string + Nodes []ast.Node + Source *source.Source + Positions []int + Locations []location.SourceLocation + OriginalError error } // implements Golang's built-in `error` interface @@ -22,7 +23,7 @@ func (g Error) Error() string { return fmt.Sprintf("%v", g.Message) } -func NewError(message string, nodes []ast.Node, stack string, source *source.Source, positions []int) *Error { +func NewError(message string, nodes []ast.Node, stack string, source *source.Source, positions []int, origError error) *Error { if stack == "" && message != "" { stack = message } @@ -49,11 +50,12 @@ func NewError(message string, nodes []ast.Node, stack string, source *source.Sou locations = append(locations, loc) } return &Error{ - Message: message, - Stack: stack, - Nodes: nodes, - Source: source, - Positions: positions, - Locations: locations, + Message: message, + Stack: stack, + Nodes: nodes, + Source: source, + Positions: positions, + Locations: locations, + OriginalError: origError, } } diff --git a/gqlerrors/located.go b/gqlerrors/located.go index d5d1b020..8bc8272e 100644 --- a/gqlerrors/located.go +++ b/gqlerrors/located.go @@ -1,16 +1,23 @@ package gqlerrors import ( + "errors" "github.com/graphql-go/graphql/language/ast" ) +// NewLocatedError +// @deprecated 0.4.18 +// Already exists in `graphql.NewLocatedError()` func NewLocatedError(err interface{}, nodes []ast.Node) *Error { + var origError error message := "An unknown error occurred." if err, ok := err.(error); ok { message = err.Error() + origError = err } if err, ok := err.(string); ok { message = err + origError = errors.New(err) } stack := message return NewError( @@ -19,6 +26,7 @@ func NewLocatedError(err interface{}, nodes []ast.Node) *Error { stack, nil, []int{}, + origError, ) } diff --git a/gqlerrors/syntax.go b/gqlerrors/syntax.go index 76a39751..45576e65 100644 --- a/gqlerrors/syntax.go +++ b/gqlerrors/syntax.go @@ -17,6 +17,7 @@ func NewSyntaxError(s *source.Source, position int, description string) *Error { "", s, []int{position}, + nil, ) } @@ -26,7 +27,7 @@ func highlightSourceAtLocation(s *source.Source, l location.SourceLocation) stri lineNum := fmt.Sprintf("%d", line) nextLineNum := fmt.Sprintf("%d", (line + 1)) padLen := len(nextLineNum) - lines := regexp.MustCompile("\r\n|[\n\r\u2028\u2029]").Split(s.Body, -1) + lines := regexp.MustCompile("\r\n|[\n\r]").Split(s.Body, -1) var highlight string if line >= 2 { highlight += fmt.Sprintf("%s: %s\n", lpad(padLen, prevLineNum), lines[line-2]) diff --git a/language/lexer/lexer_test.go b/language/lexer/lexer_test.go index 1db38bcf..0821e3b0 100644 --- a/language/lexer/lexer_test.go +++ b/language/lexer/lexer_test.go @@ -171,24 +171,6 @@ func TestLexReportsUsefulStringErrors(t *testing.T) { Body: "\"multi\rline\"", Expected: `Syntax Error GraphQL (1:7) Unterminated string. -1: "multi - ^ -2: line" -`, - }, - Test{ - Body: "\"multi\u2028line\"", - Expected: `Syntax Error GraphQL (1:7) Unterminated string. - -1: "multi - ^ -2: line" -`, - }, - Test{ - Body: "\"multi\u2029line\"", - Expected: `Syntax Error GraphQL (1:7) Unterminated string. - 1: "multi ^ 2: line" diff --git a/located.go b/located.go index e7a4cdc0..6ed8ec83 100644 --- a/located.go +++ b/located.go @@ -1,17 +1,21 @@ package graphql import ( + "errors" "github.com/graphql-go/graphql/gqlerrors" "github.com/graphql-go/graphql/language/ast" ) func NewLocatedError(err interface{}, nodes []ast.Node) *gqlerrors.Error { + var origError error message := "An unknown error occurred." if err, ok := err.(error); ok { message = err.Error() + origError = err } if err, ok := err.(string); ok { message = err + origError = errors.New(err) } stack := message return gqlerrors.NewError( @@ -20,6 +24,7 @@ func NewLocatedError(err interface{}, nodes []ast.Node) *gqlerrors.Error { stack, nil, []int{}, + origError, ) } diff --git a/rules.go b/rules.go index 80f10754..56a9901a 100644 --- a/rules.go +++ b/rules.go @@ -52,6 +52,7 @@ func newValidationRuleError(message string, nodes []ast.Node) (string, error) { "", nil, []int{}, + nil, // TODO: this is interim, until we port "better-error-messages-for-inputs" ) } diff --git a/values.go b/values.go index 6b3ff169..d48fa7f6 100644 --- a/values.go +++ b/values.go @@ -77,6 +77,7 @@ func getVariableValue(schema Schema, definitionAST *ast.VariableDefinition, inpu "", nil, []int{}, + nil, ) } @@ -99,6 +100,7 @@ func getVariableValue(schema Schema, definitionAST *ast.VariableDefinition, inpu "", nil, []int{}, + nil, ) } inputStr := "" @@ -113,6 +115,7 @@ func getVariableValue(schema Schema, definitionAST *ast.VariableDefinition, inpu "", nil, []int{}, + nil, ) } From 600db41b6d8577b0ce33afbd0a434458acd9f832 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 7 Mar 2016 17:15:47 +0800 Subject: [PATCH 04/69] Commit: dfe676c3011efe9560b9fa0fcbd2b7bd87476d02 [dfe676c] Parents: c55e9ac1ca Author: Lee Byron Date: 21 January 2016 at 4:18:39 PM SGT Ensure NamedType is a known Node --- language/ast/node.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/language/ast/node.go b/language/ast/node.go index f35eb21d..22879877 100644 --- a/language/ast/node.go +++ b/language/ast/node.go @@ -27,6 +27,7 @@ var _ Node = (*ListValue)(nil) var _ Node = (*ObjectValue)(nil) var _ Node = (*ObjectField)(nil) var _ Node = (*Directive)(nil) +var _ Node = (*Named)(nil) var _ Node = (*List)(nil) var _ Node = (*NonNull)(nil) var _ Node = (*ObjectDefinition)(nil) @@ -39,7 +40,3 @@ var _ Node = (*EnumDefinition)(nil) var _ Node = (*EnumValueDefinition)(nil) var _ Node = (*InputObjectDefinition)(nil) var _ Node = (*TypeExtensionDefinition)(nil) - -// TODO: File issue in `graphql-js` where Named is not -// defined as a Node. This might be a mistake in `graphql-js`? -var _ Node = (*Named)(nil) From 2ea07f7a76e9d2f8db59e8faaa0d05a985a6e3e5 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 7 Mar 2016 17:20:19 +0800 Subject: [PATCH 05/69] Commit: c55e9ac1ca0f2fbc94ddc8cb1fabdb9a454996e0 [c55e9ac] Parents: 9ddb2b579b Author: Lee Byron Date: 21 January 2016 at 3:30:46 PM SGT Move the extension definition out of type definition --- language/ast/definitions.go | 39 ++++++++++++++++++++++++++++++++ language/ast/type_definitions.go | 39 -------------------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/language/ast/definitions.go b/language/ast/definitions.go index 19d07ce5..f964306a 100644 --- a/language/ast/definitions.go +++ b/language/ast/definitions.go @@ -14,6 +14,7 @@ type Definition interface { // Ensure that all definition types implements Definition interface var _ Definition = (*OperationDefinition)(nil) var _ Definition = (*FragmentDefinition)(nil) +var _ Definition = (*TypeExtensionDefinition)(nil) var _ Definition = (Definition)(nil) // OperationDefinition implements Node, Definition @@ -151,3 +152,41 @@ func (vd *VariableDefinition) GetKind() string { func (vd *VariableDefinition) GetLoc() *Location { return vd.Loc } + +// TypeExtensionDefinition implements Node, Definition +type TypeExtensionDefinition struct { + Kind string + Loc *Location + Definition *ObjectDefinition +} + +func NewTypeExtensionDefinition(def *TypeExtensionDefinition) *TypeExtensionDefinition { + if def == nil { + def = &TypeExtensionDefinition{} + } + return &TypeExtensionDefinition{ + Kind: kinds.TypeExtensionDefinition, + Loc: def.Loc, + Definition: def.Definition, + } +} + +func (def *TypeExtensionDefinition) GetKind() string { + return def.Kind +} + +func (def *TypeExtensionDefinition) GetLoc() *Location { + return def.Loc +} + +func (def *TypeExtensionDefinition) GetVariableDefinitions() []*VariableDefinition { + return []*VariableDefinition{} +} + +func (def *TypeExtensionDefinition) GetSelectionSet() *SelectionSet { + return &SelectionSet{} +} + +func (def *TypeExtensionDefinition) GetOperation() string { + return "" +} diff --git a/language/ast/type_definitions.go b/language/ast/type_definitions.go index 7af1d861..95810070 100644 --- a/language/ast/type_definitions.go +++ b/language/ast/type_definitions.go @@ -11,7 +11,6 @@ var _ Definition = (*UnionDefinition)(nil) var _ Definition = (*ScalarDefinition)(nil) var _ Definition = (*EnumDefinition)(nil) var _ Definition = (*InputObjectDefinition)(nil) -var _ Definition = (*TypeExtensionDefinition)(nil) // ObjectDefinition implements Node, Definition type ObjectDefinition struct { @@ -362,41 +361,3 @@ func (def *InputObjectDefinition) GetSelectionSet() *SelectionSet { func (def *InputObjectDefinition) GetOperation() string { return "" } - -// TypeExtensionDefinition implements Node, Definition -type TypeExtensionDefinition struct { - Kind string - Loc *Location - Definition *ObjectDefinition -} - -func NewTypeExtensionDefinition(def *TypeExtensionDefinition) *TypeExtensionDefinition { - if def == nil { - def = &TypeExtensionDefinition{} - } - return &TypeExtensionDefinition{ - Kind: kinds.TypeExtensionDefinition, - Loc: def.Loc, - Definition: def.Definition, - } -} - -func (def *TypeExtensionDefinition) GetKind() string { - return def.Kind -} - -func (def *TypeExtensionDefinition) GetLoc() *Location { - return def.Loc -} - -func (def *TypeExtensionDefinition) GetVariableDefinitions() []*VariableDefinition { - return []*VariableDefinition{} -} - -func (def *TypeExtensionDefinition) GetSelectionSet() *SelectionSet { - return &SelectionSet{} -} - -func (def *TypeExtensionDefinition) GetOperation() string { - return "" -} From 79571357bd73caa23fb81602eeab9adadf747df6 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 7 Mar 2016 17:23:07 +0800 Subject: [PATCH 06/69] Add comment for Token struct --- language/lexer/lexer.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/language/lexer/lexer.go b/language/lexer/lexer.go index 7b55c37c..f698139c 100644 --- a/language/lexer/lexer.go +++ b/language/lexer/lexer.go @@ -75,6 +75,8 @@ func init() { tokenDescription[TokenKind[STRING]] = "String" } +// Token is a representation of a lexed Token. Value only appears for non-punctuation +// tokens: NAME, INT, FLOAT, and STRING. type Token struct { Kind int Start int From ef77c8df671c64ccc01d634867bb9fdcf1b14ad5 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 8 Mar 2016 06:50:45 +0800 Subject: [PATCH 07/69] Commit: 9ddb2b579bf94126c5494e64c534571f94bb420d [9ddb2b5] Parents: 1651039cf4 Author: Lee Byron Date: 21 January 2016 at 3:10:44 PM SGT flow type the parser --- Commit: 6f2b66df332625a8f2269836f774e91d750e00ac [6f2b66d] Parents: 4bd4b33a5a Author: Lee Byron Date: 1 October 2015 at 9:21:18 AM SGT Clearer lex errors for unprintable unicode Fixes #183 --- Commit: 969095e9f6be0bb13a69c715c6ee4910814a065b [969095e] Parents: 5d4d531f23 Author: Lee Byron Date: 25 September 2015 at 6:51:32 AM SGT Commit Date: 25 September 2015 at 6:53:31 AM SGT [RFC] Clarify and restrict unicode support This proposal alters the parser grammar to be more specific about what unicode characters are allowed as source, restricts those characters interpretted as white space or line breaks, and clarifies line break behavior relative to error reporting with a non-normative note. Implements https://github.com/facebook/graphql/pull/96 --- --- gqlerrors/syntax.go | 23 ++++++-- language/lexer/lexer.go | 100 ++++++++++++++++++++++++----------- language/lexer/lexer_test.go | 98 ++++++++++++++++++++++++++++++---- 3 files changed, 177 insertions(+), 44 deletions(-) diff --git a/gqlerrors/syntax.go b/gqlerrors/syntax.go index 45576e65..4235a040 100644 --- a/gqlerrors/syntax.go +++ b/gqlerrors/syntax.go @@ -7,6 +7,7 @@ import ( "github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/location" "github.com/graphql-go/graphql/language/source" + "strings" ) func NewSyntaxError(s *source.Source, position int, description string) *Error { @@ -21,6 +22,22 @@ func NewSyntaxError(s *source.Source, position int, description string) *Error { ) } +// printCharCode here is slightly different from lexer.printCharCode() +func printCharCode(code rune) string { + // print as ASCII for printable range + if code >= 0x0020 { + return fmt.Sprintf(`%c`, code) + } + // Otherwise print the escaped form. e.g. `"\\u0007"` + return fmt.Sprintf(`\u%04X`, code) +} +func printLine(str string) string { + strSlice := []string{} + for _, runeValue := range str { + strSlice = append(strSlice, printCharCode(runeValue)) + } + return fmt.Sprintf(`%s`, strings.Join(strSlice, "")) +} func highlightSourceAtLocation(s *source.Source, l location.SourceLocation) string { line := l.Line prevLineNum := fmt.Sprintf("%d", (line - 1)) @@ -30,15 +47,15 @@ func highlightSourceAtLocation(s *source.Source, l location.SourceLocation) stri lines := regexp.MustCompile("\r\n|[\n\r]").Split(s.Body, -1) var highlight string if line >= 2 { - highlight += fmt.Sprintf("%s: %s\n", lpad(padLen, prevLineNum), lines[line-2]) + highlight += fmt.Sprintf("%s: %s\n", lpad(padLen, prevLineNum), printLine(lines[line-2])) } - highlight += fmt.Sprintf("%s: %s\n", lpad(padLen, lineNum), lines[line-1]) + highlight += fmt.Sprintf("%s: %s\n", lpad(padLen, lineNum), printLine(lines[line-1])) for i := 1; i < (2 + padLen + l.Column); i++ { highlight += " " } highlight += "^\n" if line < len(lines) { - highlight += fmt.Sprintf("%s: %s\n", lpad(padLen, nextLineNum), lines[line]) + highlight += fmt.Sprintf("%s: %s\n", lpad(padLen, nextLineNum), printLine(lines[line])) } return highlight } diff --git a/language/lexer/lexer.go b/language/lexer/lexer.go index f698139c..7458035a 100644 --- a/language/lexer/lexer.go +++ b/language/lexer/lexer.go @@ -23,7 +23,6 @@ const ( PIPE BRACE_R NAME - VARIABLE INT FLOAT STRING @@ -50,7 +49,6 @@ func init() { TokenKind[PIPE] = PIPE TokenKind[BRACE_R] = BRACE_R TokenKind[NAME] = NAME - TokenKind[VARIABLE] = VARIABLE TokenKind[INT] = INT TokenKind[FLOAT] = FLOAT TokenKind[STRING] = STRING @@ -69,7 +67,6 @@ func init() { tokenDescription[TokenKind[PIPE]] = "|" tokenDescription[TokenKind[BRACE_R]] = "}" tokenDescription[TokenKind[NAME]] = "Name" - tokenDescription[TokenKind[VARIABLE]] = "Variable" tokenDescription[TokenKind[INT]] = "Int" tokenDescription[TokenKind[FLOAT]] = "Float" tokenDescription[TokenKind[STRING]] = "String" @@ -105,6 +102,12 @@ func Lex(s *source.Source) Lexer { } } +func runeStringValueAt(body string, start, end int) string { + // convert body string to runes, to handle unicode + bodyRunes := []rune(body) + return string(bodyRunes[start:end]) +} + // Reads an alphanumeric + underscore name from the source. // [_A-Za-z][_0-9A-Za-z]* func readName(source *source.Source, position int) Token { @@ -113,17 +116,18 @@ func readName(source *source.Source, position int) Token { end := position + 1 for { code := charCodeAt(body, end) - if (end != bodyLength) && (code == 95 || - code >= 48 && code <= 57 || - code >= 65 && code <= 90 || - code >= 97 && code <= 122) { + if (end != bodyLength) && code != 0 && + (code == 95 || // _ + code >= 48 && code <= 57 || // 0-9 + code >= 65 && code <= 90 || // A-Z + code >= 97 && code <= 122) { // a-z end += 1 continue } else { break } } - return makeToken(TokenKind[NAME], position, end, body[position:end]) + return makeToken(TokenKind[NAME], position, end, runeStringValueAt(body, position, end)) } // Reads a number token from the source file, either a float @@ -143,7 +147,7 @@ func readNumber(s *source.Source, start int, firstCode rune) (Token, error) { position += 1 code = charCodeAt(body, position) if code >= 48 && code <= 57 { - description := fmt.Sprintf("Invalid number, unexpected digit after 0: \"%c\".", code) + description := fmt.Sprintf("Invalid number, unexpected digit after 0: %v.", printCharCode(code)) return Token{}, gqlerrors.NewSyntaxError(s, position, description) } } else { @@ -183,7 +187,7 @@ func readNumber(s *source.Source, start int, firstCode rune) (Token, error) { if isFloat { kind = TokenKind[FLOAT] } - return makeToken(kind, start, position, body[start:position]), nil + return makeToken(kind, start, position, runeStringValueAt(body, start, position)), nil } // Returns the new position in the source after reading digits. @@ -204,11 +208,7 @@ func readDigits(s *source.Source, start int, firstCode rune) (int, error) { return position, nil } var description string - if code != 0 { - description = fmt.Sprintf("Invalid number, expected digit but got: \"%c\".", code) - } else { - description = fmt.Sprintf("Invalid number, expected digit but got: EOF.") - } + description = fmt.Sprintf("Invalid number, expected digit but got: %v.", printCharCode(code)) return position, gqlerrors.NewSyntaxError(s, position, description) } @@ -220,7 +220,16 @@ func readString(s *source.Source, start int) (Token, error) { var value string for { code = charCodeAt(body, position) - if position < len(body) && code != 34 && code != 10 && code != 13 && code != 0x2028 && code != 0x2029 { + if position < len(body) && + // not LineTerminator + code != 0x000A && code != 0x000D && + // not Quote (") + code != 34 { + + // SourceCharacter + if code < 0x0020 && code != 0x0009 { + return Token{}, gqlerrors.NewSyntaxError(s, position, fmt.Sprintf(`Invalid character within String: %v.`, printCharCode(code))) + } position += 1 if code == 92 { // \ value += body[chunkStart : position-1] @@ -250,7 +259,7 @@ func readString(s *source.Source, start int) (Token, error) { case 116: value += "\t" break - case 117: + case 117: // u charCode := uniCharCode( charCodeAt(body, position+1), charCodeAt(body, position+2), @@ -258,13 +267,16 @@ func readString(s *source.Source, start int) (Token, error) { charCodeAt(body, position+4), ) if charCode < 0 { - return Token{}, gqlerrors.NewSyntaxError(s, position, "Bad character escape sequence.") + return Token{}, gqlerrors.NewSyntaxError(s, position, + fmt.Sprintf("Invalid character escape sequence: "+ + "\\u%v", body[position+1:position+5])) } value += fmt.Sprintf("%c", charCode) position += 4 break default: - return Token{}, gqlerrors.NewSyntaxError(s, position, "Bad character escape sequence.") + return Token{}, gqlerrors.NewSyntaxError(s, position, + fmt.Sprintf(`Invalid character escape sequence: \\%c.`, code)) } position += 1 chunkStart = position @@ -274,10 +286,10 @@ func readString(s *source.Source, start int) (Token, error) { break } } - if code != 34 { + if code != 34 { // quote (") return Token{}, gqlerrors.NewSyntaxError(s, position, "Unterminated string.") } - value += body[chunkStart:position] + value += runeStringValueAt(body, chunkStart, position) return makeToken(TokenKind[STRING], start, position+1, value), nil } @@ -312,14 +324,33 @@ func makeToken(kind int, start int, end int, value string) Token { return Token{Kind: kind, Start: start, End: end, Value: value} } +func printCharCode(code rune) string { + // NaN/undefined represents access beyond the end of the file. + if code < 0 { + return "" + } + // print as ASCII for printable range + if code >= 0x0020 && code < 0x007F { + return fmt.Sprintf(`"%c"`, code) + } + // Otherwise print the escaped form. e.g. `"\\u0007"` + return fmt.Sprintf(`"\\u%04X"`, code) +} + func readToken(s *source.Source, fromPosition int) (Token, error) { body := s.Body bodyLength := len(body) position := positionAfterWhitespace(body, fromPosition) - code := charCodeAt(body, position) if position >= bodyLength { return makeToken(TokenKind[EOF], position, position, ""), nil } + code := charCodeAt(body, position) + + // SourceCharacter + if code < 0x0020 && code != 0x0009 && code != 0x000A && code != 0x000D { + return Token{}, gqlerrors.NewSyntaxError(s, position, fmt.Sprintf(`Invalid character %v`, printCharCode(code))) + } + switch code { // ! case 33: @@ -389,7 +420,7 @@ func readToken(s *source.Source, fromPosition int) (Token, error) { } return token, nil } - description := fmt.Sprintf("Unexpected character \"%c\".", code) + description := fmt.Sprintf("Unexpected character %v.", printCharCode(code)) return Token{}, gqlerrors.NewSyntaxError(s, position, description) } @@ -398,7 +429,7 @@ func charCodeAt(body string, position int) rune { if len(r) > position { return r[position] } else { - return 0 + return -1 } } @@ -411,19 +442,26 @@ func positionAfterWhitespace(body string, startPosition int) int { for { if position < bodyLength { code := charCodeAt(body, position) - if code == 32 || // space - code == 44 || // comma - code == 160 || // '\xa0' - code == 0x2028 || // line separator - code == 0x2029 || // paragraph separator - code > 8 && code < 14 { // whitespace + + // Skip Ignored + if code == 0xFEFF || // BOM + // White Space + code == 0x0009 || // tab + code == 0x0020 || // space + // Line Terminator + code == 0x000A || // new line + code == 0x000D || // carriage return + // Comma + code == 0x002C { position += 1 } else if code == 35 { // # position += 1 for { code := charCodeAt(body, position) if position < bodyLength && - code != 10 && code != 13 && code != 0x2028 && code != 0x2029 { + code != 0 && + // SourceCharacter but not LineTerminator + (code > 0x001F || code == 0x0009) && code != 0x000A && code != 0x000D { position += 1 continue } else { diff --git a/language/lexer/lexer_test.go b/language/lexer/lexer_test.go index 0821e3b0..13690d1e 100644 --- a/language/lexer/lexer_test.go +++ b/language/lexer/lexer_test.go @@ -16,6 +16,51 @@ func createSource(body string) *source.Source { return source.NewSource(&source.Source{Body: body}) } +func TestDisallowsUncommonControlCharacters(t *testing.T) { + tests := []Test{ + Test{ + Body: "\u0007", + Expected: `Syntax Error GraphQL (1:1) Invalid character "\\u0007" + +1: \u0007 + ^ +`, + }, + } + for _, test := range tests { + _, err := Lex(createSource(test.Body))(0) + if err == nil { + t.Fatalf("unexpected nil error\nexpected:\n%v\n\ngot:\n%v", test.Expected, err) + } + if err.Error() != test.Expected { + t.Fatalf("unexpected error.\nexpected:\n%v\n\ngot:\n%v", test.Expected, err.Error()) + } + } +} + +func TestAcceptsBOMHeader(t *testing.T) { + tests := []Test{ + Test{ + Body: "\uFEFF foo", + Expected: Token{ + Kind: TokenKind[NAME], + Start: 2, + End: 5, + Value: "foo", + }, + }, + } + for _, test := range tests { + token, err := Lex(&source.Source{Body: test.Body})(0) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(token, test.Expected) { + t.Fatalf("unexpected token, expected: %v, got: %v", test.Expected, token) + } + } +} + func TestSkipsWhiteSpace(t *testing.T) { tests := []Test{ Test{ @@ -150,12 +195,36 @@ func TestLexesStrings(t *testing.T) { func TestLexReportsUsefulStringErrors(t *testing.T) { tests := []Test{ + Test{ + Body: "\"", + Expected: `Syntax Error GraphQL (1:2) Unterminated string. + +1: " + ^ +`, + }, Test{ Body: "\"no end quote", Expected: `Syntax Error GraphQL (1:14) Unterminated string. 1: "no end quote ^ +`, + }, + Test{ + Body: "\"contains unescaped \u0007 control char\"", + Expected: `Syntax Error GraphQL (1:21) Invalid character within String: "\\u0007". + +1: "contains unescaped \u0007 control char" + ^ +`, + }, + Test{ + Body: "\"null-byte is not \u0000 end of file\"", + Expected: `Syntax Error GraphQL (1:19) Invalid character within String: "\\u0000". + +1: "null-byte is not \u0000 end of file" + ^ `, }, Test{ @@ -178,7 +247,7 @@ func TestLexReportsUsefulStringErrors(t *testing.T) { }, Test{ Body: "\"bad \\z esc\"", - Expected: `Syntax Error GraphQL (1:7) Bad character escape sequence. + Expected: `Syntax Error GraphQL (1:7) Invalid character escape sequence: \\z. 1: "bad \z esc" ^ @@ -186,7 +255,7 @@ func TestLexReportsUsefulStringErrors(t *testing.T) { }, Test{ Body: "\"bad \\x esc\"", - Expected: `Syntax Error GraphQL (1:7) Bad character escape sequence. + Expected: `Syntax Error GraphQL (1:7) Invalid character escape sequence: \\x. 1: "bad \x esc" ^ @@ -194,7 +263,7 @@ func TestLexReportsUsefulStringErrors(t *testing.T) { }, Test{ Body: "\"bad \\u1 esc\"", - Expected: `Syntax Error GraphQL (1:7) Bad character escape sequence. + Expected: `Syntax Error GraphQL (1:7) Invalid character escape sequence: \u1 es 1: "bad \u1 esc" ^ @@ -202,7 +271,7 @@ func TestLexReportsUsefulStringErrors(t *testing.T) { }, Test{ Body: "\"bad \\u0XX1 esc\"", - Expected: `Syntax Error GraphQL (1:7) Bad character escape sequence. + Expected: `Syntax Error GraphQL (1:7) Invalid character escape sequence: \u0XX1 1: "bad \u0XX1 esc" ^ @@ -210,7 +279,7 @@ func TestLexReportsUsefulStringErrors(t *testing.T) { }, Test{ Body: "\"bad \\uXXXX esc\"", - Expected: `Syntax Error GraphQL (1:7) Bad character escape sequence. + Expected: `Syntax Error GraphQL (1:7) Invalid character escape sequence: \uXXXX 1: "bad \uXXXX esc" ^ @@ -218,7 +287,7 @@ func TestLexReportsUsefulStringErrors(t *testing.T) { }, Test{ Body: "\"bad \\uFXXX esc\"", - Expected: `Syntax Error GraphQL (1:7) Bad character escape sequence. + Expected: `Syntax Error GraphQL (1:7) Invalid character escape sequence: \uFXXX 1: "bad \uFXXX esc" ^ @@ -226,7 +295,7 @@ func TestLexReportsUsefulStringErrors(t *testing.T) { }, Test{ Body: "\"bad \\uXXXF esc\"", - Expected: `Syntax Error GraphQL (1:7) Bad character escape sequence. + Expected: `Syntax Error GraphQL (1:7) Invalid character escape sequence: \uXXXF 1: "bad \uXXXF esc" ^ @@ -422,7 +491,7 @@ func TestLexReportsUsefulNumbeErrors(t *testing.T) { }, Test{ Body: "1.", - Expected: `Syntax Error GraphQL (1:3) Invalid number, expected digit but got: EOF. + Expected: `Syntax Error GraphQL (1:3) Invalid number, expected digit but got: . 1: 1. ^ @@ -454,7 +523,8 @@ func TestLexReportsUsefulNumbeErrors(t *testing.T) { }, Test{ Body: "1.0e", - Expected: `Syntax Error GraphQL (1:5) Invalid number, expected digit but got: EOF. + + Expected: `Syntax Error GraphQL (1:5) Invalid number, expected digit but got: . 1: 1.0e ^ @@ -631,7 +701,15 @@ func TestLexReportsUsefulUnknownCharacterError(t *testing.T) { }, Test{ Body: "\u203B", - Expected: `Syntax Error GraphQL (1:1) Unexpected character "※". + Expected: `Syntax Error GraphQL (1:1) Unexpected character "\\u203B". + +1: ※ + ^ +`, + }, + Test{ + Body: "\u203b", + Expected: `Syntax Error GraphQL (1:1) Unexpected character "\\u203B". 1: ※ ^ From 5fdc9cb1a3e4c301ced613540f5b52617381fce4 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 8 Mar 2016 07:01:17 +0800 Subject: [PATCH 08/69] Commit: 5ee1edc9022e796c4c70da70703bd8093354a444 [5ee1edc] Parents: 4977c9eedc Author: Lee Byron Date: 3 February 2016 at 9:33:33 AM SGT Remove remaining references to u2028 and u2029 --- language/location/location.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/language/location/location.go b/language/location/location.go index f0d47234..12ffff87 100644 --- a/language/location/location.go +++ b/language/location/location.go @@ -18,7 +18,7 @@ func GetLocation(s *source.Source, position int) SourceLocation { } line := 1 column := position + 1 - lineRegexp := regexp.MustCompile("\r\n|[\n\r\u2028\u2029]") + lineRegexp := regexp.MustCompile("\r\n|[\n\r]") matches := lineRegexp.FindAllStringIndex(body, -1) for _, match := range matches { matchIndex := match[0] From 117e98a087c031f1be3e38259ba31f3bc306002c Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 8 Mar 2016 07:12:50 +0800 Subject: [PATCH 09/69] Commit: 9ddb2b579bf94126c5494e64c534571f94bb420d [9ddb2b5] Parents: 1651039cf4 Author: Lee Byron Date: 21 January 2016 at 3:10:44 PM SGT flow type the parser Note: Added check for invalid operation token (valid tokens: `mutation`, `subscription`, `query`) ---- Commit: 58965620674a0245a0cc4b7ef190a450c04753cd [5896562] Parents: 9234c6da0e Author: Dmitry Minkovsky Date: 17 February 2016 at 12:38:37 AM SGT Commit Date: 17 February 2016 at 6:52:00 AM SGT Fix comment --- language/parser/parser.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/language/parser/parser.go b/language/parser/parser.go index 45382418..7ec75a07 100644 --- a/language/parser/parser.go +++ b/language/parser/parser.go @@ -212,7 +212,17 @@ func parseOperationDefinition(parser *Parser) (*ast.OperationDefinition, error) if err != nil { return nil, err } - operation := operationToken.Value + operation := "" + switch operationToken.Value { + case "mutation": + fallthrough + case "subscription": + fallthrough + case "query": + operation = operationToken.Value + default: + return nil, unexpected(parser, operationToken) + } name, err := parseName(parser) if err != nil { return nil, err @@ -1101,7 +1111,7 @@ func skip(parser *Parser, Kind int) (bool, error) { } // If the next token is of the given kind, return that token after advancing -// the parser. Otherwise, do not change the parser state and return false. +// the parser. Otherwise, do not change the parser state and return error. func expect(parser *Parser, kind int) (lexer.Token, error) { token := parser.Token if token.Kind == kind { From 7cd3ecac65da03023a6da1b3f66d5606614a0d1e Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 8 Mar 2016 09:11:31 +0800 Subject: [PATCH 10/69] GravatarCommit: 3f1f9f5759704ca9ed153c98558236a66af75153 [3f1f9f5] Parents: 529257b8d3 Author: Lee Byron Date: 2 October 2015 at 9:10:21 AM SGT [RFC] Type condition optional on inline fragments. Implements https://github.com/facebook/graphql/pull/100 --- executor.go | 17 ++- kitchen-sink.graphql | 6 + language/parser/parser.go | 179 ++++++++++++++++++++++++--- language/printer/printer.go | 8 +- language/printer/printer_test.go | 6 + language/visitor/visitor_test.go | 28 +++++ rules_fields_on_correct_type_test.go | 3 + rules_known_fragment_names_test.go | 3 + rules_known_type_names_test.go | 2 +- type_info.go | 18 ++- 10 files changed, 242 insertions(+), 28 deletions(-) diff --git a/executor.go b/executor.go index d84ecbc5..33070e0c 100644 --- a/executor.go +++ b/executor.go @@ -390,7 +390,11 @@ func doesFragmentConditionMatch(eCtx *ExecutionContext, fragment ast.Node, ttype switch fragment := fragment.(type) { case *ast.FragmentDefinition: - conditionalType, err := typeFromAST(eCtx.Schema, fragment.TypeCondition) + typeConditionAST := fragment.TypeCondition + if typeConditionAST == nil { + return true + } + conditionalType, err := typeFromAST(eCtx.Schema, typeConditionAST) if err != nil { return false } @@ -400,19 +404,24 @@ func doesFragmentConditionMatch(eCtx *ExecutionContext, fragment ast.Node, ttype if conditionalType.Name() == ttype.Name() { return true } - if conditionalType, ok := conditionalType.(Abstract); ok { return conditionalType.IsPossibleType(ttype) } case *ast.InlineFragment: - conditionalType, err := typeFromAST(eCtx.Schema, fragment.TypeCondition) + typeConditionAST := fragment.TypeCondition + if typeConditionAST == nil { + return true + } + conditionalType, err := typeFromAST(eCtx.Schema, typeConditionAST) if err != nil { return false } if conditionalType == ttype { return true } - + if conditionalType.Name() == ttype.Name() { + return true + } if conditionalType, ok := conditionalType.(Abstract); ok { return conditionalType.IsPossibleType(ttype) } diff --git a/kitchen-sink.graphql b/kitchen-sink.graphql index 1f98edc9..c28e85d6 100644 --- a/kitchen-sink.graphql +++ b/kitchen-sink.graphql @@ -12,6 +12,12 @@ query namedQuery($foo: ComplexFooType, $bar: Bar = DefaultBarValue) { } } } + ... @skip(unless: $foo) { + id + } + ... { + id + } } } diff --git a/language/parser/parser.go b/language/parser/parser.go index 7ec75a07..8c6fda4f 100644 --- a/language/parser/parser.go +++ b/language/parser/parser.go @@ -194,6 +194,13 @@ func parseDocument(parser *Parser) (*ast.Document, error) { /* Implements the parsing rules in the Operations section. */ +/** + * OperationDefinition : + * - SelectionSet + * - OperationType Name? VariableDefinitions? Directives? SelectionSet + * + * OperationType : one of query mutation + */ func parseOperationDefinition(parser *Parser) (*ast.OperationDefinition, error) { start := parser.Token.Start if peek(parser, lexer.TokenKind[lexer.BRACE_L]) { @@ -249,6 +256,9 @@ func parseOperationDefinition(parser *Parser) (*ast.OperationDefinition, error) }), nil } +/** + * VariableDefinitions : ( VariableDefinition+ ) + */ func parseVariableDefinitions(parser *Parser) ([]*ast.VariableDefinition, error) { variableDefinitions := []*ast.VariableDefinition{} if peek(parser, lexer.TokenKind[lexer.PAREN_L]) { @@ -266,6 +276,9 @@ func parseVariableDefinitions(parser *Parser) ([]*ast.VariableDefinition, error) return variableDefinitions, nil } +/** + * VariableDefinition : Variable : Type DefaultValue? + */ func parseVariableDefinition(parser *Parser) (interface{}, error) { start := parser.Token.Start variable, err := parseVariable(parser) @@ -298,6 +311,9 @@ func parseVariableDefinition(parser *Parser) (interface{}, error) { }), nil } +/** + * Variable : $ Name + */ func parseVariable(parser *Parser) (*ast.Variable, error) { start := parser.Token.Start _, err := expect(parser, lexer.TokenKind[lexer.DOLLAR]) @@ -314,6 +330,9 @@ func parseVariable(parser *Parser) (*ast.Variable, error) { }), nil } +/** + * SelectionSet : { Selection+ } + */ func parseSelectionSet(parser *Parser) (*ast.SelectionSet, error) { start := parser.Token.Start iSelections, err := many(parser, lexer.TokenKind[lexer.BRACE_L], parseSelection, lexer.TokenKind[lexer.BRACE_R]) @@ -334,6 +353,12 @@ func parseSelectionSet(parser *Parser) (*ast.SelectionSet, error) { }), nil } +/** + * Selection : + * - Field + * - FragmentSpread + * - InlineFragment + */ func parseSelection(parser *Parser) (interface{}, error) { if peek(parser, lexer.TokenKind[lexer.SPREAD]) { r, err := parseFragment(parser) @@ -343,6 +368,11 @@ func parseSelection(parser *Parser) (interface{}, error) { } } +/** + * Field : Alias? Name Arguments? Directives? SelectionSet? + * + * Alias : Name : + */ func parseField(parser *Parser) (*ast.Field, error) { start := parser.Token.Start nameOrAlias, err := parseName(parser) @@ -391,6 +421,9 @@ func parseField(parser *Parser) (*ast.Field, error) { }), nil } +/** + * Arguments : ( Argument+ ) + */ func parseArguments(parser *Parser) ([]*ast.Argument, error) { arguments := []*ast.Argument{} if peek(parser, lexer.TokenKind[lexer.PAREN_L]) { @@ -408,6 +441,9 @@ func parseArguments(parser *Parser) ([]*ast.Argument, error) { return arguments, nil } +/** + * Argument : Name : Value + */ func parseArgument(parser *Parser) (interface{}, error) { start := parser.Token.Start name, err := parseName(parser) @@ -431,17 +467,21 @@ func parseArgument(parser *Parser) (interface{}, error) { /* Implements the parsing rules in the Fragments section. */ +/** + * Corresponds to both FragmentSpread and InlineFragment in the spec. + * + * FragmentSpread : ... FragmentName Directives? + * + * InlineFragment : ... TypeCondition? Directives? SelectionSet + */ func parseFragment(parser *Parser) (interface{}, error) { start := parser.Token.Start _, err := expect(parser, lexer.TokenKind[lexer.SPREAD]) if err != nil { return nil, err } - if parser.Token.Value == "on" { - if err := advance(parser); err != nil { - return nil, err - } - name, err := parseNamed(parser) + if peek(parser, lexer.TokenKind[lexer.NAME]) && parser.Token.Value != "on" { + name, err := parseFragmentName(parser) if err != nil { return nil, err } @@ -449,32 +489,46 @@ func parseFragment(parser *Parser) (interface{}, error) { if err != nil { return nil, err } - selectionSet, err := parseSelectionSet(parser) + return ast.NewFragmentSpread(&ast.FragmentSpread{ + Name: name, + Directives: directives, + Loc: loc(parser, start), + }), nil + } + var typeCondition *ast.Named + if parser.Token.Value == "on" { + if err := advance(parser); err != nil { + return nil, err + } + name, err := parseNamed(parser) if err != nil { return nil, err } - return ast.NewInlineFragment(&ast.InlineFragment{ - TypeCondition: name, - Directives: directives, - SelectionSet: selectionSet, - Loc: loc(parser, start), - }), nil + typeCondition = name + } - name, err := parseFragmentName(parser) + directives, err := parseDirectives(parser) if err != nil { return nil, err } - directives, err := parseDirectives(parser) + selectionSet, err := parseSelectionSet(parser) if err != nil { return nil, err } - return ast.NewFragmentSpread(&ast.FragmentSpread{ - Name: name, - Directives: directives, - Loc: loc(parser, start), + return ast.NewInlineFragment(&ast.InlineFragment{ + TypeCondition: typeCondition, + Directives: directives, + SelectionSet: selectionSet, + Loc: loc(parser, start), }), nil } +/** + * FragmentDefinition : + * - fragment FragmentName on TypeCondition Directives? SelectionSet + * + * TypeCondition : NamedType + */ func parseFragmentDefinition(parser *Parser) (*ast.FragmentDefinition, error) { start := parser.Token.Start _, err := expectKeyWord(parser, "fragment") @@ -510,6 +564,9 @@ func parseFragmentDefinition(parser *Parser) (*ast.FragmentDefinition, error) { }), nil } +/** + * FragmentName : Name but not `on` + */ func parseFragmentName(parser *Parser) (*ast.Name, error) { if parser.Token.Value == "on" { return nil, unexpected(parser, lexer.Token{}) @@ -519,6 +576,21 @@ func parseFragmentName(parser *Parser) (*ast.Name, error) { /* Implements the parsing rules in the Values section. */ +/** + * Value[Const] : + * - [~Const] Variable + * - IntValue + * - FloatValue + * - StringValue + * - BooleanValue + * - EnumValue + * - ListValue[?Const] + * - ObjectValue[?Const] + * + * BooleanValue : one of `true` `false` + * + * EnumValue : Name but not `true`, `false` or `null` + */ func parseValueLiteral(parser *Parser, isConst bool) (ast.Value, error) { token := parser.Token switch token.Kind { @@ -595,6 +667,11 @@ func parseValueValue(parser *Parser) (interface{}, error) { return parseValueLiteral(parser, false) } +/** + * ListValue[Const] : + * - [ ] + * - [ Value[?Const]+ ] + */ func parseList(parser *Parser, isConst bool) (*ast.ListValue, error) { start := parser.Token.Start var item parseFn @@ -617,6 +694,11 @@ func parseList(parser *Parser, isConst bool) (*ast.ListValue, error) { }), nil } +/** + * ObjectValue[Const] : + * - { } + * - { ObjectField[?Const]+ } + */ func parseObject(parser *Parser, isConst bool) (*ast.ObjectValue, error) { start := parser.Token.Start _, err := expect(parser, lexer.TokenKind[lexer.BRACE_L]) @@ -644,6 +726,9 @@ func parseObject(parser *Parser, isConst bool) (*ast.ObjectValue, error) { }), nil } +/** + * ObjectField[Const] : Name : Value[?Const] + */ func parseObjectField(parser *Parser, isConst bool, fieldNames map[string]bool) (*ast.ObjectField, string, error) { start := parser.Token.Start name, err := parseName(parser) @@ -672,6 +757,9 @@ func parseObjectField(parser *Parser, isConst bool, fieldNames map[string]bool) /* Implements the parsing rules in the Directives section. */ +/** + * Directives : Directive+ + */ func parseDirectives(parser *Parser) ([]*ast.Directive, error) { directives := []*ast.Directive{} for { @@ -687,6 +775,9 @@ func parseDirectives(parser *Parser) ([]*ast.Directive, error) { return directives, nil } +/** + * Directive : @ Name Arguments? + */ func parseDirective(parser *Parser) (*ast.Directive, error) { start := parser.Token.Start _, err := expect(parser, lexer.TokenKind[lexer.AT]) @@ -710,6 +801,12 @@ func parseDirective(parser *Parser) (*ast.Directive, error) { /* Implements the parsing rules in the Types section. */ +/** + * Type : + * - NamedType + * - ListType + * - NonNullType + */ func parseType(parser *Parser) (ast.Type, error) { start := parser.Token.Start var ttype ast.Type @@ -748,6 +845,9 @@ func parseType(parser *Parser) (ast.Type, error) { return ttype, nil } +/** + * NamedType : Name + */ func parseNamed(parser *Parser) (*ast.Named, error) { start := parser.Token.Start name, err := parseName(parser) @@ -762,6 +862,9 @@ func parseNamed(parser *Parser) (*ast.Named, error) { /* Implements the parsing rules in the Type Definition section. */ +/** + * ObjectTypeDefinition : type Name ImplementsInterfaces? { FieldDefinition+ } + */ func parseObjectTypeDefinition(parser *Parser) (*ast.ObjectDefinition, error) { start := parser.Token.Start _, err := expectKeyWord(parser, "type") @@ -794,6 +897,9 @@ func parseObjectTypeDefinition(parser *Parser) (*ast.ObjectDefinition, error) { }), nil } +/** + * ImplementsInterfaces : implements NamedType+ + */ func parseImplementsInterfaces(parser *Parser) ([]*ast.Named, error) { types := []*ast.Named{} if parser.Token.Value == "implements" { @@ -814,6 +920,9 @@ func parseImplementsInterfaces(parser *Parser) ([]*ast.Named, error) { return types, nil } +/** + * FieldDefinition : Name ArgumentsDefinition? : Type + */ func parseFieldDefinition(parser *Parser) (interface{}, error) { start := parser.Token.Start name, err := parseName(parser) @@ -840,6 +949,9 @@ func parseFieldDefinition(parser *Parser) (interface{}, error) { }), nil } +/** + * ArgumentsDefinition : ( InputValueDefinition+ ) + */ func parseArgumentDefs(parser *Parser) ([]*ast.InputValueDefinition, error) { inputValueDefinitions := []*ast.InputValueDefinition{} @@ -858,6 +970,9 @@ func parseArgumentDefs(parser *Parser) ([]*ast.InputValueDefinition, error) { return inputValueDefinitions, err } +/** + * InputValueDefinition : Name : Type DefaultValue? + */ func parseInputValueDef(parser *Parser) (interface{}, error) { start := parser.Token.Start name, err := parseName(parser) @@ -892,6 +1007,9 @@ func parseInputValueDef(parser *Parser) (interface{}, error) { }), nil } +/** + * InterfaceTypeDefinition : interface Name { FieldDefinition+ } + */ func parseInterfaceTypeDefinition(parser *Parser) (*ast.InterfaceDefinition, error) { start := parser.Token.Start _, err := expectKeyWord(parser, "interface") @@ -919,6 +1037,9 @@ func parseInterfaceTypeDefinition(parser *Parser) (*ast.InterfaceDefinition, err }), nil } +/** + * UnionTypeDefinition : union Name = UnionMembers + */ func parseUnionTypeDefinition(parser *Parser) (*ast.UnionDefinition, error) { start := parser.Token.Start _, err := expectKeyWord(parser, "union") @@ -944,6 +1065,11 @@ func parseUnionTypeDefinition(parser *Parser) (*ast.UnionDefinition, error) { }), nil } +/** + * UnionMembers : + * - NamedType + * - UnionMembers | NamedType + */ func parseUnionMembers(parser *Parser) ([]*ast.Named, error) { members := []*ast.Named{} for { @@ -961,6 +1087,9 @@ func parseUnionMembers(parser *Parser) ([]*ast.Named, error) { return members, nil } +/** + * ScalarTypeDefinition : scalar Name + */ func parseScalarTypeDefinition(parser *Parser) (*ast.ScalarDefinition, error) { start := parser.Token.Start _, err := expectKeyWord(parser, "scalar") @@ -978,6 +1107,9 @@ func parseScalarTypeDefinition(parser *Parser) (*ast.ScalarDefinition, error) { return def, nil } +/** + * EnumTypeDefinition : enum Name { EnumValueDefinition+ } + */ func parseEnumTypeDefinition(parser *Parser) (*ast.EnumDefinition, error) { start := parser.Token.Start _, err := expectKeyWord(parser, "enum") @@ -1005,6 +1137,11 @@ func parseEnumTypeDefinition(parser *Parser) (*ast.EnumDefinition, error) { }), nil } +/** + * EnumValueDefinition : EnumValue + * + * EnumValue : Name + */ func parseEnumValueDefinition(parser *Parser) (interface{}, error) { start := parser.Token.Start name, err := parseName(parser) @@ -1017,6 +1154,9 @@ func parseEnumValueDefinition(parser *Parser) (interface{}, error) { }), nil } +/** + * InputObjectTypeDefinition : input Name { InputValueDefinition+ } + */ func parseInputObjectTypeDefinition(parser *Parser) (*ast.InputObjectDefinition, error) { start := parser.Token.Start _, err := expectKeyWord(parser, "input") @@ -1044,6 +1184,9 @@ func parseInputObjectTypeDefinition(parser *Parser) (*ast.InputObjectDefinition, }), nil } +/** + * TypeExtensionDefinition : extend ObjectTypeDefinition + */ func parseTypeExtensionDefinition(parser *Parser) (*ast.TypeExtensionDefinition, error) { start := parser.Token.Start _, err := expectKeyWord(parser, "extend") diff --git a/language/printer/printer.go b/language/printer/printer.go index 8d41b672..56c36dda 100644 --- a/language/printer/printer.go +++ b/language/printer/printer.go @@ -275,7 +275,13 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ typeCondition := getMapValueString(node, "TypeCondition") directives := toSliceString(getMapValue(node, "Directives")) selectionSet := getMapValueString(node, "SelectionSet") - return visitor.ActionUpdate, "... on " + typeCondition + " " + wrap("", join(directives, " "), " ") + selectionSet + return visitor.ActionUpdate, + join([]string{ + "...", + wrap("on ", typeCondition, ""), + join(directives, " "), + selectionSet, + }, " ") } return visitor.ActionNoChange, nil }, diff --git a/language/printer/printer_test.go b/language/printer/printer_test.go index 61d3dca1..9190802a 100644 --- a/language/printer/printer_test.go +++ b/language/printer/printer_test.go @@ -79,6 +79,12 @@ func TestPrinter_PrintsKitchenSink(t *testing.T) { } } } + ... @skip(unless: $foo) { + id + } + ... { + id + } } } diff --git a/language/visitor/visitor_test.go b/language/visitor/visitor_test.go index 412f96c0..8c198cdf 100644 --- a/language/visitor/visitor_test.go +++ b/language/visitor/visitor_test.go @@ -409,6 +409,34 @@ func TestVisitor_VisitsKitchenSink(t *testing.T) { []interface{}{"leave", "Field", 0, nil}, []interface{}{"leave", "SelectionSet", "SelectionSet", "InlineFragment"}, []interface{}{"leave", "InlineFragment", 1, nil}, + []interface{}{"enter", "InlineFragment", 2, nil}, + []interface{}{"enter", "Directive", 0, nil}, + []interface{}{"enter", "Name", "Name", "Directive"}, + []interface{}{"leave", "Name", "Name", "Directive"}, + []interface{}{"enter", "Argument", 0, nil}, + []interface{}{"enter", "Name", "Name", "Argument"}, + []interface{}{"leave", "Name", "Name", "Argument"}, + []interface{}{"enter", "Variable", "Value", "Argument"}, + []interface{}{"enter", "Name", "Name", "Variable"}, + []interface{}{"leave", "Name", "Name", "Variable"}, + []interface{}{"leave", "Variable", "Value", "Argument"}, + []interface{}{"leave", "Argument", 0, nil}, + []interface{}{"leave", "Directive", 0, nil}, + []interface{}{"enter", "SelectionSet", "SelectionSet", "InlineFragment"}, + []interface{}{"enter", "Field", 0, nil}, + []interface{}{"enter", "Name", "Name", "Field"}, + []interface{}{"leave", "Name", "Name", "Field"}, + []interface{}{"leave", "Field", 0, nil}, + []interface{}{"leave", "SelectionSet", "SelectionSet", "InlineFragment"}, + []interface{}{"leave", "InlineFragment", 2, nil}, + []interface{}{"enter", "InlineFragment", 3, nil}, + []interface{}{"enter", "SelectionSet", "SelectionSet", "InlineFragment"}, + []interface{}{"enter", "Field", 0, nil}, + []interface{}{"enter", "Name", "Name", "Field"}, + []interface{}{"leave", "Name", "Name", "Field"}, + []interface{}{"leave", "Field", 0, nil}, + []interface{}{"leave", "SelectionSet", "SelectionSet", "InlineFragment"}, + []interface{}{"leave", "InlineFragment", 3, nil}, []interface{}{"leave", "SelectionSet", "SelectionSet", "Field"}, []interface{}{"leave", "Field", 0, nil}, []interface{}{"leave", "SelectionSet", "SelectionSet", "OperationDefinition"}, diff --git a/rules_fields_on_correct_type_test.go b/rules_fields_on_correct_type_test.go index af1f571e..e8a2bbcf 100644 --- a/rules_fields_on_correct_type_test.go +++ b/rules_fields_on_correct_type_test.go @@ -162,6 +162,9 @@ func TestValidate_FieldsOnCorrectType_ValidFieldInInlineFragment(t *testing.T) { ... on Dog { name } + ... { + name + } } `) } diff --git a/rules_known_fragment_names_test.go b/rules_known_fragment_names_test.go index b3d5d52e..eb522b26 100644 --- a/rules_known_fragment_names_test.go +++ b/rules_known_fragment_names_test.go @@ -16,6 +16,9 @@ func TestValidate_KnownFragmentNames_KnownFragmentNamesAreValid(t *testing.T) { ... on Human { ...HumanFields2 } + ... { + name + } } } fragment HumanFields1 on Human { diff --git a/rules_known_type_names_test.go b/rules_known_type_names_test.go index 00c70263..f0fb664b 100644 --- a/rules_known_type_names_test.go +++ b/rules_known_type_names_test.go @@ -12,7 +12,7 @@ func TestValidate_KnownTypeNames_KnownTypeNamesAreValid(t *testing.T) { testutil.ExpectPassesRule(t, graphql.KnownTypeNamesRule, ` query Foo($var: String, $required: [String!]!) { user(id: 4) { - pets { ... on Pet { name }, ...PetFields } + pets { ... on Pet { name }, ...PetFields, ... { name } } } } fragment PetFields on Pet { diff --git a/type_info.go b/type_info.go index e7978889..a26825e3 100644 --- a/type_info.go +++ b/type_info.go @@ -100,11 +100,21 @@ func (ti *TypeInfo) Enter(node ast.Node) { } ti.typeStack = append(ti.typeStack, ttype) case *ast.InlineFragment: - ttype, _ = typeFromAST(*schema, node.TypeCondition) - ti.typeStack = append(ti.typeStack, ttype) + typeConditionAST := node.TypeCondition + if typeConditionAST != nil { + ttype, _ = typeFromAST(*schema, node.TypeCondition) + ti.typeStack = append(ti.typeStack, ttype) + } else { + ti.typeStack = append(ti.typeStack, ti.Type()) + } case *ast.FragmentDefinition: - ttype, _ = typeFromAST(*schema, node.TypeCondition) - ti.typeStack = append(ti.typeStack, ttype) + typeConditionAST := node.TypeCondition + if typeConditionAST != nil { + ttype, _ = typeFromAST(*schema, typeConditionAST) + ti.typeStack = append(ti.typeStack, ttype) + } else { + ti.typeStack = append(ti.typeStack, ti.Type()) + } case *ast.VariableDefinition: ttype, _ = typeFromAST(*schema, node.Type) ti.inputTypeStack = append(ti.inputTypeStack, ttype) From 6861a046e1d5c3f8fd14ff493b5b5151957d63f0 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 8 Mar 2016 10:26:35 +0800 Subject: [PATCH 11/69] Commit: 56f0b39911101400e462def6880f90d1117f242c [56f0b39] Parents: 2cfbfcec58 Author: Lee Byron Date: 2 October 2015 at 6:37:42 AM SGT Commit Date: 2 October 2015 at 6:44:20 AM SGT [RFC] Make operation name optional. Implements https://github.com/facebook/graphql/pull/99 --- executor.go | 45 ++++++++++++++-------------------- language/parser/parser.go | 6 ++--- language/parser/parser_test.go | 14 ++++++++++- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/executor.go b/executor.go index 33070e0c..0fe49e0c 100644 --- a/executor.go +++ b/executor.go @@ -82,45 +82,38 @@ type ExecutionContext struct { func buildExecutionContext(p BuildExecutionCtxParams) (*ExecutionContext, error) { eCtx := &ExecutionContext{} - operations := map[string]ast.Definition{} + // operations := map[string]ast.Definition{} + var operation *ast.OperationDefinition fragments := map[string]ast.Definition{} - for _, statement := range p.AST.Definitions { - switch stm := statement.(type) { + + for _, definition := range p.AST.Definitions { + switch definition := definition.(type) { case *ast.OperationDefinition: - key := "" - if stm.GetName() != nil && stm.GetName().Value != "" { - key = stm.GetName().Value + if (p.OperationName == "") && operation != nil { + return nil, errors.New("Must provide operation name if query contains multiple operations.") + } + if p.OperationName == "" || definition.GetName() != nil && definition.GetName().Value == p.OperationName { + operation = definition } - operations[key] = stm case *ast.FragmentDefinition: key := "" - if stm.GetName() != nil && stm.GetName().Value != "" { - key = stm.GetName().Value + if definition.GetName() != nil && definition.GetName().Value != "" { + key = definition.GetName().Value } - fragments[key] = stm + fragments[key] = definition default: - return nil, fmt.Errorf("GraphQL cannot execute a request containing a %v", statement.GetKind()) + return nil, fmt.Errorf("GraphQL cannot execute a request containing a %v", definition.GetKind()) } } - if (p.OperationName == "") && (len(operations) != 1) { - return nil, errors.New("Must provide operation name if query contains multiple operations.") - } - - opName := p.OperationName - if opName == "" { - // get first opName - for k, _ := range operations { - opName = k - break + if operation == nil { + if p.OperationName == "" { + return nil, fmt.Errorf(`Unknown operation named "%v".`, p.OperationName) + } else { + return nil, fmt.Errorf(`Must provide an operation`) } } - operation, found := operations[opName] - if !found { - return nil, fmt.Errorf(`Unknown operation named "%v".`, opName) - } - variableValues, err := getVariableValues(p.Schema, operation.GetVariableDefinitions(), p.Args) if err != nil { return nil, err diff --git a/language/parser/parser.go b/language/parser/parser.go index 8c6fda4f..07c28fce 100644 --- a/language/parser/parser.go +++ b/language/parser/parser.go @@ -230,9 +230,9 @@ func parseOperationDefinition(parser *Parser) (*ast.OperationDefinition, error) default: return nil, unexpected(parser, operationToken) } - name, err := parseName(parser) - if err != nil { - return nil, err + var name *ast.Name + if peek(parser, lexer.TokenKind[lexer.NAME]) { + name, err = parseName(parser) } variableDefinitions, err := parseVariableDefinitions(parser) if err != nil { diff --git a/language/parser/parser_test.go b/language/parser/parser_test.go index b89d21a7..46807d7d 100644 --- a/language/parser/parser_test.go +++ b/language/parser/parser_test.go @@ -137,7 +137,7 @@ fragment MissingOn Type func TestParseProvidesUsefulErrorsWhenUsingSource(t *testing.T) { test := errorMessageTest{ source.NewSource(&source.Source{Body: "query", Name: "MyQuery.graphql"}), - `Syntax Error MyQuery.graphql (1:6) Expected Name, found EOF`, + `Syntax Error MyQuery.graphql (1:6) Expected {, found EOF`, false, } testErrorMessage(t, test) @@ -251,6 +251,18 @@ func TestParsesExperimentalSubscriptionFeature(t *testing.T) { } } +func TestParsesAnonymousOperations(t *testing.T) { + source := ` + mutation { + mutationField + } + ` + _, err := Parse(ParseParams{Source: source}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + func TestParseCreatesAst(t *testing.T) { body := `{ node(id: 4) { From a119e6f7a121af3bad291dc77a58dc5c11473499 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 8 Mar 2016 11:31:15 +0800 Subject: [PATCH 12/69] Commit: 9046c14d135e7e0785b6a43cd0e0ceef7e8773b4 [9046c14] Parents: 657fbbba26 Author: Lee Byron Date: 24 September 2015 at 7:49:45 AM SGT [RFC] Move input field uniqueness validator This proposes moving input field uniqueness assertion from the parser to the validator. This simplifies the parser and allows these errors to be reported as part of the collection of validation errors which is actually more valuable. A follow-up RFC against the spec will be added --- language/parser/parser.go | 19 +++----- language/parser/parser_test.go | 9 ---- rules.go | 54 +++++++++++++++++++++ rules_unique_input_field_names_test.go | 65 ++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 22 deletions(-) create mode 100644 rules_unique_input_field_names_test.go diff --git a/language/parser/parser.go b/language/parser/parser.go index 07c28fce..705c0ae3 100644 --- a/language/parser/parser.go +++ b/language/parser/parser.go @@ -706,18 +706,16 @@ func parseObject(parser *Parser, isConst bool) (*ast.ObjectValue, error) { return nil, err } fields := []*ast.ObjectField{} - fieldNames := map[string]bool{} for { if skp, err := skip(parser, lexer.TokenKind[lexer.BRACE_R]); err != nil { return nil, err } else if skp { break } - field, fieldName, err := parseObjectField(parser, isConst, fieldNames) + field, err := parseObjectField(parser, isConst) if err != nil { return nil, err } - fieldNames[fieldName] = true fields = append(fields, field) } return ast.NewObjectValue(&ast.ObjectValue{ @@ -729,30 +727,25 @@ func parseObject(parser *Parser, isConst bool) (*ast.ObjectValue, error) { /** * ObjectField[Const] : Name : Value[?Const] */ -func parseObjectField(parser *Parser, isConst bool, fieldNames map[string]bool) (*ast.ObjectField, string, error) { +func parseObjectField(parser *Parser, isConst bool) (*ast.ObjectField, error) { start := parser.Token.Start name, err := parseName(parser) if err != nil { - return nil, "", err - } - fieldName := name.Value - if _, ok := fieldNames[fieldName]; ok { - descp := fmt.Sprintf("Duplicate input object field %v.", fieldName) - return nil, "", gqlerrors.NewSyntaxError(parser.Source, start, descp) + return nil, err } _, err = expect(parser, lexer.TokenKind[lexer.COLON]) if err != nil { - return nil, "", err + return nil, err } value, err := parseValueLiteral(parser, isConst) if err != nil { - return nil, "", err + return nil, err } return ast.NewObjectField(&ast.ObjectField{ Name: name, Value: value, Loc: loc(parser, start), - }), fieldName, nil + }), nil } /* Implements the parsing rules in the Directives section. */ diff --git a/language/parser/parser_test.go b/language/parser/parser_test.go index 46807d7d..09fa78d7 100644 --- a/language/parser/parser_test.go +++ b/language/parser/parser_test.go @@ -161,15 +161,6 @@ func TestParsesConstantDefaultValues(t *testing.T) { testErrorMessage(t, test) } -func TestDuplicatedKeysInInputObject(t *testing.T) { - test := errorMessageTest{ - `{ field(arg: { a: 1, a: 2 }) }'`, - `Syntax Error GraphQL (1:22) Duplicate input object field a.`, - false, - } - testErrorMessage(t, test) -} - func TestDoesNotAcceptFragmentsNameOn(t *testing.T) { test := errorMessageTest{ `fragment on on on { on }`, diff --git a/rules.go b/rules.go index 56a9901a..ba39e760 100644 --- a/rules.go +++ b/rules.go @@ -34,6 +34,7 @@ var SpecifiedRules = []ValidationRuleFn{ ScalarLeafsRule, UniqueArgumentNamesRule, UniqueFragmentNamesRule, + UniqueInputFieldNamesRule, UniqueOperationNamesRule, VariablesAreInputTypesRule, VariablesInAllowedPositionRule, @@ -1660,6 +1661,59 @@ func UniqueFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance } } +/** + * UniqueInputFieldNamesRule + * + * A GraphQL input object value is only valid if all supplied fields are + * uniquely named. + */ +func UniqueInputFieldNamesRule(context *ValidationContext) *ValidationRuleInstance { + knownNameStack := []map[string]*ast.Name{} + knownNames := map[string]*ast.Name{} + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.ObjectValue: visitor.NamedVisitFuncs{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + knownNameStack = append(knownNameStack, knownNames) + knownNames = map[string]*ast.Name{} + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + // pop + knownNames, knownNameStack = knownNameStack[len(knownNameStack)-1], knownNameStack[:len(knownNameStack)-1] + return visitor.ActionNoChange, nil + }, + }, + kinds.ObjectField: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + var action = visitor.ActionNoChange + var result interface{} + if node, ok := p.Node.(*ast.ObjectField); ok { + fieldName := "" + if node.Name != nil { + fieldName = node.Name.Value + } + if knownNameAST, ok := knownNames[fieldName]; ok { + return newValidationRuleError( + fmt.Sprintf(`There can be only one input field named "%v".`, fieldName), + []ast.Node{knownNameAST, node.Name}, + ) + } else { + knownNames[fieldName] = node.Name + } + + } + return action, result + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + /** * UniqueOperationNamesRule * Unique operation names diff --git a/rules_unique_input_field_names_test.go b/rules_unique_input_field_names_test.go new file mode 100644 index 00000000..a2e2e251 --- /dev/null +++ b/rules_unique_input_field_names_test.go @@ -0,0 +1,65 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_UniqueInputFieldNames_InputObjectWithFields(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueInputFieldNamesRule, ` + { + field(arg: { f: true }) + } + `) +} +func TestValidate_UniqueInputFieldNames_SameInputObjectWithinTwoArgs(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueInputFieldNamesRule, ` + { + field(arg1: { f: true }, arg2: { f: true }) + } + `) +} +func TestValidate_UniqueInputFieldNames_MultipleInputObjectFields(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueInputFieldNamesRule, ` + { + field(arg: { f1: "value", f2: "value", f3: "value" }) + } + `) +} +func TestValidate_UniqueInputFieldNames_AllowsForNestedInputObjectsWithSimilarFields(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueInputFieldNamesRule, ` + { + field(arg: { + deep: { + deep: { + id: 1 + } + id: 1 + } + id: 1 + }) + } + `) +} +func TestValidate_UniqueInputFieldNames_DuplicateInputObjectFields(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueInputFieldNamesRule, ` + { + field(arg: { f1: "value", f1: "value" }) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can be only one input field named "f1".`, 3, 22, 3, 35), + }) +} +func TestValidate_UniqueInputFieldNames_ManyDuplicateInputObjectFields(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueInputFieldNamesRule, ` + { + field(arg: { f1: "value", f1: "value", f1: "value" }) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can be only one input field named "f1".`, 3, 22, 3, 35), + testutil.RuleError(`There can be only one input field named "f1".`, 3, 22, 3, 48), + }) +} From 7415cd2f2f91ff22362e9ee5bcdb69ec2df42583 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 8 Mar 2016 11:41:45 +0800 Subject: [PATCH 13/69] Commit: 85bcbffcaf950b6a6dacae97ee351041f6f406c1 [85bcbff] Parents: a941554fc5 Author: Greg Hurrell Date: 3 October 2015 at 8:59:25 AM SGT Declare total war on misuse of "it's" Also fixed one use of "descendents" that happened to be on one of the touched lines. --- definition.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/definition.go b/definition.go index c7aaa3be..3c0ec299 100644 --- a/definition.go +++ b/definition.go @@ -906,7 +906,7 @@ func (ut *Union) Error() error { * }); * * Note: If a value is not provided in a definition, the name of the enum value - * will be used as it's internal value. + * will be used as its internal value. */ type Enum struct { PrivateName string `json:"name"` From fd180fb57500eddde74be36adb5c6367a61098f6 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 8 Mar 2016 12:04:26 +0800 Subject: [PATCH 14/69] Commit: 6741c3192d0c0d5a1f6a9185adcf083b01699935 [6741c31] Parents: 72e90c0db1 Author: Hyohyeon Jeong Date: 9 February 2016 at 3:16:33 AM SGT Committer: Lee Byron Commit Date: 9 February 2016 at 4:15:57 PM SGT Provides a correct OperationType without name in GraphQLPrinter --- language/printer/printer.go | 15 ++++---- language/printer/printer_test.go | 62 ++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/language/printer/printer.go b/language/printer/printer.go index 56c36dda..4cca90e4 100644 --- a/language/printer/printer.go +++ b/language/printer/printer.go @@ -147,35 +147,38 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ op := node.Operation name := fmt.Sprintf("%v", node.Name) - defs := wrap("(", join(toSliceString(node.VariableDefinitions), ", "), ")") + varDefs := wrap("(", join(toSliceString(node.VariableDefinitions), ", "), ")") directives := join(toSliceString(node.Directives), " ") selectionSet := fmt.Sprintf("%v", node.SelectionSet) + // Anonymous queries with no directives or variable definitions can use + // the query short form. str := "" - if name == "" { + if name == "" && directives == "" && varDefs == "" && op == "query" { str = selectionSet } else { str = join([]string{ op, - join([]string{name, defs}, ""), + join([]string{name, varDefs}, ""), directives, selectionSet, }, " ") } return visitor.ActionUpdate, str case map[string]interface{}: + op := getMapValueString(node, "Operation") name := getMapValueString(node, "Name") - defs := wrap("(", join(toSliceString(getMapValue(node, "VariableDefinitions")), ", "), ")") + varDefs := wrap("(", join(toSliceString(getMapValue(node, "VariableDefinitions")), ", "), ")") directives := join(toSliceString(getMapValue(node, "Directives")), " ") selectionSet := getMapValueString(node, "SelectionSet") str := "" - if name == "" { + if name == "" && directives == "" && varDefs == "" && op == "query" { str = selectionSet } else { str = join([]string{ op, - join([]string{name, defs}, ""), + join([]string{name, varDefs}, ""), directives, selectionSet, }, " ") diff --git a/language/printer/printer_test.go b/language/printer/printer_test.go index 9190802a..bd4782e5 100644 --- a/language/printer/printer_test.go +++ b/language/printer/printer_test.go @@ -59,6 +59,68 @@ func TestPrinter_PrintsMinimalAST(t *testing.T) { } } +// TestPrinter_ProducesHelpfulErrorMessages +// Skipped, can't figure out how to pass in an invalid astDoc, which is already strongly-typed + +func TestPrinter_CorrectlyPrintsNonQueryOperationsWithoutName(t *testing.T) { + + // Test #1 + queryAstShorthanded := `query { id, name }` + expected := `{ + id + name +} +` + astDoc := parse(t, queryAstShorthanded) + results := printer.Print(astDoc) + + if !reflect.DeepEqual(expected, results) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(results, expected)) + } + + // Test #2 + mutationAst := `mutation { id, name }` + expected = `mutation { + id + name +} +` + astDoc = parse(t, mutationAst) + results = printer.Print(astDoc) + + if !reflect.DeepEqual(expected, results) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(results, expected)) + } + + // Test #3 + queryAstWithArtifacts := `query ($foo: TestType) @testDirective { id, name }` + expected = `query ($foo: TestType) @testDirective { + id + name +} +` + astDoc = parse(t, queryAstWithArtifacts) + results = printer.Print(astDoc) + + if !reflect.DeepEqual(expected, results) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(results, expected)) + } + + // Test #4 + mutationAstWithArtifacts := `mutation ($foo: TestType) @testDirective { id, name }` + expected = `mutation ($foo: TestType) @testDirective { + id + name +} +` + astDoc = parse(t, mutationAstWithArtifacts) + results = printer.Print(astDoc) + + if !reflect.DeepEqual(expected, results) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(results, expected)) + } +} + func TestPrinter_PrintsKitchenSink(t *testing.T) { b, err := ioutil.ReadFile("../../kitchen-sink.graphql") if err != nil { From fec8a0de8273c7a35ae3758678f3dfac9939cb9f Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 9 Mar 2016 13:46:30 +0800 Subject: [PATCH 15/69] [Validation] Report errors rather than return them This replaces the mechanism of returning errors or lists of errors from a validator step to instead report those errors via calling a function on the context. This simplifies the implementation of the visitor mechanism, but also opens the doors for future rules being specified as warnings instead of errors. Commit: 5e545cce0104708a4ac6e994dd5f837d1d30a09b [5e545cc] Parents: ef7c755c58 Author: Lee Byron Date: 17 November 2015 at 11:32:13 AM SGT --- rules.go | 139 ++++++++++++++------------- rules_fields_on_correct_type_test.go | 16 ++- testutil/rules_test_harness.go | 8 +- validator.go | 36 +++---- validator_test.go | 43 +++++++++ 5 files changed, 147 insertions(+), 95 deletions(-) create mode 100644 validator_test.go diff --git a/rules.go b/rules.go index ba39e760..c1fd9a8f 100644 --- a/rules.go +++ b/rules.go @@ -46,8 +46,8 @@ type ValidationRuleInstance struct { } type ValidationRuleFn func(context *ValidationContext) *ValidationRuleInstance -func newValidationRuleError(message string, nodes []ast.Node) (string, error) { - return visitor.ActionNoChange, gqlerrors.NewError( +func newValidationError(message string, nodes []ast.Node) *gqlerrors.Error { + return gqlerrors.NewError( message, nodes, "", @@ -57,6 +57,11 @@ func newValidationRuleError(message string, nodes []ast.Node) (string, error) { ) } +func reportErrorAndReturn(context *ValidationContext, message string, nodes []ast.Node) (string, interface{}) { + context.ReportError(newValidationError(message, nodes)) + return visitor.ActionNoChange, nil +} + /** * ArgumentsOfCorrectTypeRule * Argument values of correct type @@ -79,7 +84,8 @@ func ArgumentsOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInsta if argAST.Name != nil { argNameValue = argAST.Name.Value } - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Argument "%v" expected type "%v" but got: %v.`, argNameValue, argDef.Type, printer.Print(value)), []ast.Node{value}, @@ -119,14 +125,16 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI ttype := context.InputType() if ttype, ok := ttype.(*NonNull); ok && defaultValue != nil { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Variable "$%v" of type "%v" is required and will not use the default value. Perhaps you meant to use type "%v".`, name, ttype, ttype.OfType), []ast.Node{defaultValue}, ) } if ttype != nil && defaultValue != nil && !isValidLiteralValue(ttype, defaultValue) { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Variable "$%v" of type "%v" has invalid default value: %v.`, name, ttype, printer.Print(defaultValue)), []ast.Node{defaultValue}, @@ -167,7 +175,8 @@ func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance if node.Name != nil { nodeName = node.Name.Value } - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Cannot query field "%v" on "%v".`, nodeName, ttype.Name()), []ast.Node{node}, @@ -201,7 +210,8 @@ func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleIn if node, ok := p.Node.(*ast.InlineFragment); ok { ttype := context.Type() if ttype != nil && !IsCompositeType(ttype) { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Fragment cannot condition on non composite type "%v".`, ttype), []ast.Node{node.TypeCondition}, ) @@ -219,7 +229,8 @@ func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleIn if node.Name != nil { nodeName = node.Name.Value } - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Fragment "%v" cannot condition on non composite type "%v".`, nodeName, printer.Print(node.TypeCondition)), []ast.Node{node.TypeCondition}, ) @@ -278,7 +289,8 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance if parentType != nil { parentTypeName = parentType.Name() } - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Unknown argument "%v" on field "%v" of type "%v".`, nodeName, fieldDef.Name, parentTypeName), []ast.Node{node}, ) @@ -299,7 +311,8 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance } } if directiveArgDef == nil { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Unknown argument "%v" on directive "@%v".`, nodeName, directive.Name), []ast.Node{node}, ) @@ -344,7 +357,8 @@ func KnownDirectivesRule(context *ValidationContext) *ValidationRuleInstance { } } if directiveDef == nil { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Unknown directive "%v".`, nodeName), []ast.Node{node}, ) @@ -359,13 +373,15 @@ func KnownDirectivesRule(context *ValidationContext) *ValidationRuleInstance { } if appliedTo.GetKind() == kinds.OperationDefinition && directiveDef.OnOperation == false { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "operation"), []ast.Node{node}, ) } if appliedTo.GetKind() == kinds.Field && directiveDef.OnField == false { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "field"), []ast.Node{node}, ) @@ -373,7 +389,8 @@ func KnownDirectivesRule(context *ValidationContext) *ValidationRuleInstance { if (appliedTo.GetKind() == kinds.FragmentSpread || appliedTo.GetKind() == kinds.InlineFragment || appliedTo.GetKind() == kinds.FragmentDefinition) && directiveDef.OnFragment == false { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "fragment"), []ast.Node{node}, ) @@ -413,7 +430,8 @@ func KnownFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance fragment := context.Fragment(fragmentName) if fragment == nil { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Unknown fragment "%v".`, fragmentName), []ast.Node{node.Name}, ) @@ -449,7 +467,8 @@ func KnownTypeNamesRule(context *ValidationContext) *ValidationRuleInstance { } ttype := context.Schema().Type(typeNameValue) if ttype == nil { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Unknown type "%v".`, typeNameValue), []ast.Node{node}, ) @@ -493,7 +512,8 @@ func LoneAnonymousOperationRule(context *ValidationContext) *ValidationRuleInsta Kind: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.OperationDefinition); ok { if node.Name == nil && operationCount > 1 { - return newValidationRuleError( + return reportErrorAndReturn( + context, `This anonymous operation must be the only defined operation.`, []ast.Node{node}, ) @@ -558,7 +578,6 @@ func NoFragmentCyclesRule(context *ValidationContext) *ValidationRuleInstance { kinds.FragmentDefinition: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.FragmentDefinition); ok && node != nil { - errors := []error{} spreadPath := []*ast.FragmentSpread{} initialName := "" if node.Name != nil { @@ -594,11 +613,11 @@ func NoFragmentCyclesRule(context *ValidationContext) *ValidationRuleInstance { if len(spreadNames) > 0 { via = " via " + strings.Join(spreadNames, ", ") } - _, err := newValidationRuleError( + err := newValidationError( fmt.Sprintf(`Cannot spread fragment "%v" within itself%v.`, initialName, via), cyclePath, ) - errors = append(errors, err) + context.ReportError(err) continue } spreadPathHasCurrentNode := false @@ -616,9 +635,6 @@ func NoFragmentCyclesRule(context *ValidationContext) *ValidationRuleInstance { } } detectCycleRecursive(initialName) - if len(errors) > 0 { - return visitor.ActionNoChange, errors - } } return visitor.ActionNoChange, nil }, @@ -681,12 +697,14 @@ func NoUndefinedVariablesRule(context *ValidationContext) *ValidationRuleInstanc } } if withinFragment == true && operation != nil && operation.Name != nil { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Variable "$%v" is not defined by operation "%v".`, variableName, operation.Name.Value), []ast.Node{variable, operation}, ) } - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Variable "$%v" is not defined.`, variableName), []ast.Node{variable}, ) @@ -791,7 +809,6 @@ func NoUnusedFragmentsRule(context *ValidationContext) *ValidationRuleInstance { for _, spreadWithinOperation := range spreadsWithinOperation { reduceSpreadFragments(spreadWithinOperation) } - errors := []error{} for _, def := range fragmentDefs { defName := "" if def.Name != nil { @@ -800,17 +817,13 @@ func NoUnusedFragmentsRule(context *ValidationContext) *ValidationRuleInstance { isFragNameUsed, ok := fragmentNameUsed[defName] if !ok || isFragNameUsed != true { - _, err := newValidationRuleError( + err := newValidationError( fmt.Sprintf(`Fragment "%v" is never used.`, defName), []ast.Node{def}, ) - - errors = append(errors, err) + context.ReportError(err) } } - if len(errors) > 0 { - return visitor.ActionNoChange, errors - } return visitor.ActionNoChange, nil }, }, @@ -844,23 +857,19 @@ func NoUnusedVariablesRule(context *ValidationContext) *ValidationRuleInstance { return visitor.ActionNoChange, nil }, Leave: func(p visitor.VisitFuncParams) (string, interface{}) { - errors := []error{} for _, def := range variableDefs { variableName := "" if def.Variable != nil && def.Variable.Name != nil { variableName = def.Variable.Name.Value } if isVariableNameUsed, _ := variableNameUsed[variableName]; isVariableNameUsed != true { - _, err := newValidationRuleError( + err := newValidationError( fmt.Sprintf(`Variable "$%v" is never used.`, variableName), []ast.Node{def}, ) - errors = append(errors, err) + context.ReportError(err) } } - if len(errors) > 0 { - return visitor.ActionNoChange, errors - } return visitor.ActionNoChange, nil }, }, @@ -1289,11 +1298,10 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul ) conflicts := findConflicts(fieldMap) if len(conflicts) > 0 { - errors := []error{} for _, c := range conflicts { responseName := c.Reason.Name reason := c.Reason - _, err := newValidationRuleError( + err := newValidationError( fmt.Sprintf( `Fields "%v" conflict because %v.`, responseName, @@ -1301,10 +1309,9 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul ), c.Fields, ) - errors = append(errors, err) - + context.ReportError(err) } - return visitor.ActionNoChange, errors + return visitor.ActionNoChange, nil } } return visitor.ActionNoChange, nil @@ -1387,7 +1394,8 @@ func PossibleFragmentSpreadsRule(context *ValidationContext) *ValidationRuleInst parentType, _ := context.ParentType().(Type) if fragType != nil && parentType != nil && !doTypesOverlap(fragType, parentType) { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Fragment cannot be spread here as objects of `+ `type "%v" can never be of type "%v".`, parentType, fragType), []ast.Node{node}, @@ -1407,7 +1415,8 @@ func PossibleFragmentSpreadsRule(context *ValidationContext) *ValidationRuleInst fragType := getFragmentType(context, fragName) parentType, _ := context.ParentType().(Type) if fragType != nil && parentType != nil && !doTypesOverlap(fragType, parentType) { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Fragment "%v" cannot be spread here as objects of `+ `type "%v" can never be of type "%v".`, fragName, parentType, fragType), []ast.Node{node}, @@ -1444,7 +1453,6 @@ func ProvidedNonNullArgumentsRule(context *ValidationContext) *ValidationRuleIns return visitor.ActionSkip, nil } - errors := []error{} argASTs := fieldAST.Arguments argASTMap := map[string]*ast.Argument{} @@ -1463,18 +1471,15 @@ func ProvidedNonNullArgumentsRule(context *ValidationContext) *ValidationRuleIns if fieldAST.Name != nil { fieldName = fieldAST.Name.Value } - _, err := newValidationRuleError( + err := newValidationError( fmt.Sprintf(`Field "%v" argument "%v" of type "%v" `+ `is required but not provided.`, fieldName, argDef.Name(), argDefType), []ast.Node{fieldAST}, ) - errors = append(errors, err) + context.ReportError(err) } } } - if len(errors) > 0 { - return visitor.ActionNoChange, errors - } } return visitor.ActionNoChange, nil }, @@ -1488,7 +1493,6 @@ func ProvidedNonNullArgumentsRule(context *ValidationContext) *ValidationRuleIns if directiveDef == nil { return visitor.ActionSkip, nil } - errors := []error{} argASTs := directiveAST.Arguments argASTMap := map[string]*ast.Argument{} @@ -1508,18 +1512,15 @@ func ProvidedNonNullArgumentsRule(context *ValidationContext) *ValidationRuleIns if directiveAST.Name != nil { directiveName = directiveAST.Name.Value } - _, err := newValidationRuleError( + err := newValidationError( fmt.Sprintf(`Directive "@%v" argument "%v" of type `+ `"%v" is required but not provided.`, directiveName, argDef.Name(), argDefType), []ast.Node{directiveAST}, ) - errors = append(errors, err) + context.ReportError(err) } } } - if len(errors) > 0 { - return visitor.ActionNoChange, errors - } } return visitor.ActionNoChange, nil }, @@ -1553,13 +1554,15 @@ func ScalarLeafsRule(context *ValidationContext) *ValidationRuleInstance { if ttype != nil { if IsLeafType(ttype) { if node.SelectionSet != nil { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Field "%v" of type "%v" must not have a sub selection.`, nodeName, ttype), []ast.Node{node.SelectionSet}, ) } } else if node.SelectionSet == nil { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Field "%v" of type "%v" must have a sub selection.`, nodeName, ttype), []ast.Node{node}, ) @@ -1608,7 +1611,8 @@ func UniqueArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance argName = node.Name.Value } if nameAST, ok := knownArgNames[argName]; ok { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`There can be only one argument named "%v".`, argName), []ast.Node{nameAST, node.Name}, ) @@ -1644,7 +1648,8 @@ func UniqueFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance fragmentName = node.Name.Value } if nameAST, ok := knownFragmentNames[fragmentName]; ok { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`There can only be one fragment named "%v".`, fragmentName), []ast.Node{nameAST, node.Name}, ) @@ -1695,7 +1700,8 @@ func UniqueInputFieldNamesRule(context *ValidationContext) *ValidationRuleInstan fieldName = node.Name.Value } if knownNameAST, ok := knownNames[fieldName]; ok { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`There can be only one input field named "%v".`, fieldName), []ast.Node{knownNameAST, node.Name}, ) @@ -1733,7 +1739,8 @@ func UniqueOperationNamesRule(context *ValidationContext) *ValidationRuleInstanc operationName = node.Name.Value } if nameAST, ok := knownOperationNames[operationName]; ok { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`There can only be one operation named "%v".`, operationName), []ast.Node{nameAST, node.Name}, ) @@ -1772,7 +1779,8 @@ func VariablesAreInputTypesRule(context *ValidationContext) *ValidationRuleInsta if node.Variable != nil && node.Variable.Name != nil { variableName = node.Variable.Name.Value } - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Variable "$%v" cannot be non-input type "%v".`, variableName, printer.Print(node.Type)), []ast.Node{node.Type}, @@ -1882,7 +1890,8 @@ func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleI } inputType := context.InputType() if varType != nil && inputType != nil && !varTypeAllowedForType(effectiveType(varType, varDef), inputType) { - return newValidationRuleError( + return reportErrorAndReturn( + context, fmt.Sprintf(`Variable "$%v" of type "%v" used in position `+ `expecting type "%v".`, varName, varType, inputType), []ast.Node{variableAST}, diff --git a/rules_fields_on_correct_type_test.go b/rules_fields_on_correct_type_test.go index e8a2bbcf..652f4272 100644 --- a/rules_fields_on_correct_type_test.go +++ b/rules_fields_on_correct_type_test.go @@ -53,6 +53,20 @@ func TestValidate_FieldsOnCorrectType_IgnoresFieldsOnUnknownType(t *testing.T) { } `) } +func TestValidate_FieldsOnCorrectType_ReportErrosWhenTheTypeIsKnownAgain(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment typeKnownAgain on Pet { + unknown_pet_field { + ... on Cat { + unknown_cat_field + } + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot query field "unknown_pet_field" on "Pet".`, 3, 9), + testutil.RuleError(`Cannot query field "unknown_cat_field" on "Cat".`, 5, 13), + }) +} func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnFragment(t *testing.T) { testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` fragment fieldNotDefined on Dog { @@ -62,7 +76,7 @@ func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnFragment(t *testing.T) { testutil.RuleError(`Cannot query field "meowVolume" on "Dog".`, 3, 9), }) } -func TestValidate_FieldsOnCorrectType_FieldNotDefinedDeeplyOnlyReportsFirst(t *testing.T) { +func TestValidate_FieldsOnCorrectType_IgnoreDeeplyUnknownField(t *testing.T) { testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` fragment deepFieldNotDefined on Dog { unknown_field { diff --git a/testutil/rules_test_harness.go b/testutil/rules_test_harness.go index 3acb9094..6f840cbc 100644 --- a/testutil/rules_test_harness.go +++ b/testutil/rules_test_harness.go @@ -11,7 +11,7 @@ import ( "reflect" ) -var defaultRulesTestSchema *graphql.Schema +var DefaultRulesTestSchema *graphql.Schema func init() { @@ -448,7 +448,7 @@ func init() { if err != nil { panic(err) } - defaultRulesTestSchema = &schema + DefaultRulesTestSchema = &schema } func expectValidRule(t *testing.T, schema *graphql.Schema, rules []graphql.ValidationRuleFn, queryString string) { @@ -498,10 +498,10 @@ func expectInvalidRule(t *testing.T, schema *graphql.Schema, rules []graphql.Val } func ExpectPassesRule(t *testing.T, rule graphql.ValidationRuleFn, queryString string) { - expectValidRule(t, defaultRulesTestSchema, []graphql.ValidationRuleFn{rule}, queryString) + expectValidRule(t, DefaultRulesTestSchema, []graphql.ValidationRuleFn{rule}, queryString) } func ExpectFailsRule(t *testing.T, rule graphql.ValidationRuleFn, queryString string, expectedErrors []gqlerrors.FormattedError) { - expectInvalidRule(t, defaultRulesTestSchema, []graphql.ValidationRuleFn{rule}, queryString, expectedErrors) + expectInvalidRule(t, DefaultRulesTestSchema, []graphql.ValidationRuleFn{rule}, queryString, expectedErrors) } func ExpectFailsRuleWithSchema(t *testing.T, schema *graphql.Schema, rule graphql.ValidationRuleFn, queryString string, expectedErrors []gqlerrors.FormattedError) { expectInvalidRule(t, schema, []graphql.ValidationRuleFn{rule}, queryString, expectedErrors) diff --git a/validator.go b/validator.go index 2873fd64..4bb0790f 100644 --- a/validator.go +++ b/validator.go @@ -33,7 +33,7 @@ func ValidateDocument(schema *Schema, astDoc *ast.Document, rules []ValidationRu return vr } -func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRuleFn) (errors []gqlerrors.FormattedError) { +func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRuleFn) []gqlerrors.FormattedError { typeInfo := NewTypeInfo(schema) context := NewValidationContext(schema, astDoc, typeInfo) @@ -66,17 +66,6 @@ func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRul action, result = enterFn(p) } - // If the visitor returned an error, log it and do not visit any - // deeper nodes. - if err, ok := result.(error); ok && err != nil { - errors = append(errors, gqlerrors.FormatError(err)) - action = visitor.ActionSkip - } - if err, ok := result.([]error); ok && err != nil { - errors = append(errors, gqlerrors.FormatErrors(err...)...) - action = visitor.ActionSkip - } - // If any validation instances provide the flag `visitSpreadFragments` // and this node is a fragment spread, visit the fragment definition // from this point. @@ -118,17 +107,6 @@ func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRul action, result = leaveFn(p) } - // If the visitor returned an error, log it and do not visit any - // deeper nodes. - if err, ok := result.(error); ok && err != nil { - errors = append(errors, gqlerrors.FormatError(err)) - action = visitor.ActionSkip - } - if err, ok := result.([]error); ok && err != nil { - errors = append(errors, gqlerrors.FormatErrors(err...)...) - action = visitor.ActionSkip - } - // Update typeInfo. typeInfo.Leave(node) } @@ -145,7 +123,7 @@ func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRul for _, instance := range instances { visitInstance(astDoc, instance) } - return errors + return context.Errors() } type ValidationContext struct { @@ -153,6 +131,7 @@ type ValidationContext struct { astDoc *ast.Document typeInfo *TypeInfo fragments map[string]*ast.FragmentDefinition + errors []gqlerrors.FormattedError } func NewValidationContext(schema *Schema, astDoc *ast.Document, typeInfo *TypeInfo) *ValidationContext { @@ -163,13 +142,20 @@ func NewValidationContext(schema *Schema, astDoc *ast.Document, typeInfo *TypeIn } } +func (ctx *ValidationContext) ReportError(err error) { + formattedErr := gqlerrors.FormatError(err) + ctx.errors = append(ctx.errors, formattedErr) +} +func (ctx *ValidationContext) Errors() []gqlerrors.FormattedError { + return ctx.errors +} + func (ctx *ValidationContext) Schema() *Schema { return ctx.schema } func (ctx *ValidationContext) Document() *ast.Document { return ctx.astDoc } - func (ctx *ValidationContext) Fragment(name string) *ast.FragmentDefinition { if len(ctx.fragments) == 0 { if ctx.Document() == nil { diff --git a/validator_test.go b/validator_test.go new file mode 100644 index 00000000..acb4ddfb --- /dev/null +++ b/validator_test.go @@ -0,0 +1,43 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/language/parser" + "github.com/graphql-go/graphql/language/source" + "github.com/graphql-go/graphql/testutil" +) + +func expectValid(t *testing.T, schema *graphql.Schema, queryString string) { + source := source.NewSource(&source.Source{ + Body: queryString, + Name: "GraphQL request", + }) + AST, err := parser.Parse(parser.ParseParams{Source: source}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + validationResult := graphql.ValidateDocument(schema, AST, nil) + + if !validationResult.IsValid || len(validationResult.Errors) > 0 { + t.Fatalf("Unexpected error: %v", validationResult.Errors) + } + +} + +func TestValidator_SupportsFullValidation_ValidatesQueries(t *testing.T) { + + expectValid(t, testutil.DefaultRulesTestSchema, ` + query { + catOrDog { + ... on Cat { + furColor + } + ... on Dog { + isHousetrained + } + } + } + `) +} From a921397e480d09062d214d3e1e15b135419e4992 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Thu, 10 Mar 2016 22:17:51 +0800 Subject: [PATCH 16/69] [Validation] Parallelize validation rules. This provides a performance improvement and simplification to the validator by providing two new generic visitor utilities. One for tracking a TypeInfo instance alongside a visitor instance, and another for stepping through multiple visitors in parallel. The two can be composed together. Rather than 23 passes of AST visitation with one rule each, this now performs one pass of AST visitation with 23 rules. Since visitation is costly but rules are inexpensive, this nets out to a much faster overall validation, especially noticable for very large queries. Commit: 957704188b0a103c5f2fe0ab99479267d5d1ae43 [9577041] Parents: 439a3e2f4f Author: Lee Byron Date: 17 November 2015 at 12:33:54 PM SGT ---- [Validation] Memoize collecting variable usage. During multiple validation passes we need to know about variable usage within a de-fragmented operation. Memoizing this ensures each pass is O(N) - each fragment is no longer visited per operation, but once total. In doing so, `visitSpreadFragments` is no longer used, which will be cleaned up in a later PR Commit: 2afbff79bfd2b89f03ca7913577556b73980f974 [2afbff7] Parents: 88acc01b99 Author: Lee Byron Date: 17 November 2015 at 9:54:30 AM SGT --- language/ast/selections.go | 12 + language/type_info/type_info.go | 14 ++ language/visitor/visitor.go | 177 ++++++++++++++- rules.go | 318 ++++++++++++--------------- rules_no_undefined_variables_test.go | 10 +- type_info.go | 28 ++- validator.go | 178 ++++++++++++++- 7 files changed, 536 insertions(+), 201 deletions(-) create mode 100644 language/type_info/type_info.go diff --git a/language/ast/selections.go b/language/ast/selections.go index 1b7e60d2..dd36cf26 100644 --- a/language/ast/selections.go +++ b/language/ast/selections.go @@ -46,6 +46,10 @@ func (f *Field) GetLoc() *Location { return f.Loc } +func (f *Field) GetSelectionSet() *SelectionSet { + return f.SelectionSet +} + // FragmentSpread implements Node, Selection type FragmentSpread struct { Kind string @@ -74,6 +78,10 @@ func (fs *FragmentSpread) GetLoc() *Location { return fs.Loc } +func (fs *FragmentSpread) GetSelectionSet() *SelectionSet { + return nil +} + // InlineFragment implements Node, Selection type InlineFragment struct { Kind string @@ -104,6 +112,10 @@ func (f *InlineFragment) GetLoc() *Location { return f.Loc } +func (f *InlineFragment) GetSelectionSet() *SelectionSet { + return f.SelectionSet +} + // SelectionSet implements Node type SelectionSet struct { Kind string diff --git a/language/type_info/type_info.go b/language/type_info/type_info.go new file mode 100644 index 00000000..02b7b04f --- /dev/null +++ b/language/type_info/type_info.go @@ -0,0 +1,14 @@ +package type_info + +import ( + "github.com/graphql-go/graphql/language/ast" +) + +/** + * TypeInfoI defines the interface for TypeInfo + * Implementation + */ +type TypeInfoI interface { + Enter(node ast.Node) + Leave(node ast.Node) +} diff --git a/language/visitor/visitor.go b/language/visitor/visitor.go index 83edbd9b..3188efec 100644 --- a/language/visitor/visitor.go +++ b/language/visitor/visitor.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "github.com/graphql-go/graphql/language/ast" + "github.com/graphql-go/graphql/language/type_info" "reflect" ) @@ -380,7 +381,7 @@ Loop: kind = node.GetKind() } - visitFn := GetVisitFn(visitorOpts, isLeaving, kind) + visitFn := GetVisitFn(visitorOpts, kind, isLeaving) if visitFn != nil { p := VisitFuncParams{ Node: nodeIn, @@ -709,7 +710,144 @@ func isNilNode(node interface{}) bool { return val.Interface() == nil } -func GetVisitFn(visitorOpts *VisitorOptions, isLeaving bool, kind string) VisitFunc { +/** + * Creates a new visitor instance which delegates to many visitors to run in + * parallel. Each visitor will be visited for each node before moving on. + * + * Visitors must not directly modify the AST nodes and only returning false to + * skip sub-branches is supported. + */ +func VisitInParallel(visitorOptsSlice []*VisitorOptions) *VisitorOptions { + skipping := map[int]interface{}{} + + return &VisitorOptions{ + Enter: func(p VisitFuncParams) (string, interface{}) { + for i, visitorOpts := range visitorOptsSlice { + if _, ok := skipping[i]; !ok { + switch node := p.Node.(type) { + case ast.Node: + kind := node.GetKind() + fn := GetVisitFn(visitorOpts, kind, false) + if fn != nil { + action, _ := fn(p) + if action == ActionSkip { + skipping[i] = node + } + } + } + } + } + return ActionNoChange, nil + }, + Leave: func(p VisitFuncParams) (string, interface{}) { + for i, visitorOpts := range visitorOptsSlice { + if _, ok := skipping[i]; !ok { + switch node := p.Node.(type) { + case ast.Node: + kind := node.GetKind() + fn := GetVisitFn(visitorOpts, kind, true) + if fn != nil { + fn(p) + } + } + } else { + delete(skipping, i) + } + } + return ActionNoChange, nil + }, + } +} + +/** + * Creates a new visitor instance which maintains a provided TypeInfo instance + * along with visiting visitor. + * + * Visitors must not directly modify the AST nodes and only returning false to + * skip sub-branches is supported. + */ +func VisitWithTypeInfo(typeInfo type_info.TypeInfoI, visitorOpts *VisitorOptions) *VisitorOptions { + return &VisitorOptions{ + Enter: func(p VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(ast.Node); ok { + typeInfo.Enter(node) + fn := GetVisitFn(visitorOpts, node.GetKind(), false) + if fn != nil { + action, _ := fn(p) + if action == ActionSkip { + typeInfo.Leave(node) + return ActionSkip, nil + } + } + } + return ActionNoChange, nil + }, + Leave: func(p VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(ast.Node); ok { + fn := GetVisitFn(visitorOpts, node.GetKind(), true) + if fn != nil { + fn(p) + } + typeInfo.Leave(node) + } + return ActionNoChange, nil + }, + } +} + +/** + * Given a visitor instance, if it is leaving or not, and a node kind, return + * the function the visitor runtime should call. + */ +func GetVisitFn(visitorOpts *VisitorOptions, kind string, isLeaving bool) VisitFunc { + if visitorOpts == nil { + return nil + } + kindVisitor, ok := visitorOpts.KindFuncMap[kind] + if ok { + if !isLeaving && kindVisitor.Kind != nil { + // { Kind() {} } + return kindVisitor.Kind + } + if isLeaving { + // { Kind: { leave() {} } } + return kindVisitor.Leave + } else { + // { Kind: { enter() {} } } + return kindVisitor.Enter + } + } else { + + if isLeaving { + // { enter() {} } + specificVisitor := visitorOpts.Leave + if specificVisitor != nil { + return specificVisitor + } + if specificKindVisitor, ok := visitorOpts.LeaveKindMap[kind]; ok { + // { leave: { Kind() {} } } + return specificKindVisitor + } + + } else { + // { leave() {} } + specificVisitor := visitorOpts.Enter + if specificVisitor != nil { + return specificVisitor + } + if specificKindVisitor, ok := visitorOpts.EnterKindMap[kind]; ok { + // { enter: { Kind() {} } } + return specificKindVisitor + } + } + } + + return nil +} + +///// DELETE //// + +func GetVisitFnOld(visitorOpts *VisitorOptions, isLeaving bool, kind string) VisitFunc { if visitorOpts == nil { return nil } @@ -753,3 +891,38 @@ func GetVisitFn(visitorOpts *VisitorOptions, isLeaving bool, kind string) VisitF return nil } + +/* + + +export function getVisitFn(visitor, isLeaving, kind) { + var kindVisitor = visitor[kind]; + if (kindVisitor) { + if (!isLeaving && typeof kindVisitor === 'function') { + // { Kind() {} } + return kindVisitor; + } + var kindSpecificVisitor = isLeaving ? kindVisitor.leave : kindVisitor.enter; + if (typeof kindSpecificVisitor === 'function') { + // { Kind: { enter() {}, leave() {} } } + return kindSpecificVisitor; + } + return; + } + var specificVisitor = isLeaving ? visitor.leave : visitor.enter; + if (specificVisitor) { + if (typeof specificVisitor === 'function') { + // { enter() {}, leave() {} } + return specificVisitor; + } + var specificKindVisitor = specificVisitor[kind]; + if (typeof specificKindVisitor === 'function') { + // { enter: { Kind() {} }, leave: { Kind() {} } } + return specificKindVisitor; + } + } +} + + + +*/ diff --git a/rules.go b/rules.go index c1fd9a8f..d46520a8 100644 --- a/rules.go +++ b/rules.go @@ -57,7 +57,7 @@ func newValidationError(message string, nodes []ast.Node) *gqlerrors.Error { ) } -func reportErrorAndReturn(context *ValidationContext, message string, nodes []ast.Node) (string, interface{}) { +func reportError(context *ValidationContext, message string, nodes []ast.Node) (string, interface{}) { context.ReportError(newValidationError(message, nodes)) return visitor.ActionNoChange, nil } @@ -84,7 +84,7 @@ func ArgumentsOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInsta if argAST.Name != nil { argNameValue = argAST.Name.Value } - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Argument "%v" expected type "%v" but got: %v.`, argNameValue, argDef.Type, printer.Print(value)), @@ -125,7 +125,7 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI ttype := context.InputType() if ttype, ok := ttype.(*NonNull); ok && defaultValue != nil { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Variable "$%v" of type "%v" is required and will not use the default value. Perhaps you meant to use type "%v".`, name, ttype, ttype.OfType), @@ -133,7 +133,7 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI ) } if ttype != nil && defaultValue != nil && !isValidLiteralValue(ttype, defaultValue) { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Variable "$%v" of type "%v" has invalid default value: %v.`, name, ttype, printer.Print(defaultValue)), @@ -175,7 +175,7 @@ func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance if node.Name != nil { nodeName = node.Name.Value } - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Cannot query field "%v" on "%v".`, nodeName, ttype.Name()), @@ -210,7 +210,7 @@ func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleIn if node, ok := p.Node.(*ast.InlineFragment); ok { ttype := context.Type() if ttype != nil && !IsCompositeType(ttype) { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Fragment cannot condition on non composite type "%v".`, ttype), []ast.Node{node.TypeCondition}, @@ -229,7 +229,7 @@ func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleIn if node.Name != nil { nodeName = node.Name.Value } - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Fragment "%v" cannot condition on non composite type "%v".`, nodeName, printer.Print(node.TypeCondition)), []ast.Node{node.TypeCondition}, @@ -289,7 +289,7 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance if parentType != nil { parentTypeName = parentType.Name() } - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Unknown argument "%v" on field "%v" of type "%v".`, nodeName, fieldDef.Name, parentTypeName), []ast.Node{node}, @@ -311,7 +311,7 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance } } if directiveArgDef == nil { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Unknown argument "%v" on directive "@%v".`, nodeName, directive.Name), []ast.Node{node}, @@ -357,7 +357,7 @@ func KnownDirectivesRule(context *ValidationContext) *ValidationRuleInstance { } } if directiveDef == nil { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Unknown directive "%v".`, nodeName), []ast.Node{node}, @@ -373,14 +373,14 @@ func KnownDirectivesRule(context *ValidationContext) *ValidationRuleInstance { } if appliedTo.GetKind() == kinds.OperationDefinition && directiveDef.OnOperation == false { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "operation"), []ast.Node{node}, ) } if appliedTo.GetKind() == kinds.Field && directiveDef.OnField == false { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "field"), []ast.Node{node}, @@ -389,7 +389,7 @@ func KnownDirectivesRule(context *ValidationContext) *ValidationRuleInstance { if (appliedTo.GetKind() == kinds.FragmentSpread || appliedTo.GetKind() == kinds.InlineFragment || appliedTo.GetKind() == kinds.FragmentDefinition) && directiveDef.OnFragment == false { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "fragment"), []ast.Node{node}, @@ -430,7 +430,7 @@ func KnownFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance fragment := context.Fragment(fragmentName) if fragment == nil { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Unknown fragment "%v".`, fragmentName), []ast.Node{node.Name}, @@ -467,7 +467,7 @@ func KnownTypeNamesRule(context *ValidationContext) *ValidationRuleInstance { } ttype := context.Schema().Type(typeNameValue) if ttype == nil { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Unknown type "%v".`, typeNameValue), []ast.Node{node}, @@ -512,7 +512,7 @@ func LoneAnonymousOperationRule(context *ValidationContext) *ValidationRuleInsta Kind: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.OperationDefinition); ok { if node.Name == nil && operationCount > 1 { - return reportErrorAndReturn( + return reportError( context, `This anonymous operation must be the only defined operation.`, []ast.Node{node}, @@ -613,11 +613,11 @@ func NoFragmentCyclesRule(context *ValidationContext) *ValidationRuleInstance { if len(spreadNames) > 0 { via = " via " + strings.Join(spreadNames, ", ") } - err := newValidationError( + reportError( + context, fmt.Sprintf(`Cannot spread fragment "%v" within itself%v.`, initialName, via), cyclePath, ) - context.ReportError(err) continue } spreadPathHasCurrentNode := false @@ -654,77 +654,64 @@ func NoFragmentCyclesRule(context *ValidationContext) *ValidationRuleInstance { * and via fragment spreads, are defined by that operation. */ func NoUndefinedVariablesRule(context *ValidationContext) *ValidationRuleInstance { - var operation *ast.OperationDefinition - var visitedFragmentNames = map[string]bool{} - var definedVariableNames = map[string]bool{} + var variableNameDefined = map[string]bool{} + visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ kinds.OperationDefinition: visitor.NamedVisitFuncs{ - Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - if node, ok := p.Node.(*ast.OperationDefinition); ok && node != nil { - operation = node - visitedFragmentNames = map[string]bool{} - definedVariableNames = map[string]bool{} - } - return visitor.ActionNoChange, nil - }, - }, - kinds.VariableDefinition: visitor.NamedVisitFuncs{ - Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - if node, ok := p.Node.(*ast.VariableDefinition); ok && node != nil { - variableName := "" - if node.Variable != nil && node.Variable.Name != nil { - variableName = node.Variable.Name.Value - } - definedVariableNames[variableName] = true - } + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + variableNameDefined = map[string]bool{} return visitor.ActionNoChange, nil }, - }, - kinds.Variable: visitor.NamedVisitFuncs{ - Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - if variable, ok := p.Node.(*ast.Variable); ok && variable != nil { - variableName := "" - if variable.Name != nil { - variableName = variable.Name.Value - } - if val, _ := definedVariableNames[variableName]; !val { - withinFragment := false - for _, node := range p.Ancestors { - if node.GetKind() == kinds.FragmentDefinition { - withinFragment = true - break - } + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + if operation, ok := p.Node.(*ast.OperationDefinition); ok && operation != nil { + usages := context.RecursiveVariableUsages(operation) + + for _, usage := range usages { + if usage == nil { + continue } - if withinFragment == true && operation != nil && operation.Name != nil { - return reportErrorAndReturn( - context, - fmt.Sprintf(`Variable "$%v" is not defined by operation "%v".`, variableName, operation.Name.Value), - []ast.Node{variable, operation}, - ) + if usage.Node == nil { + continue + } + varName := "" + if usage.Node.Name != nil { + varName = usage.Node.Name.Value + } + opName := "" + if operation.Name != nil { + opName = operation.Name.Value + } + if res, ok := variableNameDefined[varName]; !ok || !res { + if opName != "" { + reportError( + context, + fmt.Sprintf(`Variable "$%v" is not defined by operation "%v".`, varName, opName), + []ast.Node{usage.Node, operation}, + ) + } else { + + reportError( + context, + fmt.Sprintf(`Variable "$%v" is not defined.`, varName), + []ast.Node{usage.Node, operation}, + ) + } } - return reportErrorAndReturn( - context, - fmt.Sprintf(`Variable "$%v" is not defined.`, variableName), - []ast.Node{variable}, - ) } } return visitor.ActionNoChange, nil }, }, - kinds.FragmentSpread: visitor.NamedVisitFuncs{ + kinds.VariableDefinition: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - if node, ok := p.Node.(*ast.FragmentSpread); ok && node != nil { - // Only visit fragments of a particular name once per operation - fragmentName := "" - if node.Name != nil { - fragmentName = node.Name.Value - } - if val, ok := visitedFragmentNames[fragmentName]; ok && val == true { - return visitor.ActionSkip, nil + if node, ok := p.Node.(*ast.VariableDefinition); ok && node != nil { + variableName := "" + if node.Variable != nil && node.Variable.Name != nil { + variableName = node.Variable.Name.Value } - visitedFragmentNames[fragmentName] = true + // definedVariableNames[variableName] = true + variableNameDefined[variableName] = true } return visitor.ActionNoChange, nil }, @@ -817,11 +804,11 @@ func NoUnusedFragmentsRule(context *ValidationContext) *ValidationRuleInstance { isFragNameUsed, ok := fragmentNameUsed[defName] if !ok || isFragNameUsed != true { - err := newValidationError( + reportError( + context, fmt.Sprintf(`Fragment "%v" is never used.`, defName), []ast.Node{def}, ) - context.ReportError(err) } } return visitor.ActionNoChange, nil @@ -843,33 +830,45 @@ func NoUnusedFragmentsRule(context *ValidationContext) *ValidationRuleInstance { */ func NoUnusedVariablesRule(context *ValidationContext) *ValidationRuleInstance { - var visitedFragmentNames = map[string]bool{} var variableDefs = []*ast.VariableDefinition{} - var variableNameUsed = map[string]bool{} visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ kinds.OperationDefinition: visitor.NamedVisitFuncs{ Enter: func(p visitor.VisitFuncParams) (string, interface{}) { - visitedFragmentNames = map[string]bool{} variableDefs = []*ast.VariableDefinition{} - variableNameUsed = map[string]bool{} return visitor.ActionNoChange, nil }, Leave: func(p visitor.VisitFuncParams) (string, interface{}) { - for _, def := range variableDefs { - variableName := "" - if def.Variable != nil && def.Variable.Name != nil { - variableName = def.Variable.Name.Value + if operation, ok := p.Node.(*ast.OperationDefinition); ok && operation != nil { + variableNameUsed := map[string]bool{} + usages := context.RecursiveVariableUsages(operation) + + for _, usage := range usages { + varName := "" + if usage != nil && usage.Node != nil && usage.Node.Name != nil { + varName = usage.Node.Name.Value + } + if varName != "" { + variableNameUsed[varName] = true + } } - if isVariableNameUsed, _ := variableNameUsed[variableName]; isVariableNameUsed != true { - err := newValidationError( - fmt.Sprintf(`Variable "$%v" is never used.`, variableName), - []ast.Node{def}, - ) - context.ReportError(err) + for _, variableDef := range variableDefs { + variableName := "" + if variableDef != nil && variableDef.Variable != nil && variableDef.Variable.Name != nil { + variableName = variableDef.Variable.Name.Value + } + if res, ok := variableNameUsed[variableName]; !ok || !res { + reportError( + context, + fmt.Sprintf(`Variable "$%v" is never used.`, variableName), + []ast.Node{variableDef}, + ) + } } + } + return visitor.ActionNoChange, nil }, }, @@ -878,33 +877,6 @@ func NoUnusedVariablesRule(context *ValidationContext) *ValidationRuleInstance { if def, ok := p.Node.(*ast.VariableDefinition); ok && def != nil { variableDefs = append(variableDefs, def) } - // Do not visit deeper, or else the defined variable name will be visited. - return visitor.ActionSkip, nil - }, - }, - kinds.Variable: visitor.NamedVisitFuncs{ - Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - if variable, ok := p.Node.(*ast.Variable); ok && variable != nil { - if variable.Name != nil { - variableNameUsed[variable.Name.Value] = true - } - } - return visitor.ActionNoChange, nil - }, - }, - kinds.FragmentSpread: visitor.NamedVisitFuncs{ - Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - if spreadAST, ok := p.Node.(*ast.FragmentSpread); ok && spreadAST != nil { - // Only visit fragments of a particular name once per operation - spreadName := "" - if spreadAST.Name != nil { - spreadName = spreadAST.Name.Value - } - if hasVisitedFragmentNames, _ := visitedFragmentNames[spreadName]; hasVisitedFragmentNames == true { - return visitor.ActionSkip, nil - } - visitedFragmentNames[spreadName] = true - } return visitor.ActionNoChange, nil }, }, @@ -1301,7 +1273,8 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul for _, c := range conflicts { responseName := c.Reason.Name reason := c.Reason - err := newValidationError( + reportError( + context, fmt.Sprintf( `Fields "%v" conflict because %v.`, responseName, @@ -1309,7 +1282,6 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul ), c.Fields, ) - context.ReportError(err) } return visitor.ActionNoChange, nil } @@ -1394,7 +1366,7 @@ func PossibleFragmentSpreadsRule(context *ValidationContext) *ValidationRuleInst parentType, _ := context.ParentType().(Type) if fragType != nil && parentType != nil && !doTypesOverlap(fragType, parentType) { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Fragment cannot be spread here as objects of `+ `type "%v" can never be of type "%v".`, parentType, fragType), @@ -1415,7 +1387,7 @@ func PossibleFragmentSpreadsRule(context *ValidationContext) *ValidationRuleInst fragType := getFragmentType(context, fragName) parentType, _ := context.ParentType().(Type) if fragType != nil && parentType != nil && !doTypesOverlap(fragType, parentType) { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Fragment "%v" cannot be spread here as objects of `+ `type "%v" can never be of type "%v".`, fragName, parentType, fragType), @@ -1471,12 +1443,12 @@ func ProvidedNonNullArgumentsRule(context *ValidationContext) *ValidationRuleIns if fieldAST.Name != nil { fieldName = fieldAST.Name.Value } - err := newValidationError( + reportError( + context, fmt.Sprintf(`Field "%v" argument "%v" of type "%v" `+ `is required but not provided.`, fieldName, argDef.Name(), argDefType), []ast.Node{fieldAST}, ) - context.ReportError(err) } } } @@ -1512,12 +1484,12 @@ func ProvidedNonNullArgumentsRule(context *ValidationContext) *ValidationRuleIns if directiveAST.Name != nil { directiveName = directiveAST.Name.Value } - err := newValidationError( + reportError( + context, fmt.Sprintf(`Directive "@%v" argument "%v" of type `+ `"%v" is required but not provided.`, directiveName, argDef.Name(), argDefType), []ast.Node{directiveAST}, ) - context.ReportError(err) } } } @@ -1554,14 +1526,14 @@ func ScalarLeafsRule(context *ValidationContext) *ValidationRuleInstance { if ttype != nil { if IsLeafType(ttype) { if node.SelectionSet != nil { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Field "%v" of type "%v" must not have a sub selection.`, nodeName, ttype), []ast.Node{node.SelectionSet}, ) } } else if node.SelectionSet == nil { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Field "%v" of type "%v" must have a sub selection.`, nodeName, ttype), []ast.Node{node}, @@ -1611,7 +1583,7 @@ func UniqueArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance argName = node.Name.Value } if nameAST, ok := knownArgNames[argName]; ok { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`There can be only one argument named "%v".`, argName), []ast.Node{nameAST, node.Name}, @@ -1648,7 +1620,7 @@ func UniqueFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance fragmentName = node.Name.Value } if nameAST, ok := knownFragmentNames[fragmentName]; ok { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`There can only be one fragment named "%v".`, fragmentName), []ast.Node{nameAST, node.Name}, @@ -1700,7 +1672,7 @@ func UniqueInputFieldNamesRule(context *ValidationContext) *ValidationRuleInstan fieldName = node.Name.Value } if knownNameAST, ok := knownNames[fieldName]; ok { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`There can be only one input field named "%v".`, fieldName), []ast.Node{knownNameAST, node.Name}, @@ -1739,7 +1711,7 @@ func UniqueOperationNamesRule(context *ValidationContext) *ValidationRuleInstanc operationName = node.Name.Value } if nameAST, ok := knownOperationNames[operationName]; ok { - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`There can only be one operation named "%v".`, operationName), []ast.Node{nameAST, node.Name}, @@ -1779,7 +1751,7 @@ func VariablesAreInputTypesRule(context *ValidationContext) *ValidationRuleInsta if node.Variable != nil && node.Variable.Name != nil { variableName = node.Variable.Name.Value } - return reportErrorAndReturn( + return reportError( context, fmt.Sprintf(`Variable "$%v" cannot be non-input type "%v".`, variableName, printer.Print(node.Type)), @@ -1837,14 +1809,45 @@ func varTypeAllowedForType(varType Type, expectedType Type) bool { func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleInstance { varDefMap := map[string]*ast.VariableDefinition{} - visitedFragmentNames := map[string]bool{} visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ kinds.OperationDefinition: visitor.NamedVisitFuncs{ - Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { varDefMap = map[string]*ast.VariableDefinition{} - visitedFragmentNames = map[string]bool{} + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + if operation, ok := p.Node.(*ast.OperationDefinition); ok { + + usages := context.RecursiveVariableUsages(operation) + for _, usage := range usages { + varName := "" + if usage != nil && usage.Node != nil && usage.Node.Name != nil { + varName = usage.Node.Name.Value + } + var varType Type + varDef, ok := varDefMap[varName] + if ok { + var err error + varType, err = typeFromAST(*context.Schema(), varDef.Type) + if err != nil { + varType = nil + } + } + if varType != nil && + usage.Type != nil && + !varTypeAllowedForType(effectiveType(varType, varDef), usage.Type) { + reportError( + context, + fmt.Sprintf(`Variable "$%v" of type "%v" used in position `+ + `expecting type "%v".`, varName, varType, usage.Type), + []ast.Node{usage.Node}, + ) + } + } + + } return visitor.ActionNoChange, nil }, }, @@ -1855,47 +1858,8 @@ func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleI if varDefAST.Variable != nil && varDefAST.Variable.Name != nil { defName = varDefAST.Variable.Name.Value } - varDefMap[defName] = varDefAST - } - return visitor.ActionNoChange, nil - }, - }, - kinds.FragmentSpread: visitor.NamedVisitFuncs{ - Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - // Only visit fragments of a particular name once per operation - if spreadAST, ok := p.Node.(*ast.FragmentSpread); ok { - spreadName := "" - if spreadAST.Name != nil { - spreadName = spreadAST.Name.Value - } - if hasVisited, _ := visitedFragmentNames[spreadName]; hasVisited { - return visitor.ActionSkip, nil - } - visitedFragmentNames[spreadName] = true - } - return visitor.ActionNoChange, nil - }, - }, - kinds.Variable: visitor.NamedVisitFuncs{ - Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - if variableAST, ok := p.Node.(*ast.Variable); ok && variableAST != nil { - varName := "" - if variableAST.Name != nil { - varName = variableAST.Name.Value - } - varDef, _ := varDefMap[varName] - var varType Type - if varDef != nil { - varType, _ = typeFromAST(*context.Schema(), varDef.Type) - } - inputType := context.InputType() - if varType != nil && inputType != nil && !varTypeAllowedForType(effectiveType(varType, varDef), inputType) { - return reportErrorAndReturn( - context, - fmt.Sprintf(`Variable "$%v" of type "%v" used in position `+ - `expecting type "%v".`, varName, varType, inputType), - []ast.Node{variableAST}, - ) + if defName != "" { + varDefMap[defName] = varDefAST } } return visitor.ActionNoChange, nil diff --git a/rules_no_undefined_variables_test.go b/rules_no_undefined_variables_test.go index 64449842..0b253715 100644 --- a/rules_no_undefined_variables_test.go +++ b/rules_no_undefined_variables_test.go @@ -108,7 +108,7 @@ func TestValidate_NoUndefinedVariables_VariableNotDefined(t *testing.T) { field(a: $a, b: $b, c: $c, d: $d) } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$d" is not defined.`, 3, 39), + testutil.RuleError(`Variable "$d" is not defined by operation "Foo".`, 3, 39, 2, 7), }) } func TestValidate_NoUndefinedVariables_VariableNotDefinedByUnnamedQuery(t *testing.T) { @@ -117,7 +117,7 @@ func TestValidate_NoUndefinedVariables_VariableNotDefinedByUnnamedQuery(t *testi field(a: $a) } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$a" is not defined.`, 3, 18), + testutil.RuleError(`Variable "$a" is not defined.`, 3, 18, 2, 7), }) } func TestValidate_NoUndefinedVariables_MultipleVariablesNotDefined(t *testing.T) { @@ -126,8 +126,8 @@ func TestValidate_NoUndefinedVariables_MultipleVariablesNotDefined(t *testing.T) field(a: $a, b: $b, c: $c) } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$a" is not defined.`, 3, 18), - testutil.RuleError(`Variable "$c" is not defined.`, 3, 32), + testutil.RuleError(`Variable "$a" is not defined by operation "Foo".`, 3, 18, 2, 7), + testutil.RuleError(`Variable "$c" is not defined by operation "Foo".`, 3, 32, 2, 7), }) } func TestValidate_NoUndefinedVariables_VariableInFragmentNotDefinedByUnnamedQuery(t *testing.T) { @@ -139,7 +139,7 @@ func TestValidate_NoUndefinedVariables_VariableInFragmentNotDefinedByUnnamedQuer field(a: $a) } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$a" is not defined.`, 6, 18), + testutil.RuleError(`Variable "$a" is not defined.`, 6, 18, 2, 7), }) } func TestValidate_NoUndefinedVariables_VariableInFragmentNotDefinedByOperation(t *testing.T) { diff --git a/type_info.go b/type_info.go index a26825e3..3c6dd2e2 100644 --- a/type_info.go +++ b/type_info.go @@ -173,12 +173,18 @@ func (ti *TypeInfo) Leave(node ast.Node) { switch kind { case kinds.SelectionSet: // pop ti.parentTypeStack - _, ti.parentTypeStack = ti.parentTypeStack[len(ti.parentTypeStack)-1], ti.parentTypeStack[:len(ti.parentTypeStack)-1] + if len(ti.parentTypeStack) > 0 { + _, ti.parentTypeStack = ti.parentTypeStack[len(ti.parentTypeStack)-1], ti.parentTypeStack[:len(ti.parentTypeStack)-1] + } case kinds.Field: // pop ti.fieldDefStack - _, ti.fieldDefStack = ti.fieldDefStack[len(ti.fieldDefStack)-1], ti.fieldDefStack[:len(ti.fieldDefStack)-1] + if len(ti.fieldDefStack) > 0 { + _, ti.fieldDefStack = ti.fieldDefStack[len(ti.fieldDefStack)-1], ti.fieldDefStack[:len(ti.fieldDefStack)-1] + } // pop ti.typeStack - _, ti.typeStack = ti.typeStack[len(ti.typeStack)-1], ti.typeStack[:len(ti.typeStack)-1] + if len(ti.typeStack) > 0 { + _, ti.typeStack = ti.typeStack[len(ti.typeStack)-1], ti.typeStack[:len(ti.typeStack)-1] + } case kinds.Directive: ti.directive = nil case kinds.OperationDefinition: @@ -187,19 +193,27 @@ func (ti *TypeInfo) Leave(node ast.Node) { fallthrough case kinds.FragmentDefinition: // pop ti.typeStack - _, ti.typeStack = ti.typeStack[len(ti.typeStack)-1], ti.typeStack[:len(ti.typeStack)-1] + if len(ti.typeStack) > 0 { + _, ti.typeStack = ti.typeStack[len(ti.typeStack)-1], ti.typeStack[:len(ti.typeStack)-1] + } case kinds.VariableDefinition: // pop ti.inputTypeStack - _, ti.inputTypeStack = ti.inputTypeStack[len(ti.inputTypeStack)-1], ti.inputTypeStack[:len(ti.inputTypeStack)-1] + if len(ti.inputTypeStack) > 0 { + _, ti.inputTypeStack = ti.inputTypeStack[len(ti.inputTypeStack)-1], ti.inputTypeStack[:len(ti.inputTypeStack)-1] + } case kinds.Argument: ti.argument = nil // pop ti.inputTypeStack - _, ti.inputTypeStack = ti.inputTypeStack[len(ti.inputTypeStack)-1], ti.inputTypeStack[:len(ti.inputTypeStack)-1] + if len(ti.inputTypeStack) > 0 { + _, ti.inputTypeStack = ti.inputTypeStack[len(ti.inputTypeStack)-1], ti.inputTypeStack[:len(ti.inputTypeStack)-1] + } case kinds.ListValue: fallthrough case kinds.ObjectField: // pop ti.inputTypeStack - _, ti.inputTypeStack = ti.inputTypeStack[len(ti.inputTypeStack)-1], ti.inputTypeStack[:len(ti.inputTypeStack)-1] + if len(ti.inputTypeStack) > 0 { + _, ti.inputTypeStack = ti.inputTypeStack[len(ti.inputTypeStack)-1], ti.inputTypeStack[:len(ti.inputTypeStack)-1] + } } } diff --git a/validator.go b/validator.go index 4bb0790f..b3714f09 100644 --- a/validator.go +++ b/validator.go @@ -34,6 +34,22 @@ func ValidateDocument(schema *Schema, astDoc *ast.Document, rules []ValidationRu } func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRuleFn) []gqlerrors.FormattedError { + + typeInfo := NewTypeInfo(schema) + context := NewValidationContext(schema, astDoc, typeInfo) + visitors := []*visitor.VisitorOptions{} + + for _, rule := range rules { + instance := rule(context) + visitors = append(visitors, instance.VisitorOpts) + } + + // Visit the whole document with each instance of all provided rules. + visitor.Visit(astDoc, visitor.VisitWithTypeInfo(typeInfo, visitor.VisitInParallel(visitors)), nil) + return context.Errors() +} + +func visitUsingRulesOld(schema *Schema, astDoc *ast.Document, rules []ValidationRuleFn) []gqlerrors.FormattedError { typeInfo := NewTypeInfo(schema) context := NewValidationContext(schema, astDoc, typeInfo) @@ -61,7 +77,7 @@ func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRul // Get the visitor function from the validation instance, and if it // exists, call it with the visitor arguments. - enterFn := visitor.GetVisitFn(instance.VisitorOpts, false, kind) + enterFn := visitor.GetVisitFn(instance.VisitorOpts, kind, false) if enterFn != nil { action, result = enterFn(p) } @@ -102,7 +118,7 @@ func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRul // Get the visitor function from the validation instance, and if it // exists, call it with the visitor arguments. - leaveFn := visitor.GetVisitFn(instance.VisitorOpts, true, kind) + leaveFn := visitor.GetVisitFn(instance.VisitorOpts, kind, true) if leaveFn != nil { action, result = leaveFn(p) } @@ -126,19 +142,42 @@ func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRul return context.Errors() } +type HasSelectionSet interface { + GetKind() string + GetLoc() *ast.Location + GetSelectionSet() *ast.SelectionSet +} + +var _ HasSelectionSet = (*ast.OperationDefinition)(nil) +var _ HasSelectionSet = (*ast.FragmentDefinition)(nil) + +type VariableUsage struct { + Node *ast.Variable + Type Input +} + type ValidationContext struct { - schema *Schema - astDoc *ast.Document - typeInfo *TypeInfo - fragments map[string]*ast.FragmentDefinition - errors []gqlerrors.FormattedError + schema *Schema + astDoc *ast.Document + typeInfo *TypeInfo + errors []gqlerrors.FormattedError + fragments map[string]*ast.FragmentDefinition + variableUsages map[HasSelectionSet][]*VariableUsage + recursiveVariableUsages map[*ast.OperationDefinition][]*VariableUsage + recursivelyReferencedFragments map[*ast.OperationDefinition][]*ast.FragmentDefinition + fragmentSpreads map[HasSelectionSet][]*ast.FragmentSpread } func NewValidationContext(schema *Schema, astDoc *ast.Document, typeInfo *TypeInfo) *ValidationContext { return &ValidationContext{ - schema: schema, - astDoc: astDoc, - typeInfo: typeInfo, + schema: schema, + astDoc: astDoc, + typeInfo: typeInfo, + fragments: map[string]*ast.FragmentDefinition{}, + variableUsages: map[HasSelectionSet][]*VariableUsage{}, + recursiveVariableUsages: map[*ast.OperationDefinition][]*VariableUsage{}, + recursivelyReferencedFragments: map[*ast.OperationDefinition][]*ast.FragmentDefinition{}, + fragmentSpreads: map[HasSelectionSet][]*ast.FragmentSpread{}, } } @@ -177,7 +216,126 @@ func (ctx *ValidationContext) Fragment(name string) *ast.FragmentDefinition { f, _ := ctx.fragments[name] return f } +func (ctx *ValidationContext) FragmentSpreads(node HasSelectionSet) []*ast.FragmentSpread { + if spreads, ok := ctx.fragmentSpreads[node]; ok && spreads != nil { + return spreads + } + + spreads := []*ast.FragmentSpread{} + setsToVisit := []*ast.SelectionSet{node.GetSelectionSet()} + + for { + if len(setsToVisit) == 0 { + break + } + var set *ast.SelectionSet + // pop + set, setsToVisit = setsToVisit[len(setsToVisit)-1], setsToVisit[:len(setsToVisit)-1] + if set.Selections != nil { + for _, selection := range set.Selections { + switch selection := selection.(type) { + case *ast.FragmentSpread: + spreads = append(spreads, selection) + case *ast.Field: + if selection.SelectionSet != nil { + setsToVisit = append(setsToVisit, selection.SelectionSet) + } + case *ast.InlineFragment: + if selection.SelectionSet != nil { + setsToVisit = append(setsToVisit, selection.SelectionSet) + } + } + } + } + ctx.fragmentSpreads[node] = spreads + } + return spreads +} +func (ctx *ValidationContext) RecursivelyReferencedFragments(operation *ast.OperationDefinition) []*ast.FragmentDefinition { + if fragments, ok := ctx.recursivelyReferencedFragments[operation]; ok && fragments != nil { + return fragments + } + + fragments := []*ast.FragmentDefinition{} + collectedNames := map[string]bool{} + nodesToVisit := []HasSelectionSet{operation} + + for { + if len(nodesToVisit) == 0 { + break + } + + var node HasSelectionSet + + node, nodesToVisit = nodesToVisit[len(nodesToVisit)-1], nodesToVisit[:len(nodesToVisit)-1] + spreads := ctx.FragmentSpreads(node) + for _, spread := range spreads { + fragName := "" + if spread.Name != nil { + fragName = spread.Name.Value + } + if res, ok := collectedNames[fragName]; !ok || !res { + collectedNames[fragName] = true + fragment := ctx.Fragment(fragName) + if fragment != nil { + fragments = append(fragments, fragment) + nodesToVisit = append(nodesToVisit, fragment) + } + } + + } + } + + ctx.recursivelyReferencedFragments[operation] = fragments + return fragments +} +func (ctx *ValidationContext) VariableUsages(node HasSelectionSet) []*VariableUsage { + if usages, ok := ctx.variableUsages[node]; ok && usages != nil { + return usages + } + usages := []*VariableUsage{} + typeInfo := NewTypeInfo(ctx.schema) + + visitor.Visit(node, visitor.VisitWithTypeInfo(typeInfo, &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.VariableDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + return visitor.ActionSkip, nil + }, + }, + kinds.Variable: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.Variable); ok && node != nil { + usages = append(usages, &VariableUsage{ + Node: node, + Type: typeInfo.InputType(), + }) + } + return visitor.ActionNoChange, nil + }, + }, + }, + }), nil) + + ctx.variableUsages[node] = usages + return usages +} +func (ctx *ValidationContext) RecursiveVariableUsages(operation *ast.OperationDefinition) []*VariableUsage { + if usages, ok := ctx.recursiveVariableUsages[operation]; ok && usages != nil { + return usages + } + usages := ctx.VariableUsages(operation) + + fragments := ctx.RecursivelyReferencedFragments(operation) + for _, fragment := range fragments { + fragmentUsages := ctx.VariableUsages(fragment) + usages = append(usages, fragmentUsages...) + } + + ctx.recursiveVariableUsages[operation] = usages + return usages +} func (ctx *ValidationContext) Type() Output { return ctx.typeInfo.Type() } From 64019fdbd62f05a6c7e062125e04169e7ab09c7a Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Thu, 10 Mar 2016 22:20:42 +0800 Subject: [PATCH 17/69] Removed commented section --- language/visitor/visitor.go | 82 ------------------------------------- 1 file changed, 82 deletions(-) diff --git a/language/visitor/visitor.go b/language/visitor/visitor.go index 3188efec..459d4a99 100644 --- a/language/visitor/visitor.go +++ b/language/visitor/visitor.go @@ -844,85 +844,3 @@ func GetVisitFn(visitorOpts *VisitorOptions, kind string, isLeaving bool) VisitF return nil } - -///// DELETE //// - -func GetVisitFnOld(visitorOpts *VisitorOptions, isLeaving bool, kind string) VisitFunc { - if visitorOpts == nil { - return nil - } - kindVisitor, ok := visitorOpts.KindFuncMap[kind] - if ok { - if !isLeaving && kindVisitor.Kind != nil { - // { Kind() {} } - return kindVisitor.Kind - } - if isLeaving { - // { Kind: { leave() {} } } - return kindVisitor.Leave - } else { - // { Kind: { enter() {} } } - return kindVisitor.Enter - } - } - - if isLeaving { - // { enter() {} } - specificVisitor := visitorOpts.Leave - if specificVisitor != nil { - return specificVisitor - } - if specificKindVisitor, ok := visitorOpts.LeaveKindMap[kind]; ok { - // { leave: { Kind() {} } } - return specificKindVisitor - } - - } else { - // { leave() {} } - specificVisitor := visitorOpts.Enter - if specificVisitor != nil { - return specificVisitor - } - if specificKindVisitor, ok := visitorOpts.EnterKindMap[kind]; ok { - // { enter: { Kind() {} } } - return specificKindVisitor - } - } - - return nil -} - -/* - - -export function getVisitFn(visitor, isLeaving, kind) { - var kindVisitor = visitor[kind]; - if (kindVisitor) { - if (!isLeaving && typeof kindVisitor === 'function') { - // { Kind() {} } - return kindVisitor; - } - var kindSpecificVisitor = isLeaving ? kindVisitor.leave : kindVisitor.enter; - if (typeof kindSpecificVisitor === 'function') { - // { Kind: { enter() {}, leave() {} } } - return kindSpecificVisitor; - } - return; - } - var specificVisitor = isLeaving ? visitor.leave : visitor.enter; - if (specificVisitor) { - if (typeof specificVisitor === 'function') { - // { enter() {}, leave() {} } - return specificVisitor; - } - var specificKindVisitor = specificVisitor[kind]; - if (typeof specificKindVisitor === 'function') { - // { enter: { Kind() {} }, leave: { Kind() {} } } - return specificKindVisitor; - } - } -} - - - -*/ From 313e3069dd8966f0b23d6c9eca6cc4579f0bb6fa Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Thu, 10 Mar 2016 22:40:42 +0800 Subject: [PATCH 18/69] Fix issue with skipping subtrees in parallel visitor Fixes #254 Commit: 699dc10eeea70a8a341c68ea1a34dc0f2f8a6906 [699dc10] Parents: 3a6d35b59f Author: Lee Byron Date: 1 December 2015 at 11:31:52 AM SGT Commit Date: 1 December 2015 at 11:31:54 AM SGT --- language/visitor/visitor.go | 5 +- language/visitor/visitor_test.go | 177 +++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 2 deletions(-) diff --git a/language/visitor/visitor.go b/language/visitor/visitor.go index 459d4a99..15a88d59 100644 --- a/language/visitor/visitor.go +++ b/language/visitor/visitor.go @@ -741,7 +741,8 @@ func VisitInParallel(visitorOptsSlice []*VisitorOptions) *VisitorOptions { }, Leave: func(p VisitFuncParams) (string, interface{}) { for i, visitorOpts := range visitorOptsSlice { - if _, ok := skipping[i]; !ok { + skippedNode, ok := skipping[i] + if !ok { switch node := p.Node.(type) { case ast.Node: kind := node.GetKind() @@ -750,7 +751,7 @@ func VisitInParallel(visitorOptsSlice []*VisitorOptions) *VisitorOptions { fn(p) } } - } else { + } else if skippedNode == p.Node { delete(skipping, i) } } diff --git a/language/visitor/visitor_test.go b/language/visitor/visitor_test.go index 8c198cdf..3f967639 100644 --- a/language/visitor/visitor_test.go +++ b/language/visitor/visitor_test.go @@ -573,3 +573,180 @@ func TestVisitor_VisitsKitchenSink(t *testing.T) { t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) } } + +func TestVisitor_VisitInParallel_AllowsSkippingASubTree(t *testing.T) { + + // Note: nearly identical to the above test of the same test but + // using visitInParallel. + + query := `{ a, b { x }, c }` + astDoc := parse(t, query) + + visited := []interface{}{} + expectedVisited := []interface{}{ + []interface{}{"enter", "Document", nil}, + []interface{}{"enter", "OperationDefinition", nil}, + []interface{}{"enter", "SelectionSet", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "a"}, + []interface{}{"leave", "Name", "a"}, + []interface{}{"leave", "Field", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "c"}, + []interface{}{"leave", "Name", "c"}, + []interface{}{"leave", "Field", nil}, + []interface{}{"leave", "SelectionSet", nil}, + []interface{}{"leave", "OperationDefinition", nil}, + []interface{}{"leave", "Document", nil}, + } + + v := []*visitor.VisitorOptions{ + &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value}) + case *ast.Field: + visited = append(visited, []interface{}{"enter", node.Kind, nil}) + if node.Name != nil && node.Name.Value == "b" { + return visitor.ActionSkip, nil + } + case ast.Node: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + }, + } + + _ = visitor.Visit(astDoc, visitor.VisitInParallel(v), nil) + + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) + } +} + +func TestVisitor_VisitInParallel_AllowsSkippingDifferentSubTrees(t *testing.T) { + + query := `{ a { x }, b { y} }` + astDoc := parse(t, query) + + visited := []interface{}{} + expectedVisited := []interface{}{ + []interface{}{"no-a", "enter", "Document", nil}, + []interface{}{"no-b", "enter", "Document", nil}, + []interface{}{"no-a", "enter", "OperationDefinition", nil}, + []interface{}{"no-b", "enter", "OperationDefinition", nil}, + []interface{}{"no-a", "enter", "SelectionSet", nil}, + []interface{}{"no-b", "enter", "SelectionSet", nil}, + []interface{}{"no-a", "enter", "Field", nil}, + []interface{}{"no-b", "enter", "Field", nil}, + []interface{}{"no-b", "enter", "Name", "a"}, + []interface{}{"no-b", "leave", "Name", "a"}, + []interface{}{"no-b", "enter", "SelectionSet", nil}, + []interface{}{"no-b", "enter", "Field", nil}, + []interface{}{"no-b", "enter", "Name", "x"}, + []interface{}{"no-b", "leave", "Name", "x"}, + []interface{}{"no-b", "leave", "Field", nil}, + []interface{}{"no-b", "leave", "SelectionSet", nil}, + []interface{}{"no-b", "leave", "Field", nil}, + []interface{}{"no-a", "enter", "Field", nil}, + []interface{}{"no-b", "enter", "Field", nil}, + []interface{}{"no-a", "enter", "Name", "b"}, + []interface{}{"no-a", "leave", "Name", "b"}, + []interface{}{"no-a", "enter", "SelectionSet", nil}, + []interface{}{"no-a", "enter", "Field", nil}, + []interface{}{"no-a", "enter", "Name", "y"}, + []interface{}{"no-a", "leave", "Name", "y"}, + []interface{}{"no-a", "leave", "Field", nil}, + []interface{}{"no-a", "leave", "SelectionSet", nil}, + []interface{}{"no-a", "leave", "Field", nil}, + []interface{}{"no-a", "leave", "SelectionSet", nil}, + []interface{}{"no-b", "leave", "SelectionSet", nil}, + []interface{}{"no-a", "leave", "OperationDefinition", nil}, + []interface{}{"no-b", "leave", "OperationDefinition", nil}, + []interface{}{"no-a", "leave", "Document", nil}, + []interface{}{"no-b", "leave", "Document", nil}, + } + + v := []*visitor.VisitorOptions{ + &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"no-a", "enter", node.Kind, node.Value}) + case *ast.Field: + visited = append(visited, []interface{}{"no-a", "enter", node.Kind, nil}) + if node.Name != nil && node.Name.Value == "a" { + return visitor.ActionSkip, nil + } + case ast.Node: + visited = append(visited, []interface{}{"no-a", "enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"no-a", "enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"no-a", "leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"no-a", "leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"no-a", "leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + }, + &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"no-b", "enter", node.Kind, node.Value}) + case *ast.Field: + visited = append(visited, []interface{}{"no-b", "enter", node.Kind, nil}) + if node.Name != nil && node.Name.Value == "b" { + return visitor.ActionSkip, nil + } + case ast.Node: + visited = append(visited, []interface{}{"no-b", "enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"no-b", "enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"no-b", "leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"no-b", "leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"no-b", "leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + }, + } + + _ = visitor.Visit(astDoc, visitor.VisitInParallel(v), nil) + + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) + } +} From 77963312ed1391e1851187da02afed66a01c9a28 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Fri, 11 Mar 2016 07:37:51 +0800 Subject: [PATCH 19/69] Support breaking in parallel visitor Commit: b2bdae08cfcb1dd7ee78fff156bcdfbf09b2cd47 [b2bdae0] Parents: 699dc10eee Author: Lee Byron Date: 1 December 2015 at 11:46:35 AM SGT Commit Date: 1 December 2015 at 11:53:03 AM SGT Labels: HEAD ----- Updated `visitor.VisitInParallel` to accept variadic arg --- language/visitor/visitor.go | 11 +- language/visitor/visitor_test.go | 439 +++++++++++++++++++++++++++++-- validator.go | 2 +- 3 files changed, 420 insertions(+), 32 deletions(-) diff --git a/language/visitor/visitor.go b/language/visitor/visitor.go index 15a88d59..b875d6ea 100644 --- a/language/visitor/visitor.go +++ b/language/visitor/visitor.go @@ -715,9 +715,9 @@ func isNilNode(node interface{}) bool { * parallel. Each visitor will be visited for each node before moving on. * * Visitors must not directly modify the AST nodes and only returning false to - * skip sub-branches is supported. + * skip sub-branches or BREAK to exit early is supported. */ -func VisitInParallel(visitorOptsSlice []*VisitorOptions) *VisitorOptions { +func VisitInParallel(visitorOptsSlice ...*VisitorOptions) *VisitorOptions { skipping := map[int]interface{}{} return &VisitorOptions{ @@ -732,6 +732,8 @@ func VisitInParallel(visitorOptsSlice []*VisitorOptions) *VisitorOptions { action, _ := fn(p) if action == ActionSkip { skipping[i] = node + } else if action == ActionBreak { + skipping[i] = ActionBreak } } } @@ -748,7 +750,10 @@ func VisitInParallel(visitorOptsSlice []*VisitorOptions) *VisitorOptions { kind := node.GetKind() fn := GetVisitFn(visitorOpts, kind, true) if fn != nil { - fn(p) + action, _ := fn(p) + if action == ActionBreak { + skipping[i] = ActionBreak + } } } } else if skippedNode == p.Node { diff --git a/language/visitor/visitor_test.go b/language/visitor/visitor_test.go index 3f967639..a1c9bef3 100644 --- a/language/visitor/visitor_test.go +++ b/language/visitor/visitor_test.go @@ -234,6 +234,65 @@ func TestVisitor_AllowsEarlyExitWhileVisiting(t *testing.T) { } } +func TestVisitor_AllowsEarlyExitWhileLeaving(t *testing.T) { + + visited := []interface{}{} + + query := `{ a, b { x }, c }` + astDoc := parse(t, query) + + expectedVisited := []interface{}{ + []interface{}{"enter", "Document", nil}, + []interface{}{"enter", "OperationDefinition", nil}, + []interface{}{"enter", "SelectionSet", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "a"}, + []interface{}{"leave", "Name", "a"}, + []interface{}{"leave", "Field", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "b"}, + []interface{}{"leave", "Name", "b"}, + []interface{}{"enter", "SelectionSet", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "x"}, + []interface{}{"leave", "Name", "x"}, + } + + v := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"leave", node.Kind, node.Value}) + if node.Value == "x" { + return visitor.ActionBreak, nil + } + case ast.Node: + visited = append(visited, []interface{}{"leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + } + + _ = visitor.Visit(astDoc, v, nil) + + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) + } +} + func TestVisitor_AllowsANamedFunctionsVisitorAPI(t *testing.T) { query := `{ a, b { x }, c }` @@ -601,35 +660,33 @@ func TestVisitor_VisitInParallel_AllowsSkippingASubTree(t *testing.T) { []interface{}{"leave", "Document", nil}, } - v := []*visitor.VisitorOptions{ - &visitor.VisitorOptions{ - Enter: func(p visitor.VisitFuncParams) (string, interface{}) { - switch node := p.Node.(type) { - case *ast.Name: - visited = append(visited, []interface{}{"enter", node.Kind, node.Value}) - case *ast.Field: - visited = append(visited, []interface{}{"enter", node.Kind, nil}) - if node.Name != nil && node.Name.Value == "b" { - return visitor.ActionSkip, nil - } - case ast.Node: - visited = append(visited, []interface{}{"enter", node.GetKind(), nil}) - default: - visited = append(visited, []interface{}{"enter", nil, nil}) - } - return visitor.ActionNoChange, nil - }, - Leave: func(p visitor.VisitFuncParams) (string, interface{}) { - switch node := p.Node.(type) { - case *ast.Name: - visited = append(visited, []interface{}{"leave", node.Kind, node.Value}) - case ast.Node: - visited = append(visited, []interface{}{"leave", node.GetKind(), nil}) - default: - visited = append(visited, []interface{}{"leave", nil, nil}) + v := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value}) + case *ast.Field: + visited = append(visited, []interface{}{"enter", node.Kind, nil}) + if node.Name != nil && node.Name.Value == "b" { + return visitor.ActionSkip, nil } - return visitor.ActionNoChange, nil - }, + case ast.Node: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"leave", nil, nil}) + } + return visitor.ActionNoChange, nil }, } @@ -744,9 +801,335 @@ func TestVisitor_VisitInParallel_AllowsSkippingDifferentSubTrees(t *testing.T) { }, } + _ = visitor.Visit(astDoc, visitor.VisitInParallel(v...), nil) + + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) + } +} + +func TestVisitor_VisitInParallel_AllowsEarlyExitWhileVisiting(t *testing.T) { + + // Note: nearly identical to the above test of the same test but + // using visitInParallel. + + visited := []interface{}{} + + query := `{ a, b { x }, c }` + astDoc := parse(t, query) + + expectedVisited := []interface{}{ + []interface{}{"enter", "Document", nil}, + []interface{}{"enter", "OperationDefinition", nil}, + []interface{}{"enter", "SelectionSet", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "a"}, + []interface{}{"leave", "Name", "a"}, + []interface{}{"leave", "Field", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "b"}, + []interface{}{"leave", "Name", "b"}, + []interface{}{"enter", "SelectionSet", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "x"}, + } + + v := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value}) + if node.Value == "x" { + return visitor.ActionBreak, nil + } + case ast.Node: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + } + + _ = visitor.Visit(astDoc, visitor.VisitInParallel(v), nil) + + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) + } +} + +func TestVisitor_VisitInParallel_AllowsEarlyExitFromDifferentPoints(t *testing.T) { + + visited := []interface{}{} + + query := `{ a { y }, b { x } }` + astDoc := parse(t, query) + + expectedVisited := []interface{}{ + []interface{}{"break-a", "enter", "Document", nil}, + []interface{}{"break-b", "enter", "Document", nil}, + []interface{}{"break-a", "enter", "OperationDefinition", nil}, + []interface{}{"break-b", "enter", "OperationDefinition", nil}, + []interface{}{"break-a", "enter", "SelectionSet", nil}, + []interface{}{"break-b", "enter", "SelectionSet", nil}, + []interface{}{"break-a", "enter", "Field", nil}, + []interface{}{"break-b", "enter", "Field", nil}, + []interface{}{"break-a", "enter", "Name", "a"}, + []interface{}{"break-b", "enter", "Name", "a"}, + []interface{}{"break-b", "leave", "Name", "a"}, + []interface{}{"break-b", "enter", "SelectionSet", nil}, + []interface{}{"break-b", "enter", "Field", nil}, + []interface{}{"break-b", "enter", "Name", "y"}, + []interface{}{"break-b", "leave", "Name", "y"}, + []interface{}{"break-b", "leave", "Field", nil}, + []interface{}{"break-b", "leave", "SelectionSet", nil}, + []interface{}{"break-b", "leave", "Field", nil}, + []interface{}{"break-b", "enter", "Field", nil}, + []interface{}{"break-b", "enter", "Name", "b"}, + } + + v := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"break-a", "enter", node.Kind, node.Value}) + if node != nil && node.Value == "a" { + return visitor.ActionBreak, nil + } + case ast.Node: + visited = append(visited, []interface{}{"break-a", "enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"break-a", "enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"break-a", "leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"break-a", "leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"break-a", "leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + } + + v2 := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"break-b", "enter", node.Kind, node.Value}) + if node != nil && node.Value == "b" { + return visitor.ActionBreak, nil + } + case ast.Node: + visited = append(visited, []interface{}{"break-b", "enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"break-b", "enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"break-b", "leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"break-b", "leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"break-b", "leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + } + + _ = visitor.Visit(astDoc, visitor.VisitInParallel(v, v2), nil) + + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) + } +} + +func TestVisitor_VisitInParallel_AllowsEarlyExitWhileLeaving(t *testing.T) { + + visited := []interface{}{} + + query := `{ a, b { x }, c }` + astDoc := parse(t, query) + + expectedVisited := []interface{}{ + []interface{}{"enter", "Document", nil}, + []interface{}{"enter", "OperationDefinition", nil}, + []interface{}{"enter", "SelectionSet", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "a"}, + []interface{}{"leave", "Name", "a"}, + []interface{}{"leave", "Field", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "b"}, + []interface{}{"leave", "Name", "b"}, + []interface{}{"enter", "SelectionSet", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "x"}, + []interface{}{"leave", "Name", "x"}, + } + + v := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"leave", node.Kind, node.Value}) + if node.Value == "x" { + return visitor.ActionBreak, nil + } + case ast.Node: + visited = append(visited, []interface{}{"leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + } + _ = visitor.Visit(astDoc, visitor.VisitInParallel(v), nil) if !reflect.DeepEqual(visited, expectedVisited) { t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) } } + +func TestVisitor_VisitInParallel_AllowsEarlyExitFromLeavingDifferentPoints(t *testing.T) { + + visited := []interface{}{} + + query := `{ a { y }, b { x } }` + astDoc := parse(t, query) + + expectedVisited := []interface{}{ + []interface{}{"break-a", "enter", "Document", nil}, + []interface{}{"break-b", "enter", "Document", nil}, + []interface{}{"break-a", "enter", "OperationDefinition", nil}, + []interface{}{"break-b", "enter", "OperationDefinition", nil}, + []interface{}{"break-a", "enter", "SelectionSet", nil}, + []interface{}{"break-b", "enter", "SelectionSet", nil}, + []interface{}{"break-a", "enter", "Field", nil}, + []interface{}{"break-b", "enter", "Field", nil}, + []interface{}{"break-a", "enter", "Name", "a"}, + []interface{}{"break-b", "enter", "Name", "a"}, + []interface{}{"break-a", "leave", "Name", "a"}, + []interface{}{"break-b", "leave", "Name", "a"}, + []interface{}{"break-a", "enter", "SelectionSet", nil}, + []interface{}{"break-b", "enter", "SelectionSet", nil}, + []interface{}{"break-a", "enter", "Field", nil}, + []interface{}{"break-b", "enter", "Field", nil}, + []interface{}{"break-a", "enter", "Name", "y"}, + []interface{}{"break-b", "enter", "Name", "y"}, + []interface{}{"break-a", "leave", "Name", "y"}, + []interface{}{"break-b", "leave", "Name", "y"}, + []interface{}{"break-a", "leave", "Field", nil}, + []interface{}{"break-b", "leave", "Field", nil}, + []interface{}{"break-a", "leave", "SelectionSet", nil}, + []interface{}{"break-b", "leave", "SelectionSet", nil}, + []interface{}{"break-a", "leave", "Field", nil}, + []interface{}{"break-b", "leave", "Field", nil}, + []interface{}{"break-b", "enter", "Field", nil}, + []interface{}{"break-b", "enter", "Name", "b"}, + []interface{}{"break-b", "leave", "Name", "b"}, + []interface{}{"break-b", "enter", "SelectionSet", nil}, + []interface{}{"break-b", "enter", "Field", nil}, + []interface{}{"break-b", "enter", "Name", "x"}, + []interface{}{"break-b", "leave", "Name", "x"}, + []interface{}{"break-b", "leave", "Field", nil}, + []interface{}{"break-b", "leave", "SelectionSet", nil}, + []interface{}{"break-b", "leave", "Field", nil}, + } + + v := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"break-a", "enter", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"break-a", "enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"break-a", "enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Field: + visited = append(visited, []interface{}{"break-a", "leave", node.GetKind(), nil}) + if node.Name != nil && node.Name.Value == "a" { + return visitor.ActionBreak, nil + } + case *ast.Name: + visited = append(visited, []interface{}{"break-a", "leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"break-a", "leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"break-a", "leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + } + + v2 := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"break-b", "enter", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"break-b", "enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"break-b", "enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Field: + visited = append(visited, []interface{}{"break-b", "leave", node.GetKind(), nil}) + if node.Name != nil && node.Name.Value == "b" { + return visitor.ActionBreak, nil + } + case *ast.Name: + visited = append(visited, []interface{}{"break-b", "leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"break-b", "leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"break-b", "leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + } + + _ = visitor.Visit(astDoc, visitor.VisitInParallel(v, v2), nil) + + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) + } +} diff --git a/validator.go b/validator.go index b3714f09..03d1fc44 100644 --- a/validator.go +++ b/validator.go @@ -45,7 +45,7 @@ func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRul } // Visit the whole document with each instance of all provided rules. - visitor.Visit(astDoc, visitor.VisitWithTypeInfo(typeInfo, visitor.VisitInParallel(visitors)), nil) + visitor.Visit(astDoc, visitor.VisitWithTypeInfo(typeInfo, visitor.VisitInParallel(visitors...)), nil) return context.Errors() } From f87436dc69d694d09c0fcf7ce585f1e903872773 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Fri, 11 Mar 2016 08:14:03 +0800 Subject: [PATCH 20/69] Parallel visitor supports editing Commit: bd87fd312061a2cedcf590d0eca5a56ab26c1b5c [bd87fd3] Parents: b2bdae08cf Author: Lee Byron Date: 1 December 2015 at 12:09:47 PM SGT --- language/visitor/visitor.go | 11 +- language/visitor/visitor_test.go | 167 +++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 4 deletions(-) diff --git a/language/visitor/visitor.go b/language/visitor/visitor.go index b875d6ea..2b072b2f 100644 --- a/language/visitor/visitor.go +++ b/language/visitor/visitor.go @@ -714,8 +714,7 @@ func isNilNode(node interface{}) bool { * Creates a new visitor instance which delegates to many visitors to run in * parallel. Each visitor will be visited for each node before moving on. * - * Visitors must not directly modify the AST nodes and only returning false to - * skip sub-branches or BREAK to exit early is supported. + * If a prior visitor edits a node, no following visitors will see that node. */ func VisitInParallel(visitorOptsSlice ...*VisitorOptions) *VisitorOptions { skipping := map[int]interface{}{} @@ -729,11 +728,13 @@ func VisitInParallel(visitorOptsSlice ...*VisitorOptions) *VisitorOptions { kind := node.GetKind() fn := GetVisitFn(visitorOpts, kind, false) if fn != nil { - action, _ := fn(p) + action, result := fn(p) if action == ActionSkip { skipping[i] = node } else if action == ActionBreak { skipping[i] = ActionBreak + } else if action == ActionUpdate { + return ActionUpdate, result } } } @@ -750,9 +751,11 @@ func VisitInParallel(visitorOptsSlice ...*VisitorOptions) *VisitorOptions { kind := node.GetKind() fn := GetVisitFn(visitorOpts, kind, true) if fn != nil { - action, _ := fn(p) + action, result := fn(p) if action == ActionBreak { skipping[i] = ActionBreak + } else if action == ActionUpdate { + return ActionUpdate, result } } } diff --git a/language/visitor/visitor_test.go b/language/visitor/visitor_test.go index a1c9bef3..81423350 100644 --- a/language/visitor/visitor_test.go +++ b/language/visitor/visitor_test.go @@ -1133,3 +1133,170 @@ func TestVisitor_VisitInParallel_AllowsEarlyExitFromLeavingDifferentPoints(t *te t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) } } + +func TestVisitor_VisitInParallel_AllowsForEditingOnEnter(t *testing.T) { + + visited := []interface{}{} + + query := `{ a, b, c { a, b, c } }` + astDoc := parse(t, query) + + expectedVisited := []interface{}{ + []interface{}{"enter", "Document", nil}, + []interface{}{"enter", "OperationDefinition", nil}, + []interface{}{"enter", "SelectionSet", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "a"}, + []interface{}{"leave", "Name", "a"}, + []interface{}{"leave", "Field", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "c"}, + []interface{}{"leave", "Name", "c"}, + []interface{}{"enter", "SelectionSet", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "a"}, + []interface{}{"leave", "Name", "a"}, + []interface{}{"leave", "Field", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "c"}, + []interface{}{"leave", "Name", "c"}, + []interface{}{"leave", "Field", nil}, + []interface{}{"leave", "SelectionSet", nil}, + []interface{}{"leave", "Field", nil}, + []interface{}{"leave", "SelectionSet", nil}, + []interface{}{"leave", "OperationDefinition", nil}, + []interface{}{"leave", "Document", nil}, + } + + v := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Field: + if node != nil && node.Name != nil && node.Name.Value == "b" { + return visitor.ActionUpdate, nil + } + } + return visitor.ActionNoChange, nil + }, + } + + v2 := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + } + + _ = visitor.Visit(astDoc, visitor.VisitInParallel(v, v2), nil) + + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) + } +} + +func TestVisitor_VisitInParallel_AllowsForEditingOnLeave(t *testing.T) { + + visited := []interface{}{} + + query := `{ a, b, c { a, b, c } }` + astDoc := parse(t, query) + + expectedVisited := []interface{}{ + []interface{}{"enter", "Document", nil}, + []interface{}{"enter", "OperationDefinition", nil}, + []interface{}{"enter", "SelectionSet", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "a"}, + []interface{}{"leave", "Name", "a"}, + []interface{}{"leave", "Field", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "b"}, + []interface{}{"leave", "Name", "b"}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "c"}, + []interface{}{"leave", "Name", "c"}, + []interface{}{"enter", "SelectionSet", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "a"}, + []interface{}{"leave", "Name", "a"}, + []interface{}{"leave", "Field", nil}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "b"}, + []interface{}{"leave", "Name", "b"}, + []interface{}{"enter", "Field", nil}, + []interface{}{"enter", "Name", "c"}, + []interface{}{"leave", "Name", "c"}, + []interface{}{"leave", "Field", nil}, + []interface{}{"leave", "SelectionSet", nil}, + []interface{}{"leave", "Field", nil}, + []interface{}{"leave", "SelectionSet", nil}, + []interface{}{"leave", "OperationDefinition", nil}, + []interface{}{"leave", "Document", nil}, + } + + v := &visitor.VisitorOptions{ + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Field: + if node != nil && node.Name != nil && node.Name.Value == "b" { + return visitor.ActionUpdate, nil + } + } + return visitor.ActionNoChange, nil + }, + } + + v2 := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"enter", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"leave", nil, nil}) + } + return visitor.ActionNoChange, nil + }, + } + + editedAST := visitor.Visit(astDoc, visitor.VisitInParallel(v, v2), nil) + + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) + } + + expectedEditedAST := parse(t, `{ a, c { a, c } }`) + if !reflect.DeepEqual(editedAST, expectedEditedAST) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(editedAST, expectedEditedAST)) + } +} From 9af43e268818a7cf6db1ecf825ca4e14b1448d52 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Fri, 11 Mar 2016 11:26:44 +0800 Subject: [PATCH 21/69] Tests for visitWithTypeInfo, support for editing Tests for visitWithTypeInfo, support for editing Commit: 32a54926803ee6b353ffa26c87dfc2c846d4c4f7 [32a5492] Parents: bd87fd3120 Author: Lee Byron Date: 1 December 2015 at 12:53:27 PM SGT Commit Date: 1 December 2015 at 12:58:22 PM SGT --- language/visitor/visitor.go | 20 ++- language/visitor/visitor_test.go | 258 ++++++++++++++++++++++++++++++- 2 files changed, 268 insertions(+), 10 deletions(-) diff --git a/language/visitor/visitor.go b/language/visitor/visitor.go index 2b072b2f..49033981 100644 --- a/language/visitor/visitor.go +++ b/language/visitor/visitor.go @@ -771,9 +771,6 @@ func VisitInParallel(visitorOptsSlice ...*VisitorOptions) *VisitorOptions { /** * Creates a new visitor instance which maintains a provided TypeInfo instance * along with visiting visitor. - * - * Visitors must not directly modify the AST nodes and only returning false to - * skip sub-branches is supported. */ func VisitWithTypeInfo(typeInfo type_info.TypeInfoI, visitorOpts *VisitorOptions) *VisitorOptions { return &VisitorOptions{ @@ -782,24 +779,31 @@ func VisitWithTypeInfo(typeInfo type_info.TypeInfoI, visitorOpts *VisitorOptions typeInfo.Enter(node) fn := GetVisitFn(visitorOpts, node.GetKind(), false) if fn != nil { - action, _ := fn(p) - if action == ActionSkip { + action, result := fn(p) + if action == ActionUpdate { typeInfo.Leave(node) - return ActionSkip, nil + if isNode(result) { + if result, ok := result.(ast.Node); ok { + typeInfo.Enter(result) + } + } } + return action, result } } return ActionNoChange, nil }, Leave: func(p VisitFuncParams) (string, interface{}) { + action := ActionNoChange + var result interface{} if node, ok := p.Node.(ast.Node); ok { fn := GetVisitFn(visitorOpts, node.GetKind(), true) if fn != nil { - fn(p) + action, result = fn(p) } typeInfo.Leave(node) } - return ActionNoChange, nil + return action, result }, } } diff --git a/language/visitor/visitor_test.go b/language/visitor/visitor_test.go index 81423350..f31cc7eb 100644 --- a/language/visitor/visitor_test.go +++ b/language/visitor/visitor_test.go @@ -5,8 +5,11 @@ import ( "reflect" "testing" + "fmt" + "github.com/graphql-go/graphql" "github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/parser" + "github.com/graphql-go/graphql/language/printer" "github.com/graphql-go/graphql/language/visitor" "github.com/graphql-go/graphql/testutil" ) @@ -97,10 +100,10 @@ func TestVisitor_VisitsEditedNode(t *testing.T) { s = append(s, addedField) ss := node.SelectionSet ss.Selections = s - return visitor.ActionUpdate, &ast.Field{ + return visitor.ActionUpdate, ast.NewField(&ast.Field{ Kind: "Field", SelectionSet: ss, - } + }) } if reflect.DeepEqual(node, addedField) { didVisitAddedField = true @@ -1300,3 +1303,254 @@ func TestVisitor_VisitInParallel_AllowsForEditingOnLeave(t *testing.T) { t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(editedAST, expectedEditedAST)) } } + +func TestVisitor_VisitWithTypeInfo_MaintainsTypeInfoDuringVisit(t *testing.T) { + + visited := []interface{}{} + + typeInfo := graphql.NewTypeInfo(testutil.DefaultRulesTestSchema) + + query := `{ human(id: 4) { name, pets { name }, unknown } }` + astDoc := parse(t, query) + + expectedVisited := []interface{}{ + []interface{}{"enter", "Document", nil, nil, nil, nil}, + []interface{}{"enter", "OperationDefinition", nil, nil, "QueryRoot", nil}, + []interface{}{"enter", "SelectionSet", nil, "QueryRoot", "QueryRoot", nil}, + []interface{}{"enter", "Field", nil, "QueryRoot", "Human", nil}, + []interface{}{"enter", "Name", "human", "QueryRoot", "Human", nil}, + []interface{}{"leave", "Name", "human", "QueryRoot", "Human", nil}, + []interface{}{"enter", "Argument", nil, "QueryRoot", "Human", "ID"}, + []interface{}{"enter", "Name", "id", "QueryRoot", "Human", "ID"}, + []interface{}{"leave", "Name", "id", "QueryRoot", "Human", "ID"}, + []interface{}{"enter", "IntValue", nil, "QueryRoot", "Human", "ID"}, + []interface{}{"leave", "IntValue", nil, "QueryRoot", "Human", "ID"}, + []interface{}{"leave", "Argument", nil, "QueryRoot", "Human", "ID"}, + []interface{}{"enter", "SelectionSet", nil, "Human", "Human", nil}, + []interface{}{"enter", "Field", nil, "Human", "String", nil}, + []interface{}{"enter", "Name", "name", "Human", "String", nil}, + []interface{}{"leave", "Name", "name", "Human", "String", nil}, + []interface{}{"leave", "Field", nil, "Human", "String", nil}, + []interface{}{"enter", "Field", nil, "Human", "[Pet]", nil}, + []interface{}{"enter", "Name", "pets", "Human", "[Pet]", nil}, + []interface{}{"leave", "Name", "pets", "Human", "[Pet]", nil}, + []interface{}{"enter", "SelectionSet", nil, "Pet", "[Pet]", nil}, + []interface{}{"enter", "Field", nil, "Pet", "String", nil}, + []interface{}{"enter", "Name", "name", "Pet", "String", nil}, + []interface{}{"leave", "Name", "name", "Pet", "String", nil}, + []interface{}{"leave", "Field", nil, "Pet", "String", nil}, + []interface{}{"leave", "SelectionSet", nil, "Pet", "[Pet]", nil}, + []interface{}{"leave", "Field", nil, "Human", "[Pet]", nil}, + []interface{}{"enter", "Field", nil, "Human", nil, nil}, + []interface{}{"enter", "Name", "unknown", "Human", nil, nil}, + []interface{}{"leave", "Name", "unknown", "Human", nil, nil}, + []interface{}{"leave", "Field", nil, "Human", nil, nil}, + []interface{}{"leave", "SelectionSet", nil, "Human", "Human", nil}, + []interface{}{"leave", "Field", nil, "QueryRoot", "Human", nil}, + []interface{}{"leave", "SelectionSet", nil, "QueryRoot", "QueryRoot", nil}, + []interface{}{"leave", "OperationDefinition", nil, nil, "QueryRoot", nil}, + []interface{}{"leave", "Document", nil, nil, nil, nil}, + } + + v := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + var parentType interface{} + var ttype interface{} + var inputType interface{} + + if typeInfo.ParentType() != nil { + parentType = fmt.Sprintf("%v", typeInfo.ParentType()) + } + if typeInfo.Type() != nil { + ttype = fmt.Sprintf("%v", typeInfo.Type()) + } + if typeInfo.InputType() != nil { + inputType = fmt.Sprintf("%v", typeInfo.InputType()) + } + + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value, parentType, ttype, inputType}) + case ast.Node: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil, parentType, ttype, inputType}) + default: + visited = append(visited, []interface{}{"enter", nil, nil, parentType, ttype, inputType}) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + var parentType interface{} + var ttype interface{} + var inputType interface{} + + if typeInfo.ParentType() != nil { + parentType = fmt.Sprintf("%v", typeInfo.ParentType()) + } + if typeInfo.Type() != nil { + ttype = fmt.Sprintf("%v", typeInfo.Type()) + } + if typeInfo.InputType() != nil { + inputType = fmt.Sprintf("%v", typeInfo.InputType()) + } + + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"leave", node.Kind, node.Value, parentType, ttype, inputType}) + case ast.Node: + visited = append(visited, []interface{}{"leave", node.GetKind(), nil, parentType, ttype, inputType}) + default: + visited = append(visited, []interface{}{"leave", nil, nil, parentType, ttype, inputType}) + } + return visitor.ActionNoChange, nil + }, + } + + _ = visitor.Visit(astDoc, visitor.VisitWithTypeInfo(typeInfo, v), nil) + + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) + } + +} + +func TestVisitor_VisitWithTypeInfo_MaintainsTypeInfoDuringEdit(t *testing.T) { + + visited := []interface{}{} + + typeInfo := graphql.NewTypeInfo(testutil.DefaultRulesTestSchema) + + astDoc := parse(t, `{ human(id: 4) { name, pets }, alien }`) + + expectedVisited := []interface{}{ + []interface{}{"enter", "Document", nil, nil, nil, nil}, + []interface{}{"enter", "OperationDefinition", nil, nil, "QueryRoot", nil}, + []interface{}{"enter", "SelectionSet", nil, "QueryRoot", "QueryRoot", nil}, + []interface{}{"enter", "Field", nil, "QueryRoot", "Human", nil}, + []interface{}{"enter", "Name", "human", "QueryRoot", "Human", nil}, + []interface{}{"leave", "Name", "human", "QueryRoot", "Human", nil}, + []interface{}{"enter", "Argument", nil, "QueryRoot", "Human", "ID"}, + []interface{}{"enter", "Name", "id", "QueryRoot", "Human", "ID"}, + []interface{}{"leave", "Name", "id", "QueryRoot", "Human", "ID"}, + []interface{}{"enter", "IntValue", nil, "QueryRoot", "Human", "ID"}, + []interface{}{"leave", "IntValue", nil, "QueryRoot", "Human", "ID"}, + []interface{}{"leave", "Argument", nil, "QueryRoot", "Human", "ID"}, + []interface{}{"enter", "SelectionSet", nil, "Human", "Human", nil}, + []interface{}{"enter", "Field", nil, "Human", "String", nil}, + []interface{}{"enter", "Name", "name", "Human", "String", nil}, + []interface{}{"leave", "Name", "name", "Human", "String", nil}, + []interface{}{"leave", "Field", nil, "Human", "String", nil}, + []interface{}{"enter", "Field", nil, "Human", "[Pet]", nil}, + []interface{}{"enter", "Name", "pets", "Human", "[Pet]", nil}, + []interface{}{"leave", "Name", "pets", "Human", "[Pet]", nil}, + []interface{}{"enter", "SelectionSet", nil, "Pet", "[Pet]", nil}, + []interface{}{"enter", "Field", nil, "Pet", "String!", nil}, + []interface{}{"enter", "Name", "__typename", "Pet", "String!", nil}, + []interface{}{"leave", "Name", "__typename", "Pet", "String!", nil}, + []interface{}{"leave", "Field", nil, "Pet", "String!", nil}, + []interface{}{"leave", "SelectionSet", nil, "Pet", "[Pet]", nil}, + []interface{}{"leave", "Field", nil, "Human", "[Pet]", nil}, + []interface{}{"leave", "SelectionSet", nil, "Human", "Human", nil}, + []interface{}{"leave", "Field", nil, "QueryRoot", "Human", nil}, + []interface{}{"enter", "Field", nil, "QueryRoot", "Alien", nil}, + []interface{}{"enter", "Name", "alien", "QueryRoot", "Alien", nil}, + []interface{}{"leave", "Name", "alien", "QueryRoot", "Alien", nil}, + []interface{}{"enter", "SelectionSet", nil, "Alien", "Alien", nil}, + []interface{}{"enter", "Field", nil, "Alien", "String!", nil}, + []interface{}{"enter", "Name", "__typename", "Alien", "String!", nil}, + []interface{}{"leave", "Name", "__typename", "Alien", "String!", nil}, + []interface{}{"leave", "Field", nil, "Alien", "String!", nil}, + []interface{}{"leave", "SelectionSet", nil, "Alien", "Alien", nil}, + []interface{}{"leave", "Field", nil, "QueryRoot", "Alien", nil}, + []interface{}{"leave", "SelectionSet", nil, "QueryRoot", "QueryRoot", nil}, + []interface{}{"leave", "OperationDefinition", nil, nil, "QueryRoot", nil}, + []interface{}{"leave", "Document", nil, nil, nil, nil}, + } + + v := &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + var parentType interface{} + var ttype interface{} + var inputType interface{} + + if typeInfo.ParentType() != nil { + parentType = fmt.Sprintf("%v", typeInfo.ParentType()) + } + if typeInfo.Type() != nil { + ttype = fmt.Sprintf("%v", typeInfo.Type()) + } + if typeInfo.InputType() != nil { + inputType = fmt.Sprintf("%v", typeInfo.InputType()) + } + + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value, parentType, ttype, inputType}) + case *ast.Field: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil, parentType, ttype, inputType}) + + // Make a query valid by adding missing selection sets. + if node.SelectionSet == nil && graphql.IsCompositeType(graphql.GetNamed(typeInfo.Type())) { + return visitor.ActionUpdate, ast.NewField(&ast.Field{ + Alias: node.Alias, + Name: node.Name, + Arguments: node.Arguments, + Directives: node.Directives, + SelectionSet: ast.NewSelectionSet(&ast.SelectionSet{ + Selections: []ast.Selection{ + ast.NewField(&ast.Field{ + Name: ast.NewName(&ast.Name{ + Value: "__typename", + }), + }), + }, + }), + }) + } + case ast.Node: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil, parentType, ttype, inputType}) + default: + visited = append(visited, []interface{}{"enter", nil, nil, parentType, ttype, inputType}) + } + + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + var parentType interface{} + var ttype interface{} + var inputType interface{} + + if typeInfo.ParentType() != nil { + parentType = fmt.Sprintf("%v", typeInfo.ParentType()) + } + if typeInfo.Type() != nil { + ttype = fmt.Sprintf("%v", typeInfo.Type()) + } + if typeInfo.InputType() != nil { + inputType = fmt.Sprintf("%v", typeInfo.InputType()) + } + + switch node := p.Node.(type) { + case *ast.Name: + visited = append(visited, []interface{}{"leave", node.Kind, node.Value, parentType, ttype, inputType}) + case ast.Node: + visited = append(visited, []interface{}{"leave", node.GetKind(), nil, parentType, ttype, inputType}) + default: + visited = append(visited, []interface{}{"leave", nil, nil, parentType, ttype, inputType}) + } + return visitor.ActionNoChange, nil + }, + } + + editedAST := visitor.Visit(astDoc, visitor.VisitWithTypeInfo(typeInfo, v), nil) + + editedASTQuery := printer.Print(editedAST.(ast.Node)) + expectedEditedASTQuery := printer.Print(parse(t, `{ human(id: 4) { name, pets { __typename } }, alien { __typename } }`)) + + if !reflect.DeepEqual(editedASTQuery, expectedEditedASTQuery) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(editedASTQuery, expectedEditedASTQuery)) + } + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) + } + +} From 7da312e3b08d8391c65cdd7941a3dc45869db780 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Fri, 11 Mar 2016 11:47:20 +0800 Subject: [PATCH 22/69] Proposed fix Commit: f79ba42e30d9f1380f378440e4fe66b1aa428386 [f79ba42] Parents: 4256230557 Author: Dmitry Minkovsky Date: 21 February 2016 at 6:03:39 AM SGT Commit Date: 21 February 2016 at 12:06:22 PM SGT --- Tests to demonstrate problem Commit: 42562305570b04a5b7ec1ee7714f020f366023c9 [4256230] Parents: 75e7be0028 Author: Dmitry Minkovsky Date: 21 February 2016 at 5:40:46 AM SGT Commit Date: 21 February 2016 at 12:06:07 PM SGT --- Note: See PR #298 for context: https://github.com/graphql/graphql-js/pull/298 --- language/visitor/visitor.go | 2 +- language/visitor/visitor_test.go | 96 ++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/language/visitor/visitor.go b/language/visitor/visitor.go index 49033981..a88369c5 100644 --- a/language/visitor/visitor.go +++ b/language/visitor/visitor.go @@ -490,7 +490,7 @@ Loop: } } if len(edits) != 0 { - result = edits[0].Value + result = edits[len(edits)-1].Value } return result } diff --git a/language/visitor/visitor_test.go b/language/visitor/visitor_test.go index f31cc7eb..c3d95c0c 100644 --- a/language/visitor/visitor_test.go +++ b/language/visitor/visitor_test.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/graphql-go/graphql" "github.com/graphql-go/graphql/language/ast" + "github.com/graphql-go/graphql/language/kinds" "github.com/graphql-go/graphql/language/parser" "github.com/graphql-go/graphql/language/printer" "github.com/graphql-go/graphql/language/visitor" @@ -27,6 +28,101 @@ func parse(t *testing.T, query string) *ast.Document { return astDoc } +func TestVisitor_AllowsEditingANodeBothOnEnterAndOnLeave(t *testing.T) { + + query := `{ a, b, c { a, b, c } }` + astDoc := parse(t, query) + + var selectionSet *ast.SelectionSet + + expectedQuery := `{ a, b, c { a, b, c } }` + expectedAST := parse(t, expectedQuery) + + v := &visitor.VisitorOptions{ + + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.OperationDefinition: visitor.NamedVisitFuncs{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.OperationDefinition); ok { + selectionSet = node.SelectionSet + return visitor.ActionUpdate, ast.NewOperationDefinition(&ast.OperationDefinition{ + Loc: node.Loc, + Operation: node.Operation, + Name: node.Name, + VariableDefinitions: node.VariableDefinitions, + Directives: node.Directives, + SelectionSet: ast.NewSelectionSet(&ast.SelectionSet{ + Selections: []ast.Selection{}, + }), + }) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.OperationDefinition); ok { + return visitor.ActionUpdate, ast.NewOperationDefinition(&ast.OperationDefinition{ + Loc: node.Loc, + Operation: node.Operation, + Name: node.Name, + VariableDefinitions: node.VariableDefinitions, + Directives: node.Directives, + SelectionSet: selectionSet, + }) + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + + editedAst := visitor.Visit(astDoc, v, nil) + if !reflect.DeepEqual(expectedAST, editedAst) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedAST, editedAst)) + } + +} +func TestVisitor_AllowsEditingTheRootNodeOnEnterAndOnLeave(t *testing.T) { + + query := `{ a, b, c { a, b, c } }` + astDoc := parse(t, query) + + definitions := astDoc.Definitions + + expectedQuery := `{ a, b, c { a, b, c } }` + expectedAST := parse(t, expectedQuery) + + v := &visitor.VisitorOptions{ + + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.Document: visitor.NamedVisitFuncs{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.Document); ok { + return visitor.ActionUpdate, ast.NewDocument(&ast.Document{ + Loc: node.Loc, + Definitions: []ast.Node{}, + }) + } + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.Document); ok { + return visitor.ActionUpdate, ast.NewDocument(&ast.Document{ + Loc: node.Loc, + Definitions: definitions, + }) + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + + editedAst := visitor.Visit(astDoc, v, nil) + if !reflect.DeepEqual(expectedAST, editedAst) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedAST, editedAst)) + } + +} func TestVisitor_AllowsForEditingOnEnter(t *testing.T) { query := `{ a, b, c { a, b, c } }` From 778b5fdc04a2245f75cac521d5618a2ccea3d2c1 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Fri, 11 Mar 2016 12:10:37 +0800 Subject: [PATCH 23/69] Add parser test containing tricky multi-byte Commit: da5c4b0814887382067dcaeaddd6fed8a76a3614 [da5c4b0] Parents: 160c67a74e Author: Lee Byron Date: 29 September 2015 at 5:13:20 AM SGT --- language/parser/parser_test.go | 78 ++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/language/parser/parser_test.go b/language/parser/parser_test.go index 09fa78d7..896a9d46 100644 --- a/language/parser/parser_test.go +++ b/language/parser/parser_test.go @@ -10,6 +10,7 @@ import ( "github.com/graphql-go/graphql/gqlerrors" "github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/location" + "github.com/graphql-go/graphql/language/printer" "github.com/graphql-go/graphql/language/source" ) @@ -188,6 +189,83 @@ func TestDoesNotAllowNullAsValue(t *testing.T) { testErrorMessage(t, test) } +func TestParsesMultiByteCharacters(t *testing.T) { + + doc := ` + # This comment has a \u0A0A multi-byte character. + { field(arg: "Has a \u0A0A multi-byte character.") } + ` + astDoc := parse(t, doc) + + expectedASTDoc := ast.NewDocument(&ast.Document{ + Loc: ast.NewLocation(&ast.Location{ + Start: 67, + End: 121, + }), + Definitions: []ast.Node{ + ast.NewOperationDefinition(&ast.OperationDefinition{ + Loc: ast.NewLocation(&ast.Location{ + Start: 67, + End: 119, + }), + Operation: "query", + SelectionSet: ast.NewSelectionSet(&ast.SelectionSet{ + Loc: ast.NewLocation(&ast.Location{ + Start: 67, + End: 119, + }), + Selections: []ast.Selection{ + ast.NewField(&ast.Field{ + Loc: ast.NewLocation(&ast.Location{ + Start: 67, + End: 117, + }), + Name: ast.NewName(&ast.Name{ + Loc: ast.NewLocation(&ast.Location{ + Start: 69, + End: 74, + }), + Value: "field", + }), + Arguments: []*ast.Argument{ + ast.NewArgument(&ast.Argument{ + Loc: ast.NewLocation(&ast.Location{ + Start: 75, + End: 116, + }), + Name: ast.NewName(&ast.Name{ + + Loc: ast.NewLocation(&ast.Location{ + Start: 75, + End: 78, + }), + Value: "arg", + }), + Value: ast.NewStringValue(&ast.StringValue{ + + Loc: ast.NewLocation(&ast.Location{ + Start: 80, + End: 116, + }), + Value: "Has a \u0A0A multi-byte character.", + }), + }), + }, + }), + }, + }), + }), + }, + }) + + astDocQuery := printer.Print(astDoc) + expectedASTDocQuery := printer.Print(expectedASTDoc) + + if !reflect.DeepEqual(astDocQuery, expectedASTDocQuery) { + t.Fatalf("unexpected document, expected: %v, got: %v", astDocQuery, expectedASTDocQuery) + } +} + func TestParsesKitchenSink(t *testing.T) { b, err := ioutil.ReadFile("../../kitchen-sink.graphql") if err != nil { From 59b3554b612d5ad9ee1c3da7b4187c9ee1201047 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Fri, 11 Mar 2016 12:46:12 +0800 Subject: [PATCH 24/69] first pass at tests for subscriptions Commit: 87c6a274fb24e08a2c85a93a9294e5711eed874b [87c6a27] Parents: a0c30d3475 Author: Adam Miskiewicz Date: 17 October 2015 at 4:55:00 AM SGT --- language/parser/parser_test.go | 46 +++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/language/parser/parser_test.go b/language/parser/parser_test.go index 896a9d46..f8697310 100644 --- a/language/parser/parser_test.go +++ b/language/parser/parser_test.go @@ -284,6 +284,7 @@ func TestAllowsNonKeywordsAnywhereNameIsAllowed(t *testing.T) { "fragment", "query", "mutation", + "subscription", "true", "false", } @@ -308,9 +309,34 @@ func TestAllowsNonKeywordsAnywhereNameIsAllowed(t *testing.T) { } } -func TestParsesExperimentalSubscriptionFeature(t *testing.T) { +// +//func TestParsesExperimentalSubscriptionFeature(t *testing.T) { +// source := ` +// subscription Foo { +// subscriptionField +// } +// ` +// _, err := Parse(ParseParams{Source: source}) +// if err != nil { +// t.Fatalf("unexpected error: %v", err) +// } +//} + +func TestParsesAnonymousMutationOperations(t *testing.T) { source := ` - subscription Foo { + mutation { + mutationField + } + ` + _, err := Parse(ParseParams{Source: source}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestParsesAnonymousSubscriptionOperations(t *testing.T) { + source := ` + subscription { subscriptionField } ` @@ -320,9 +346,9 @@ func TestParsesExperimentalSubscriptionFeature(t *testing.T) { } } -func TestParsesAnonymousOperations(t *testing.T) { +func TestParsesNamedMutationOperations(t *testing.T) { source := ` - mutation { + mutation Foo { mutationField } ` @@ -332,6 +358,18 @@ func TestParsesAnonymousOperations(t *testing.T) { } } +func TestParsesNamedSubscriptionOperations(t *testing.T) { + source := ` + subscription Foo { + subscriptionField + } + ` + _, err := Parse(ParseParams{Source: source}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + func TestParseCreatesAst(t *testing.T) { body := `{ node(id: 4) { From 1109e2241dd39b3aed714a86d84d8eec8f83a4a1 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Fri, 11 Mar 2016 13:29:52 +0800 Subject: [PATCH 25/69] initial subscription commit. Makes "subscription" behave like "query" Commit: 8a589f6e88ecf675ce34d39d813a7b271dae46aa [8a589f6] Parents: 3bc93a7440 Author: Adam Miskiewicz Date: 7 September 2015 at 5:32:29 AM SGT Labels: HEAD --- executor.go | 31 +++++++++++++++++++++++++++++-- introspection.go | 9 +++++++-- introspection_test.go | 19 +++++++++++++++++++ language/ast/definitions.go | 2 ++ schema.go | 17 ++++++++++++----- testutil/introspection_query.go | 1 + type_info.go | 2 ++ 7 files changed, 72 insertions(+), 9 deletions(-) diff --git a/executor.go b/executor.go index 0fe49e0c..1a25b755 100644 --- a/executor.go +++ b/executor.go @@ -173,11 +173,38 @@ func getOperationRootType(schema Schema, operation ast.Definition) (*Object, err case "mutation": mutationType := schema.MutationType() if mutationType.PrivateName == "" { - return nil, errors.New("Schema is not configured for mutations") + return nil, gqlerrors.NewError( + "Schema is not configured for mutations", + []ast.Node{operation}, + "", + nil, + []int{}, + nil, + ) } return mutationType, nil + case "subscription": + subscriptionType := schema.SubscriptionType() + if subscriptionType.PrivateName == "" { + return nil, gqlerrors.NewError( + "Schema is not configured for subscriptions", + []ast.Node{operation}, + "", + nil, + []int{}, + nil, + ) + } + return subscriptionType, nil default: - return nil, errors.New("Can only execute queries and mutations") + return nil, gqlerrors.NewError( + "Can only execute queries, mutations and subscription", + []ast.Node{operation}, + "", + nil, + []int{}, + nil, + ) } } diff --git a/introspection.go b/introspection.go index 81bc61f4..5552d37f 100644 --- a/introspection.go +++ b/introspection.go @@ -267,10 +267,15 @@ mutation operations.`, }, }, "subscriptionType": &Field{ - Description: `If this server support subscription, the type that ' + - 'subscription operations will be rooted at.`, + Description: `If this server supports subscription, the type that ` + + `subscription operations will be rooted at.`, Type: __Type, Resolve: func(p ResolveParams) (interface{}, error) { + if schema, ok := p.Source.(Schema); ok { + if schema.SubscriptionType() != nil { + return schema.SubscriptionType(), nil + } + } return nil, nil }, }, diff --git a/introspection_test.go b/introspection_test.go index eabcfc62..9d155d06 100644 --- a/introspection_test.go +++ b/introspection_test.go @@ -1257,6 +1257,20 @@ func TestIntrospection_ExposesDescriptionsOnTypesAndFields(t *testing.T) { } } ` + + /* + + [Data["schemaType"]["fields"][0]["name"]: "types" != "queryType" Data["schemaType"]["fields"][0]["description"]: "A list of all types supported by this server." != "The type that query operations will be rooted at." + Data["schemaType"]["fields"][1]["name"]: "queryType" != "mutationType" + Data["schemaType"]["fields"][1]["description"]: "The type that query operations will be rooted at." != "If this server supports mutation, the type that mutation operations will be rooted at." + Data["schemaType"]["fields"][2]["name"]: "mutationType" != "subscriptionType" + Data["schemaType"]["fields"][2]["description"]: "If this server supports mutation, the type that mutation + operations will be rooted at." != "If this server support subscription, the type that subscription operations will be rooted at." Data["schemaType"]["fields"][3]["name"]: "subscriptionType" != "directives" + Data["schemaType"]["fields"][3]["description"]: "If this server supports subscription, the type that subscription operations will be rooted at." != "A list of all directives supported by this server." + Data["schemaType"]["fields"][4]["description"]: "A list of all directives supported by this server." != "A list of all types supported by this server." + Data["schemaType"]["fields"][4]["name"]: "directives" != "types"] + + */ expected := &graphql.Result{ Data: map[string]interface{}{ "schemaType": map[string]interface{}{ @@ -1279,6 +1293,11 @@ mutation operations.`, "description": "If this server supports mutation, the type that " + "mutation operations will be rooted at.", }, + map[string]interface{}{ + "name": "subscriptionType", + "description": "If this server supports subscription, the type that " + + "subscription operations will be rooted at.", + }, map[string]interface{}{ "name": "directives", "description": "A list of all directives supported by this server.", diff --git a/language/ast/definitions.go b/language/ast/definitions.go index f964306a..a619e78d 100644 --- a/language/ast/definitions.go +++ b/language/ast/definitions.go @@ -9,6 +9,8 @@ type Definition interface { GetOperation() string GetVariableDefinitions() []*VariableDefinition GetSelectionSet() *SelectionSet + GetKind() string + GetLoc() *Location } // Ensure that all definition types implements Definition interface diff --git a/schema.go b/schema.go index b2be7bba..2bc1d53a 100644 --- a/schema.go +++ b/schema.go @@ -7,17 +7,19 @@ import ( /** Schema Definition A Schema is created by supplying the root types of each type of operation, -query and mutation (optional). A schema definition is then supplied to the +query, mutation (optional) and subscription (optional). A schema definition is then supplied to the validator and executor. Example: myAppSchema, err := NewSchema(SchemaConfig({ - Query: MyAppQueryRootType - Mutation: MyAppMutationRootType + Query: MyAppQueryRootType, + Mutation: MyAppMutationRootType, + Subscription: MyAppSubscriptionRootType, }); */ type SchemaConfig struct { - Query *Object - Mutation *Object + Query *Object + Mutation *Object + Subscription *Object } // chose to name as TypeMap instead of TypeMap @@ -54,6 +56,7 @@ func NewSchema(config SchemaConfig) (Schema, error) { objectTypes := []*Object{ schema.QueryType(), schema.MutationType(), + schema.SubscriptionType(), __Type, __Schema, } @@ -94,6 +97,10 @@ func (gq *Schema) MutationType() *Object { return gq.schemaConfig.Mutation } +func (gq *Schema) SubscriptionType() *Object { + return gq.schemaConfig.Subscription +} + func (gq *Schema) Directives() []*Directive { if len(gq.directives) == 0 { gq.directives = []*Directive{ diff --git a/testutil/introspection_query.go b/testutil/introspection_query.go index 9d336353..ce908fdb 100644 --- a/testutil/introspection_query.go +++ b/testutil/introspection_query.go @@ -5,6 +5,7 @@ var IntrospectionQuery = ` __schema { queryType { name } mutationType { name } + subscriptionType { name } types { ...FullType } diff --git a/type_info.go b/type_info.go index 3c6dd2e2..834daa3c 100644 --- a/type_info.go +++ b/type_info.go @@ -97,6 +97,8 @@ func (ti *TypeInfo) Enter(node ast.Node) { ttype = schema.QueryType() } else if node.Operation == "mutation" { ttype = schema.MutationType() + } else if node.Operation == "subscription" { + ttype = schema.SubscriptionType() } ti.typeStack = append(ti.typeStack, ttype) case *ast.InlineFragment: From 9287d089f1748ae9dc09edef4e3acd429aa1b344 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Fri, 11 Mar 2016 17:58:09 +0800 Subject: [PATCH 26/69] first pass at tests for subscriptions Commit: 87c6a274fb24e08a2c85a93a9294e5711eed874b [87c6a27] Parents: a0c30d3475 Author: Adam Miskiewicz Date: 17 October 2015 at 4:55:00 AM SGT --- definition_test.go | 62 ++++++++++++++++++- enum_type_test.go | 42 ++++++++++++- executor_test.go | 65 +++++++++++++++++++- introspection.go | 7 +-- introspection_test.go | 34 +++++------ kitchen-sink.graphql | 13 ++++ language/printer/printer_test.go | 13 ++++ language/visitor/visitor_test.go | 68 +++++++++++++++++++-- rules_lone_anonymous_operation_rule_test.go | 15 ++++- rules_unique_operation_names_test.go | 19 +++++- validation_test.go | 17 ++++++ 11 files changed, 322 insertions(+), 33 deletions(-) diff --git a/definition_test.go b/definition_test.go index 6664feab..363c8024 100644 --- a/definition_test.go +++ b/definition_test.go @@ -98,6 +98,20 @@ var blogMutation = graphql.NewObject(graphql.ObjectConfig{ }, }) +var blogSubscription = graphql.NewObject(graphql.ObjectConfig{ + Name: "Subscription", + Fields: graphql.Fields{ + "articleSubscribe": &graphql.Field{ + Type: blogArticle, + Args: graphql.FieldConfigArgument{ + "id": &graphql.ArgumentConfig{ + Type: graphql.String, + }, + }, + }, + }, +}) + var objectType = graphql.NewObject(graphql.ObjectConfig{ Name: "Object", IsTypeOf: func(value interface{}, info graphql.ResolveInfo) bool { @@ -204,6 +218,7 @@ func TestTypeSystem_DefinitionExample_DefinesAQueryOnlySchema(t *testing.T) { t.Fatalf("feedField.Name expected to equal `feed`, got: %v", feedField.Name) } } + func TestTypeSystem_DefinitionExample_DefinesAMutationScheme(t *testing.T) { blogSchema, err := graphql.NewSchema(graphql.SchemaConfig{ Query: blogQuery, @@ -233,6 +248,35 @@ func TestTypeSystem_DefinitionExample_DefinesAMutationScheme(t *testing.T) { } } +func TestTypeSystem_DefinitionExample_DefinesASubscriptionScheme(t *testing.T) { + blogSchema, err := graphql.NewSchema(graphql.SchemaConfig{ + Query: blogQuery, + Subscription: blogSubscription, + }) + if err != nil { + t.Fatalf("unexpected error, got: %v", err) + } + + if blogSchema.SubscriptionType() != blogSubscription { + t.Fatalf("expected blogSchema.SubscriptionType() == blogSubscription") + } + + subMutation, _ := blogSubscription.Fields()["articleSubscribe"] + if subMutation == nil { + t.Fatalf("subMutation is nil") + } + subMutationType := subMutation.Type + if subMutationType != blogArticle { + t.Fatalf("subMutationType expected to equal blogArticle, got: %v", subMutationType) + } + if subMutationType.Name() != "Article" { + t.Fatalf("subMutationType.Name expected to equal `Article`, got: %v", subMutationType.Name()) + } + if subMutation.Name != "articleSubscribe" { + t.Fatalf("subMutation.Name expected to equal `articleSubscribe`, got: %v", subMutation.Name) + } +} + func TestTypeSystem_DefinitionExample_IncludesNestedInputObjectsInTheMap(t *testing.T) { nestedInputObject := graphql.NewInputObject(graphql.InputObjectConfig{ Name: "NestedInputObject", @@ -263,9 +307,23 @@ func TestTypeSystem_DefinitionExample_IncludesNestedInputObjectsInTheMap(t *test }, }, }) + someSubscription := graphql.NewObject(graphql.ObjectConfig{ + Name: "SomeSubscription", + Fields: graphql.Fields{ + "subscribeToSomething": &graphql.Field{ + Type: blogArticle, + Args: graphql.FieldConfigArgument{ + "input": &graphql.ArgumentConfig{ + Type: someInputObject, + }, + }, + }, + }, + }) schema, err := graphql.NewSchema(graphql.SchemaConfig{ - Query: blogQuery, - Mutation: someMutation, + Query: blogQuery, + Mutation: someMutation, + Subscription: someSubscription, }) if err != nil { t.Fatalf("unexpected error, got: %v", err) diff --git a/enum_type_test.go b/enum_type_test.go index 7187a686..2d3a038e 100644 --- a/enum_type_test.go +++ b/enum_type_test.go @@ -93,9 +93,31 @@ var enumTypeTestMutationType = graphql.NewObject(graphql.ObjectConfig{ }, }, }) + +var enumTypeTestSubscriptionType = graphql.NewObject(graphql.ObjectConfig{ + Name: "Subscription", + Fields: graphql.Fields{ + "subscribeToEnum": &graphql.Field{ + Type: enumTypeTestColorType, + Args: graphql.FieldConfigArgument{ + "color": &graphql.ArgumentConfig{ + Type: enumTypeTestColorType, + }, + }, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + if color, ok := p.Args["color"]; ok { + return color, nil + } + return nil, nil + }, + }, + }, +}) + var enumTypeTestSchema, _ = graphql.NewSchema(graphql.SchemaConfig{ - Query: enumTypeTestQueryType, - Mutation: enumTypeTestMutationType, + Query: enumTypeTestQueryType, + Mutation: enumTypeTestMutationType, + Subscription: enumTypeTestSubscriptionType, }) func executeEnumTypeTest(t *testing.T, query string) *graphql.Result { @@ -240,6 +262,22 @@ func TestTypeSystem_EnumValues_AcceptsEnumLiteralsAsInputArgumentsToMutations(t t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result)) } } + +func TestTypeSystem_EnumValues_AcceptsEnumLiteralsAsInputArgumentsToSubscriptions(t *testing.T) { + query := `subscription x($color: Color!) { subscribeToEnum(color: $color) }` + params := map[string]interface{}{ + "color": "GREEN", + } + expected := &graphql.Result{ + Data: map[string]interface{}{ + "subscribeToEnum": "GREEN", + }, + } + result := executeEnumTypeTestWithParams(t, query, params) + if !reflect.DeepEqual(expected, result) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result)) + } +} func TestTypeSystem_EnumValues_DoesNotAcceptInternalValueAsEnumVariable(t *testing.T) { query := `query test($color: Color!) { colorEnum(fromEnum: $color) }` params := map[string]interface{}{ diff --git a/executor_test.go b/executor_test.go index 7922d8c8..f7915ec6 100644 --- a/executor_test.go +++ b/executor_test.go @@ -662,7 +662,7 @@ func TestThrowsIfNoOperationIsProvidedWithMultipleOperations(t *testing.T) { func TestUsesTheQuerySchemaForQueries(t *testing.T) { - doc := `query Q { a } mutation M { c }` + doc := `query Q { a } mutation M { c } subscription S { a }` data := map[string]interface{}{ "a": "b", "c": "d", @@ -691,6 +691,14 @@ func TestUsesTheQuerySchemaForQueries(t *testing.T) { }, }, }), + Subscription: graphql.NewObject(graphql.ObjectConfig{ + Name: "S", + Fields: graphql.Fields{ + "a": &graphql.Field{ + Type: graphql.String, + }, + }, + }), }) if err != nil { t.Fatalf("Error in schema %v", err.Error()) @@ -770,6 +778,61 @@ func TestUsesTheMutationSchemaForMutations(t *testing.T) { } } +func TestUsesTheSubscriptionSchemaForSubscriptions(t *testing.T) { + + doc := `query Q { a } subscription S { a }` + data := map[string]interface{}{ + "a": "b", + "c": "d", + } + + expected := &graphql.Result{ + Data: map[string]interface{}{ + "a": "b", + }, + } + + schema, err := graphql.NewSchema(graphql.SchemaConfig{ + Query: graphql.NewObject(graphql.ObjectConfig{ + Name: "Q", + Fields: graphql.Fields{ + "a": &graphql.Field{ + Type: graphql.String, + }, + }, + }), + Subscription: graphql.NewObject(graphql.ObjectConfig{ + Name: "S", + Fields: graphql.Fields{ + "a": &graphql.Field{ + Type: graphql.String, + }, + }, + }), + }) + if err != nil { + t.Fatalf("Error in schema %v", err.Error()) + } + + // parse query + ast := testutil.TestParse(t, doc) + + // execute + ep := graphql.ExecuteParams{ + Schema: schema, + AST: ast, + Root: data, + OperationName: "S", + } + result := testutil.TestExecute(t, ep) + if len(result.Errors) > 0 { + t.Fatalf("wrong result, unexpected errors: %v", result.Errors) + } + if !reflect.DeepEqual(expected, result) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result)) + } +} + func TestCorrectFieldOrderingDespiteExecutionOrder(t *testing.T) { doc := ` diff --git a/introspection.go b/introspection.go index 5552d37f..f1defd0e 100644 --- a/introspection.go +++ b/introspection.go @@ -222,10 +222,9 @@ func init() { __Schema = NewObject(ObjectConfig{ Name: "__Schema", - Description: `A GraphQL Schema defines the capabilities of a GraphQL -server. It exposes all available types and directives on -the server, as well as the entry points for query and -mutation operations.`, + Description: `A GraphQL Schema defines the capabilities of a GraphQL server. ` + + `It exposes all available types and directives on the server, as well as ` + + `the entry points for query, mutation, and subscription operations.`, Fields: Fields{ "types": &Field{ Description: "A list of all types supported by this server.", diff --git a/introspection_test.go b/introspection_test.go index 9d155d06..4fe94437 100644 --- a/introspection_test.go +++ b/introspection_test.go @@ -30,7 +30,8 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { } expectedDataSubSet := map[string]interface{}{ "__schema": map[string]interface{}{ - "mutationType": nil, + "mutationType": nil, + "subscriptionType": nil, "queryType": map[string]interface{}{ "name": "QueryRoot", }, @@ -93,6 +94,16 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "isDeprecated": false, "deprecationReason": nil, }, + map[string]interface{}{ + "name": "subscriptionType", + "args": []interface{}{}, + "type": map[string]interface{}{ + "kind": "OBJECT", + "name": "__Type", + }, + "isDeprecated": false, + "deprecationReason": nil, + }, map[string]interface{}{ "name": "directives", "args": []interface{}{}, @@ -1258,27 +1269,14 @@ func TestIntrospection_ExposesDescriptionsOnTypesAndFields(t *testing.T) { } ` - /* - - [Data["schemaType"]["fields"][0]["name"]: "types" != "queryType" Data["schemaType"]["fields"][0]["description"]: "A list of all types supported by this server." != "The type that query operations will be rooted at." - Data["schemaType"]["fields"][1]["name"]: "queryType" != "mutationType" - Data["schemaType"]["fields"][1]["description"]: "The type that query operations will be rooted at." != "If this server supports mutation, the type that mutation operations will be rooted at." - Data["schemaType"]["fields"][2]["name"]: "mutationType" != "subscriptionType" - Data["schemaType"]["fields"][2]["description"]: "If this server supports mutation, the type that mutation - operations will be rooted at." != "If this server support subscription, the type that subscription operations will be rooted at." Data["schemaType"]["fields"][3]["name"]: "subscriptionType" != "directives" - Data["schemaType"]["fields"][3]["description"]: "If this server supports subscription, the type that subscription operations will be rooted at." != "A list of all directives supported by this server." - Data["schemaType"]["fields"][4]["description"]: "A list of all directives supported by this server." != "A list of all types supported by this server." - Data["schemaType"]["fields"][4]["name"]: "directives" != "types"] - - */ expected := &graphql.Result{ Data: map[string]interface{}{ "schemaType": map[string]interface{}{ "name": "__Schema", - "description": `A GraphQL Schema defines the capabilities of a GraphQL -server. It exposes all available types and directives on -the server, as well as the entry points for query and -mutation operations.`, + "description": `A GraphQL Schema defines the capabilities of a GraphQL ` + + `server. It exposes all available types and directives on ` + + `the server, as well as the entry points for query, mutation, ` + + `and subscription operations.`, "fields": []interface{}{ map[string]interface{}{ "name": "types", diff --git a/kitchen-sink.graphql b/kitchen-sink.graphql index c28e85d6..d075edfd 100644 --- a/kitchen-sink.graphql +++ b/kitchen-sink.graphql @@ -29,6 +29,19 @@ mutation favPost { } } +subscription PostFavSubscription($input: StoryLikeSubscribeInput) { + postFavSubscribe(input: $input) { + post { + favers { + count + } + favSentence { + text + } + } + } +} + fragment frag on Follower { foo(size: $size, bar: $b, obj: {key: "value"}) } diff --git a/language/printer/printer_test.go b/language/printer/printer_test.go index bd4782e5..760ec65a 100644 --- a/language/printer/printer_test.go +++ b/language/printer/printer_test.go @@ -158,6 +158,19 @@ mutation favPost { } } +subscription PostFavSubscription($input: StoryLikeSubscribeInput) { + postFavSubscribe(input: $input) { + post { + favers { + count + } + favSentence { + text + } + } + } +} + fragment frag on Follower { foo(size: $size, bar: $b, obj: {key: "value"}) } diff --git a/language/visitor/visitor_test.go b/language/visitor/visitor_test.go index c3d95c0c..c4f57788 100644 --- a/language/visitor/visitor_test.go +++ b/language/visitor/visitor_test.go @@ -513,6 +513,7 @@ func TestVisitor_VisitsKitchenSink(t *testing.T) { []interface{}{"leave", "Name", "Name", "Directive"}, []interface{}{"leave", "Directive", 0, nil}, []interface{}{"enter", "SelectionSet", "SelectionSet", "InlineFragment"}, + []interface{}{"enter", "Field", 0, nil}, []interface{}{"enter", "Name", "Name", "Field"}, []interface{}{"leave", "Name", "Name", "Field"}, @@ -577,6 +578,7 @@ func TestVisitor_VisitsKitchenSink(t *testing.T) { []interface{}{"enter", "Variable", "Value", "Argument"}, []interface{}{"enter", "Name", "Name", "Variable"}, []interface{}{"leave", "Name", "Name", "Variable"}, + []interface{}{"leave", "Variable", "Value", "Argument"}, []interface{}{"leave", "Argument", 0, nil}, []interface{}{"leave", "Directive", 0, nil}, @@ -631,7 +633,64 @@ func TestVisitor_VisitsKitchenSink(t *testing.T) { []interface{}{"leave", "Field", 0, nil}, []interface{}{"leave", "SelectionSet", "SelectionSet", "OperationDefinition"}, []interface{}{"leave", "OperationDefinition", 1, nil}, - []interface{}{"enter", "FragmentDefinition", 2, nil}, + []interface{}{"enter", "OperationDefinition", 2, nil}, + []interface{}{"enter", "Name", "Name", "OperationDefinition"}, + []interface{}{"leave", "Name", "Name", "OperationDefinition"}, + []interface{}{"enter", "VariableDefinition", 0, nil}, + []interface{}{"enter", "Variable", "Variable", "VariableDefinition"}, + []interface{}{"enter", "Name", "Name", "Variable"}, + []interface{}{"leave", "Name", "Name", "Variable"}, + + []interface{}{"leave", "Variable", "Variable", "VariableDefinition"}, + []interface{}{"enter", "Named", "Type", "VariableDefinition"}, + []interface{}{"enter", "Name", "Name", "Named"}, + []interface{}{"leave", "Name", "Name", "Named"}, + []interface{}{"leave", "Named", "Type", "VariableDefinition"}, + []interface{}{"leave", "VariableDefinition", 0, nil}, + []interface{}{"enter", "SelectionSet", "SelectionSet", "OperationDefinition"}, + []interface{}{"enter", "Field", 0, nil}, + []interface{}{"enter", "Name", "Name", "Field"}, + []interface{}{"leave", "Name", "Name", "Field"}, + []interface{}{"enter", "Argument", 0, nil}, + []interface{}{"enter", "Name", "Name", "Argument"}, + []interface{}{"leave", "Name", "Name", "Argument"}, + []interface{}{"enter", "Variable", "Value", "Argument"}, + []interface{}{"enter", "Name", "Name", "Variable"}, + []interface{}{"leave", "Name", "Name", "Variable"}, + []interface{}{"leave", "Variable", "Value", "Argument"}, + []interface{}{"leave", "Argument", 0, nil}, + []interface{}{"enter", "SelectionSet", "SelectionSet", "Field"}, + []interface{}{"enter", "Field", 0, nil}, + []interface{}{"enter", "Name", "Name", "Field"}, + []interface{}{"leave", "Name", "Name", "Field"}, + []interface{}{"enter", "SelectionSet", "SelectionSet", "Field"}, + []interface{}{"enter", "Field", 0, nil}, + []interface{}{"enter", "Name", "Name", "Field"}, + []interface{}{"leave", "Name", "Name", "Field"}, + []interface{}{"enter", "SelectionSet", "SelectionSet", "Field"}, + []interface{}{"enter", "Field", 0, nil}, + []interface{}{"enter", "Name", "Name", "Field"}, + []interface{}{"leave", "Name", "Name", "Field"}, + []interface{}{"leave", "Field", 0, nil}, + []interface{}{"leave", "SelectionSet", "SelectionSet", "Field"}, + []interface{}{"leave", "Field", 0, nil}, + []interface{}{"enter", "Field", 1, nil}, + []interface{}{"enter", "Name", "Name", "Field"}, + []interface{}{"leave", "Name", "Name", "Field"}, + []interface{}{"enter", "SelectionSet", "SelectionSet", "Field"}, + []interface{}{"enter", "Field", 0, nil}, + []interface{}{"enter", "Name", "Name", "Field"}, + []interface{}{"leave", "Name", "Name", "Field"}, + []interface{}{"leave", "Field", 0, nil}, + []interface{}{"leave", "SelectionSet", "SelectionSet", "Field"}, + []interface{}{"leave", "Field", 1, nil}, + []interface{}{"leave", "SelectionSet", "SelectionSet", "Field"}, + []interface{}{"leave", "Field", 0, nil}, + []interface{}{"leave", "SelectionSet", "SelectionSet", "Field"}, + []interface{}{"leave", "Field", 0, nil}, + []interface{}{"leave", "SelectionSet", "SelectionSet", "OperationDefinition"}, + []interface{}{"leave", "OperationDefinition", 2, nil}, + []interface{}{"enter", "FragmentDefinition", 3, nil}, []interface{}{"enter", "Name", "Name", "FragmentDefinition"}, []interface{}{"leave", "Name", "Name", "FragmentDefinition"}, []interface{}{"enter", "Named", "TypeCondition", "FragmentDefinition"}, @@ -645,6 +704,7 @@ func TestVisitor_VisitsKitchenSink(t *testing.T) { []interface{}{"enter", "Argument", 0, nil}, []interface{}{"enter", "Name", "Name", "Argument"}, []interface{}{"leave", "Name", "Name", "Argument"}, + []interface{}{"enter", "Variable", "Value", "Argument"}, []interface{}{"enter", "Name", "Name", "Variable"}, []interface{}{"leave", "Name", "Name", "Variable"}, @@ -672,8 +732,8 @@ func TestVisitor_VisitsKitchenSink(t *testing.T) { []interface{}{"leave", "Argument", 2, nil}, []interface{}{"leave", "Field", 0, nil}, []interface{}{"leave", "SelectionSet", "SelectionSet", "FragmentDefinition"}, - []interface{}{"leave", "FragmentDefinition", 2, nil}, - []interface{}{"enter", "OperationDefinition", 3, nil}, + []interface{}{"leave", "FragmentDefinition", 3, nil}, + []interface{}{"enter", "OperationDefinition", 4, nil}, []interface{}{"enter", "SelectionSet", "SelectionSet", "OperationDefinition"}, []interface{}{"enter", "Field", 0, nil}, []interface{}{"enter", "Name", "Name", "Field"}, @@ -696,7 +756,7 @@ func TestVisitor_VisitsKitchenSink(t *testing.T) { []interface{}{"leave", "Name", "Name", "Field"}, []interface{}{"leave", "Field", 1, nil}, []interface{}{"leave", "SelectionSet", "SelectionSet", "OperationDefinition"}, - []interface{}{"leave", "OperationDefinition", 3, nil}, + []interface{}{"leave", "OperationDefinition", 4, nil}, []interface{}{"leave", "Document", nil, nil}, } diff --git a/rules_lone_anonymous_operation_rule_test.go b/rules_lone_anonymous_operation_rule_test.go index cefaff64..8fb6894f 100644 --- a/rules_lone_anonymous_operation_rule_test.go +++ b/rules_lone_anonymous_operation_rule_test.go @@ -56,7 +56,20 @@ func TestValidate_AnonymousOperationMustBeAlone_MultipleAnonOperations(t *testin testutil.RuleError(`This anonymous operation must be the only defined operation.`, 5, 7), }) } -func TestValidate_AnonymousOperationMustBeAlone_AnonOperationWithAnotherOperation(t *testing.T) { +func TestValidate_AnonymousOperationMustBeAlone_AnonOperationWithAMutation(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.LoneAnonymousOperationRule, ` + { + fieldA + } + mutation Foo { + fieldB + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`This anonymous operation must be the only defined operation.`, 2, 7), + }) +} + +func TestValidate_AnonymousOperationMustBeAlone_AnonOperationWithASubscription(t *testing.T) { testutil.ExpectFailsRule(t, graphql.LoneAnonymousOperationRule, ` { fieldA diff --git a/rules_unique_operation_names_test.go b/rules_unique_operation_names_test.go index 7004819e..8903cdcb 100644 --- a/rules_unique_operation_names_test.go +++ b/rules_unique_operation_names_test.go @@ -49,6 +49,10 @@ func TestValidate_UniqueOperationNames_MultipleOperationsOfDifferentTypes(t *tes mutation Bar { field } + + subscription Baz { + field + } `) } func TestValidate_UniqueOperationNames_FragmentAndOperationNamedTheSame(t *testing.T) { @@ -73,7 +77,7 @@ func TestValidate_UniqueOperationNames_MultipleOperationsOfSameName(t *testing.T testutil.RuleError(`There can only be one operation named "Foo".`, 2, 13, 5, 13), }) } -func TestValidate_UniqueOperationNames_MultipleOperationsOfSameNameOfDifferentTypes(t *testing.T) { +func TestValidate_UniqueOperationNames_MultipleOperationsOfSameNameOfDifferentTypes_Mutation(t *testing.T) { testutil.ExpectFailsRule(t, graphql.UniqueOperationNamesRule, ` query Foo { fieldA @@ -85,3 +89,16 @@ func TestValidate_UniqueOperationNames_MultipleOperationsOfSameNameOfDifferentTy testutil.RuleError(`There can only be one operation named "Foo".`, 2, 13, 5, 16), }) } + +func TestValidate_UniqueOperationNames_MultipleOperationsOfSameNameOfDifferentTypes_Subscription(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueOperationNamesRule, ` + query Foo { + fieldA + } + subscription Foo { + fieldB + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can only be one operation named "Foo".`, 2, 13, 5, 20), + }) +} diff --git a/validation_test.go b/validation_test.go index 42fde9cc..cd2ecb90 100644 --- a/validation_test.go +++ b/validation_test.go @@ -294,6 +294,23 @@ func TestTypeSystem_SchemaMustHaveObjectRootTypes_AcceptsASchemaWhoseQueryAndMut t.Fatalf("unexpected error: %v", err) } } +func TestTypeSystem_SchemaMustHaveObjectRootTypes_AcceptsASchemaWhoseQueryAndSubscriptionTypesAreObjectType(t *testing.T) { + subscriptionType := graphql.NewObject(graphql.ObjectConfig{ + Name: "Subscription", + Fields: graphql.Fields{ + "subscribe": &graphql.Field{ + Type: graphql.String, + }, + }, + }) + _, err := graphql.NewSchema(graphql.SchemaConfig{ + Query: someObjectType, + Mutation: subscriptionType, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} func TestTypeSystem_SchemaMustHaveObjectRootTypes_RejectsASchemaWithoutAQueryType(t *testing.T) { _, err := graphql.NewSchema(graphql.SchemaConfig{}) expectedError := "Schema query must be Object Type but got: nil." From 201054b521799e57dcaa91d45d0bd03d9680bf2f Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Fri, 11 Mar 2016 19:16:10 +0800 Subject: [PATCH 27/69] Add to unit tests to ensure test accepts both edits of enter and leave Commit: d2c005afc87353eceadc03592e74bd6e44063e8e [d2c005a] Parents: f79ba42e30 Author: Lee Byron Date: 23 February 2016 at 8:08:25 AM SGT --- language/visitor/visitor_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/language/visitor/visitor_test.go b/language/visitor/visitor_test.go index c4f57788..127937b2 100644 --- a/language/visitor/visitor_test.go +++ b/language/visitor/visitor_test.go @@ -38,6 +38,16 @@ func TestVisitor_AllowsEditingANodeBothOnEnterAndOnLeave(t *testing.T) { expectedQuery := `{ a, b, c { a, b, c } }` expectedAST := parse(t, expectedQuery) + visited := map[string]bool{ + "didEnter": false, + "didLeave": false, + } + + expectedVisited := map[string]bool{ + "didEnter": true, + "didLeave": true, + } + v := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ @@ -45,6 +55,7 @@ func TestVisitor_AllowsEditingANodeBothOnEnterAndOnLeave(t *testing.T) { Enter: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.OperationDefinition); ok { selectionSet = node.SelectionSet + visited["didEnter"] = true return visitor.ActionUpdate, ast.NewOperationDefinition(&ast.OperationDefinition{ Loc: node.Loc, Operation: node.Operation, @@ -60,6 +71,7 @@ func TestVisitor_AllowsEditingANodeBothOnEnterAndOnLeave(t *testing.T) { }, Leave: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.OperationDefinition); ok { + visited["didLeave"] = true return visitor.ActionUpdate, ast.NewOperationDefinition(&ast.OperationDefinition{ Loc: node.Loc, Operation: node.Operation, @@ -80,6 +92,10 @@ func TestVisitor_AllowsEditingANodeBothOnEnterAndOnLeave(t *testing.T) { t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedAST, editedAst)) } + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(visited, expectedVisited)) + } + } func TestVisitor_AllowsEditingTheRootNodeOnEnterAndOnLeave(t *testing.T) { @@ -91,12 +107,23 @@ func TestVisitor_AllowsEditingTheRootNodeOnEnterAndOnLeave(t *testing.T) { expectedQuery := `{ a, b, c { a, b, c } }` expectedAST := parse(t, expectedQuery) + visited := map[string]bool{ + "didEnter": false, + "didLeave": false, + } + + expectedVisited := map[string]bool{ + "didEnter": true, + "didLeave": true, + } + v := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ kinds.Document: visitor.NamedVisitFuncs{ Enter: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.Document); ok { + visited["didEnter"] = true return visitor.ActionUpdate, ast.NewDocument(&ast.Document{ Loc: node.Loc, Definitions: []ast.Node{}, @@ -106,6 +133,7 @@ func TestVisitor_AllowsEditingTheRootNodeOnEnterAndOnLeave(t *testing.T) { }, Leave: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.Document); ok { + visited["didLeave"] = true return visitor.ActionUpdate, ast.NewDocument(&ast.Document{ Loc: node.Loc, Definitions: definitions, @@ -122,6 +150,9 @@ func TestVisitor_AllowsEditingTheRootNodeOnEnterAndOnLeave(t *testing.T) { t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedAST, editedAst)) } + if !reflect.DeepEqual(visited, expectedVisited) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(visited, expectedVisited)) + } } func TestVisitor_AllowsForEditingOnEnter(t *testing.T) { From ba491dd28a7e687d349af4f17fd8149629ff1d46 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 14 Mar 2016 08:05:08 +0800 Subject: [PATCH 28/69] Allow providing directives to GraphQLSchema This allows GraphQLSchema to represent different sets of directives than those graphql-js is able to use during execution. Critically, this allows GraphQLSchema to be used against servers which support different sets of directives and still use validation utilities. Commit: 2fd6287e3b12f94c5314a0e5a4a74af3ebda82a3 [2fd6287] Parents: 3a6b4d130f Author: Lee Byron Date: 27 October 2015 at 6:29:12 AM SGT Commit Date: 27 October 2015 at 7:40:04 AM SGT --- rules_known_directives_rule_test.go | 6 +++-- schema.go | 35 ++++++++++++++++++----------- testutil/rules_test_harness.go | 8 +++++++ 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/rules_known_directives_rule_test.go b/rules_known_directives_rule_test.go index 0ece1888..1a5e7d5e 100644 --- a/rules_known_directives_rule_test.go +++ b/rules_known_directives_rule_test.go @@ -75,10 +75,12 @@ func TestValidate_KnownDirectives_WithWellPlacedDirectives(t *testing.T) { func TestValidate_KnownDirectives_WithMisplacedDirectives(t *testing.T) { testutil.ExpectFailsRule(t, graphql.KnownDirectivesRule, ` query Foo @include(if: true) { - name - ...Frag + name @operationOnly + ...Frag @operationOnly } `, []gqlerrors.FormattedError{ testutil.RuleError(`Directive "include" may not be used on "operation".`, 2, 17), + testutil.RuleError(`Directive "operationOnly" may not be used on "field".`, 3, 14), + testutil.RuleError(`Directive "operationOnly" may not be used on "fragment".`, 4, 17), }) } diff --git a/schema.go b/schema.go index 2bc1d53a..18dc7138 100644 --- a/schema.go +++ b/schema.go @@ -20,15 +20,19 @@ type SchemaConfig struct { Query *Object Mutation *Object Subscription *Object + Directives []*Directive } // chose to name as TypeMap instead of TypeMap type TypeMap map[string]Type type Schema struct { - schemaConfig SchemaConfig - typeMap TypeMap - directives []*Directive + typeMap TypeMap + directives []*Directive + + queryType *Object + mutationType *Object + subscriptionType *Object } func NewSchema(config SchemaConfig) (Schema, error) { @@ -49,7 +53,18 @@ func NewSchema(config SchemaConfig) (Schema, error) { return schema, config.Mutation.err } - schema.schemaConfig = config + schema.queryType = config.Query + schema.mutationType = config.Mutation + schema.subscriptionType = config.Subscription + + // Provide `@include() and `@skip()` directives by default. + schema.directives = config.Directives + if len(schema.directives) == 0 { + schema.directives = []*Directive{ + IncludeDirective, + SkipDirective, + } + } // Build type map now to detect any errors within this schema. typeMap := TypeMap{} @@ -90,24 +105,18 @@ func NewSchema(config SchemaConfig) (Schema, error) { } func (gq *Schema) QueryType() *Object { - return gq.schemaConfig.Query + return gq.queryType } func (gq *Schema) MutationType() *Object { - return gq.schemaConfig.Mutation + return gq.mutationType } func (gq *Schema) SubscriptionType() *Object { - return gq.schemaConfig.Subscription + return gq.subscriptionType } func (gq *Schema) Directives() []*Directive { - if len(gq.directives) == 0 { - gq.directives = []*Directive{ - IncludeDirective, - SkipDirective, - } - } return gq.directives } diff --git a/testutil/rules_test_harness.go b/testutil/rules_test_harness.go index 6f840cbc..0a973519 100644 --- a/testutil/rules_test_harness.go +++ b/testutil/rules_test_harness.go @@ -444,6 +444,14 @@ func init() { }) schema, err := graphql.NewSchema(graphql.SchemaConfig{ Query: queryRoot, + Directives: []*graphql.Directive{ + graphql.NewDirective(&graphql.Directive{ + Name: "operationOnly", + OnOperation: true, + }), + graphql.IncludeDirective, + graphql.SkipDirective, + }, }) if err != nil { panic(err) From 723a432f414ed00968af31c73906efb3d61f2dd1 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 14 Mar 2016 08:16:20 +0800 Subject: [PATCH 29/69] introspection descriptions for scalars and introspection Commit: 0530394f9ac81e235070b67d9dd0a95b54b3c754 [0530394] Parents: 662e316f04 Author: Lee Byron Date: 18 September 2015 at 5:09:26 AM ----- Fix tests Commit: 8c52207af26bf9818479681811e881d747ef17c4 [8c52207] Parents: 0530394f9a Author: Lee Byron Date: 18 September 2015 at 9:37:18 AM SGT --- introspection.go | 27 ++++++++++++++++++++++++++- introspection_test.go | 2 +- scalars.go | 30 +++++++++++++++++++++++------- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/introspection.go b/introspection.go index f1defd0e..d6ab0c04 100644 --- a/introspection.go +++ b/introspection.go @@ -36,7 +36,7 @@ func init() { __TypeKind = NewEnum(EnumConfig{ Name: "__TypeKind", - Description: "An enum describing what kind of type a given __Type is", + Description: "An enum describing what kind of type a given `__Type` is", Values: EnumValueConfigMap{ "SCALAR": &EnumValueConfig{ Value: TypeKindScalar, @@ -83,6 +83,15 @@ func init() { // Note: some fields (for e.g "fields", "interfaces") are defined later due to cyclic reference __Type = NewObject(ObjectConfig{ Name: "__Type", + Description: "The fundamental unit of any GraphQL Schema is the type. There are " + + "many kinds of types in GraphQL as represented by the `__TypeKind` enum." + + "\n\nDepending on the kind of a type, certain fields describe " + + "information about that type. Scalar types provide no information " + + "beyond a name and description, while Enum types provide their values. " + + "Object and Interface types provide the fields they describe. Abstract " + + "types, Union and Interface, provide the Object types possible " + + "at runtime. List and NonNull types compose other types.", + Fields: Fields{ "kind": &Field{ Type: NewNonNull(__TypeKind), @@ -125,6 +134,9 @@ func init() { __InputValue = NewObject(ObjectConfig{ Name: "__InputValue", + Description: "Arguments provided to Fields or Directives and the input fields of an " + + "InputObject are represented as Input Values which describe their type " + + "and optionally a default value.", Fields: Fields{ "name": &Field{ Type: NewNonNull(String), @@ -137,6 +149,8 @@ func init() { }, "defaultValue": &Field{ Type: String, + Description: "A GraphQL-formatted string representing the default value for this " + + "input value.", Resolve: func(p ResolveParams) (interface{}, error) { if inputVal, ok := p.Source.(*Argument); ok { if inputVal.DefaultValue == nil { @@ -160,6 +174,8 @@ func init() { __Field = NewObject(ObjectConfig{ Name: "__Field", + Description: "Object and Interface types are described by a list of Fields, each of " + + "which has a name, potentially a list of arguments, and a return type.", Fields: Fields{ "name": &Field{ Type: NewNonNull(String), @@ -196,6 +212,12 @@ func init() { __Directive = NewObject(ObjectConfig{ Name: "__Directive", + Description: "A Directives provides a way to describe alternate runtime execution and " + + "type validation behavior in a GraphQL document. " + + "\n\nIn some cases, you need to provide options to alter GraphQL's " + + "execution behavior in ways field arguments will not suffice, such as " + + "conditionally including or skipping a field. Directives provide this by " + + "describing additional information to the executor.", Fields: Fields{ "name": &Field{ Type: NewNonNull(String), @@ -295,6 +317,9 @@ func init() { __EnumValue = NewObject(ObjectConfig{ Name: "__EnumValue", + Description: "One possible value for a given Enum. Enum values are unique values, not " + + "a placeholder for a string or numeric value. However an Enum value is " + + "returned in a JSON response as a string.", Fields: Fields{ "name": &Field{ Type: NewNonNull(String), diff --git a/introspection_test.go b/introspection_test.go index 4fe94437..6425b9db 100644 --- a/introspection_test.go +++ b/introspection_test.go @@ -1344,7 +1344,7 @@ func TestIntrospection_ExposesDescriptionsOnEnums(t *testing.T) { Data: map[string]interface{}{ "typeKindType": map[string]interface{}{ "name": "__TypeKind", - "description": `An enum describing what kind of type a given __Type is`, + "description": "An enum describing what kind of type a given `__Type` is", "enumValues": []interface{}{ map[string]interface{}{ "name": "SCALAR", diff --git a/scalars.go b/scalars.go index 14782369..c752b852 100644 --- a/scalars.go +++ b/scalars.go @@ -69,7 +69,11 @@ func coerceInt(value interface{}) interface{} { // Int is the GraphQL Integer type definition. var Int *Scalar = NewScalar(ScalarConfig{ - Name: "Int", + Name: "Int", + Description: "The `Int` scalar type represents non-fractional signed whole numeric " + + "values. Int can represent values between -(2^53 - 1) and 2^53 - 1 since " + + "represented in JSON as double-precision floating point numbers specified" + + "by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).", Serialize: coerceInt, ParseValue: coerceInt, ParseLiteral: func(valueAST ast.Value) interface{} { @@ -108,7 +112,10 @@ func coerceFloat32(value interface{}) interface{} { // Float is the GraphQL float type definition. var Float *Scalar = NewScalar(ScalarConfig{ - Name: "Float", + Name: "Float", + Description: "The `Float` scalar type represents signed double-precision fractional " + + "values as specified by " + + "[IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ", Serialize: coerceFloat32, ParseValue: coerceFloat32, ParseLiteral: func(valueAST ast.Value) interface{} { @@ -132,7 +139,10 @@ func coerceString(value interface{}) interface{} { // String is the GraphQL string type definition var String *Scalar = NewScalar(ScalarConfig{ - Name: "String", + Name: "String", + Description: "The `String` scalar type represents textual data, represented as UTF-8 " + + "character sequences. The String type is most often used by GraphQL to " + + "represent free-form human-readable text.", Serialize: coerceString, ParseValue: coerceString, ParseLiteral: func(valueAST ast.Value) interface{} { @@ -175,9 +185,10 @@ func coerceBool(value interface{}) interface{} { // Boolean is the GraphQL boolean type definition var Boolean *Scalar = NewScalar(ScalarConfig{ - Name: "Boolean", - Serialize: coerceBool, - ParseValue: coerceBool, + Name: "Boolean", + Description: "The `Boolean` scalar type represents `true` or `false`.", + Serialize: coerceBool, + ParseValue: coerceBool, ParseLiteral: func(valueAST ast.Value) interface{} { switch valueAST := valueAST.(type) { case *ast.BooleanValue: @@ -189,7 +200,12 @@ var Boolean *Scalar = NewScalar(ScalarConfig{ // ID is the GraphQL id type definition var ID *Scalar = NewScalar(ScalarConfig{ - Name: "ID", + Name: "ID", + Description: "The `ID` scalar type represents a unique identifier, often used to " + + "refetch an object or as key for a cache. The ID type appears in a JSON " + + "response as a String; however, it is not intended to be human-readable. " + + "When expected as an input type, any string (such as `\"4\"`) or integer " + + "(such as `4`) input value will be accepted as an ID.", Serialize: coerceString, ParseValue: coerceString, ParseLiteral: func(valueAST ast.Value) interface{} { From 793e4d812d51036fc5e7c7c37cdcdd71420d745a Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 14 Mar 2016 08:18:21 +0800 Subject: [PATCH 30/69] Fix typo on introspection Commit: a4c4a97eadc670dca8ddf61d8eecb9585ecc99cf [a4c4a97] Parents: e41ffa5950 Author: Jan Kassens Date: 12 November 2015 at 6:30:53 AM SGT --- introspection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/introspection.go b/introspection.go index d6ab0c04..f3be69a2 100644 --- a/introspection.go +++ b/introspection.go @@ -212,7 +212,7 @@ func init() { __Directive = NewObject(ObjectConfig{ Name: "__Directive", - Description: "A Directives provides a way to describe alternate runtime execution and " + + Description: "A Directive provides a way to describe alternate runtime execution and " + "type validation behavior in a GraphQL document. " + "\n\nIn some cases, you need to provide options to alter GraphQL's " + "execution behavior in ways field arguments will not suffice, such as " + From 198b94fe79bf194ea142029bec55ce0e6b25204c Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 14 Mar 2016 08:20:03 +0800 Subject: [PATCH 31/69] Update dependencies and fix newly found lint error Commit: 30c39ecc13776b7a2ad4488ca38f6b3ea613f181 [30c39ec] Parents: ca003e48c9 Author: Lee Byron Date: 26 November 2015 at 7:38:06 AM SGT --- introspection.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/introspection.go b/introspection.go index f3be69a2..a59d6c90 100644 --- a/introspection.go +++ b/introspection.go @@ -156,6 +156,9 @@ func init() { if inputVal.DefaultValue == nil { return nil, nil } + if isNullish(inputVal.DefaultValue) { + return nil, nil + } astVal := astFromValue(inputVal.DefaultValue, inputVal) return printer.Print(astVal), nil } From c129ede581ccdfa08981c28954208fae18b051e3 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 14 Mar 2016 08:45:04 +0800 Subject: [PATCH 32/69] Spec compliant Int sizing As discussed in #182, this was a deviation between spec and reference implementation, where the spec is more reasonable. This tightens the allowed range of Int values to valid 32-bit signed integers, supporting the broadest collection of platforms. For those who stumble upon this rev looking for ways to represent numeric-looking large values, see ID or Custom Scalars. Additional notes: ------------------ Added spec compliance for `uint` as well. Commit: 06f97b67491f0215df7536aac50361bd90d5097d [06f97b6] Parents: 0d03392427 Author: Lee Byron Date: 1 December 2015 at 4:09:50 PM SGT Commit Date: 1 December 2015 at 4:09:52 PM SGT --- scalars.go | 11 +++++++++++ scalars_serialization_test.go | 11 ++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/scalars.go b/scalars.go index c752b852..b736a886 100644 --- a/scalars.go +++ b/scalars.go @@ -8,6 +8,11 @@ import ( "github.com/graphql-go/graphql/language/ast" ) +// As per the GraphQL Spec, Integers are only treated as valid when a valid +// 32-bit signed integer, providing the broadest support across platforms. +// +// n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because +// they are internally represented as IEEE 754 doubles. func coerceInt(value interface{}) interface{} { switch value := value.(type) { case bool: @@ -16,6 +21,9 @@ func coerceInt(value interface{}) interface{} { } return 0 case int: + if value < int(math.MinInt32) || value > int(math.MaxInt32) { + return nil + } return value case int8: return int(value) @@ -29,6 +37,9 @@ func coerceInt(value interface{}) interface{} { } return int(value) case uint: + if value > math.MaxInt32 { + return nil + } return int(value) case uint8: return int(value) diff --git a/scalars_serialization_test.go b/scalars_serialization_test.go index 96c5ff4d..4dbe2488 100644 --- a/scalars_serialization_test.go +++ b/scalars_serialization_test.go @@ -35,11 +35,13 @@ func TestTypeSystem_Scalar_SerializesOutputInt(t *testing.T) { {float32(0.1), 0}, {float32(1.1), 1}, {float32(-1.1), -1}, - // Bigger than 2^32, but still representable as an Int {float32(1e5), 100000}, {float32(math.MaxFloat32), nil}, - {9876504321, 9876504321}, - {-9876504321, -9876504321}, + // Maybe a safe Go/Javascript `int`, but bigger than 2^32, so not + // representable as a GraphQL Int + {9876504321, nil}, + {-9876504321, nil}, + // Too big to represent as an Int in Go, JavaScript or GraphQL {float64(1e100), nil}, {float64(-1e100), nil}, {"-1.1", -1}, @@ -51,6 +53,9 @@ func TestTypeSystem_Scalar_SerializesOutputInt(t *testing.T) { {int32(1), 1}, {int64(1), 1}, {uint(1), 1}, + // Maybe a safe Go `uint`, but bigger than 2^32, so not + // representable as a GraphQL Int + {uint(math.MaxInt32 + 1), nil}, {uint8(1), 1}, {uint16(1), 1}, {uint32(1), 1}, From 68e94af728ef3c4b63544f7c54df99f43ac25f4f Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 14 Mar 2016 08:51:16 +0800 Subject: [PATCH 33/69] Update Int type description to match implementation This commits updates the `GraphQLInt` type description to match the changes made in 06f97b6. Commit: 611f4b1d473907fde31c99a6008f5ef387836a14 [611f4b1] Parents: a6bcc75d3c Author: Ben Johnson Date: 4 December 2015 at 3:43:09 AM SGT --- scalars.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scalars.go b/scalars.go index b736a886..ea04e3a5 100644 --- a/scalars.go +++ b/scalars.go @@ -82,9 +82,7 @@ func coerceInt(value interface{}) interface{} { var Int *Scalar = NewScalar(ScalarConfig{ Name: "Int", Description: "The `Int` scalar type represents non-fractional signed whole numeric " + - "values. Int can represent values between -(2^53 - 1) and 2^53 - 1 since " + - "represented in JSON as double-precision floating point numbers specified" + - "by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).", + "values. Int can represent values between -(2^31) and 2^31 - 1. ", Serialize: coerceInt, ParseValue: coerceInt, ParseLiteral: func(valueAST ast.Value) interface{} { From 04849a07502fe80c9ec72103bdd9e0e800b8f416 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 14 Mar 2016 11:04:35 +0800 Subject: [PATCH 34/69] [Schema] Implementing interfaces with covariant return types. This proposes loosening the definition of implementing an interface by allowing an implementing field to return a subtype of the interface field's return type. This example would previously be an illegal schema, but become legal after this diff: ```graphql interface Friendly { bestFriend: Friendly } type Person implements Friendly { bestFriend: Person } ``` Commit: edbe063718590d84594f2b8e06dc6fd67e1f3ec2 [edbe063] Parents: e81cf39750 Author: Lee Byron Date: 17 November 2015 at 2:38:16 PM SGT Commit Date: 18 November 2015 at 2:17:57 AM SGT --- rules.go | 36 ++-------- schema.go | 64 ++++++++++++++++- validation_test.go | 173 +++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 228 insertions(+), 45 deletions(-) diff --git a/rules.go b/rules.go index d46520a8..dac0dc07 100644 --- a/rules.go +++ b/rules.go @@ -1085,11 +1085,6 @@ func sameValue(value1 ast.Value, value2 ast.Value) bool { return val1 == val2 } -func sameType(type1 Type, type2 Type) bool { - t := fmt.Sprintf("%v", type1) - t2 := fmt.Sprintf("%v", type2) - return t == t2 -} /** * OverlappingFieldsCanBeMergedRule @@ -1143,7 +1138,7 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul type2 = def2.Type } - if type1 != nil && type2 != nil && !sameType(type1, type2) { + if type1 != nil && type2 != nil && !isEqualType(type1, type2) { return &conflict{ Reason: conflictReason{ Name: responseName, @@ -1780,28 +1775,6 @@ func effectiveType(varType Type, varDef *ast.VariableDefinition) Type { return NewNonNull(varType) } -// A var type is allowed if it is the same or more strict than the expected -// type. It can be more strict if the variable type is non-null when the -// expected type is nullable. If both are list types, the variable item type can -// be more strict than the expected item type. -func varTypeAllowedForType(varType Type, expectedType Type) bool { - if expectedType, ok := expectedType.(*NonNull); ok { - if varType, ok := varType.(*NonNull); ok { - return varTypeAllowedForType(varType.OfType, expectedType.OfType) - } - return false - } - if varType, ok := varType.(*NonNull); ok { - return varTypeAllowedForType(varType.OfType, expectedType) - } - if varType, ok := varType.(*List); ok { - if expectedType, ok := expectedType.(*List); ok { - return varTypeAllowedForType(varType.OfType, expectedType.OfType) - } - } - return varType == expectedType -} - /** * VariablesInAllowedPositionRule * Variables passed to field arguments conform to type @@ -1829,6 +1802,11 @@ func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleI var varType Type varDef, ok := varDefMap[varName] if ok { + // A var type is allowed if it is the same or more strict (e.g. is + // a subtype of) than the expected type. It can be more strict if + // the variable type is non-null when the expected type is nullable. + // If both are list types, the variable item type can be more strict + // than the expected item type (contravariant). var err error varType, err = typeFromAST(*context.Schema(), varDef.Type) if err != nil { @@ -1837,7 +1815,7 @@ func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleI } if varType != nil && usage.Type != nil && - !varTypeAllowedForType(effectiveType(varType, varDef), usage.Type) { + !isTypeSubTypeOf(effectiveType(varType, varDef), usage.Type) { reportError( context, fmt.Sprintf(`Variable "$%v" of type "%v" used in position `+ diff --git a/schema.go b/schema.go index 18dc7138..ac013e5b 100644 --- a/schema.go +++ b/schema.go @@ -288,9 +288,10 @@ func assertObjectImplementsInterface(object *Object, iface *Interface) error { return err } - // Assert interface field type matches object field type. (invariant) + // Assert interface field type is satisfied by object field type, by being + // a valid subtype. (covariant) err = invariant( - isEqualType(ifaceField.Type, objectField.Type), + isTypeSubTypeOf(objectField.Type, ifaceField.Type), fmt.Sprintf(`%v.%v expects type "%v" but `+ `%v.%v provides type "%v".`, iface, fieldName, ifaceField.Type, @@ -363,15 +364,72 @@ func assertObjectImplementsInterface(object *Object, iface *Interface) error { } func isEqualType(typeA Type, typeB Type) bool { + // Equivalent type is a valid subtype + if typeA == typeB { + return true + } + // If either type is non-null, the other must also be non-null. if typeA, ok := typeA.(*NonNull); ok { if typeB, ok := typeB.(*NonNull); ok { return isEqualType(typeA.OfType, typeB.OfType) } } + // If either type is a list, the other must also be a list. if typeA, ok := typeA.(*List); ok { if typeB, ok := typeB.(*List); ok { return isEqualType(typeA.OfType, typeB.OfType) } } - return typeA == typeB + // Otherwise the types are not equal. + return false +} + +/** + * Provided a type and a super type, return true if the first type is either + * equal or a subset of the second super type (covariant). + */ +func isTypeSubTypeOf(maybeSubType Type, superType Type) bool { + // Equivalent type is a valid subtype + if maybeSubType == superType { + return true + } + + // If superType is non-null, maybeSubType must also be nullable. + if superType, ok := superType.(*NonNull); ok { + if maybeSubType, ok := maybeSubType.(*NonNull); ok { + return isTypeSubTypeOf(maybeSubType.OfType, superType.OfType) + } + return false + } + if maybeSubType, ok := maybeSubType.(*NonNull); ok { + // If superType is nullable, maybeSubType may be non-null. + return isTypeSubTypeOf(maybeSubType.OfType, superType) + } + + // If superType type is a list, maybeSubType type must also be a list. + if superType, ok := superType.(*List); ok { + if maybeSubType, ok := maybeSubType.(*List); ok { + return isTypeSubTypeOf(maybeSubType.OfType, superType.OfType) + } + return false + } else if _, ok := maybeSubType.(*List); ok { + // If superType is not a list, maybeSubType must also be not a list. + return false + } + + // If superType type is an abstract type, maybeSubType type may be a currently + // possible object type. + if superType, ok := superType.(*Interface); ok { + if maybeSubType, ok := maybeSubType.(*Object); ok && superType.IsPossibleType(maybeSubType) { + return true + } + } + if superType, ok := superType.(*Union); ok { + if maybeSubType, ok := maybeSubType.(*Object); ok && superType.IsPossibleType(maybeSubType) { + return true + } + } + + // Otherwise, the child type is not a valid subtype of the parent type. + return false } diff --git a/validation_test.go b/validation_test.go index cd2ecb90..c174832f 100644 --- a/validation_test.go +++ b/validation_test.go @@ -1264,6 +1264,7 @@ func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectMis t.Fatalf("Expected error: %v, got %v", expectedError, err) } } + func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWithAnIncorrectlyTypedInterfaceField(t *testing.T) { anotherInterface := graphql.NewInterface(graphql.InterfaceConfig{ Name: "AnotherInterface", @@ -1273,11 +1274,6 @@ func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWit Fields: graphql.Fields{ "field": &graphql.Field{ Type: graphql.String, - Args: graphql.FieldConfigArgument{ - "input": &graphql.ArgumentConfig{ - Type: graphql.String, - }, - }, }, }, }) @@ -1287,11 +1283,6 @@ func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWit Fields: graphql.Fields{ "field": &graphql.Field{ Type: someScalarType, - Args: graphql.FieldConfigArgument{ - "input": &graphql.ArgumentConfig{ - Type: graphql.String, - }, - }, }, }, }) @@ -1301,6 +1292,79 @@ func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWit t.Fatalf("Expected error: %v, got %v", expectedError, err) } } + +func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWithADifferentlyTypeInterfaceField(t *testing.T) { + + typeA := graphql.NewObject(graphql.ObjectConfig{ + Name: "A", + Fields: graphql.Fields{ + "foo": &graphql.Field{ + Type: graphql.String, + }, + }, + }) + typeB := graphql.NewObject(graphql.ObjectConfig{ + Name: "B", + Fields: graphql.Fields{ + "foo": &graphql.Field{ + Type: graphql.String, + }, + }, + }) + + anotherInterface := graphql.NewInterface(graphql.InterfaceConfig{ + Name: "AnotherInterface", + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + return nil + }, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: typeA, + }, + }, + }) + anotherObject := graphql.NewObject(graphql.ObjectConfig{ + Name: "AnotherObject", + Interfaces: []*graphql.Interface{anotherInterface}, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: typeB, + }, + }, + }) + _, err := schemaWithObjectFieldOfType(anotherObject) + expectedError := `AnotherInterface.field expects type "A" but AnotherObject.field provides type "B".` + if err == nil || err.Error() != expectedError { + t.Fatalf("Expected error: %v, got %v", expectedError, err) + } +} + +func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_AcceptsAnObjectWithASubtyedInterfaceField_Union(t *testing.T) { + anotherInterface := graphql.NewInterface(graphql.InterfaceConfig{ + Name: "AnotherInterface", + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + return nil + }, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: someUnionType, + }, + }, + }) + anotherObject := graphql.NewObject(graphql.ObjectConfig{ + Name: "AnotherObject", + Interfaces: []*graphql.Interface{anotherInterface}, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: someObjectType, + }, + }, + }) + _, err := schemaWithFieldType(anotherObject) + if err != nil { + t.Fatalf(`unexpected error: %v for type "%v"`, err, anotherObject) + } +} func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectMissingAnInterfaceArgument(t *testing.T) { anotherInterface := graphql.NewInterface(graphql.InterfaceConfig{ Name: "AnotherInterface", @@ -1396,7 +1460,63 @@ func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_AcceptsAnObjectWit t.Fatalf(`unexpected error: %v for type "%v"`, err, anotherObject) } } -func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWithADifferentlyModifiedInterfaceFieldType(t *testing.T) { +func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWithANonListInterfaceFieldListType(t *testing.T) { + anotherInterface := graphql.NewInterface(graphql.InterfaceConfig{ + Name: "AnotherInterface", + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + return nil + }, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: graphql.NewList(graphql.String), + }, + }, + }) + anotherObject := graphql.NewObject(graphql.ObjectConfig{ + Name: "AnotherObject", + Interfaces: []*graphql.Interface{anotherInterface}, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: graphql.String, + }, + }, + }) + _, err := schemaWithFieldType(anotherObject) + expectedError := `AnotherInterface.field expects type "[String]" but AnotherObject.field provides type "String".` + if err == nil || err.Error() != expectedError { + t.Fatalf("Expected error: %v, got %v", expectedError, err) + } +} + +func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWithAListInterfaceFieldNonListType(t *testing.T) { + anotherInterface := graphql.NewInterface(graphql.InterfaceConfig{ + Name: "AnotherInterface", + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + return nil + }, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: graphql.String, + }, + }, + }) + anotherObject := graphql.NewObject(graphql.ObjectConfig{ + Name: "AnotherObject", + Interfaces: []*graphql.Interface{anotherInterface}, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: graphql.NewList(graphql.String), + }, + }, + }) + _, err := schemaWithFieldType(anotherObject) + expectedError := `AnotherInterface.field expects type "String" but AnotherObject.field provides type "[String]".` + if err == nil || err.Error() != expectedError { + t.Fatalf("Expected error: %v, got %v", expectedError, err) + } +} + +func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_AcceptsAnObjectWithSubsetNonNullInterfaceFieldType(t *testing.T) { anotherInterface := graphql.NewInterface(graphql.InterfaceConfig{ Name: "AnotherInterface", ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { @@ -1417,8 +1537,35 @@ func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWit }, }, }) - _, err := schemaWithObjectFieldOfType(anotherObject) - expectedError := `AnotherInterface.field expects type "String" but AnotherObject.field provides type "String!".` + _, err := schemaWithFieldType(anotherObject) + if err != nil { + t.Fatalf(`unexpected error: %v for type "%v"`, err, anotherObject) + } +} + +func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWithASupersetNullableInterfaceFieldType(t *testing.T) { + anotherInterface := graphql.NewInterface(graphql.InterfaceConfig{ + Name: "AnotherInterface", + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + return nil + }, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + }, + }, + }) + anotherObject := graphql.NewObject(graphql.ObjectConfig{ + Name: "AnotherObject", + Interfaces: []*graphql.Interface{anotherInterface}, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: graphql.String, + }, + }, + }) + _, err := schemaWithFieldType(anotherObject) + expectedError := `AnotherInterface.field expects type "String!" but AnotherObject.field provides type "String".` if err == nil || err.Error() != expectedError { t.Fatalf("Expected error: %v, got %v", expectedError, err) } From c2391b307044dc0b088b5e103de9e641043489f8 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 5 Apr 2016 00:22:03 +0800 Subject: [PATCH 35/69] Make input objects and lists return better errors Add index of invalid element or field of invalid field and make it recurse into children until faulty place is found. Commit: 2ada5c69ecf3901e6ce1a5fe73e04a6fbfa2c0f8 [2ada5c6] Parents: a941554fc5 Author: Mikhail Novikov Date: 12 October 2015 at 11:18:09 PM SGT Commit Date: 10 November 2015 at 9:59:42 PM SGT Improve error messages further Commit: 98364f61e716f5ff82ccd9432c264368d1052455 [98364f6] Parents: 2ada5c69ec Author: Mikhail Novikov Date: 11 November 2015 at 5:10:41 PM SGT Labels: HEAD --- enum_type_test.go | 21 ++++- rules.go | 70 +++++++++------ rules_arguments_of_correct_type_test.go | 72 +++++++-------- rules_default_values_of_correct_type_test.go | 23 +++-- values.go | 81 ++++++++++++----- variables_test.go | 92 +++++++++++++++++--- 6 files changed, 258 insertions(+), 101 deletions(-) diff --git a/enum_type_test.go b/enum_type_test.go index 2d3a038e..b436dd09 100644 --- a/enum_type_test.go +++ b/enum_type_test.go @@ -6,6 +6,7 @@ import ( "github.com/graphql-go/graphql" "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/language/location" "github.com/graphql-go/graphql/testutil" ) @@ -178,7 +179,10 @@ func TestTypeSystem_EnumValues_DoesNotAcceptStringLiterals(t *testing.T) { Data: nil, Errors: []gqlerrors.FormattedError{ gqlerrors.FormattedError{ - Message: `Argument "fromEnum" expected type "Color" but got: "GREEN".`, + Message: "Argument \"fromEnum\" has invalid value \"GREEN\".\nExpected type \"Color\", found \"GREEN\".", + Locations: []location.SourceLocation{ + {Line: 1, Column: 23}, + }, }, }, } @@ -205,7 +209,10 @@ func TestTypeSystem_EnumValues_DoesNotAcceptInternalValueInPlaceOfEnumLiteral(t Data: nil, Errors: []gqlerrors.FormattedError{ gqlerrors.FormattedError{ - Message: `Argument "fromEnum" expected type "Color" but got: 1.`, + Message: "Argument \"fromEnum\" has invalid value 1.\nExpected type \"Color\", found 1.", + Locations: []location.SourceLocation{ + {Line: 1, Column: 23}, + }, }, }, } @@ -221,7 +228,10 @@ func TestTypeSystem_EnumValues_DoesNotAcceptEnumLiteralInPlaceOfInt(t *testing.T Data: nil, Errors: []gqlerrors.FormattedError{ gqlerrors.FormattedError{ - Message: `Argument "fromInt" expected type "Int" but got: GREEN.`, + Message: "Argument \"fromInt\" has invalid value GREEN.\nExpected type \"Int\", found GREEN.", + Locations: []location.SourceLocation{ + {Line: 1, Column: 23}, + }, }, }, } @@ -287,7 +297,10 @@ func TestTypeSystem_EnumValues_DoesNotAcceptInternalValueAsEnumVariable(t *testi Data: nil, Errors: []gqlerrors.FormattedError{ gqlerrors.FormattedError{ - Message: `Variable "$color" expected value of type "Color!" but got: 2.`, + Message: "Variable \"$color\" got invalid value 2.\nExpected type \"Color\", found \"2\".", + Locations: []location.SourceLocation{ + {Line: 1, Column: 12}, + }, }, }, } diff --git a/rules.go b/rules.go index dac0dc07..13c4e40c 100644 --- a/rules.go +++ b/rules.go @@ -79,15 +79,21 @@ func ArgumentsOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInsta if argAST, ok := p.Node.(*ast.Argument); ok { value := argAST.Value argDef := context.Argument() - if argDef != nil && !isValidLiteralValue(argDef.Type, value) { + isValid, messages := isValidLiteralValue(argDef.Type, value) + if argDef != nil && !isValid { argNameValue := "" if argAST.Name != nil { argNameValue = argAST.Name.Value } + + messagesStr := "" + if len(messages) > 0 { + messagesStr = "\n" + strings.Join(messages, "\n") + } return reportError( context, - fmt.Sprintf(`Argument "%v" expected type "%v" but got: %v.`, - argNameValue, argDef.Type, printer.Print(value)), + fmt.Sprintf(`Argument "%v" has invalid value %v.%v`, + argNameValue, printer.Print(value), messagesStr), []ast.Node{value}, ) } @@ -132,11 +138,16 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI []ast.Node{defaultValue}, ) } - if ttype != nil && defaultValue != nil && !isValidLiteralValue(ttype, defaultValue) { + isValid, messages := isValidLiteralValue(ttype, defaultValue) + if ttype != nil && defaultValue != nil && !isValid { + messagesStr := "" + if len(messages) > 0 { + messagesStr = "\n" + strings.Join(messages, "\n") + } return reportError( context, - fmt.Sprintf(`Variable "$%v" of type "%v" has invalid default value: %v.`, - name, ttype, printer.Print(defaultValue)), + fmt.Sprintf(`Variable "$%v" has invalid default value: %v.%v`, + name, printer.Print(defaultValue), messagesStr), []ast.Node{defaultValue}, ) } @@ -1858,36 +1869,41 @@ func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleI * Note that this only validates literal values, variables are assumed to * provide values of the correct type. */ -func isValidLiteralValue(ttype Input, valueAST ast.Value) bool { +func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) { // A value must be provided if the type is non-null. if ttype, ok := ttype.(*NonNull); ok { if valueAST == nil { - return false + if ttype.OfType.Name() != "" { + return false, []string{fmt.Sprintf(`Expected "%v!", found null.`, ttype.OfType.Name())} + } + return false, []string{"Expected non-null value, found null."} } ofType, _ := ttype.OfType.(Input) return isValidLiteralValue(ofType, valueAST) } if valueAST == nil { - return true + return true, nil } // This function only tests literals, and assumes variables will provide // values of the correct type. if valueAST.GetKind() == kinds.Variable { - return true + return true, nil } // Lists accept a non-list value as a list of one. if ttype, ok := ttype.(*List); ok { itemType, _ := ttype.OfType.(Input) if valueAST, ok := valueAST.(*ast.ListValue); ok { + messagesReduce := []string{} for _, value := range valueAST.Values { - if isValidLiteralValue(itemType, value) == false { - return false + _, messages := isValidLiteralValue(itemType, value) + for idx, message := range messages { + messagesReduce = append(messagesReduce, fmt.Sprintf(`In element #%v: %v`, idx+1, message)) } } - return true + return (len(messagesReduce) == 0), messagesReduce } return isValidLiteralValue(itemType, valueAST) @@ -1897,12 +1913,12 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) bool { if ttype, ok := ttype.(*InputObject); ok { valueAST, ok := valueAST.(*ast.ObjectValue) if !ok { - return false + return false, []string{fmt.Sprintf(`Expected "%v", found not an object.`, ttype.Name())} } fields := ttype.Fields() + messagesReduce := []string{} // Ensure every provided field is defined. - // Ensure every defined field is valid. fieldASTs := valueAST.Fields fieldASTMap := map[string]*ast.ObjectField{} for _, fieldAST := range fieldASTs { @@ -1913,35 +1929,39 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) bool { fieldASTMap[fieldASTName] = fieldAST - // check if field is defined field, ok := fields[fieldASTName] if !ok || field == nil { - return false + messagesReduce = append(messagesReduce, fmt.Sprintf(`Unknown field "%v".`, fieldASTName)) } } + // Ensure every defined field is valid. for fieldName, field := range fields { fieldAST, _ := fieldASTMap[fieldName] var fieldASTValue ast.Value if fieldAST != nil { fieldASTValue = fieldAST.Value } - if !isValidLiteralValue(field.Type, fieldASTValue) { - return false + if isValid, messages := isValidLiteralValue(field.Type, fieldASTValue); !isValid { + for _, message := range messages { + messagesReduce = append(messagesReduce, fmt.Sprintf("In field \"%v\": %v", fieldName, message)) + } } } - return true + return (len(messagesReduce) == 0), messagesReduce } if ttype, ok := ttype.(*Scalar); ok { - return !isNullish(ttype.ParseLiteral(valueAST)) + if isNullish(ttype.ParseLiteral(valueAST)) { + return false, []string{fmt.Sprintf(`Expected type "%v", found %v.`, ttype.Name(), printer.Print(valueAST))} + } } if ttype, ok := ttype.(*Enum); ok { - return !isNullish(ttype.ParseLiteral(valueAST)) + if isNullish(ttype.ParseLiteral(valueAST)) { + return false, []string{fmt.Sprintf(`Expected type "%v", found %v.`, ttype.Name(), printer.Print(valueAST))} + } } - // Must be input type (not scalar or enum) - // Silently fail, instead of panic() - return false + return true, nil } /** diff --git a/rules_arguments_of_correct_type_test.go b/rules_arguments_of_correct_type_test.go index 27a2443b..6c8de45d 100644 --- a/rules_arguments_of_correct_type_test.go +++ b/rules_arguments_of_correct_type_test.go @@ -91,7 +91,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidStringValues_IntIntoString(t *te `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "stringArg" expected type "String" but got: 1.`, + "Argument \"stringArg\" has invalid value 1.\nExpected type \"String\", found 1.", 4, 39, ), }) @@ -106,7 +106,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidStringValues_FloatIntoString(t * `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "stringArg" expected type "String" but got: 1.0.`, + "Argument \"stringArg\" has invalid value 1.0.\nExpected type \"String\", found 1.0.", 4, 39, ), }) @@ -121,7 +121,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidStringValues_BooleanIntoString(t `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "stringArg" expected type "String" but got: true.`, + "Argument \"stringArg\" has invalid value true.\nExpected type \"String\", found true.", 4, 39, ), }) @@ -136,7 +136,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidStringValues_UnquotedStringIntoS `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "stringArg" expected type "String" but got: BAR.`, + "Argument \"stringArg\" has invalid value BAR.\nExpected type \"String\", found BAR.", 4, 39, ), }) @@ -152,7 +152,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidIntValues_StringIntoInt(t *testi `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "intArg" expected type "Int" but got: "3".`, + "Argument \"intArg\" has invalid value \"3\".\nExpected type \"Int\", found \"3\".", 4, 33, ), }) @@ -167,7 +167,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidIntValues_BigIntIntoInt(t *testi `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "intArg" expected type "Int" but got: 829384293849283498239482938.`, + "Argument \"intArg\" has invalid value 829384293849283498239482938.\nExpected type \"Int\", found 829384293849283498239482938.", 4, 33, ), }) @@ -182,7 +182,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidIntValues_UnquotedStringIntoInt( `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "intArg" expected type "Int" but got: FOO.`, + "Argument \"intArg\" has invalid value FOO.\nExpected type \"Int\", found FOO.", 4, 33, ), }) @@ -197,7 +197,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidIntValues_SimpleFloatIntoInt(t * `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "intArg" expected type "Int" but got: 3.0.`, + "Argument \"intArg\" has invalid value 3.0.\nExpected type \"Int\", found 3.0.", 4, 33, ), }) @@ -212,7 +212,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidIntValues_FloatIntoInt(t *testin `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "intArg" expected type "Int" but got: 3.333.`, + "Argument \"intArg\" has invalid value 3.333.\nExpected type \"Int\", found 3.333.", 4, 33, ), }) @@ -228,7 +228,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidFloatValues_StringIntoFloat(t *t `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "floatArg" expected type "Float" but got: "3.333".`, + "Argument \"floatArg\" has invalid value \"3.333\".\nExpected type \"Float\", found \"3.333\".", 4, 37, ), }) @@ -243,7 +243,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidFloatValues_BooleanIntoFloat(t * `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "floatArg" expected type "Float" but got: true.`, + "Argument \"floatArg\" has invalid value true.\nExpected type \"Float\", found true.", 4, 37, ), }) @@ -258,7 +258,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidFloatValues_UnquotedIntoFloat(t `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "floatArg" expected type "Float" but got: FOO.`, + "Argument \"floatArg\" has invalid value FOO.\nExpected type \"Float\", found FOO.", 4, 37, ), }) @@ -274,7 +274,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidBooleanValues_IntIntoBoolean(t * `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "booleanArg" expected type "Boolean" but got: 2.`, + "Argument \"booleanArg\" has invalid value 2.\nExpected type \"Boolean\", found 2.", 4, 41, ), }) @@ -289,7 +289,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidBooleanValues_FloatIntoBoolean(t `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "booleanArg" expected type "Boolean" but got: 1.0.`, + "Argument \"booleanArg\" has invalid value 1.0.\nExpected type \"Boolean\", found 1.0.", 4, 41, ), }) @@ -304,7 +304,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidBooleanValues_StringIntoBoolean( `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "booleanArg" expected type "Boolean" but got: "true".`, + "Argument \"booleanArg\" has invalid value \"true\".\nExpected type \"Boolean\", found \"true\".", 4, 41, ), }) @@ -319,7 +319,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidBooleanValues_UnquotedStringInto `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "booleanArg" expected type "Boolean" but got: TRUE.`, + "Argument \"booleanArg\" has invalid value TRUE.\nExpected type \"Boolean\", found TRUE.", 4, 41, ), }) @@ -335,7 +335,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidIDValue_FloatIntoID(t *testing.T `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "idArg" expected type "ID" but got: 1.0.`, + "Argument \"idArg\" has invalid value 1.0.\nExpected type \"ID\", found 1.0.", 4, 31, ), }) @@ -350,7 +350,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidIDValue_BooleanIntoID(t *testing `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "idArg" expected type "ID" but got: true.`, + "Argument \"idArg\" has invalid value true.\nExpected type \"ID\", found true.", 4, 31, ), }) @@ -365,7 +365,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidIDValue_UnquotedIntoID(t *testin `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "idArg" expected type "ID" but got: SOMETHING.`, + "Argument \"idArg\" has invalid value SOMETHING.\nExpected type \"ID\", found SOMETHING.", 4, 31, ), }) @@ -381,7 +381,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_IntIntoEnum(t *testing `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "dogCommand" expected type "DogCommand" but got: 2.`, + "Argument \"dogCommand\" has invalid value 2.\nExpected type \"DogCommand\", found 2.", 4, 41, ), }) @@ -396,7 +396,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_FloatIntoEnum(t *testi `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "dogCommand" expected type "DogCommand" but got: 1.0.`, + "Argument \"dogCommand\" has invalid value 1.0.\nExpected type \"DogCommand\", found 1.0.", 4, 41, ), }) @@ -411,7 +411,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_StringIntoEnum(t *test `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "dogCommand" expected type "DogCommand" but got: "SIT".`, + "Argument \"dogCommand\" has invalid value \"SIT\".\nExpected type \"DogCommand\", found \"SIT\".", 4, 41, ), }) @@ -426,7 +426,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_BooleanIntoEnum(t *tes `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "dogCommand" expected type "DogCommand" but got: true.`, + "Argument \"dogCommand\" has invalid value true.\nExpected type \"DogCommand\", found true.", 4, 41, ), }) @@ -441,7 +441,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_UnknownEnumValueIntoEn `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "dogCommand" expected type "DogCommand" but got: JUGGLE.`, + "Argument \"dogCommand\" has invalid value JUGGLE.\nExpected type \"DogCommand\", found JUGGLE.", 4, 41, ), }) @@ -456,7 +456,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_DifferentCaseEnumValue `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "dogCommand" expected type "DogCommand" but got: sit.`, + "Argument \"dogCommand\" has invalid value sit.\nExpected type \"DogCommand\", found sit.", 4, 41, ), }) @@ -500,7 +500,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidListValue_IncorrectItemType(t *t `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "stringListArg" expected type "[String]" but got: ["one", 2].`, + "Argument \"stringListArg\" has invalid value [\"one\", 2].\nIn element #1: Expected type \"String\", found 2.", 4, 47, ), }) @@ -515,7 +515,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidListValue_SingleValueOfIncorrent `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "stringListArg" expected type "[String]" but got: 1.`, + "Argument \"stringListArg\" has invalid value 1.\nExpected type \"String\", found 1.", 4, 47, ), }) @@ -622,11 +622,11 @@ func TestValidate_ArgValuesOfCorrectType_InvalidNonNullableValue_IncorrectValueT `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "req2" expected type "Int!" but got: "two".`, + "Argument \"req2\" has invalid value \"two\".\nExpected type \"Int\", found \"two\".", 4, 32, ), testutil.RuleError( - `Argument "req1" expected type "Int!" but got: "one".`, + "Argument \"req1\" has invalid value \"one\".\nExpected type \"Int\", found \"one\".", 4, 45, ), }) @@ -641,7 +641,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidNonNullableValue_IncorrectValueA `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "req1" expected type "Int!" but got: "one".`, + "Argument \"req1\" has invalid value \"one\".\nExpected type \"Int\", found \"one\".", 4, 32, ), }) @@ -724,7 +724,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidInputObjectValue_PartialObject_M `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "complexArg" expected type "ComplexInput" but got: {intField: 4}.`, + "Argument \"complexArg\" has invalid value {intField: 4}.\nIn field \"requiredField\": Expected \"Boolean!\", found null.", 4, 41, ), }) @@ -742,7 +742,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidInputObjectValue_PartialObject_I `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "complexArg" expected type "ComplexInput" but got: {stringListField: ["one", 2], requiredField: true}.`, + "Argument \"complexArg\" has invalid value {stringListField: [\"one\", 2], requiredField: true}.\nIn field \"stringListField\": In element #1: Expected type \"String\", found 2.", 4, 41, ), }) @@ -760,7 +760,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidInputObjectValue_PartialObject_U `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "complexArg" expected type "ComplexInput" but got: {requiredField: true, unknownField: "value"}.`, + "Argument \"complexArg\" has invalid value {requiredField: true, unknownField: \"value\"}.\nUnknown field \"unknownField\".", 4, 41, ), }) @@ -788,11 +788,13 @@ func TestValidate_ArgValuesOfCorrectType_DirectiveArguments_WithDirectivesWithIn `, []gqlerrors.FormattedError{ testutil.RuleError( - `Argument "if" expected type "Boolean!" but got: "yes".`, + `Argument "if" has invalid value "yes".`+ + "\nExpected type \"Boolean\", found \"yes\".", 3, 28, ), testutil.RuleError( - `Argument "if" expected type "Boolean!" but got: ENUM.`, + `Argument "if" has invalid value ENUM.`+ + "\nExpected type \"Boolean\", found ENUM.", 4, 28, ), }) diff --git a/rules_default_values_of_correct_type_test.go b/rules_default_values_of_correct_type_test.go index 8ef76210..bc9545be 100644 --- a/rules_default_values_of_correct_type_test.go +++ b/rules_default_values_of_correct_type_test.go @@ -63,9 +63,16 @@ func TestValidate_VariableDefaultValuesOfCorrectType_VariablesWithInvalidDefault } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$a" of type "Int" has invalid default value: "one".`, 3, 19), - testutil.RuleError(`Variable "$b" of type "String" has invalid default value: 4.`, 4, 22), - testutil.RuleError(`Variable "$c" of type "ComplexInput" has invalid default value: "notverycomplex".`, 5, 28), + testutil.RuleError(`Variable "$a" has invalid default value: "one".`+ + "\nExpected type \"Int\", found \"one\".", + 3, 19), + testutil.RuleError(`Variable "$b" has invalid default value: 4.`+ + "\nExpected type \"String\", found 4.", + 4, 22), + testutil.RuleError( + `Variable "$c" has invalid default value: "notverycomplex".`+ + "\nExpected \"ComplexInput\", found not an object.", + 5, 28), }) } func TestValidate_VariableDefaultValuesOfCorrectType_ComplexVariablesMissingRequiredField(t *testing.T) { @@ -75,7 +82,10 @@ func TestValidate_VariableDefaultValuesOfCorrectType_ComplexVariablesMissingRequ } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$a" of type "ComplexInput" has invalid default value: {intField: 3}.`, 2, 53), + testutil.RuleError( + `Variable "$a" has invalid default value: {intField: 3}.`+ + "\nIn field \"requiredField\": Expected \"Boolean!\", found null.", + 2, 53), }) } func TestValidate_VariableDefaultValuesOfCorrectType_ListVariablesWithInvalidItem(t *testing.T) { @@ -85,6 +95,9 @@ func TestValidate_VariableDefaultValuesOfCorrectType_ListVariablesWithInvalidIte } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$a" of type "[String]" has invalid default value: ["one", 2].`, 2, 40), + testutil.RuleError( + `Variable "$a" has invalid default value: ["one", 2].`+ + "\nIn element #1: Expected type \"String\", found 2.", + 2, 40), }) } diff --git a/values.go b/values.go index d48fa7f6..1e4f3bb7 100644 --- a/values.go +++ b/values.go @@ -5,11 +5,13 @@ import ( "fmt" "math" "reflect" + "strings" "github.com/graphql-go/graphql/gqlerrors" "github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/kinds" "github.com/graphql-go/graphql/language/printer" + "sort" ) // Prepares an object map of variableValues of the correct type based on the @@ -81,7 +83,8 @@ func getVariableValue(schema Schema, definitionAST *ast.VariableDefinition, inpu ) } - if isValidInputValue(input, ttype) { + isValid, messages := isValidInputValue(input, ttype) + if isValid { if isNullish(input) { defaultValue := definitionAST.DefaultValue if defaultValue != nil { @@ -103,14 +106,20 @@ func getVariableValue(schema Schema, definitionAST *ast.VariableDefinition, inpu nil, ) } + // convert input interface into string for error message inputStr := "" b, err := json.Marshal(input) if err == nil { inputStr = string(b) } + messagesStr := "" + if len(messages) > 0 { + messagesStr = "\n" + strings.Join(messages, "\n") + } + return "", gqlerrors.NewError( - fmt.Sprintf(`Variable "$%v" expected value of type `+ - `"%v" but got: %v.`, variable.Name.Value, printer.Print(definitionAST.Type), inputStr), + fmt.Sprintf(`Variable "$%v" got invalid value `+ + `%v.%v`, variable.Name.Value, inputStr, messagesStr), []ast.Node{definitionAST}, "", nil, @@ -211,16 +220,19 @@ func typeFromAST(schema Schema, inputTypeAST ast.Type) (Type, error) { // Given a value and a GraphQL type, determine if the value will be // accepted for that type. This is primarily useful for validating the // runtime values of query variables. -func isValidInputValue(value interface{}, ttype Input) bool { +func isValidInputValue(value interface{}, ttype Input) (bool, []string) { if ttype, ok := ttype.(*NonNull); ok { if isNullish(value) { - return false + if ttype.OfType.Name() != "" { + return false, []string{fmt.Sprintf(`Expected "%v!", found null.`, ttype.OfType.Name())} + } + return false, []string{"Expected non-null value, found null."} } return isValidInputValue(value, ttype.OfType) } if isNullish(value) { - return true + return true, nil } switch ttype := ttype.(type) { @@ -231,48 +243,77 @@ func isValidInputValue(value interface{}, ttype Input) bool { valType = valType.Elem() } if valType.Kind() == reflect.Slice { + messagesReduce := []string{} for i := 0; i < valType.Len(); i++ { val := valType.Index(i).Interface() - if !isValidInputValue(val, itemType) { - return false + _, messages := isValidInputValue(val, itemType) + for idx, message := range messages { + messagesReduce = append(messagesReduce, fmt.Sprintf(`In element #%v: %v`, idx+1, message)) } } - return true + return (len(messagesReduce) == 0), messagesReduce } return isValidInputValue(value, itemType) case *InputObject: + messagesReduce := []string{} + valueMap, ok := value.(map[string]interface{}) if !ok { - return false + return false, []string{fmt.Sprintf(`Expected "%v", found not an object.`, ttype.Name())} } fields := ttype.Fields() - // Ensure every provided field is defined. + // to ensure stable order of field evaluation + fieldNames := []string{} + valueMapFieldNames := []string{} + + for fieldName, _ := range fields { + fieldNames = append(fieldNames, fieldName) + } + sort.Strings(fieldNames) + for fieldName, _ := range valueMap { + valueMapFieldNames = append(valueMapFieldNames, fieldName) + } + sort.Strings(valueMapFieldNames) + + // Ensure every provided field is defined. + for _, fieldName := range valueMapFieldNames { if _, ok := fields[fieldName]; !ok { - return false + messagesReduce = append(messagesReduce, fmt.Sprintf(`Unknown field "%v".`, fieldName)) } } + // Ensure every defined field is valid. - for fieldName, _ := range fields { - isValid := isValidInputValue(valueMap[fieldName], fields[fieldName].Type) - if !isValid { - return false + for _, fieldName := range fieldNames { + _, messages := isValidInputValue(valueMap[fieldName], fields[fieldName].Type) + if messages != nil { + for _, message := range messages { + messagesReduce = append(messagesReduce, fmt.Sprintf(`In field "%v": %v`, fieldName, message)) + } } } - return true + return (len(messagesReduce) == 0), messagesReduce } switch ttype := ttype.(type) { case *Scalar: parsedVal := ttype.ParseValue(value) - return !isNullish(parsedVal) + if isNullish(parsedVal) { + return false, []string{fmt.Sprintf(`Expected type "%v", found "%v".`, ttype.Name(), value)} + } else { + return true, nil + } case *Enum: parsedVal := ttype.ParseValue(value) - return !isNullish(parsedVal) + if isNullish(parsedVal) { + return false, []string{fmt.Sprintf(`Expected type "%v", found "%v".`, ttype.Name(), value)} + } else { + return true, nil + } } - return false + return true, nil } // Returns true if a value is null, undefined, or NaN. diff --git a/variables_test.go b/variables_test.go index b8e118f2..5c35201b 100644 --- a/variables_test.go +++ b/variables_test.go @@ -53,6 +53,18 @@ var testInputObject *graphql.InputObject = graphql.NewInputObject(graphql.InputO }, }) +var testNestedInputObject *graphql.InputObject = graphql.NewInputObject(graphql.InputObjectConfig{ + Name: "TestNestedInputObject", + Fields: graphql.InputObjectConfigFieldMap{ + "na": &graphql.InputObjectFieldConfig{ + Type: graphql.NewNonNull(testInputObject), + }, + "nb": &graphql.InputObjectFieldConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + }, +}) + func inputResolved(p graphql.ResolveParams) (interface{}, error) { input, ok := p.Args["input"] if !ok { @@ -105,6 +117,16 @@ var testType *graphql.Object = graphql.NewObject(graphql.ObjectConfig{ }, Resolve: inputResolved, }, + "fieldWithNestedInputObject": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "input": &graphql.ArgumentConfig{ + Type: testNestedInputObject, + DefaultValue: "Hello World", + }, + }, + Resolve: inputResolved, + }, "list": &graphql.Field{ Type: graphql.String, Args: graphql.FieldConfigArgument{ @@ -369,8 +391,8 @@ func TestVariables_ObjectsAndNullability_UsingVariables_ErrorsOnNullForNestedNon Data: nil, Errors: []gqlerrors.FormattedError{ gqlerrors.FormattedError{ - Message: `Variable "$input" expected value of type "TestInputObject" but ` + - `got: {"a":"foo","b":"bar","c":null}.`, + Message: `Variable "$input" got invalid value {"a":"foo","b":"bar","c":null}.` + + "\nIn field \"c\": Expected \"String!\", found null.", Locations: []location.SourceLocation{ location.SourceLocation{ Line: 2, Column: 17, @@ -404,8 +426,7 @@ func TestVariables_ObjectsAndNullability_UsingVariables_ErrorsOnIncorrectType(t Data: nil, Errors: []gqlerrors.FormattedError{ gqlerrors.FormattedError{ - Message: `Variable "$input" expected value of type "TestInputObject" but ` + - `got: "foo bar".`, + Message: "Variable \"$input\" got invalid value \"foo bar\".\nExpected \"TestInputObject\", found not an object.", Locations: []location.SourceLocation{ location.SourceLocation{ Line: 2, Column: 17, @@ -442,8 +463,8 @@ func TestVariables_ObjectsAndNullability_UsingVariables_ErrorsOnOmissionOfNested Data: nil, Errors: []gqlerrors.FormattedError{ gqlerrors.FormattedError{ - Message: `Variable "$input" expected value of type "TestInputObject" but ` + - `got: {"a":"foo","b":"bar"}.`, + Message: `Variable "$input" got invalid value {"a":"foo","b":"bar"}.` + + "\nIn field \"c\": Expected \"String!\", found null.", Locations: []location.SourceLocation{ location.SourceLocation{ Line: 2, Column: 17, @@ -469,6 +490,51 @@ func TestVariables_ObjectsAndNullability_UsingVariables_ErrorsOnOmissionOfNested t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result)) } } +func TestVariables_ObjectsAndNullability_UsingVariables_ErrorsOnDeepNestedErrorsAndWithManyErrors(t *testing.T) { + params := map[string]interface{}{ + "input": map[string]interface{}{ + "na": map[string]interface{}{ + "a": "foo", + }, + }, + } + expected := &graphql.Result{ + Data: nil, + Errors: []gqlerrors.FormattedError{ + gqlerrors.FormattedError{ + Message: `Variable "$input" got invalid value {"na":{"a":"foo"}}.` + + "\nIn field \"na\": In field \"c\": Expected \"String!\", found null." + + "\nIn field \"nb\": Expected \"String!\", found null.", + Locations: []location.SourceLocation{ + location.SourceLocation{ + Line: 2, Column: 19, + }, + }, + }, + }, + } + doc := ` + query q($input: TestNestedInputObject) { + fieldWithNestedObjectInput(input: $input) + } + ` + + nestedAST := testutil.TestParse(t, doc) + + // execute + ep := graphql.ExecuteParams{ + Schema: variablesTestSchema, + AST: nestedAST, + Args: params, + } + result := testutil.TestExecute(t, ep) + if len(result.Errors) != len(expected.Errors) { + t.Fatalf("Unexpected errors, Diff: %v", testutil.Diff(expected.Errors, result.Errors)) + } + if !reflect.DeepEqual(expected, result) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result)) + } +} func TestVariables_ObjectsAndNullability_UsingVariables_ErrorsOnAdditionOfUnknownInputField(t *testing.T) { params := map[string]interface{}{ "input": map[string]interface{}{ @@ -482,8 +548,8 @@ func TestVariables_ObjectsAndNullability_UsingVariables_ErrorsOnAdditionOfUnknow Data: nil, Errors: []gqlerrors.FormattedError{ gqlerrors.FormattedError{ - Message: `Variable "$input" expected value of type "TestInputObject" but ` + - `got: {"a":"foo","b":"bar","c":"baz","d":"dog"}.`, + Message: `Variable "$input" got invalid value {"a":"foo","b":"bar","c":"baz","d":"dog"}.` + + "\nIn field \"d\": Expected type \"ComplexScalar\", found \"dog\".", Locations: []location.SourceLocation{ location.SourceLocation{ Line: 2, Column: 17, @@ -1117,8 +1183,9 @@ func TestVariables_ListsAndNullability_DoesNotAllowListOfNonNullsToContainNull(t Data: nil, Errors: []gqlerrors.FormattedError{ gqlerrors.FormattedError{ - Message: `Variable "$input" expected value of type "[String!]" but got: ` + - `["A",null,"B"].`, + Message: `Variable "$input" got invalid value ` + + `["A",null,"B"].` + + "\nIn element #1: Expected \"String!\", found null.", Locations: []location.SourceLocation{ location.SourceLocation{ Line: 2, Column: 17, @@ -1224,8 +1291,9 @@ func TestVariables_ListsAndNullability_DoesNotAllowNonNullListOfNonNullsToContai Data: nil, Errors: []gqlerrors.FormattedError{ gqlerrors.FormattedError{ - Message: `Variable "$input" expected value of type "[String!]!" but got: ` + - `["A",null,"B"].`, + Message: `Variable "$input" got invalid value ` + + `["A",null,"B"].` + + "\nIn element #1: Expected \"String!\", found null.", Locations: []location.SourceLocation{ location.SourceLocation{ Line: 2, Column: 17, From 42a1aebc9787add497c08c1cecc46b9e7eb73259 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 5 Apr 2016 10:21:26 +0800 Subject: [PATCH 36/69] Fix the error message for unknown field The error message for unknown field was literal: "In field "${providedField}": Unknown field." Add necessary back ticks to the template literal so the message includes the actual field name. Also change the test that claims to test this case to actually test it. --- rules.go | 2 +- rules_arguments_of_correct_type_test.go | 2 +- values.go | 2 +- variables_test.go | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rules.go b/rules.go index 13c4e40c..80fc7711 100644 --- a/rules.go +++ b/rules.go @@ -1931,7 +1931,7 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) { field, ok := fields[fieldASTName] if !ok || field == nil { - messagesReduce = append(messagesReduce, fmt.Sprintf(`Unknown field "%v".`, fieldASTName)) + messagesReduce = append(messagesReduce, fmt.Sprintf(`In field "%v": Unknown field.`, fieldASTName)) } } // Ensure every defined field is valid. diff --git a/rules_arguments_of_correct_type_test.go b/rules_arguments_of_correct_type_test.go index 6c8de45d..ecd4bea4 100644 --- a/rules_arguments_of_correct_type_test.go +++ b/rules_arguments_of_correct_type_test.go @@ -760,7 +760,7 @@ func TestValidate_ArgValuesOfCorrectType_InvalidInputObjectValue_PartialObject_U `, []gqlerrors.FormattedError{ testutil.RuleError( - "Argument \"complexArg\" has invalid value {requiredField: true, unknownField: \"value\"}.\nUnknown field \"unknownField\".", + "Argument \"complexArg\" has invalid value {requiredField: true, unknownField: \"value\"}.\nIn field \"unknownField\": Unknown field.", 4, 41, ), }) diff --git a/values.go b/values.go index 1e4f3bb7..15c650ae 100644 --- a/values.go +++ b/values.go @@ -281,7 +281,7 @@ func isValidInputValue(value interface{}, ttype Input) (bool, []string) { // Ensure every provided field is defined. for _, fieldName := range valueMapFieldNames { if _, ok := fields[fieldName]; !ok { - messagesReduce = append(messagesReduce, fmt.Sprintf(`Unknown field "%v".`, fieldName)) + messagesReduce = append(messagesReduce, fmt.Sprintf(`In field "%v": Unknown field.`, fieldName)) } } diff --git a/variables_test.go b/variables_test.go index 5c35201b..adfe823c 100644 --- a/variables_test.go +++ b/variables_test.go @@ -538,18 +538,18 @@ func TestVariables_ObjectsAndNullability_UsingVariables_ErrorsOnDeepNestedErrors func TestVariables_ObjectsAndNullability_UsingVariables_ErrorsOnAdditionOfUnknownInputField(t *testing.T) { params := map[string]interface{}{ "input": map[string]interface{}{ - "a": "foo", - "b": "bar", - "c": "baz", - "d": "dog", + "a": "foo", + "b": "bar", + "c": "baz", + "extra": "dog", }, } expected := &graphql.Result{ Data: nil, Errors: []gqlerrors.FormattedError{ gqlerrors.FormattedError{ - Message: `Variable "$input" got invalid value {"a":"foo","b":"bar","c":"baz","d":"dog"}.` + - "\nIn field \"d\": Expected type \"ComplexScalar\", found \"dog\".", + Message: `Variable "$input" got invalid value {"a":"foo","b":"bar","c":"baz","extra":"dog"}.` + + "\nIn field \"extra\": Unknown field.", Locations: []location.SourceLocation{ location.SourceLocation{ Line: 2, Column: 17, From 9969e7fe2648725c2e4a69fb7fd4947aba438696 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 6 Apr 2016 16:44:44 +0800 Subject: [PATCH 37/69] [RFC] Additional optional arguments Implements https://github.com/facebook/graphql/pull/110 Commit: 08ffd091b78c594d92d308970f986cd9258f0596 [08ffd09] Parents: defbb03617 Author: Lee Byron Date: 22 October 2015 at 3:38:45 AM SGT Commit Date: 22 October 2015 at 3:38:47 AM SGT --- Allowed `graphql.Interface` to accept thunked Fields --- definition.go | 21 ++++++++++--- schema.go | 24 +++++++++------ validation_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 105 insertions(+), 17 deletions(-) diff --git a/definition.go b/definition.go index 3c0ec299..fc744883 100644 --- a/definition.go +++ b/definition.go @@ -657,11 +657,12 @@ type Interface struct { err error } type InterfaceConfig struct { - Name string `json:"name"` - Fields Fields `json:"fields"` + Name string `json:"name"` + Fields interface{} `json:"fields"` ResolveType ResolveTypeFn Description string `json:"description"` } + type ResolveTypeFn func(value interface{}, info ResolveInfo) *Object func NewInterface(config InterfaceConfig) *Interface { @@ -690,7 +691,10 @@ func (it *Interface) AddFieldConfig(fieldName string, fieldConfig *Field) { if fieldName == "" || fieldConfig == nil { return } - it.typeConfig.Fields[fieldName] = fieldConfig + switch it.typeConfig.Fields.(type) { + case Fields: + it.typeConfig.Fields.(Fields)[fieldName] = fieldConfig + } } func (it *Interface) Name() string { return it.PrivateName @@ -699,7 +703,16 @@ func (it *Interface) Description() string { return it.PrivateDescription } func (it *Interface) Fields() (fields FieldDefinitionMap) { - it.fields, it.err = defineFieldMap(it, it.typeConfig.Fields) + var configureFields Fields + switch it.typeConfig.Fields.(type) { + case Fields: + configureFields = it.typeConfig.Fields.(Fields) + case FieldsThunk: + configureFields = it.typeConfig.Fields.(FieldsThunk)() + } + fields, err := defineFieldMap(it, configureFields) + it.err = err + it.fields = fields return it.fields } func (it *Interface) PossibleTypes() []*Object { diff --git a/schema.go b/schema.go index ac013e5b..a65b788a 100644 --- a/schema.go +++ b/schema.go @@ -338,7 +338,7 @@ func assertObjectImplementsInterface(object *Object, iface *Interface) error { return err } } - // Assert argument set invariance. + // Assert additional arguments must not be required. for _, objectArg := range objectField.Args { argName := objectArg.PrivateName var ifaceArg *Argument @@ -348,15 +348,19 @@ func assertObjectImplementsInterface(object *Object, iface *Interface) error { break } } - err = invariant( - ifaceArg != nil, - fmt.Sprintf(`%v.%v does not define argument "%v" but `+ - `%v.%v provides it.`, - iface, fieldName, argName, - object, fieldName), - ) - if err != nil { - return err + + if ifaceArg == nil { + _, ok := objectArg.Type.(*NonNull) + err = invariant( + !ok, + fmt.Sprintf(`%v.%v(%v:) is of required type `+ + `"%v" but is not also provided by the interface %v.%v.`, + object, fieldName, argName, + objectArg.Type, iface, fieldName), + ) + if err != nil { + return err + } } } } diff --git a/validation_test.go b/validation_test.go index c174832f..85d2c98d 100644 --- a/validation_test.go +++ b/validation_test.go @@ -1192,7 +1192,7 @@ func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_AcceptsAnObjectWhi t.Fatalf(`unexpected error: %v for type "%v"`, err, anotherObject) } } -func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWhichImplementsAnInterfaceFieldAlongWithMoreArguments(t *testing.T) { +func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_AcceptsAnObjectWhichImpementsAnInterfaceFieldAlongWithAdditionalOptionalArguments(t *testing.T) { anotherInterface := graphql.NewInterface(graphql.InterfaceConfig{ Name: "AnotherInterface", ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { @@ -1227,7 +1227,46 @@ func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWhi }, }) _, err := schemaWithObjectFieldOfType(anotherObject) - expectedError := `AnotherInterface.field does not define argument "anotherInput" but AnotherObject.field provides it.` + if err != nil { + t.Fatalf(`unexpected error: %v for type "%v"`, err, anotherObject) + } +} +func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalRequiredArguments(t *testing.T) { + anotherInterface := graphql.NewInterface(graphql.InterfaceConfig{ + Name: "AnotherInterface", + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + return nil + }, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "input": &graphql.ArgumentConfig{ + Type: graphql.String, + }, + }, + }, + }, + }) + anotherObject := graphql.NewObject(graphql.ObjectConfig{ + Name: "AnotherObject", + Interfaces: []*graphql.Interface{anotherInterface}, + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "input": &graphql.ArgumentConfig{ + Type: graphql.String, + }, + "anotherInput": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + }, + }, + }, + }) + _, err := schemaWithObjectFieldOfType(anotherObject) + expectedError := `AnotherObject.field(anotherInput:) is of required type "String!" but is not also provided by the interface AnotherInterface.field.` if err == nil || err.Error() != expectedError { t.Fatalf("Expected error: %v, got %v", expectedError, err) } @@ -1339,7 +1378,39 @@ func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_RejectsAnObjectWit } } -func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_AcceptsAnObjectWithASubtyedInterfaceField_Union(t *testing.T) { +func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_AcceptsAnObjectWithASubtypedInterfaceField_Interface(t *testing.T) { + var anotherInterface *graphql.Interface + anotherInterface = graphql.NewInterface(graphql.InterfaceConfig{ + Name: "AnotherInterface", + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + return nil + }, + Fields: (graphql.FieldsThunk)(func() graphql.Fields { + return graphql.Fields{ + "field": &graphql.Field{ + Type: anotherInterface, + }, + } + }), + }) + var anotherObject *graphql.Object + anotherObject = graphql.NewObject(graphql.ObjectConfig{ + Name: "AnotherObject", + Interfaces: []*graphql.Interface{anotherInterface}, + Fields: (graphql.FieldsThunk)(func() graphql.Fields { + return graphql.Fields{ + "field": &graphql.Field{ + Type: anotherObject, + }, + } + }), + }) + _, err := schemaWithFieldType(anotherObject) + if err != nil { + t.Fatalf(`unexpected error: %v for type "%v"`, err, anotherObject) + } +} +func TestTypeSystem_ObjectsMustAdhereToInterfaceTheyImplement_AcceptsAnObjectWithASubtypedInterfaceField_Union(t *testing.T) { anotherInterface := graphql.NewInterface(graphql.InterfaceConfig{ Name: "AnotherInterface", ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { From d75ad3e5067335a97aa3b2e116c1951d14bca339 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 6 Apr 2016 17:23:31 +0800 Subject: [PATCH 38/69] Rename "runtimeType" variable Calling this variable "runtimeType" makes it a little more clear that this value cannot be known statically, which is an easy misinterpretation. Also added a bit of documentation above the function. Commit: 662e316f04939a58922de948efe6e8273db0f029 [662e316] Parents: 0ca1946e5f Author: Lee Byron Date: 16 September 2015 at 4:21:46 AM SGT --- executor.go | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/executor.go b/executor.go index 1a25b755..ed4fdb4c 100644 --- a/executor.go +++ b/executor.go @@ -142,9 +142,9 @@ func executeOperation(p ExecuteOperationParams) *Result { } fields := collectFields(CollectFieldsParams{ - ExeContext: p.ExecutionContext, - OperationType: operationType, - SelectionSet: p.Operation.GetSelectionSet(), + ExeContext: p.ExecutionContext, + RuntimeType: operationType, + SelectionSet: p.Operation.GetSelectionSet(), }) executeFieldsParams := ExecuteFieldsParams{ @@ -265,7 +265,7 @@ func executeFields(p ExecuteFieldsParams) *Result { type CollectFieldsParams struct { ExeContext *ExecutionContext - OperationType *Object + RuntimeType *Object // previously known as OperationType SelectionSet *ast.SelectionSet Fields map[string][]*ast.Field VisitedFragmentNames map[string]bool @@ -273,6 +273,9 @@ type CollectFieldsParams struct { // Given a selectionSet, adds all of the fields in that selection to // the passed in map of fields, and returns it at the end. +// CollectFields requires the "runtime type" of an object. For a field which +// returns and Interface or Union type, the "runtime type" will be the actual +// Object type returned by that field. func collectFields(p CollectFieldsParams) map[string][]*ast.Field { fields := p.Fields @@ -299,12 +302,12 @@ func collectFields(p CollectFieldsParams) map[string][]*ast.Field { case *ast.InlineFragment: if !shouldIncludeNode(p.ExeContext, selection.Directives) || - !doesFragmentConditionMatch(p.ExeContext, selection, p.OperationType) { + !doesFragmentConditionMatch(p.ExeContext, selection, p.RuntimeType) { continue } innerParams := CollectFieldsParams{ ExeContext: p.ExeContext, - OperationType: p.OperationType, + RuntimeType: p.RuntimeType, SelectionSet: selection.SelectionSet, Fields: fields, VisitedFragmentNames: p.VisitedFragmentNames, @@ -327,12 +330,12 @@ func collectFields(p CollectFieldsParams) map[string][]*ast.Field { if fragment, ok := fragment.(*ast.FragmentDefinition); ok { if !shouldIncludeNode(p.ExeContext, fragment.Directives) || - !doesFragmentConditionMatch(p.ExeContext, fragment, p.OperationType) { + !doesFragmentConditionMatch(p.ExeContext, fragment, p.RuntimeType) { continue } innerParams := CollectFieldsParams{ ExeContext: p.ExeContext, - OperationType: p.OperationType, + RuntimeType: p.RuntimeType, SelectionSet: fragment.GetSelectionSet(), Fields: fields, VisitedFragmentNames: p.VisitedFragmentNames, @@ -657,29 +660,29 @@ func completeValue(eCtx *ExecutionContext, returnType Type, fieldASTs []*ast.Fie } // ast.Field type must be Object, Interface or Union and expect sub-selections. - var objectType *Object + var runtimeType *Object switch returnType := returnType.(type) { case *Object: - objectType = returnType + runtimeType = returnType case Abstract: - objectType = returnType.ObjectType(result, info) - if objectType != nil && !returnType.IsPossibleType(objectType) { + runtimeType = returnType.ObjectType(result, info) + if runtimeType != nil && !returnType.IsPossibleType(runtimeType) { panic(gqlerrors.NewFormattedError( fmt.Sprintf(`Runtime Object type "%v" is not a possible type `+ - `for "%v".`, objectType, returnType), + `for "%v".`, runtimeType, returnType), )) } } - if objectType == nil { + if runtimeType == nil { return nil } // If there is an isTypeOf predicate function, call it with the // current result. If isTypeOf returns false, then raise an error rather // than continuing execution. - if objectType.IsTypeOf != nil && !objectType.IsTypeOf(result, info) { + if runtimeType.IsTypeOf != nil && !runtimeType.IsTypeOf(result, info) { panic(gqlerrors.NewFormattedError( - fmt.Sprintf(`Expected value of type "%v" but got: %T.`, objectType, result), + fmt.Sprintf(`Expected value of type "%v" but got: %T.`, runtimeType, result), )) } @@ -694,7 +697,7 @@ func completeValue(eCtx *ExecutionContext, returnType Type, fieldASTs []*ast.Fie if selectionSet != nil { innerParams := CollectFieldsParams{ ExeContext: eCtx, - OperationType: objectType, + RuntimeType: runtimeType, SelectionSet: selectionSet, Fields: subFieldASTs, VisitedFragmentNames: visitedFragmentNames, @@ -704,7 +707,7 @@ func completeValue(eCtx *ExecutionContext, returnType Type, fieldASTs []*ast.Fie } executeFieldsParams := ExecuteFieldsParams{ ExecutionContext: eCtx, - ParentType: objectType, + ParentType: runtimeType, Source: result, Fields: subFieldASTs, } From e9f6f56285b970dee897ffcbf556557e3d5ad839 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 6 Apr 2016 17:56:02 +0800 Subject: [PATCH 39/69] Better error for GraphQLList Error message now contains field name. Commit: 6447badc64c6d4171cebeed5081b8c48e3a2c0d6 [6447bad] Parents: 89783419eb Author: Pavel Chertorogov Date: 15 January 2016 at 2:13:53 PM SGT --- executor.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/executor.go b/executor.go index ed4fdb4c..182be531 100644 --- a/executor.go +++ b/executor.go @@ -538,10 +538,6 @@ func resolveField(eCtx *ExecutionContext, parentType *Object, source interface{} VariableValues: eCtx.VariableValues, } - // TODO: If an error occurs while calling the field `resolve` function, ensure that - // it is wrapped as a Error with locations. Log this error and return - // null if allowed, otherwise throw the error so the parent field can handle - // it. var resolveFnError error result, resolveFnError = resolveFn(ResolveParams{ @@ -624,9 +620,14 @@ func completeValue(eCtx *ExecutionContext, returnType Type, fieldASTs []*ast.Fie if returnType, ok := returnType.(*List); ok { resultVal := reflect.ValueOf(result) + parentTypeName := "" + if info.ParentType != nil { + parentTypeName = info.ParentType.Name() + } err := invariant( resultVal.IsValid() && resultVal.Type().Kind() == reflect.Slice, - "User Error: expected iterable, but did not find one.", + fmt.Sprintf("User Error: expected iterable, but did not find one "+ + "for field %v.%v", parentTypeName, info.FieldName), ) if err != nil { panic(gqlerrors.FormatError(err)) From 660db9f25520822ed8e9bbf1f01541fa36247a0c Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 6 Apr 2016 19:02:50 +0800 Subject: [PATCH 40/69] Fix typo Commit: 318d41e782b9672f99770ccafde82210043c52c5 [318d41e] Parents: 6447badc64 Author: Pavel Chertorogov Date: 15 January 2016 at 2:24:29 PM SGT Added a test to cover case where list is expected but received a string --- executor.go | 2 +- lists_test.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/executor.go b/executor.go index 182be531..3d31f8a4 100644 --- a/executor.go +++ b/executor.go @@ -627,7 +627,7 @@ func completeValue(eCtx *ExecutionContext, returnType Type, fieldASTs []*ast.Fie err := invariant( resultVal.IsValid() && resultVal.Type().Kind() == reflect.Slice, fmt.Sprintf("User Error: expected iterable, but did not find one "+ - "for field %v.%v", parentTypeName, info.FieldName), + "for field %v.%v.", parentTypeName, info.FieldName), ) if err != nil { panic(gqlerrors.FormatError(err)) diff --git a/lists_test.go b/lists_test.go index 4bc47cd7..4c30b500 100644 --- a/lists_test.go +++ b/lists_test.go @@ -761,3 +761,22 @@ func TestLists_NonNullListOfNonNullArrayOfFunc_ContainsNulls(t *testing.T) { } checkList(t, ttype, data, expected) } + +func TestLists_UserErrorExpectIterableButDidNotGetOne(t *testing.T) { + ttype := graphql.NewList(graphql.Int) + data := "Not an iterable" + expected := &graphql.Result{ + Data: map[string]interface{}{ + "nest": map[string]interface{}{ + "test": nil, + }, + }, + Errors: []gqlerrors.FormattedError{ + gqlerrors.FormattedError{ + Message: "User Error: expected iterable, but did not find one for field DataType.test.", + Locations: []location.SourceLocation{}, + }, + }, + } + checkList(t, ttype, data, expected) +} From 4323bff72af9a0285c9b78555e3af2f7914f51ba Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 6 Apr 2016 21:17:50 +0800 Subject: [PATCH 41/69] Implement anonymous inline fragment directive tests Commit: 28b85b80ff73af7faa34e170c55148e90ea7e2f5 [28b85b8] Parents: a941554fc5 Author: Jake Date: 9 October 2015 at 6:18:21 AM SGT --- directives_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/directives_test.go b/directives_test.go index 5c87aa5d..f376015c 100644 --- a/directives_test.go +++ b/directives_test.go @@ -324,6 +324,100 @@ func TestDirectivesWorksOnInlineFragmentUnlessTrueIncludesInlineFragment(t *test } } +func TestDirectivesWorksOnAnonymousInlineFragmentIfFalseOmitsAnonymousInlineFragment(t *testing.T) { + query := ` + query Q { + a + ... @include(if: false) { + b + } + } + ` + expected := &graphql.Result{ + Data: map[string]interface{}{ + "a": "a", + }, + } + result := executeDirectivesTestQuery(t, query) + if len(result.Errors) != 0 { + t.Fatalf("wrong result, unexpected errors: %v", result.Errors) + } + if !reflect.DeepEqual(expected, result) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result)) + } +} + +func TestDirectivesWorksOnAnonymousInlineFragmentIfTrueIncludesAnonymousInlineFragment(t *testing.T) { + query := ` + query Q { + a + ... @include(if: true) { + b + } + } + ` + expected := &graphql.Result{ + Data: map[string]interface{}{ + "a": "a", + "b": "b", + }, + } + result := executeDirectivesTestQuery(t, query) + if len(result.Errors) != 0 { + t.Fatalf("wrong result, unexpected errors: %v", result.Errors) + } + if !reflect.DeepEqual(expected, result) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result)) + } +} + +func TestDirectivesWorksOnAnonymousInlineFragmentUnlessFalseIncludesAnonymousInlineFragment(t *testing.T) { + query := ` + query Q { + a + ... @skip(if: false) { + b + } + } + ` + expected := &graphql.Result{ + Data: map[string]interface{}{ + "a": "a", + "b": "b", + }, + } + result := executeDirectivesTestQuery(t, query) + if len(result.Errors) != 0 { + t.Fatalf("wrong result, unexpected errors: %v", result.Errors) + } + if !reflect.DeepEqual(expected, result) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result)) + } +} + +func TestDirectivesWorksOnAnonymousInlineFragmentUnlessTrueIncludesAnonymousInlineFragment(t *testing.T) { + query := ` + query Q { + a + ... @skip(if: true) { + b + } + } + ` + expected := &graphql.Result{ + Data: map[string]interface{}{ + "a": "a", + }, + } + result := executeDirectivesTestQuery(t, query) + if len(result.Errors) != 0 { + t.Fatalf("wrong result, unexpected errors: %v", result.Errors) + } + if !reflect.DeepEqual(expected, result) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result)) + } +} + func TestDirectivesWorksOnFragmentIfFalseOmitsFragment(t *testing.T) { query := ` query Q { From 507dfd23c1184503b894797fdde4f57d5ac869ea Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 6 Apr 2016 21:43:00 +0800 Subject: [PATCH 42/69] Validate: Unique variable names Commit: 089caad3628e69003d79675558fe38023af02d31 [089caad] Parents: 9234c6da0e Author: Jan Jergus Date: 18 February 2016 at 7:00:39 AM SGT Commit Date: 18 February 2016 at 8:47:14 AM SGT --- rules.go | 49 +++++++++++++++++++++++++++++ rules_unique_variable_names_test.go | 28 +++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 rules_unique_variable_names_test.go diff --git a/rules.go b/rules.go index 80fc7711..f3dc0f6b 100644 --- a/rules.go +++ b/rules.go @@ -36,6 +36,7 @@ var SpecifiedRules = []ValidationRuleFn{ UniqueFragmentNamesRule, UniqueInputFieldNamesRule, UniqueOperationNamesRule, + UniqueVariableNamesRule, VariablesAreInputTypesRule, VariablesInAllowedPositionRule, } @@ -1735,6 +1736,54 @@ func UniqueOperationNamesRule(context *ValidationContext) *ValidationRuleInstanc } } +/** + * Unique variable names + * + * A GraphQL operation is only valid if all its variables are uniquely named. + */ +func UniqueVariableNamesRule(context *ValidationContext) *ValidationRuleInstance { + knownVariableNames := map[string]*ast.Name{} + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.OperationDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.OperationDefinition); ok && node != nil { + knownVariableNames = map[string]*ast.Name{} + } + return visitor.ActionNoChange, nil + }, + }, + kinds.VariableDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.VariableDefinition); ok && node != nil { + variableName := "" + var variableNameAST *ast.Name + if node.Variable != nil && node.Variable.Name != nil { + variableNameAST = node.Variable.Name + variableName = node.Variable.Name.Value + } + if nameAST, ok := knownVariableNames[variableName]; ok { + return reportError( + context, + fmt.Sprintf(`There can only be one variable named "%v".`, variableName), + []ast.Node{nameAST, variableNameAST}, + ) + } + if variableNameAST != nil { + knownVariableNames[variableName] = variableNameAST + } + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + /** * VariablesAreInputTypesRule * Variables are input types diff --git a/rules_unique_variable_names_test.go b/rules_unique_variable_names_test.go new file mode 100644 index 00000000..63bf7778 --- /dev/null +++ b/rules_unique_variable_names_test.go @@ -0,0 +1,28 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_UniqueVariableNames_UniqueVariableNames(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueVariableNamesRule, ` + query A($x: Int, $y: String) { __typename } + query B($x: String, $y: Int) { __typename } + `) +} +func TestValidate_UniqueVariableNames_DuplicateVariableNames(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueVariableNamesRule, ` + query A($x: Int, $x: Int, $x: String) { __typename } + query B($x: String, $x: Int) { __typename } + query C($x: Int, $x: Int) { __typename } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can only be one variable named "x".`, 2, 16, 2, 25), + testutil.RuleError(`There can only be one variable named "x".`, 2, 16, 2, 34), + testutil.RuleError(`There can only be one variable named "x".`, 3, 16, 3, 28), + testutil.RuleError(`There can only be one variable named "x".`, 4, 16, 4, 25), + }) +} From c50554b0ec8a2fd663ed934d71f29e8eb0d364a9 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Thu, 7 Apr 2016 00:20:10 +0800 Subject: [PATCH 43/69] Experimental: customizable validation Commit: 5a6ab116cadafbaf613387dd22a8e4b7b36738e2 [5a6ab11] Parents: dd41b83981 Author: Lee Byron Date: 27 October 2015 at 11:56:52 AM SGT --- language/visitor/visitor_test.go | 8 +++-- testutil/rules_test_harness.go | 8 ++--- type_info.go | 25 +++++++++++--- validator.go | 22 +++++++++--- validator_test.go | 59 +++++++++++++++++++++++++++++++- 5 files changed, 106 insertions(+), 16 deletions(-) diff --git a/language/visitor/visitor_test.go b/language/visitor/visitor_test.go index 127937b2..b0770125 100644 --- a/language/visitor/visitor_test.go +++ b/language/visitor/visitor_test.go @@ -1495,7 +1495,9 @@ func TestVisitor_VisitWithTypeInfo_MaintainsTypeInfoDuringVisit(t *testing.T) { visited := []interface{}{} - typeInfo := graphql.NewTypeInfo(testutil.DefaultRulesTestSchema) + typeInfo := graphql.NewTypeInfo(&graphql.TypeInfoConfig{ + Schema: testutil.TestSchema, + }) query := `{ human(id: 4) { name, pets { name }, unknown } }` astDoc := parse(t, query) @@ -1604,7 +1606,9 @@ func TestVisitor_VisitWithTypeInfo_MaintainsTypeInfoDuringEdit(t *testing.T) { visited := []interface{}{} - typeInfo := graphql.NewTypeInfo(testutil.DefaultRulesTestSchema) + typeInfo := graphql.NewTypeInfo(&graphql.TypeInfoConfig{ + Schema: testutil.TestSchema, + }) astDoc := parse(t, `{ human(id: 4) { name, pets }, alien }`) diff --git a/testutil/rules_test_harness.go b/testutil/rules_test_harness.go index 0a973519..4e7aab24 100644 --- a/testutil/rules_test_harness.go +++ b/testutil/rules_test_harness.go @@ -11,7 +11,7 @@ import ( "reflect" ) -var DefaultRulesTestSchema *graphql.Schema +var TestSchema *graphql.Schema func init() { @@ -456,7 +456,7 @@ func init() { if err != nil { panic(err) } - DefaultRulesTestSchema = &schema + TestSchema = &schema } func expectValidRule(t *testing.T, schema *graphql.Schema, rules []graphql.ValidationRuleFn, queryString string) { @@ -506,10 +506,10 @@ func expectInvalidRule(t *testing.T, schema *graphql.Schema, rules []graphql.Val } func ExpectPassesRule(t *testing.T, rule graphql.ValidationRuleFn, queryString string) { - expectValidRule(t, DefaultRulesTestSchema, []graphql.ValidationRuleFn{rule}, queryString) + expectValidRule(t, TestSchema, []graphql.ValidationRuleFn{rule}, queryString) } func ExpectFailsRule(t *testing.T, rule graphql.ValidationRuleFn, queryString string, expectedErrors []gqlerrors.FormattedError) { - expectInvalidRule(t, DefaultRulesTestSchema, []graphql.ValidationRuleFn{rule}, queryString, expectedErrors) + expectInvalidRule(t, TestSchema, []graphql.ValidationRuleFn{rule}, queryString, expectedErrors) } func ExpectFailsRuleWithSchema(t *testing.T, schema *graphql.Schema, rule graphql.ValidationRuleFn, queryString string, expectedErrors []gqlerrors.FormattedError) { expectInvalidRule(t, schema, []graphql.ValidationRuleFn{rule}, queryString, expectedErrors) diff --git a/type_info.go b/type_info.go index 834daa3c..16ce9f93 100644 --- a/type_info.go +++ b/type_info.go @@ -11,6 +11,8 @@ import ( * of the current field and type definitions at any point in a GraphQL document * AST during a recursive descent by calling `enter(node)` and `leave(node)`. */ +type fieldDefFn func(schema *Schema, parentType Type, fieldAST *ast.Field) *FieldDefinition + type TypeInfo struct { schema *Schema typeStack []Output @@ -19,11 +21,26 @@ type TypeInfo struct { fieldDefStack []*FieldDefinition directive *Directive argument *Argument + getFieldDef fieldDefFn } -func NewTypeInfo(schema *Schema) *TypeInfo { +type TypeInfoConfig struct { + Schema *Schema + + // NOTE: this experimental optional second parameter is only needed in order + // to support non-spec-compliant codebases. You should never need to use it. + // It may disappear in the future. + FieldDefFn fieldDefFn +} + +func NewTypeInfo(opts *TypeInfoConfig) *TypeInfo { + getFieldDef := opts.FieldDefFn + if getFieldDef == nil { + getFieldDef = DefaultTypeInfoFieldDef + } return &TypeInfo{ - schema: schema, + schema: opts.Schema, + getFieldDef: getFieldDef, } } @@ -78,7 +95,7 @@ func (ti *TypeInfo) Enter(node ast.Node) { parentType := ti.ParentType() var fieldDef *FieldDefinition if parentType != nil { - fieldDef = TypeInfoFieldDef(*schema, parentType.(Type), node) + fieldDef = ti.getFieldDef(schema, parentType.(Type), node) } ti.fieldDefStack = append(ti.fieldDefStack, fieldDef) if fieldDef != nil { @@ -224,7 +241,7 @@ func (ti *TypeInfo) Leave(node ast.Node) { * statically evaluated environment we do not always have an Object type, * and need to handle Interface and Union types. */ -func TypeInfoFieldDef(schema Schema, parentType Type, fieldAST *ast.Field) *FieldDefinition { +func DefaultTypeInfoFieldDef(schema *Schema, parentType Type, fieldAST *ast.Field) *FieldDefinition { name := "" if fieldAST.Name != nil { name = fieldAST.Name.Value diff --git a/validator.go b/validator.go index 03d1fc44..85d0ca54 100644 --- a/validator.go +++ b/validator.go @@ -26,16 +26,24 @@ func ValidateDocument(schema *Schema, astDoc *ast.Document, rules []ValidationRu vr.Errors = append(vr.Errors, gqlerrors.NewFormattedError("Must provide document")) return vr } - vr.Errors = visitUsingRules(schema, astDoc, rules) + + typeInfo := NewTypeInfo(&TypeInfoConfig{ + Schema: schema, + }) + vr.Errors = VisitUsingRules(schema, typeInfo, astDoc, rules) if len(vr.Errors) == 0 { vr.IsValid = true } return vr } -func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRuleFn) []gqlerrors.FormattedError { +/** + * VisitUsingRules This uses a specialized visitor which runs multiple visitors in parallel, + * while maintaining the visitor skip and break API. + * @internal + */ +func VisitUsingRules(schema *Schema, typeInfo *TypeInfo, astDoc *ast.Document, rules []ValidationRuleFn) []gqlerrors.FormattedError { - typeInfo := NewTypeInfo(schema) context := NewValidationContext(schema, astDoc, typeInfo) visitors := []*visitor.VisitorOptions{} @@ -50,7 +58,9 @@ func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRul } func visitUsingRulesOld(schema *Schema, astDoc *ast.Document, rules []ValidationRuleFn) []gqlerrors.FormattedError { - typeInfo := NewTypeInfo(schema) + typeInfo := NewTypeInfo(&TypeInfoConfig{ + Schema: schema, + }) context := NewValidationContext(schema, astDoc, typeInfo) var visitInstance func(astNode ast.Node, instance *ValidationRuleInstance) @@ -295,7 +305,9 @@ func (ctx *ValidationContext) VariableUsages(node HasSelectionSet) []*VariableUs return usages } usages := []*VariableUsage{} - typeInfo := NewTypeInfo(ctx.schema) + typeInfo := NewTypeInfo(&TypeInfoConfig{ + Schema: ctx.schema, + }) visitor.Visit(node, visitor.VisitWithTypeInfo(typeInfo, &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ diff --git a/validator_test.go b/validator_test.go index acb4ddfb..e7089e32 100644 --- a/validator_test.go +++ b/validator_test.go @@ -4,9 +4,14 @@ import ( "testing" "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/language/ast" + "github.com/graphql-go/graphql/language/location" "github.com/graphql-go/graphql/language/parser" "github.com/graphql-go/graphql/language/source" "github.com/graphql-go/graphql/testutil" + "github.com/kr/pretty" + "reflect" ) func expectValid(t *testing.T, schema *graphql.Schema, queryString string) { @@ -28,7 +33,7 @@ func expectValid(t *testing.T, schema *graphql.Schema, queryString string) { func TestValidator_SupportsFullValidation_ValidatesQueries(t *testing.T) { - expectValid(t, testutil.DefaultRulesTestSchema, ` + expectValid(t, testutil.TestSchema, ` query { catOrDog { ... on Cat { @@ -41,3 +46,55 @@ func TestValidator_SupportsFullValidation_ValidatesQueries(t *testing.T) { } `) } + +// NOTE: experimental +func TestValidator_SupportsFullValidation_ValidatesUsingACustomTypeInfo(t *testing.T) { + + // This TypeInfo will never return a valid field. + typeInfo := graphql.NewTypeInfo(&graphql.TypeInfoConfig{ + Schema: testutil.TestSchema, + FieldDefFn: func(schema *graphql.Schema, parentType graphql.Type, fieldAST *ast.Field) *graphql.FieldDefinition { + return nil + }, + }) + + ast := testutil.TestParse(t, ` + query { + catOrDog { + ... on Cat { + furColor + } + ... on Dog { + isHousetrained + } + } + } + `) + + errors := graphql.VisitUsingRules(testutil.TestSchema, typeInfo, ast, graphql.SpecifiedRules) + + expectedErrors := []gqlerrors.FormattedError{ + { + Message: "Cannot query field \"catOrDog\" on \"QueryRoot\".", + Locations: []location.SourceLocation{ + {Line: 3, Column: 9}, + }, + }, + { + Message: "Cannot query field \"furColor\" on \"Cat\".", + Locations: []location.SourceLocation{ + {Line: 5, Column: 13}, + }, + }, + { + Message: "Cannot query field \"isHousetrained\" on \"Dog\".", + Locations: []location.SourceLocation{ + {Line: 8, Column: 13}, + }, + }, + } + pretty.Println(errors) + if !reflect.DeepEqual(expectedErrors, errors) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedErrors, errors)) + } +} From 13d7acd4d1fb78853a52f3c1fe3f5c2cb4d59e83 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Thu, 7 Apr 2016 00:40:02 +0800 Subject: [PATCH 44/69] [Validation] Remove visitFragmentSpreads This removes the mechanism that automatically expanded fragment spreads during a visit. It's no longer used after recent changes to some validator rules, but also makes over-visiting suboptimizations too easy to accidentally create. Commit: c73cc727e06a29140615d3b8beb452b3e2d09e58 [c73cc72] Parents: 320ca24977 Author: Lee Byron Date: 17 November 2015 at 11:45:57 AM SG --- rules.go | 13 ++---- validator.go | 112 ++++++++------------------------------------------- 2 files changed, 21 insertions(+), 104 deletions(-) diff --git a/rules.go b/rules.go index f3dc0f6b..813d5cb1 100644 --- a/rules.go +++ b/rules.go @@ -42,8 +42,7 @@ var SpecifiedRules = []ValidationRuleFn{ } type ValidationRuleInstance struct { - VisitorOpts *visitor.VisitorOptions - VisitSpreadFragments bool + VisitorOpts *visitor.VisitorOptions } type ValidationRuleFn func(context *ValidationContext) *ValidationRuleInstance @@ -731,8 +730,7 @@ func NoUndefinedVariablesRule(context *ValidationContext) *ValidationRuleInstanc }, } return &ValidationRuleInstance{ - VisitSpreadFragments: true, - VisitorOpts: visitorOpts, + VisitorOpts: visitorOpts, } } @@ -895,9 +893,7 @@ func NoUnusedVariablesRule(context *ValidationContext) *ValidationRuleInstance { }, } return &ValidationRuleInstance{ - // Visit FragmentDefinition after visiting FragmentSpread - VisitSpreadFragments: true, - VisitorOpts: visitorOpts, + VisitorOpts: visitorOpts, } } @@ -1906,8 +1902,7 @@ func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleI }, } return &ValidationRuleInstance{ - VisitSpreadFragments: true, - VisitorOpts: visitorOpts, + VisitorOpts: visitorOpts, } } diff --git a/validator.go b/validator.go index 85d0ca54..3d3a1ac2 100644 --- a/validator.go +++ b/validator.go @@ -12,6 +12,20 @@ type ValidationResult struct { Errors []gqlerrors.FormattedError } +/** + * Implements the "Validation" section of the spec. + * + * Validation runs synchronously, returning an array of encountered errors, or + * an empty array if no errors were encountered and the document is valid. + * + * A list of specific validation rules may be provided. If not provided, the + * default list of rules defined by the GraphQL specification will be used. + * + * Each validation rules is a function which returns a visitor + * (see the language/visitor API). Visitor methods are expected to return + * GraphQLErrors, or Arrays of GraphQLErrors when invalid. + */ + func ValidateDocument(schema *Schema, astDoc *ast.Document, rules []ValidationRuleFn) (vr ValidationResult) { if len(rules) == 0 { rules = SpecifiedRules @@ -40,7 +54,10 @@ func ValidateDocument(schema *Schema, astDoc *ast.Document, rules []ValidationRu /** * VisitUsingRules This uses a specialized visitor which runs multiple visitors in parallel, * while maintaining the visitor skip and break API. + * * @internal + * Had to expose it to unit test experimental customizable validation feature, + * but not meant for public consumption */ func VisitUsingRules(schema *Schema, typeInfo *TypeInfo, astDoc *ast.Document, rules []ValidationRuleFn) []gqlerrors.FormattedError { @@ -57,101 +74,6 @@ func VisitUsingRules(schema *Schema, typeInfo *TypeInfo, astDoc *ast.Document, r return context.Errors() } -func visitUsingRulesOld(schema *Schema, astDoc *ast.Document, rules []ValidationRuleFn) []gqlerrors.FormattedError { - typeInfo := NewTypeInfo(&TypeInfoConfig{ - Schema: schema, - }) - context := NewValidationContext(schema, astDoc, typeInfo) - - var visitInstance func(astNode ast.Node, instance *ValidationRuleInstance) - - visitInstance = func(astNode ast.Node, instance *ValidationRuleInstance) { - visitor.Visit(astNode, &visitor.VisitorOptions{ - Enter: func(p visitor.VisitFuncParams) (string, interface{}) { - var action = visitor.ActionNoChange - var result interface{} - switch node := p.Node.(type) { - case ast.Node: - // Collect type information about the current position in the AST. - typeInfo.Enter(node) - - // Do not visit top level fragment definitions if this instance will - // visit those fragments inline because it - // provided `visitSpreadFragments`. - kind := node.GetKind() - - if kind == kinds.FragmentDefinition && - p.Key != nil && instance.VisitSpreadFragments == true { - return visitor.ActionSkip, nil - } - - // Get the visitor function from the validation instance, and if it - // exists, call it with the visitor arguments. - enterFn := visitor.GetVisitFn(instance.VisitorOpts, kind, false) - if enterFn != nil { - action, result = enterFn(p) - } - - // If any validation instances provide the flag `visitSpreadFragments` - // and this node is a fragment spread, visit the fragment definition - // from this point. - if action == visitor.ActionNoChange && result == nil && - instance.VisitSpreadFragments == true && kind == kinds.FragmentSpread { - node, _ := node.(*ast.FragmentSpread) - name := node.Name - nameVal := "" - if name != nil { - nameVal = name.Value - } - fragment := context.Fragment(nameVal) - if fragment != nil { - visitInstance(fragment, instance) - } - } - - // If the result is "false" (ie action === Action.Skip), we're not visiting any descendent nodes, - // but need to update typeInfo. - if action == visitor.ActionSkip { - typeInfo.Leave(node) - } - - } - - return action, result - }, - Leave: func(p visitor.VisitFuncParams) (string, interface{}) { - var action = visitor.ActionNoChange - var result interface{} - switch node := p.Node.(type) { - case ast.Node: - kind := node.GetKind() - - // Get the visitor function from the validation instance, and if it - // exists, call it with the visitor arguments. - leaveFn := visitor.GetVisitFn(instance.VisitorOpts, kind, true) - if leaveFn != nil { - action, result = leaveFn(p) - } - - // Update typeInfo. - typeInfo.Leave(node) - } - return action, result - }, - }, nil) - } - - instances := []*ValidationRuleInstance{} - for _, rule := range rules { - instance := rule(context) - instances = append(instances, instance) - } - for _, instance := range instances { - visitInstance(astDoc, instance) - } - return context.Errors() -} - type HasSelectionSet interface { GetKind() string GetLoc() *ast.Location From f04b6073a3d1eec765403ae7b48b932afc2a44ee Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 11 Apr 2016 15:57:45 +0800 Subject: [PATCH 45/69] [validator] Add suggested types to incorrect field message Commit: 7861b226b364979feaba4deef70dc472c54c8d3d [7861b22] Parents: 337925e19c Author: dschafer Date: 2 February 2016 at 4:33:10 AM SGT Commit Date: 3 February 2016 at 2:49:18 AM SGT --- definition.go | 1 + rules.go | 136 ++++++++++++++++++++++++++- rules_fields_on_correct_type_test.go | 55 ++++++++--- testutil/rules_test_harness.go | 14 +++ validator_test.go | 8 +- 5 files changed, 193 insertions(+), 21 deletions(-) diff --git a/definition.go b/definition.go index fc744883..bc5a3a3d 100644 --- a/definition.go +++ b/definition.go @@ -127,6 +127,7 @@ func IsCompositeType(ttype interface{}) bool { // These types may describe the parent context of a selection set. type Abstract interface { + Name() string ObjectType(value interface{}, info ResolveInfo) *Object PossibleTypes() []*Object IsPossibleType(ttype *Object) bool diff --git a/rules.go b/rules.go index 813d5cb1..5b01f304 100644 --- a/rules.go +++ b/rules.go @@ -162,6 +162,32 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI } } +func UndefinedFieldMessage(fieldName string, ttypeName string, suggestedTypes []string) string { + + quoteStrings := func(slice []string) []string { + quoted := []string{} + for _, s := range slice { + quoted = append(quoted, fmt.Sprintf(`"%v"`, s)) + } + return quoted + } + + // construct helpful (but long) message + message := fmt.Sprintf(`Cannot query field "%v" on type "%v".`, fieldName, ttypeName) + suggestions := strings.Join(quoteStrings(suggestedTypes), ", ") + const MAX_LENGTH = 5 + if len(suggestedTypes) > 0 { + if len(suggestedTypes) > MAX_LENGTH { + suggestions = strings.Join(quoteStrings(suggestedTypes[0:MAX_LENGTH]), ", ") + + fmt.Sprintf(`, and %v other types`, len(suggestedTypes)-MAX_LENGTH) + } + message = message + fmt.Sprintf(` However, this field exists on %v.`, suggestions) + message = message + ` Perhaps you meant to use an inline fragment?` + } + + return message +} + /** * FieldsOnCorrectTypeRule * Fields on correct type @@ -182,14 +208,37 @@ func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance if ttype != nil { fieldDef := context.FieldDef() if fieldDef == nil { + // This isn't valid. Let's find suggestions, if any. + suggestedTypes := []string{} + nodeName := "" if node.Name != nil { nodeName = node.Name.Value } + + if ttype, ok := ttype.(Abstract); ok { + siblingInterfaces := getSiblingInterfacesIncludingField(ttype, nodeName) + implementations := getImplementationsIncludingField(ttype, nodeName) + suggestedMaps := map[string]bool{} + for _, s := range siblingInterfaces { + if _, ok := suggestedMaps[s]; !ok { + suggestedMaps[s] = true + suggestedTypes = append(suggestedTypes, s) + } + } + for _, s := range implementations { + if _, ok := suggestedMaps[s]; !ok { + suggestedMaps[s] = true + suggestedTypes = append(suggestedTypes, s) + } + } + } + + message := UndefinedFieldMessage(nodeName, ttype.Name(), suggestedTypes) + return reportError( context, - fmt.Sprintf(`Cannot query field "%v" on "%v".`, - nodeName, ttype.Name()), + message, []ast.Node{node}, ) } @@ -205,6 +254,89 @@ func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance } } +/** + * Return implementations of `type` that include `fieldName` as a valid field. + */ +func getImplementationsIncludingField(ttype Abstract, fieldName string) []string { + + result := []string{} + for _, t := range ttype.PossibleTypes() { + fields := t.Fields() + if _, ok := fields[fieldName]; ok { + result = append(result, fmt.Sprintf(`%v`, t.Name())) + } + } + + sort.Strings(result) + return result +} + +/** + * Go through all of the implementations of type, and find other interaces + * that they implement. If those interfaces include `field` as a valid field, + * return them, sorted by how often the implementations include the other + * interface. + */ +func getSiblingInterfacesIncludingField(ttype Abstract, fieldName string) []string { + implementingObjects := ttype.PossibleTypes() + + result := []string{} + suggestedInterfaceSlice := []*suggestedInterface{} + + // stores a map of interface name => index in suggestedInterfaceSlice + suggestedInterfaceMap := map[string]int{} + + for _, t := range implementingObjects { + for _, i := range t.Interfaces() { + if i == nil { + continue + } + fields := i.Fields() + if _, ok := fields[fieldName]; !ok { + continue + } + index, ok := suggestedInterfaceMap[i.Name()] + if !ok { + suggestedInterfaceSlice = append(suggestedInterfaceSlice, &suggestedInterface{ + name: i.Name(), + count: 0, + }) + index = len(suggestedInterfaceSlice) - 1 + } + if index < len(suggestedInterfaceSlice) { + s := suggestedInterfaceSlice[index] + if s.name == i.Name() { + s.count = s.count + 1 + } + } + } + } + sort.Sort(suggestedInterfaceSortedSlice(suggestedInterfaceSlice)) + + for _, s := range suggestedInterfaceSlice { + result = append(result, fmt.Sprintf(`%v`, s.name)) + } + return result + +} + +type suggestedInterface struct { + name string + count int +} + +type suggestedInterfaceSortedSlice []*suggestedInterface + +func (s suggestedInterfaceSortedSlice) Len() int { + return len(s) +} +func (s suggestedInterfaceSortedSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s suggestedInterfaceSortedSlice) Less(i, j int) bool { + return s[i].count < s[j].count +} + /** * FragmentsOnCompositeTypesRule * Fragments on composite type diff --git a/rules_fields_on_correct_type_test.go b/rules_fields_on_correct_type_test.go index 652f4272..294a0682 100644 --- a/rules_fields_on_correct_type_test.go +++ b/rules_fields_on_correct_type_test.go @@ -53,7 +53,7 @@ func TestValidate_FieldsOnCorrectType_IgnoresFieldsOnUnknownType(t *testing.T) { } `) } -func TestValidate_FieldsOnCorrectType_ReportErrosWhenTheTypeIsKnownAgain(t *testing.T) { +func TestValidate_FieldsOnCorrectType_ReportErrorsWhenTheTypeIsKnownAgain(t *testing.T) { testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` fragment typeKnownAgain on Pet { unknown_pet_field { @@ -63,8 +63,8 @@ func TestValidate_FieldsOnCorrectType_ReportErrosWhenTheTypeIsKnownAgain(t *test } } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot query field "unknown_pet_field" on "Pet".`, 3, 9), - testutil.RuleError(`Cannot query field "unknown_cat_field" on "Cat".`, 5, 13), + testutil.RuleError(`Cannot query field "unknown_pet_field" on type "Pet".`, 3, 9), + testutil.RuleError(`Cannot query field "unknown_cat_field" on type "Cat".`, 5, 13), }) } func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnFragment(t *testing.T) { @@ -73,7 +73,7 @@ func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnFragment(t *testing.T) { meowVolume } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot query field "meowVolume" on "Dog".`, 3, 9), + testutil.RuleError(`Cannot query field "meowVolume" on type "Dog".`, 3, 9), }) } func TestValidate_FieldsOnCorrectType_IgnoreDeeplyUnknownField(t *testing.T) { @@ -84,7 +84,7 @@ func TestValidate_FieldsOnCorrectType_IgnoreDeeplyUnknownField(t *testing.T) { } } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot query field "unknown_field" on "Dog".`, 3, 9), + testutil.RuleError(`Cannot query field "unknown_field" on type "Dog".`, 3, 9), }) } func TestValidate_FieldsOnCorrectType_SubFieldNotDefined(t *testing.T) { @@ -95,7 +95,7 @@ func TestValidate_FieldsOnCorrectType_SubFieldNotDefined(t *testing.T) { } } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot query field "unknown_field" on "Pet".`, 4, 11), + testutil.RuleError(`Cannot query field "unknown_field" on type "Pet".`, 4, 11), }) } func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnInlineFragment(t *testing.T) { @@ -106,7 +106,7 @@ func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnInlineFragment(t *testing } } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot query field "meowVolume" on "Dog".`, 4, 11), + testutil.RuleError(`Cannot query field "meowVolume" on type "Dog".`, 4, 11), }) } func TestValidate_FieldsOnCorrectType_AliasedFieldTargetNotDefined(t *testing.T) { @@ -115,7 +115,7 @@ func TestValidate_FieldsOnCorrectType_AliasedFieldTargetNotDefined(t *testing.T) volume : mooVolume } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot query field "mooVolume" on "Dog".`, 3, 9), + testutil.RuleError(`Cannot query field "mooVolume" on type "Dog".`, 3, 9), }) } func TestValidate_FieldsOnCorrectType_AliasedLyingFieldTargetNotDefined(t *testing.T) { @@ -124,7 +124,7 @@ func TestValidate_FieldsOnCorrectType_AliasedLyingFieldTargetNotDefined(t *testi barkVolume : kawVolume } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot query field "kawVolume" on "Dog".`, 3, 9), + testutil.RuleError(`Cannot query field "kawVolume" on type "Dog".`, 3, 9), }) } func TestValidate_FieldsOnCorrectType_NotDefinedOnInterface(t *testing.T) { @@ -133,7 +133,7 @@ func TestValidate_FieldsOnCorrectType_NotDefinedOnInterface(t *testing.T) { tailLength } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot query field "tailLength" on "Pet".`, 3, 9), + testutil.RuleError(`Cannot query field "tailLength" on type "Pet".`, 3, 9), }) } func TestValidate_FieldsOnCorrectType_DefinedOnImplementorsButNotOnInterface(t *testing.T) { @@ -142,7 +142,7 @@ func TestValidate_FieldsOnCorrectType_DefinedOnImplementorsButNotOnInterface(t * nickname } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot query field "nickname" on "Pet".`, 3, 9), + testutil.RuleError(`Cannot query field "nickname" on type "Pet". However, this field exists on "Cat", "Dog". Perhaps you meant to use an inline fragment?`, 3, 9), }) } func TestValidate_FieldsOnCorrectType_MetaFieldSelectionOnUnion(t *testing.T) { @@ -158,16 +158,16 @@ func TestValidate_FieldsOnCorrectType_DirectFieldSelectionOnUnion(t *testing.T) directField } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot query field "directField" on "CatOrDog".`, 3, 9), + testutil.RuleError(`Cannot query field "directField" on type "CatOrDog".`, 3, 9), }) } -func TestValidate_FieldsOnCorrectType_DirectImplementorsQueriedOnUnion(t *testing.T) { +func TestValidate_FieldsOnCorrectType_DefinedImplementorsQueriedOnUnion(t *testing.T) { testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` fragment definedOnImplementorsQueriedOnUnion on CatOrDog { name } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot query field "name" on "CatOrDog".`, 3, 9), + testutil.RuleError(`Cannot query field "name" on type "CatOrDog". However, this field exists on "Being", "Pet", "Canine", "Cat", "Dog". Perhaps you meant to use an inline fragment?`, 3, 9), }) } func TestValidate_FieldsOnCorrectType_ValidFieldInInlineFragment(t *testing.T) { @@ -182,3 +182,30 @@ func TestValidate_FieldsOnCorrectType_ValidFieldInInlineFragment(t *testing.T) { } `) } + +func TestValidate_FieldsOnCorrectTypeErrorMessage_WorksWithNoSuggestions(t *testing.T) { + message := graphql.UndefinedFieldMessage("T", "f", []string{}) + expected := `Cannot query field "T" on type "f".` + if message != expected { + t.Fatalf("Unexpected message, expected: %v, got %v", expected, message) + } +} + +func TestValidate_FieldsOnCorrectTypeErrorMessage_WorksWithNoSmallNumbersOfSuggestions(t *testing.T) { + message := graphql.UndefinedFieldMessage("T", "f", []string{"A", "B"}) + expected := `Cannot query field "T" on type "f". ` + + `However, this field exists on "A", "B". ` + + `Perhaps you meant to use an inline fragment?` + if message != expected { + t.Fatalf("Unexpected message, expected: %v, got %v", expected, message) + } +} +func TestValidate_FieldsOnCorrectTypeErrorMessage_WorksWithLotsOfSuggestions(t *testing.T) { + message := graphql.UndefinedFieldMessage("T", "f", []string{"A", "B", "C", "D", "E", "F"}) + expected := `Cannot query field "T" on type "f". ` + + `However, this field exists on "A", "B", "C", "D", "E", and 1 other types. ` + + `Perhaps you meant to use an inline fragment?` + if message != expected { + t.Fatalf("Unexpected message, expected: %v, got %v", expected, message) + } +} diff --git a/testutil/rules_test_harness.go b/testutil/rules_test_harness.go index 4e7aab24..3691f6c4 100644 --- a/testutil/rules_test_harness.go +++ b/testutil/rules_test_harness.go @@ -41,6 +41,19 @@ func init() { }, }, }) + var canineInterface = graphql.NewInterface(graphql.InterfaceConfig{ + Name: "Canine", + Fields: graphql.Fields{ + "name": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "surname": &graphql.ArgumentConfig{ + Type: graphql.Boolean, + }, + }, + }, + }, + }) var dogCommandEnum = graphql.NewEnum(graphql.EnumConfig{ Name: "DogCommand", Values: graphql.EnumValueConfigMap{ @@ -110,6 +123,7 @@ func init() { Interfaces: []*graphql.Interface{ beingInterface, petInterface, + canineInterface, }, }) var furColorEnum = graphql.NewEnum(graphql.EnumConfig{ diff --git a/validator_test.go b/validator_test.go index e7089e32..f7f19e57 100644 --- a/validator_test.go +++ b/validator_test.go @@ -10,7 +10,6 @@ import ( "github.com/graphql-go/graphql/language/parser" "github.com/graphql-go/graphql/language/source" "github.com/graphql-go/graphql/testutil" - "github.com/kr/pretty" "reflect" ) @@ -75,25 +74,24 @@ func TestValidator_SupportsFullValidation_ValidatesUsingACustomTypeInfo(t *testi expectedErrors := []gqlerrors.FormattedError{ { - Message: "Cannot query field \"catOrDog\" on \"QueryRoot\".", + Message: "Cannot query field \"catOrDog\" on type \"QueryRoot\".", Locations: []location.SourceLocation{ {Line: 3, Column: 9}, }, }, { - Message: "Cannot query field \"furColor\" on \"Cat\".", + Message: "Cannot query field \"furColor\" on type \"Cat\".", Locations: []location.SourceLocation{ {Line: 5, Column: 13}, }, }, { - Message: "Cannot query field \"isHousetrained\" on \"Dog\".", + Message: "Cannot query field \"isHousetrained\" on type \"Dog\".", Locations: []location.SourceLocation{ {Line: 8, Column: 13}, }, }, } - pretty.Println(errors) if !reflect.DeepEqual(expectedErrors, errors) { t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedErrors, errors)) } From 72e6332318a00bc102dd1e27d8d8f3543e17ac4d Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 11 Apr 2016 16:51:37 +0800 Subject: [PATCH 46/69] Fix validators to be aware of type-condition-less inline frags Commit: a0bf6f9bb1155a502649c8de743b4e35841b08b0 [a0bf6f9] Parents: b6a326bfd3 Author: Lee Byron Date: 2 October 2015 at 11:14:39 AM SGT --- rules.go | 10 +++++++--- rules_fragments_on_composite_types_test.go | 9 +++++++++ rules_overlapping_fields_can_be_merged_test.go | 10 ++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/rules.go b/rules.go index 5b01f304..dd4315d1 100644 --- a/rules.go +++ b/rules.go @@ -352,7 +352,7 @@ func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleIn Kind: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.InlineFragment); ok { ttype := context.Type() - if ttype != nil && !IsCompositeType(ttype) { + if node.TypeCondition != nil && ttype != nil && !IsCompositeType(ttype) { return reportError( context, fmt.Sprintf(`Fragment cannot condition on non composite type "%v".`, ttype), @@ -1073,10 +1073,14 @@ func collectFieldASTsAndDefs(context *ValidationContext, parentType Named, selec FieldDef: fieldDef, }) case *ast.InlineFragment: - parentType, _ := typeFromAST(*context.Schema(), selection.TypeCondition) + inlineFragmentType := parentType + if selection.TypeCondition != nil { + parentType, _ := typeFromAST(*context.Schema(), selection.TypeCondition) + inlineFragmentType = parentType + } astAndDefs = collectFieldASTsAndDefs( context, - parentType, + inlineFragmentType, selection.SelectionSet, visitedFragmentNames, astAndDefs, diff --git a/rules_fragments_on_composite_types_test.go b/rules_fragments_on_composite_types_test.go index 31fbf08b..efe072ab 100644 --- a/rules_fragments_on_composite_types_test.go +++ b/rules_fragments_on_composite_types_test.go @@ -31,6 +31,15 @@ func TestValidate_FragmentsOnCompositeTypes_ObjectIsValidInlineFragmentType(t *t } `) } +func TestValidate_FragmentsOnCompositeTypes_InlineFragmentWithoutTypeIsValid(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FragmentsOnCompositeTypesRule, ` + fragment validFragment on Pet { + ... { + name + } + } + `) +} func TestValidate_FragmentsOnCompositeTypes_UnionIsValidFragmentType(t *testing.T) { testutil.ExpectPassesRule(t, graphql.FragmentsOnCompositeTypesRule, ` fragment validFragment on CatOrDog { diff --git a/rules_overlapping_fields_can_be_merged_test.go b/rules_overlapping_fields_can_be_merged_test.go index 755c8bbe..db446bb0 100644 --- a/rules_overlapping_fields_can_be_merged_test.go +++ b/rules_overlapping_fields_can_be_merged_test.go @@ -367,6 +367,16 @@ func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_Same } `) } +func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_AllowsInlineTypelessFragments(t *testing.T) { + testutil.ExpectPassesRuleWithSchema(t, &schema, graphql.OverlappingFieldsCanBeMergedRule, ` + { + a + ... { + a + } + } + `) +} func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_ComparesDeepTypesIncludingList(t *testing.T) { testutil.ExpectFailsRuleWithSchema(t, &schema, graphql.OverlappingFieldsCanBeMergedRule, ` { From c58e583f4842e5d756dd0e6f95c2d06ba1f9768f Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 11 Apr 2016 17:01:30 +0800 Subject: [PATCH 47/69] Fix false-positive validation error for unknown type Fixes #255 Commit: 812e09d681c2f10d4e5d09f75314e47953eeb7d4 [812e09d] Parents: 32a5492680 Author: Lee Byron Date: 1 December 2015 at 3:36:46 PM SGT Commit Date: 1 December 2015 at 3:38:14 PM SGT --- rules.go | 20 ++++++++++++++++++++ rules_known_type_names_test.go | 22 ++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/rules.go b/rules.go index dd4315d1..f3ac0622 100644 --- a/rules.go +++ b/rules.go @@ -600,6 +600,26 @@ func KnownFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance func KnownTypeNamesRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.ObjectDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + return visitor.ActionSkip, nil + }, + }, + kinds.InterfaceDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + return visitor.ActionSkip, nil + }, + }, + kinds.UnionDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + return visitor.ActionSkip, nil + }, + }, + kinds.InputObjectDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + return visitor.ActionSkip, nil + }, + }, kinds.Named: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.Named); ok { diff --git a/rules_known_type_names_test.go b/rules_known_type_names_test.go index f0fb664b..eec9a0ae 100644 --- a/rules_known_type_names_test.go +++ b/rules_known_type_names_test.go @@ -37,3 +37,25 @@ func TestValidate_KnownTypeNames_UnknownTypeNamesAreInValid(t *testing.T) { testutil.RuleError(`Unknown type "Peettt".`, 8, 29), }) } + +func TestValidate_KnownTypeNames_IgnoresTypeDefinitions(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.KnownTypeNamesRule, ` + type NotInTheSchema { + field: FooBar + } + interface FooBar { + field: NotInTheSchema + } + union U = A | B + input Blob { + field: UnknownType + } + query Foo($var: NotInTheSchema) { + user(id: $var) { + id + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Unknown type "NotInTheSchema".`, 12, 23), + }) +} From 85bc4e52e1084664a8e6f6b1e8c8a3c9f7416ca6 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Mon, 11 Apr 2016 17:17:18 +0800 Subject: [PATCH 48/69] Fix false positive validation error from fragment cycle when unknown fragment is used Commit: 8c62789fd156761ca83992fb8d2192d41d768d13 [8c62789] Parents: b160c58bf5 Author: Lee Byron Date: 27 October 2015 at 10:54:22 AM SGT (Note: Already fixed previously, added a test for it) --- rules_no_fragment_cycles_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rules_no_fragment_cycles_test.go b/rules_no_fragment_cycles_test.go index 0eabdb77..a68d9bd4 100644 --- a/rules_no_fragment_cycles_test.go +++ b/rules_no_fragment_cycles_test.go @@ -40,6 +40,13 @@ func TestValidate_NoCircularFragmentSpreads_DoubleSpreadWithinAbstractTypes(t *t } `) } +func TestValidate_NoCircularFragmentSpreads_DoesNotFalsePositiveOnUnknownFragment(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoFragmentCyclesRule, ` + fragment nameFragment on Pet { + ...UnknownFragment + } + `) +} func TestValidate_NoCircularFragmentSpreads_SpreadingRecursivelyWithinFieldFails(t *testing.T) { testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, ` fragment fragA on Human { relatives { ...fragA } }, From 1b32cd8957b4dac3bee573664ce25092b58c30c7 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 12 Apr 2016 05:53:37 +0800 Subject: [PATCH 49/69] [Validation] perf improvements for fragment cycle detection This converts the O(N^2) cycle detector to an O(N) detector and optimizes the visit to short-circuit where parts of the validation visitor were unnecessary. On an internal very large stress test query this was previously ~30s and after this diff runs in 15ms. Commit: 4cf2190b54fefc34a7b37d22a489933fc15e14ce [4cf2190] Parents: a738c6d90d Author: Lee Byron Date: 13 November 2015 at 3:47:01 PM SGT [Validation] Performance improvements Fairly dramatic improvement of some validation rules by short-circuiting branches which do not need to be checked and memoizing the process of collecting fragment spreads from a given context - which is necessary by multiple rules. Commit: 0bc9088187b9902ab19c0ec34e0e9f036dc9d9ea [0bc9088] Parents: 7278b7c92c Author: Lee Byron Date: 13 November 2015 at 4:26:05 PM SGT Labels: HEAD [Validation] Report errors rather than return them This replaces the mechanism of returning errors or lists of errors from a validator step to instead report those errors via calling a function on the context. This simplifies the implementation of the visitor mechanism, but also opens the doors for future rules being specified as warnings instead of errors. --- language/ast/selections.go | 1 + rules.go | 180 +++++++++++++++---------------- rules_no_fragment_cycles_test.go | 55 +++++++++- 3 files changed, 140 insertions(+), 96 deletions(-) diff --git a/language/ast/selections.go b/language/ast/selections.go index dd36cf26..0dc0ea12 100644 --- a/language/ast/selections.go +++ b/language/ast/selections.go @@ -5,6 +5,7 @@ import ( ) type Selection interface { + GetSelectionSet() *SelectionSet } // Ensure that all definition types implements Selection interface diff --git a/rules.go b/rules.go index f3ac0622..677beb56 100644 --- a/rules.go +++ b/rules.go @@ -713,93 +713,113 @@ func (set *nodeSet) Add(node ast.Node) bool { return true } +func CycleErrorMessage(fragName string, spreadNames []string) string { + via := "" + if len(spreadNames) > 0 { + via = " via " + strings.Join(spreadNames, ", ") + } + return fmt.Sprintf(`Cannot spread fragment "%v" within itself%v.`, fragName, via) +} + /** * NoFragmentCyclesRule */ func NoFragmentCyclesRule(context *ValidationContext) *ValidationRuleInstance { - // Gather all the fragment spreads ASTs for each fragment definition. - // Importantly this does not include inline fragments. - definitions := context.Document().Definitions - spreadsInFragment := map[string][]*ast.FragmentSpread{} - for _, node := range definitions { - if node.GetKind() == kinds.FragmentDefinition { - if node, ok := node.(*ast.FragmentDefinition); ok && node != nil { - nodeName := "" - if node.Name != nil { - nodeName = node.Name.Value + + // Tracks already visited fragments to maintain O(N) and to ensure that cycles + // are not redundantly reported. + visitedFrags := map[string]bool{} + + // Array of AST nodes used to produce meaningful errors + spreadPath := []*ast.FragmentSpread{} + + // Position in the spread path + spreadPathIndexByName := map[string]int{} + + // This does a straight-forward DFS to find cycles. + // It does not terminate when a cycle was found but continues to explore + // the graph to find all possible cycles. + var detectCycleRecursive func(fragment *ast.FragmentDefinition) + detectCycleRecursive = func(fragment *ast.FragmentDefinition) { + + fragmentName := "" + if fragment.Name != nil { + fragmentName = fragment.Name.Value + } + visitedFrags[fragmentName] = true + + spreadNodes := context.FragmentSpreads(fragment) + if len(spreadNodes) == 0 { + return + } + + spreadPathIndexByName[fragmentName] = len(spreadPath) + + for _, spreadNode := range spreadNodes { + + spreadName := "" + if spreadNode.Name != nil { + spreadName = spreadNode.Name.Value + } + cycleIndex, ok := spreadPathIndexByName[spreadName] + if !ok { + spreadPath = append(spreadPath, spreadNode) + if visited, ok := visitedFrags[spreadName]; !ok || !visited { + spreadFragment := context.Fragment(spreadName) + if spreadFragment != nil { + detectCycleRecursive(spreadFragment) + } + } + spreadPath = spreadPath[:len(spreadPath)-1] + } else { + cyclePath := spreadPath[cycleIndex:] + + spreadNames := []string{} + for _, s := range cyclePath { + name := "" + if s.Name != nil { + name = s.Name.Value + } + spreadNames = append(spreadNames, name) + } + + nodes := []ast.Node{} + for _, c := range cyclePath { + nodes = append(nodes, c) } - spreadsInFragment[nodeName] = gatherSpreads(node) + nodes = append(nodes, spreadNode) + + reportError( + context, + CycleErrorMessage(spreadName, spreadNames), + nodes, + ) } + } + delete(spreadPathIndexByName, fragmentName) + } - // Tracks spreads known to lead to cycles to ensure that cycles are not - // redundantly reported. - knownToLeadToCycle := newNodeSet() visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.OperationDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + return visitor.ActionSkip, nil + }, + }, kinds.FragmentDefinition: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.FragmentDefinition); ok && node != nil { - spreadPath := []*ast.FragmentSpread{} - initialName := "" + nodeName := "" if node.Name != nil { - initialName = node.Name.Value + nodeName = node.Name.Value } - var detectCycleRecursive func(fragmentName string) - detectCycleRecursive = func(fragmentName string) { - spreadNodes, _ := spreadsInFragment[fragmentName] - for _, spreadNode := range spreadNodes { - if knownToLeadToCycle.Has(spreadNode) { - continue - } - spreadNodeName := "" - if spreadNode.Name != nil { - spreadNodeName = spreadNode.Name.Value - } - if spreadNodeName == initialName { - cyclePath := []ast.Node{} - for _, path := range spreadPath { - cyclePath = append(cyclePath, path) - } - cyclePath = append(cyclePath, spreadNode) - for _, spread := range cyclePath { - knownToLeadToCycle.Add(spread) - } - via := "" - spreadNames := []string{} - for _, s := range spreadPath { - if s.Name != nil { - spreadNames = append(spreadNames, s.Name.Value) - } - } - if len(spreadNames) > 0 { - via = " via " + strings.Join(spreadNames, ", ") - } - reportError( - context, - fmt.Sprintf(`Cannot spread fragment "%v" within itself%v.`, initialName, via), - cyclePath, - ) - continue - } - spreadPathHasCurrentNode := false - for _, spread := range spreadPath { - if spread == spreadNode { - spreadPathHasCurrentNode = true - } - } - if spreadPathHasCurrentNode { - continue - } - spreadPath = append(spreadPath, spreadNode) - detectCycleRecursive(spreadNodeName) - _, spreadPath = spreadPath[len(spreadPath)-1], spreadPath[:len(spreadPath)-1] - } + if _, ok := visitedFrags[nodeName]; !ok { + detectCycleRecursive(node) } - detectCycleRecursive(initialName) } - return visitor.ActionNoChange, nil + return visitor.ActionSkip, nil }, }, }, @@ -2163,25 +2183,3 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) { return true, nil } - -/** - * Given an operation or fragment AST node, gather all the - * named spreads defined within the scope of the fragment - * or operation - */ -func gatherSpreads(node ast.Node) (spreadNodes []*ast.FragmentSpread) { - visitorOpts := &visitor.VisitorOptions{ - KindFuncMap: map[string]visitor.NamedVisitFuncs{ - kinds.FragmentSpread: visitor.NamedVisitFuncs{ - Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - if node, ok := p.Node.(*ast.FragmentSpread); ok && node != nil { - spreadNodes = append(spreadNodes, node) - } - return visitor.ActionNoChange, nil - }, - }, - }, - } - visitor.Visit(node, visitorOpts, nil) - return spreadNodes -} diff --git a/rules_no_fragment_cycles_test.go b/rules_no_fragment_cycles_test.go index a68d9bd4..f194e305 100644 --- a/rules_no_fragment_cycles_test.go +++ b/rules_no_fragment_cycles_test.go @@ -115,10 +115,21 @@ func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeply(t *testing.T fragment fragX on Dog { ...fragY } fragment fragY on Dog { ...fragZ } fragment fragZ on Dog { ...fragO } - fragment fragO on Dog { ...fragA, ...fragX } + fragment fragO on Dog { ...fragP } + fragment fragP on Dog { ...fragA, ...fragX } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB, fragC, fragO.`, 2, 31, 3, 31, 4, 31, 8, 31), - testutil.RuleError(`Cannot spread fragment "fragX" within itself via fragY, fragZ, fragO.`, 5, 31, 6, 31, 7, 31, 8, 41), + testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB, fragC, fragO, fragP.`, + 2, 31, + 3, 31, + 4, 31, + 8, 31, + 9, 31), + testutil.RuleError(`Cannot spread fragment "fragO" within itself via fragP, fragX, fragY, fragZ.`, + 8, 31, + 9, 41, + 5, 31, + 6, 31, + 7, 31), }) } func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeplyTwoPaths(t *testing.T) { @@ -127,7 +138,41 @@ func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeplyTwoPaths(t *t fragment fragB on Dog { ...fragA } fragment fragC on Dog { ...fragA } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB.`, 2, 31, 3, 31), - testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragC.`, 2, 41, 4, 31), + testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB.`, + 2, 31, + 3, 31), + testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragC.`, + 2, 41, + 4, 31), + }) +} +func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeplyTwoPaths_AltTraverseOrder(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Dog { ...fragC } + fragment fragB on Dog { ...fragC } + fragment fragC on Dog { ...fragA, ...fragB } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragC.`, + 2, 31, + 4, 31), + testutil.RuleError(`Cannot spread fragment "fragC" within itself via fragB.`, + 4, 41, + 3, 31), + }) +} +func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeplyAndImmediately(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Dog { ...fragB } + fragment fragB on Dog { ...fragB, ...fragC } + fragment fragC on Dog { ...fragA, ...fragB } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot spread fragment "fragB" within itself.`, 3, 31), + testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB, fragC.`, + 2, 31, + 3, 41, + 4, 31), + testutil.RuleError(`Cannot spread fragment "fragB" within itself via fragC.`, + 3, 41, + 4, 41), }) } From 8e99c5763c6bc77475ee60f5a3c2ac98d19e7f1b Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 12 Apr 2016 08:40:48 +0800 Subject: [PATCH 50/69] [Validation] Memoize collecting variable usage. (Note: already ported previously, refactored error message) During multiple validation passes we need to know about variable usage within a de-fragmented operation. Memoizing this ensures each pass is O(N) - each fragment is no longer visited per operation, but once total. In doing so, `visitSpreadFragments` is no longer used, which will be cleaned up in a later PR Commit: 2afbff79bfd2b89f03ca7913577556b73980f974 [2afbff7] Parents: 88acc01b99 Author: Lee Byron Date: 17 November 2015 at 9:54:30 AM SGT Commit: a921397e480d09062d214d3e1e15b135419e4992 [a921397] Parents: --- rules.go | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/rules.go b/rules.go index 677beb56..d8e00e59 100644 --- a/rules.go +++ b/rules.go @@ -829,6 +829,13 @@ func NoFragmentCyclesRule(context *ValidationContext) *ValidationRuleInstance { } } +func UndefinedVarMessage(varName string, opName string) string { + if opName != "" { + return fmt.Sprintf(`Variable "$%v" is not defined by operation "%v".`, varName, opName) + } + return fmt.Sprintf(`Variable "$%v" is not defined.`, varName) +} + /** * NoUndefinedVariables * No undefined variables @@ -866,20 +873,11 @@ func NoUndefinedVariablesRule(context *ValidationContext) *ValidationRuleInstanc opName = operation.Name.Value } if res, ok := variableNameDefined[varName]; !ok || !res { - if opName != "" { - reportError( - context, - fmt.Sprintf(`Variable "$%v" is not defined by operation "%v".`, varName, opName), - []ast.Node{usage.Node, operation}, - ) - } else { - - reportError( - context, - fmt.Sprintf(`Variable "$%v" is not defined.`, varName), - []ast.Node{usage.Node, operation}, - ) - } + reportError( + context, + UndefinedVarMessage(varName, opName), + []ast.Node{usage.Node, operation}, + ) } } } @@ -893,7 +891,6 @@ func NoUndefinedVariablesRule(context *ValidationContext) *ValidationRuleInstanc if node.Variable != nil && node.Variable.Name != nil { variableName = node.Variable.Name.Value } - // definedVariableNames[variableName] = true variableNameDefined[variableName] = true } return visitor.ActionNoChange, nil From 15ce17bfa0ebd72f01d6b38efcfcd71eb180b8d8 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 12 Apr 2016 08:50:48 +0800 Subject: [PATCH 51/69] Improve validation error for unused variable Commit: 1b639e3a6538d2184e1a2b96c410d164a20c08ed [1b639e3] Parents: 71b7b4aa2d Author: Lee Byron Date: 2 February 2016 at 11:07:08 AM SGT --- rules.go | 13 ++++++++++++- rules_no_unused_variables_test.go | 22 +++++++++++----------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/rules.go b/rules.go index d8e00e59..6cb1b181 100644 --- a/rules.go +++ b/rules.go @@ -1000,6 +1000,13 @@ func NoUnusedFragmentsRule(context *ValidationContext) *ValidationRuleInstance { } } +func UnusedVariableMessage(varName string, opName string) string { + if opName != "" { + return fmt.Sprintf(`Variable "$%v" is never used in operation "%v".`, varName, opName) + } + return fmt.Sprintf(`Variable "$%v" is never used.`, varName) +} + /** * NoUnusedVariablesRule * No unused variables @@ -1037,10 +1044,14 @@ func NoUnusedVariablesRule(context *ValidationContext) *ValidationRuleInstance { if variableDef != nil && variableDef.Variable != nil && variableDef.Variable.Name != nil { variableName = variableDef.Variable.Name.Value } + opName := "" + if operation.Name != nil { + opName = operation.Name.Value + } if res, ok := variableNameUsed[variableName]; !ok || !res { reportError( context, - fmt.Sprintf(`Variable "$%v" is never used.`, variableName), + UnusedVariableMessage(variableName, opName), []ast.Node{variableDef}, ) } diff --git a/rules_no_unused_variables_test.go b/rules_no_unused_variables_test.go index d3bcdae4..7c331f4a 100644 --- a/rules_no_unused_variables_test.go +++ b/rules_no_unused_variables_test.go @@ -10,7 +10,7 @@ import ( func TestValidate_NoUnusedVariables_UsesAllVariables(t *testing.T) { testutil.ExpectPassesRule(t, graphql.NoUnusedVariablesRule, ` - query Foo($a: String, $b: String, $c: String) { + query ($a: String, $b: String, $c: String) { field(a: $a, b: $b, c: $c) } `) @@ -91,11 +91,11 @@ func TestValidate_NoUnusedVariables_VariableUsedByRecursiveFragment(t *testing.T } func TestValidate_NoUnusedVariables_VariableNotUsed(t *testing.T) { testutil.ExpectFailsRule(t, graphql.NoUnusedVariablesRule, ` - query Foo($a: String, $b: String, $c: String) { + query ($a: String, $b: String, $c: String) { field(a: $a, b: $b) } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$c" is never used.`, 2, 41), + testutil.RuleError(`Variable "$c" is never used.`, 2, 38), }) } func TestValidate_NoUnusedVariables_MultipleVariablesNotUsed(t *testing.T) { @@ -104,8 +104,8 @@ func TestValidate_NoUnusedVariables_MultipleVariablesNotUsed(t *testing.T) { field(b: $b) } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$a" is never used.`, 2, 17), - testutil.RuleError(`Variable "$c" is never used.`, 2, 41), + testutil.RuleError(`Variable "$a" is never used in operation "Foo".`, 2, 17), + testutil.RuleError(`Variable "$c" is never used in operation "Foo".`, 2, 41), }) } func TestValidate_NoUnusedVariables_VariableNotUsedInFragments(t *testing.T) { @@ -127,7 +127,7 @@ func TestValidate_NoUnusedVariables_VariableNotUsedInFragments(t *testing.T) { field } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$c" is never used.`, 2, 41), + testutil.RuleError(`Variable "$c" is never used in operation "Foo".`, 2, 41), }) } func TestValidate_NoUnusedVariables_MultipleVariablesNotUsed2(t *testing.T) { @@ -149,8 +149,8 @@ func TestValidate_NoUnusedVariables_MultipleVariablesNotUsed2(t *testing.T) { field } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$a" is never used.`, 2, 17), - testutil.RuleError(`Variable "$c" is never used.`, 2, 41), + testutil.RuleError(`Variable "$a" is never used in operation "Foo".`, 2, 17), + testutil.RuleError(`Variable "$c" is never used in operation "Foo".`, 2, 41), }) } func TestValidate_NoUnusedVariables_VariableNotUsedByUnreferencedFragment(t *testing.T) { @@ -165,7 +165,7 @@ func TestValidate_NoUnusedVariables_VariableNotUsedByUnreferencedFragment(t *tes field(b: $b) } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$b" is never used.`, 2, 17), + testutil.RuleError(`Variable "$b" is never used in operation "Foo".`, 2, 17), }) } func TestValidate_NoUnusedVariables_VariableNotUsedByFragmentUsedByOtherOperation(t *testing.T) { @@ -183,7 +183,7 @@ func TestValidate_NoUnusedVariables_VariableNotUsedByFragmentUsedByOtherOperatio field(b: $b) } `, []gqlerrors.FormattedError{ - testutil.RuleError(`Variable "$b" is never used.`, 2, 17), - testutil.RuleError(`Variable "$a" is never used.`, 5, 17), + testutil.RuleError(`Variable "$b" is never used in operation "Foo".`, 2, 17), + testutil.RuleError(`Variable "$a" is never used in operation "Bar".`, 5, 17), }) } From 0058fcdf55e1f2b35c548cf37dc86b2889215129 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 12 Apr 2016 09:04:28 +0800 Subject: [PATCH 52/69] [Validation] Un-interleave overlapping field messages When deep field collisions are detected, the existing validator interleaved the resulting fields in a way that made reading the error more difficult. This solves the issue by internally tracking two parallel lists of blame nodes, one for each side of the comparison, and concat-ing only at the end to ensure a stable non-interleaved output. Commit: 228215a704e9d7f67078fc2652eafa6b6e22026f [228215a] Parents: fee4fe322f Author: Lee Byron Date: 13 November 2015 at 8:24:24 AM SGT --- rules.go | 28 +++++++++------ ...s_overlapping_fields_can_be_merged_test.go | 34 +++++++++++++++---- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/rules.go b/rules.go index 6cb1b181..5f8fedcf 100644 --- a/rules.go +++ b/rules.go @@ -1201,8 +1201,9 @@ type conflictReason struct { Message interface{} // conflictReason || []conflictReason } type conflict struct { - Reason conflictReason - Fields []ast.Node + Reason conflictReason + FieldsLeft []ast.Node + FieldsRight []ast.Node } func sameDirectives(directives1 []*ast.Directive, directives2 []*ast.Directive) bool { @@ -1317,7 +1318,8 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul Name: responseName, Message: fmt.Sprintf(`%v and %v are different fields`, name1, name2), }, - Fields: []ast.Node{ast1, ast2}, + FieldsLeft: []ast.Node{ast1}, + FieldsRight: []ast.Node{ast2}, } } @@ -1336,7 +1338,8 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul Name: responseName, Message: fmt.Sprintf(`they return differing types %v and %v`, type1, type2), }, - Fields: []ast.Node{ast1, ast2}, + FieldsLeft: []ast.Node{ast1}, + FieldsRight: []ast.Node{ast2}, } } if !sameArguments(ast1.Arguments, ast2.Arguments) { @@ -1345,7 +1348,8 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul Name: responseName, Message: `they have differing arguments`, }, - Fields: []ast.Node{ast1, ast2}, + FieldsLeft: []ast.Node{ast1}, + FieldsRight: []ast.Node{ast2}, } } if !sameDirectives(ast1.Directives, ast2.Directives) { @@ -1354,7 +1358,8 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul Name: responseName, Message: `they have differing directives`, }, - Fields: []ast.Node{ast1, ast2}, + FieldsLeft: []ast.Node{ast1}, + FieldsRight: []ast.Node{ast2}, } } @@ -1380,10 +1385,12 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul if len(conflicts) > 0 { conflictReasons := []conflictReason{} - conflictFields := []ast.Node{ast1, ast2} + conflictFieldsLeft := []ast.Node{ast1} + conflictFieldsRight := []ast.Node{ast2} for _, c := range conflicts { conflictReasons = append(conflictReasons, c.Reason) - conflictFields = append(conflictFields, c.Fields...) + conflictFieldsLeft = append(conflictFieldsLeft, c.FieldsLeft...) + conflictFieldsRight = append(conflictFieldsRight, c.FieldsRight...) } return &conflict{ @@ -1391,7 +1398,8 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul Name: responseName, Message: conflictReasons, }, - Fields: conflictFields, + FieldsLeft: conflictFieldsLeft, + FieldsRight: conflictFieldsRight, } } } @@ -1467,7 +1475,7 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul responseName, reasonMessage(reason), ), - c.Fields, + append(c.FieldsLeft, c.FieldsRight...), ) } return visitor.ActionNoChange, nil diff --git a/rules_overlapping_fields_can_be_merged_test.go b/rules_overlapping_fields_can_be_merged_test.go index db446bb0..6dd29289 100644 --- a/rules_overlapping_fields_can_be_merged_test.go +++ b/rules_overlapping_fields_can_be_merged_test.go @@ -183,7 +183,10 @@ func TestValidate_OverlappingFieldsCanBeMerged_DeepConflict(t *testing.T) { } `, []gqlerrors.FormattedError{ testutil.RuleError(`Fields "field" conflict because subfields "x" conflict because a and b are different fields.`, - 3, 9, 6, 9, 4, 11, 7, 11), + 3, 9, + 4, 11, + 6, 9, + 7, 11), }) } func TestValidate_OverlappingFieldsCanBeMerged_DeepConflictWithMultipleIssues(t *testing.T) { @@ -202,7 +205,12 @@ func TestValidate_OverlappingFieldsCanBeMerged_DeepConflictWithMultipleIssues(t testutil.RuleError( `Fields "field" conflict because subfields "x" conflict because a and b are different fields and `+ `subfields "y" conflict because c and d are different fields.`, - 3, 9, 7, 9, 4, 11, 8, 11, 5, 11, 9, 11), + 3, 9, + 4, 11, + 5, 11, + 7, 9, + 8, 11, + 9, 11), }) } func TestValidate_OverlappingFieldsCanBeMerged_VeryDeepConflict(t *testing.T) { @@ -223,7 +231,12 @@ func TestValidate_OverlappingFieldsCanBeMerged_VeryDeepConflict(t *testing.T) { testutil.RuleError( `Fields "field" conflict because subfields "deepField" conflict because subfields "x" conflict because `+ `a and b are different fields.`, - 3, 9, 8, 9, 4, 11, 9, 11, 5, 13, 10, 13), + 3, 9, + 4, 11, + 5, 13, + 8, 9, + 9, 11, + 10, 13), }) } func TestValidate_OverlappingFieldsCanBeMerged_ReportsDeepConflictToNearestCommonAncestor(t *testing.T) { @@ -247,7 +260,10 @@ func TestValidate_OverlappingFieldsCanBeMerged_ReportsDeepConflictToNearestCommo testutil.RuleError( `Fields "deepField" conflict because subfields "x" conflict because `+ `a and b are different fields.`, - 4, 11, 7, 11, 5, 13, 8, 13), + 4, 11, + 5, 13, + 7, 11, + 8, 13), }) } @@ -350,7 +366,8 @@ func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_Conf `, []gqlerrors.FormattedError{ testutil.RuleError( `Fields "scalar" conflict because they return differing types Int and String.`, - 5, 15, 8, 15), + 5, 15, + 8, 15), }) } func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_SameWrappedScalarReturnTypes(t *testing.T) { @@ -401,7 +418,12 @@ func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_Comp testutil.RuleError( `Fields "edges" conflict because subfields "node" conflict because subfields "id" conflict because `+ `id and name are different fields.`, - 14, 11, 5, 13, 15, 13, 6, 15, 16, 15, 7, 17), + 14, 11, + 15, 13, + 16, 15, + 5, 13, + 6, 15, + 7, 17), }) } func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_IgnoresUnknownTypes(t *testing.T) { From a1c04d94d34a85d3cd0aee730f4c694aedd57100 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 12 Apr 2016 09:42:55 +0800 Subject: [PATCH 53/69] [validation] Allow safe divergence As pointed out in #53, our validation is more conservative than it needs to be in some cases. Particularly, two fields which could not overlap are still treated as a potential conflict. This change loosens this rule, allowing fields which can never both apply in a frame of execution to diverge. Commit: d71e063fdd1d4c376b4948147e54438b6f1e13de [d71e063] Parents: 228215a704 Author: Lee Byron Date: 13 November 2015 at 9:39:54 AM SGT Commit Date: 13 November 2015 at 10:01:11 AM SGT --- rules.go | 62 +++- ...s_overlapping_fields_can_be_merged_test.go | 282 +++++++++++++----- 2 files changed, 250 insertions(+), 94 deletions(-) diff --git a/rules.go b/rules.go index 5f8fedcf..5eccac02 100644 --- a/rules.go +++ b/rules.go @@ -1078,8 +1078,9 @@ func NoUnusedVariablesRule(context *ValidationContext) *ValidationRuleInstance { } type fieldDefPair struct { - Field *ast.Field - FieldDef *FieldDefinition + ParentType Composite + Field *ast.Field + FieldDef *FieldDefinition } func collectFieldASTsAndDefs(context *ValidationContext, parentType Named, selectionSet *ast.SelectionSet, visitedFragmentNames map[string]bool, astAndDefs map[string][]*fieldDefPair) map[string][]*fieldDefPair { @@ -1116,10 +1117,18 @@ func collectFieldASTsAndDefs(context *ValidationContext, parentType Named, selec if !ok { astAndDefs[responseName] = []*fieldDefPair{} } - astAndDefs[responseName] = append(astAndDefs[responseName], &fieldDefPair{ - Field: selection, - FieldDef: fieldDef, - }) + if parentType, ok := parentType.(Composite); ok { + astAndDefs[responseName] = append(astAndDefs[responseName], &fieldDefPair{ + ParentType: parentType, + Field: selection, + FieldDef: fieldDef, + }) + } else { + astAndDefs[responseName] = append(astAndDefs[responseName], &fieldDefPair{ + Field: selection, + FieldDef: fieldDef, + }) + } case *ast.InlineFragment: inlineFragmentType := parentType if selection.TypeCondition != nil { @@ -1279,6 +1288,10 @@ func sameValue(value1 ast.Value, value2 ast.Value) bool { return val1 == val2 } +func sameType(typeA, typeB Type) bool { + return fmt.Sprintf("%v", typeA) == fmt.Sprintf("%v", typeB) +} + /** * OverlappingFieldsCanBeMergedRule * Overlapping fields can be merged @@ -1291,15 +1304,38 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul comparedSet := newPairSet() var findConflicts func(fieldMap map[string][]*fieldDefPair) (conflicts []*conflict) - findConflict := func(responseName string, pair *fieldDefPair, pair2 *fieldDefPair) *conflict { + findConflict := func(responseName string, field *fieldDefPair, field2 *fieldDefPair) *conflict { - ast1 := pair.Field - def1 := pair.FieldDef + parentType1 := field.ParentType + ast1 := field.Field + def1 := field.FieldDef - ast2 := pair2.Field - def2 := pair2.FieldDef + parentType2 := field2.ParentType + ast2 := field2.Field + def2 := field2.FieldDef + + // Not a pair. + if ast1 == ast2 { + return nil + } + + // If the statically known parent types could not possibly apply at the same + // time, then it is safe to permit them to diverge as they will not present + // any ambiguity by differing. + // It is known that two parent types could never overlap if they are + // different Object types. Interface or Union types might overlap - if not + // in the current state of the schema, then perhaps in some future version, + // thus may not safely diverge. + if parentType1 != parentType2 { + _, ok1 := parentType1.(*Object) + _, ok2 := parentType2.(*Object) + if ok1 && ok2 { + return nil + } + } - if ast1 == ast2 || comparedSet.Has(ast1, ast2) { + // Memoize, do not report the same issue twice. + if comparedSet.Has(ast1, ast2) { return nil } comparedSet.Add(ast1, ast2) @@ -1332,7 +1368,7 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul type2 = def2.Type } - if type1 != nil && type2 != nil && !isEqualType(type1, type2) { + if type1 != nil && type2 != nil && !sameType(type1, type2) { return &conflict{ Reason: conflictReason{ Name: responseName, diff --git a/rules_overlapping_fields_can_be_merged_test.go b/rules_overlapping_fields_can_be_merged_test.go index 6dd29289..d85e66a7 100644 --- a/rules_overlapping_fields_can_be_merged_test.go +++ b/rules_overlapping_fields_can_be_merged_test.go @@ -66,7 +66,19 @@ func TestValidate_OverlappingFieldsCanBeMerged_SameAliasesWithDifferentFieldTarg testutil.RuleError(`Fields "fido" conflict because name and nickname are different fields.`, 3, 9, 4, 9), }) } -func TestValidate_OverlappingFieldsCanBeMerged_AliasMakingDirectFieldAccess(t *testing.T) { +func TestValidate_OverlappingFieldsCanBeMerged_SameAliasesAllowedOnNonOverlappingFields(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment sameAliasesWithDifferentFieldTargets on Pet { + ... on Dog { + name + } + ... on Cat { + name: nickname + } + } + `) +} +func TestValidate_OverlappingFieldsCanBeMerged_AliasMaskingDirectFieldAccess(t *testing.T) { testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` fragment aliasMaskingDirectFieldAccess on Dog { name: nickname @@ -76,6 +88,26 @@ func TestValidate_OverlappingFieldsCanBeMerged_AliasMakingDirectFieldAccess(t *t testutil.RuleError(`Fields "name" conflict because nickname and name are different fields.`, 3, 9, 4, 9), }) } +func TestValidate_OverlappingFieldsCanBeMerged_DifferentArgs_SecondAddsAnArgument(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment conflictingArgs on Dog { + doesKnowCommand + doesKnowCommand(dogCommand: HEEL) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "doesKnowCommand" conflict because they have differing arguments.`, 3, 9, 4, 9), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_DifferentArgs_SecondMissingAnArgument(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment conflictingArgs on Dog { + doesKnowCommand(dogCommand: SIT) + doesKnowCommand + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "doesKnowCommand" conflict because they have differing arguments.`, 3, 9, 4, 9), + }) +} func TestValidate_OverlappingFieldsCanBeMerged_ConflictingArgs(t *testing.T) { testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` fragment conflictingArgs on Dog { @@ -86,6 +118,20 @@ func TestValidate_OverlappingFieldsCanBeMerged_ConflictingArgs(t *testing.T) { testutil.RuleError(`Fields "doesKnowCommand" conflict because they have differing arguments.`, 3, 9, 4, 9), }) } +func TestValidate_OverlappingFieldsCanBeMerged_AllowDifferentArgsWhereNoConflictIsPossible(t *testing.T) { + // This is valid since no object can be both a "Dog" and a "Cat", thus + // these fields can never overlap. + testutil.ExpectPassesRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment conflictingArgs on Pet { + ... on Dog { + name(surname: true) + } + ... on Cat { + name + } + } + `) +} func TestValidate_OverlappingFieldsCanBeMerged_ConflictingDirectives(t *testing.T) { testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` fragment conflictingDirectiveArgs on Dog { @@ -267,113 +313,187 @@ func TestValidate_OverlappingFieldsCanBeMerged_ReportsDeepConflictToNearestCommo }) } -var stringBoxObject = graphql.NewObject(graphql.ObjectConfig{ - Name: "StringBox", - Fields: graphql.Fields{ - "scalar": &graphql.Field{ - Type: graphql.String, +var someBoxInterface *graphql.Interface +var stringBoxObject *graphql.Object +var schema graphql.Schema + +func init() { + someBoxInterface = graphql.NewInterface(graphql.InterfaceConfig{ + Name: "SomeBox", + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + return stringBoxObject }, - }, -}) -var intBoxObject = graphql.NewObject(graphql.ObjectConfig{ - Name: "IntBox", - Fields: graphql.Fields{ - "scalar": &graphql.Field{ - Type: graphql.Int, + Fields: graphql.Fields{ + "unrelatedField": &graphql.Field{ + Type: graphql.String, + }, }, - }, -}) -var nonNullStringBox1Object = graphql.NewObject(graphql.ObjectConfig{ - Name: "NonNullStringBox1", - Fields: graphql.Fields{ - "scalar": &graphql.Field{ - Type: graphql.NewNonNull(graphql.String), + }) + stringBoxObject = graphql.NewObject(graphql.ObjectConfig{ + Name: "StringBox", + Interfaces: (graphql.InterfacesThunk)(func() []*graphql.Interface { + return []*graphql.Interface{someBoxInterface} + }), + Fields: graphql.Fields{ + "scalar": &graphql.Field{ + Type: graphql.String, + }, + "unrelatedField": &graphql.Field{ + Type: graphql.String, + }, }, - }, -}) -var nonNullStringBox2Object = graphql.NewObject(graphql.ObjectConfig{ - Name: "NonNullStringBox2", - Fields: graphql.Fields{ - "scalar": &graphql.Field{ - Type: graphql.NewNonNull(graphql.String), + }) + _ = graphql.NewObject(graphql.ObjectConfig{ + Name: "IntBox", + Interfaces: (graphql.InterfacesThunk)(func() []*graphql.Interface { + return []*graphql.Interface{someBoxInterface} + }), + Fields: graphql.Fields{ + "scalar": &graphql.Field{ + Type: graphql.Int, + }, + "unrelatedField": &graphql.Field{ + Type: graphql.String, + }, }, - }, -}) -var boxUnionObject = graphql.NewUnion(graphql.UnionConfig{ - Name: "BoxUnion", - ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { - return stringBoxObject - }, - Types: []*graphql.Object{ - stringBoxObject, - intBoxObject, - nonNullStringBox1Object, - nonNullStringBox2Object, - }, -}) - -var connectionObject = graphql.NewObject(graphql.ObjectConfig{ - Name: "Connection", - Fields: graphql.Fields{ - "edges": &graphql.Field{ - Type: graphql.NewList(graphql.NewObject(graphql.ObjectConfig{ - Name: "Edge", - Fields: graphql.Fields{ - "node": &graphql.Field{ - Type: graphql.NewObject(graphql.ObjectConfig{ - Name: "Node", - Fields: graphql.Fields{ - "id": &graphql.Field{ - Type: graphql.ID, - }, - "name": &graphql.Field{ - Type: graphql.String, - }, - }, - }), - }, - }, - })), + }) + var nonNullStringBox1Interface = graphql.NewInterface(graphql.InterfaceConfig{ + Name: "NonNullStringBox1", + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + return stringBoxObject + }, + Fields: graphql.Fields{ + "scalar": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + }, + }, + }) + _ = graphql.NewObject(graphql.ObjectConfig{ + Name: "NonNullStringBox1Impl", + Interfaces: (graphql.InterfacesThunk)(func() []*graphql.Interface { + return []*graphql.Interface{someBoxInterface, nonNullStringBox1Interface} + }), + Fields: graphql.Fields{ + "scalar": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + }, + "unrelatedField": &graphql.Field{ + Type: graphql.String, + }, + }, + }) + var nonNullStringBox2Interface = graphql.NewInterface(graphql.InterfaceConfig{ + Name: "NonNullStringBox2", + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + return stringBoxObject + }, + Fields: graphql.Fields{ + "scalar": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + }, }, - }, -}) -var schema, _ = graphql.NewSchema(graphql.SchemaConfig{ - Query: graphql.NewObject(graphql.ObjectConfig{ - Name: "QueryRoot", + }) + _ = graphql.NewObject(graphql.ObjectConfig{ + Name: "NonNullStringBox2Impl", + Interfaces: (graphql.InterfacesThunk)(func() []*graphql.Interface { + return []*graphql.Interface{someBoxInterface, nonNullStringBox2Interface} + }), Fields: graphql.Fields{ - "boxUnion": &graphql.Field{ - Type: boxUnionObject, + "scalar": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), }, - "connection": &graphql.Field{ - Type: connectionObject, + "unrelatedField": &graphql.Field{ + Type: graphql.String, }, }, - }), -}) + }) -func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_ConflictingScalarReturnTypes(t *testing.T) { + var connectionObject = graphql.NewObject(graphql.ObjectConfig{ + Name: "Connection", + Fields: graphql.Fields{ + "edges": &graphql.Field{ + Type: graphql.NewList(graphql.NewObject(graphql.ObjectConfig{ + Name: "Edge", + Fields: graphql.Fields{ + "node": &graphql.Field{ + Type: graphql.NewObject(graphql.ObjectConfig{ + Name: "Node", + Fields: graphql.Fields{ + "id": &graphql.Field{ + Type: graphql.ID, + }, + "name": &graphql.Field{ + Type: graphql.String, + }, + }, + }), + }, + }, + })), + }, + }, + }) + var err error + schema, err = graphql.NewSchema(graphql.SchemaConfig{ + Query: graphql.NewObject(graphql.ObjectConfig{ + Name: "QueryRoot", + Fields: graphql.Fields{ + "someBox": &graphql.Field{ + Type: someBoxInterface, + }, + "connection": &graphql.Field{ + Type: connectionObject, + }, + }, + }), + }) + if err != nil { + panic(err) + } +} + +func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_ConflictingReturnTypesWhichPotentiallyOverlap(t *testing.T) { + // This is invalid since an object could potentially be both the Object + // type IntBox and the interface type NonNullStringBox1. While that + // condition does not exist in the current schema, the schema could + // expand in the future to allow this. Thus it is invalid. testutil.ExpectFailsRuleWithSchema(t, &schema, graphql.OverlappingFieldsCanBeMergedRule, ` { - boxUnion { + someBox { ...on IntBox { scalar } - ...on StringBox { + ...on NonNullStringBox1 { scalar } } } `, []gqlerrors.FormattedError{ testutil.RuleError( - `Fields "scalar" conflict because they return differing types Int and String.`, + `Fields "scalar" conflict because they return differing types Int and String!.`, 5, 15, 8, 15), }) } +func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_AllowsDiffereingReturnTypesWhichCannotOverlap(t *testing.T) { + // This is valid since an object cannot be both an IntBox and a StringBox. + testutil.ExpectPassesRuleWithSchema(t, &schema, graphql.OverlappingFieldsCanBeMergedRule, ` + { + someBox { + ...on IntBox { + scalar + } + ...on StringBox { + scalar + } + } + } + `) +} func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_SameWrappedScalarReturnTypes(t *testing.T) { testutil.ExpectPassesRuleWithSchema(t, &schema, graphql.OverlappingFieldsCanBeMergedRule, ` { - boxUnion { + someBox { ...on NonNullStringBox1 { scalar } @@ -429,7 +549,7 @@ func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_Comp func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_IgnoresUnknownTypes(t *testing.T) { testutil.ExpectPassesRuleWithSchema(t, &schema, graphql.OverlappingFieldsCanBeMergedRule, ` { - boxUnion { + someBox { ...on UnknownType { scalar } From a0a5d162f790563acd9b7601b272d24b45710bf9 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 12 Apr 2016 10:01:45 +0800 Subject: [PATCH 54/69] [validation] Allow differing directives. The currently supported directives: @skip and @include do not create an ambiguous or erroneous query when used on conflicting fields in a divergent fashion. In fact, this may be intended when many different conditions should result in the same outcome. For example: ```graphql { field @include(if: $firstCondition) field @include(if: $secondCondition) } ``` This example could be considered as the intent of fetching `field` given `$firstCondition || $secondCondition`. While this example is contrived, there are more complex examples where such fields are nested within fragments that this condition is reasonable. Commit: 9b11df2efc66ad3c07e0d373a7e04a3ba5ee581a [9b11df2] Parents: f010e86c3d Author: Lee Byron Date: 13 November 2015 at 11:48:47 AM SGT --- rules.go | 41 --------------- ...s_overlapping_fields_can_be_merged_test.go | 51 ++++--------------- 2 files changed, 11 insertions(+), 81 deletions(-) diff --git a/rules.go b/rules.go index 5eccac02..d7aee54b 100644 --- a/rules.go +++ b/rules.go @@ -1215,37 +1215,6 @@ type conflict struct { FieldsRight []ast.Node } -func sameDirectives(directives1 []*ast.Directive, directives2 []*ast.Directive) bool { - if len(directives1) != len(directives1) { - return false - } - for _, directive1 := range directives1 { - directive1Name := "" - if directive1.Name != nil { - directive1Name = directive1.Name.Value - } - - var foundDirective2 *ast.Directive - for _, directive2 := range directives2 { - directive2Name := "" - if directive2.Name != nil { - directive2Name = directive2.Name.Value - } - if directive1Name == directive2Name { - foundDirective2 = directive2 - } - break - } - if foundDirective2 == nil { - return false - } - if sameArguments(directive1.Arguments, foundDirective2.Arguments) == false { - return false - } - } - - return true -} func sameArguments(args1 []*ast.Argument, args2 []*ast.Argument) bool { if len(args1) != len(args2) { return false @@ -1388,16 +1357,6 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul FieldsRight: []ast.Node{ast2}, } } - if !sameDirectives(ast1.Directives, ast2.Directives) { - return &conflict{ - Reason: conflictReason{ - Name: responseName, - Message: `they have differing directives`, - }, - FieldsLeft: []ast.Node{ast1}, - FieldsRight: []ast.Node{ast2}, - } - } selectionSet1 := ast1.SelectionSet selectionSet2 := ast2.SelectionSet diff --git a/rules_overlapping_fields_can_be_merged_test.go b/rules_overlapping_fields_can_be_merged_test.go index d85e66a7..903367ea 100644 --- a/rules_overlapping_fields_can_be_merged_test.go +++ b/rules_overlapping_fields_can_be_merged_test.go @@ -56,6 +56,17 @@ func TestValidate_OverlappingFieldsCanBeMerged_DifferentDirectivesWithDifferentA } `) } +func TestValidate_OverlappingFieldsCanBeMerged_DifferentSkipIncludeDirectivesAccepted(t *testing.T) { + // Note: Differing skip/include directives don't create an ambiguous return + // value and are acceptable in conditions where differing runtime values + // may have the same desired effect of including or skipping a field. + testutil.ExpectPassesRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment differentDirectivesWithDifferentAliases on Dog { + name @include(if: true) + name @include(if: false) + } + `) +} func TestValidate_OverlappingFieldsCanBeMerged_SameAliasesWithDifferentFieldTargets(t *testing.T) { testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` fragment sameAliasesWithDifferentFieldTargets on Dog { @@ -132,46 +143,6 @@ func TestValidate_OverlappingFieldsCanBeMerged_AllowDifferentArgsWhereNoConflict } `) } -func TestValidate_OverlappingFieldsCanBeMerged_ConflictingDirectives(t *testing.T) { - testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` - fragment conflictingDirectiveArgs on Dog { - name @include(if: true) - name @skip(if: false) - } - `, []gqlerrors.FormattedError{ - testutil.RuleError(`Fields "name" conflict because they have differing directives.`, 3, 9, 4, 9), - }) -} -func TestValidate_OverlappingFieldsCanBeMerged_ConflictingDirectiveArgs(t *testing.T) { - testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` - fragment conflictingDirectiveArgs on Dog { - name @include(if: true) - name @include(if: false) - } - `, []gqlerrors.FormattedError{ - testutil.RuleError(`Fields "name" conflict because they have differing directives.`, 3, 9, 4, 9), - }) -} -func TestValidate_OverlappingFieldsCanBeMerged_ConflictingArgsWithMatchingDirectives(t *testing.T) { - testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` - fragment conflictingArgsWithMatchingDirectiveArgs on Dog { - doesKnowCommand(dogCommand: SIT) @include(if: true) - doesKnowCommand(dogCommand: HEEL) @include(if: true) - } - `, []gqlerrors.FormattedError{ - testutil.RuleError(`Fields "doesKnowCommand" conflict because they have differing arguments.`, 3, 9, 4, 9), - }) -} -func TestValidate_OverlappingFieldsCanBeMerged_ConflictingDirectivesWithMatchingArgs(t *testing.T) { - testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` - fragment conflictingDirectiveArgsWithMatchingArgs on Dog { - doesKnowCommand(dogCommand: SIT) @include(if: true) - doesKnowCommand(dogCommand: SIT) @skip(if: false) - } - `, []gqlerrors.FormattedError{ - testutil.RuleError(`Fields "doesKnowCommand" conflict because they have differing directives.`, 3, 9, 4, 9), - }) -} func TestValidate_OverlappingFieldsCanBeMerged_EncountersConflictInFragments(t *testing.T) { testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` { From 77cba81ab8158c8fc1b290637e1672b1d4ce9982 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 12 Apr 2016 10:04:00 +0800 Subject: [PATCH 55/69] Fix test for unique arg names Commit: 657fbbba26ed1fa18915b2a9f050a5fae08a7469 [657fbbb] Parents: 9cedbc3ffe Author: Lee Byron Date: 24 September 2015 at 7:39:39 AM SGT --- rules_unique_argument_names_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules_unique_argument_names_test.go b/rules_unique_argument_names_test.go index 2c111b80..b0e3ec51 100644 --- a/rules_unique_argument_names_test.go +++ b/rules_unique_argument_names_test.go @@ -18,7 +18,7 @@ func TestValidate_UniqueArgumentNames_NoArgumentsOnField(t *testing.T) { func TestValidate_UniqueArgumentNames_NoArgumentsOnDirective(t *testing.T) { testutil.ExpectPassesRule(t, graphql.UniqueArgumentNamesRule, ` { - field + field @directive } `) } From ed671115c176e97436b0d83de7a732a19768c6e8 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 12 Apr 2016 10:18:52 +0800 Subject: [PATCH 56/69] [Validation] Include variable definition node when reporting bad var type Commit: 1d14db78b8e38d6cb5b0698dadc774ed794f7398 [1d14db7] Parents: 81e759620b Author: Lee Byron Date: 17 November 2015 at 12:56:43 PM SGT --- rules.go | 31 +++++++----------- rules_variables_in_allowed_position_test.go | 35 +++++++++------------ 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/rules.go b/rules.go index d7aee54b..fd67ac56 100644 --- a/rules.go +++ b/rules.go @@ -2042,29 +2042,20 @@ func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleI if usage != nil && usage.Node != nil && usage.Node.Name != nil { varName = usage.Node.Name.Value } - var varType Type - varDef, ok := varDefMap[varName] - if ok { - // A var type is allowed if it is the same or more strict (e.g. is - // a subtype of) than the expected type. It can be more strict if - // the variable type is non-null when the expected type is nullable. - // If both are list types, the variable item type can be more strict - // than the expected item type (contravariant). - var err error - varType, err = typeFromAST(*context.Schema(), varDef.Type) + varDef, _ := varDefMap[varName] + if varDef != nil && usage.Type != nil { + varType, err := typeFromAST(*context.Schema(), varDef.Type) if err != nil { varType = nil } - } - if varType != nil && - usage.Type != nil && - !isTypeSubTypeOf(effectiveType(varType, varDef), usage.Type) { - reportError( - context, - fmt.Sprintf(`Variable "$%v" of type "%v" used in position `+ - `expecting type "%v".`, varName, varType, usage.Type), - []ast.Node{usage.Node}, - ) + if varType != nil && !isTypeSubTypeOf(effectiveType(varType, varDef), usage.Type) { + reportError( + context, + fmt.Sprintf(`Variable "$%v" of type "%v" used in position `+ + `expecting type "%v".`, varName, varType, usage.Type), + []ast.Node{usage.Node, varDef}, + ) + } } } diff --git a/rules_variables_in_allowed_position_test.go b/rules_variables_in_allowed_position_test.go index 83ee2aa7..59db585a 100644 --- a/rules_variables_in_allowed_position_test.go +++ b/rules_variables_in_allowed_position_test.go @@ -154,15 +154,14 @@ func TestValidate_VariablesInAllowedPosition_NonNullableBooleanToNonNullableBool } func TestValidate_VariablesInAllowedPosition_IntToNonNullableInt(t *testing.T) { testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` - query Query($intArg: Int) - { + query Query($intArg: Int) { complicatedArgs { nonNullIntArgField(nonNullIntArg: $intArg) } } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$intArg" of type "Int" used in position `+ - `expecting type "Int!".`, 5, 45), + `expecting type "Int!".`, 4, 45, 2, 19), }) } func TestValidate_VariablesInAllowedPosition_IntToNonNullableIntWithinFragment(t *testing.T) { @@ -171,15 +170,14 @@ func TestValidate_VariablesInAllowedPosition_IntToNonNullableIntWithinFragment(t nonNullIntArgField(nonNullIntArg: $intArg) } - query Query($intArg: Int) - { + query Query($intArg: Int) { complicatedArgs { ...nonNullIntArgFieldFrag } } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$intArg" of type "Int" used in position `+ - `expecting type "Int!".`, 3, 43), + `expecting type "Int!".`, 3, 43, 6, 19), }) } func TestValidate_VariablesInAllowedPosition_IntToNonNullableIntWithinNestedFragment(t *testing.T) { @@ -192,62 +190,57 @@ func TestValidate_VariablesInAllowedPosition_IntToNonNullableIntWithinNestedFrag nonNullIntArgField(nonNullIntArg: $intArg) } - query Query($intArg: Int) - { + query Query($intArg: Int) { complicatedArgs { ...outerFrag } } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$intArg" of type "Int" used in position `+ - `expecting type "Int!".`, 7, 43), + `expecting type "Int!".`, 7, 43, 10, 19), }) } func TestValidate_VariablesInAllowedPosition_StringOverBoolean(t *testing.T) { testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` - query Query($stringVar: String) - { + query Query($stringVar: String) { complicatedArgs { booleanArgField(booleanArg: $stringVar) } } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$stringVar" of type "String" used in position `+ - `expecting type "Boolean".`, 5, 39), + `expecting type "Boolean".`, 4, 39, 2, 19), }) } func TestValidate_VariablesInAllowedPosition_StringToListOfString(t *testing.T) { testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` - query Query($stringVar: String) - { + query Query($stringVar: String) { complicatedArgs { stringListArgField(stringListArg: $stringVar) } } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$stringVar" of type "String" used in position `+ - `expecting type "[String]".`, 5, 45), + `expecting type "[String]".`, 4, 45, 2, 19), }) } func TestValidate_VariablesInAllowedPosition_BooleanToNonNullableBooleanInDirective(t *testing.T) { testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` - query Query($boolVar: Boolean) - { + query Query($boolVar: Boolean) { dog @include(if: $boolVar) } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$boolVar" of type "Boolean" used in position `+ - `expecting type "Boolean!".`, 4, 26), + `expecting type "Boolean!".`, 3, 26, 2, 19), }) } func TestValidate_VariablesInAllowedPosition_StringToNonNullableBooleanInDirective(t *testing.T) { testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` - query Query($stringVar: String) - { + query Query($stringVar: String) { dog @include(if: $stringVar) } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$stringVar" of type "String" used in position `+ - `expecting type "Boolean!".`, 4, 26), + `expecting type "Boolean!".`, 3, 26, 2, 19), }) } From efc3708f7abba3474a4ab58d6b3b1bb78158d2a7 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 12 Apr 2016 13:49:55 +0800 Subject: [PATCH 57/69] [Validation] Report var def before var usage Commit: e81cf39750e8c2dbde7a7910e13893aa644e02dd [e81cf39] Parents: 1d14db78b8 Author: Lee Byron Date: 17 November 2015 at 1:00:16 PM SGT --- rules.go | 2 +- rules_variables_in_allowed_position_test.go | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rules.go b/rules.go index fd67ac56..1419bf39 100644 --- a/rules.go +++ b/rules.go @@ -2053,7 +2053,7 @@ func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleI context, fmt.Sprintf(`Variable "$%v" of type "%v" used in position `+ `expecting type "%v".`, varName, varType, usage.Type), - []ast.Node{usage.Node, varDef}, + []ast.Node{varDef, usage.Node}, ) } } diff --git a/rules_variables_in_allowed_position_test.go b/rules_variables_in_allowed_position_test.go index 59db585a..aa923c2b 100644 --- a/rules_variables_in_allowed_position_test.go +++ b/rules_variables_in_allowed_position_test.go @@ -161,7 +161,7 @@ func TestValidate_VariablesInAllowedPosition_IntToNonNullableInt(t *testing.T) { } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$intArg" of type "Int" used in position `+ - `expecting type "Int!".`, 4, 45, 2, 19), + `expecting type "Int!".`, 2, 19, 4, 45), }) } func TestValidate_VariablesInAllowedPosition_IntToNonNullableIntWithinFragment(t *testing.T) { @@ -177,7 +177,7 @@ func TestValidate_VariablesInAllowedPosition_IntToNonNullableIntWithinFragment(t } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$intArg" of type "Int" used in position `+ - `expecting type "Int!".`, 3, 43, 6, 19), + `expecting type "Int!".`, 6, 19, 3, 43), }) } func TestValidate_VariablesInAllowedPosition_IntToNonNullableIntWithinNestedFragment(t *testing.T) { @@ -197,7 +197,7 @@ func TestValidate_VariablesInAllowedPosition_IntToNonNullableIntWithinNestedFrag } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$intArg" of type "Int" used in position `+ - `expecting type "Int!".`, 7, 43, 10, 19), + `expecting type "Int!".`, 10, 19, 7, 43), }) } func TestValidate_VariablesInAllowedPosition_StringOverBoolean(t *testing.T) { @@ -209,7 +209,7 @@ func TestValidate_VariablesInAllowedPosition_StringOverBoolean(t *testing.T) { } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$stringVar" of type "String" used in position `+ - `expecting type "Boolean".`, 4, 39, 2, 19), + `expecting type "Boolean".`, 2, 19, 4, 39), }) } func TestValidate_VariablesInAllowedPosition_StringToListOfString(t *testing.T) { @@ -221,7 +221,7 @@ func TestValidate_VariablesInAllowedPosition_StringToListOfString(t *testing.T) } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$stringVar" of type "String" used in position `+ - `expecting type "[String]".`, 4, 45, 2, 19), + `expecting type "[String]".`, 2, 19, 4, 45), }) } func TestValidate_VariablesInAllowedPosition_BooleanToNonNullableBooleanInDirective(t *testing.T) { @@ -231,7 +231,7 @@ func TestValidate_VariablesInAllowedPosition_BooleanToNonNullableBooleanInDirect } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$boolVar" of type "Boolean" used in position `+ - `expecting type "Boolean!".`, 3, 26, 2, 19), + `expecting type "Boolean!".`, 2, 19, 3, 26), }) } func TestValidate_VariablesInAllowedPosition_StringToNonNullableBooleanInDirective(t *testing.T) { @@ -241,6 +241,6 @@ func TestValidate_VariablesInAllowedPosition_StringToNonNullableBooleanInDirecti } `, []gqlerrors.FormattedError{ testutil.RuleError(`Variable "$stringVar" of type "String" used in position `+ - `expecting type "Boolean!".`, 3, 26, 2, 19), + `expecting type "Boolean!".`, 2, 19, 3, 26), }) } From 679c06da674cccf2e967f8c6cc6c74d37eb6fd4e Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 12 Apr 2016 13:55:40 +0800 Subject: [PATCH 58/69] Fix typo in unit test, closes #269 Commit: 71b7b4aa2d0c82c71efb9c941dbd93e65e93a21c [71b7b4a] Parents: 3eb5b7aa57 Author: Lee Byron Date: 2 February 2016 at 9:43:02 AM SGT --- rules_variables_in_allowed_position_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules_variables_in_allowed_position_test.go b/rules_variables_in_allowed_position_test.go index aa923c2b..78dd77ea 100644 --- a/rules_variables_in_allowed_position_test.go +++ b/rules_variables_in_allowed_position_test.go @@ -121,7 +121,7 @@ func TestValidate_VariablesInAllowedPosition_ComplexInputToComplexInput(t *testi query Query($complexVar: ComplexInput) { complicatedArgs { - complexArgField(complexArg: $ComplexInput) + complexArgField(complexArg: $complexVar) } } `) From 2c13d3f6e4dcd0995fd8dbd6ca31c98a2575050f Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Tue, 12 Apr 2016 14:24:26 +0800 Subject: [PATCH 59/69] [Validation] Performance improvements Fairly dramatic improvement of some validation rules by short-circuiting branches which do not need to be checked and memoizing the process of collecting fragment spreads from a given context - which is necessary by multiple rules. Commit: 0bc9088187b9902ab19c0ec34e0e9f036dc9d9ea [0bc9088] Parents: 7278b7c92c Author: Lee Byron Date: 13 November 2015 at 4:26:05 PM SGT --- rules.go | 82 +++++++++++++++++++++++++++----------------------------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/rules.go b/rules.go index 1419bf39..caa76a8a 100644 --- a/rules.go +++ b/rules.go @@ -74,8 +74,6 @@ func ArgumentsOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInsta KindFuncMap: map[string]visitor.NamedVisitFuncs{ kinds.Argument: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - var action = visitor.ActionNoChange - var result interface{} if argAST, ok := p.Node.(*ast.Argument); ok { value := argAST.Value argDef := context.Argument() @@ -98,7 +96,7 @@ func ArgumentsOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInsta ) } } - return action, result + return visitor.ActionSkip, nil }, }, }, @@ -120,8 +118,6 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI KindFuncMap: map[string]visitor.NamedVisitFuncs{ kinds.VariableDefinition: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - var action = visitor.ActionNoChange - var result interface{} if varDefAST, ok := p.Node.(*ast.VariableDefinition); ok { name := "" if varDefAST.Variable != nil && varDefAST.Variable.Name != nil { @@ -152,7 +148,17 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI ) } } - return action, result + return visitor.ActionSkip, nil + }, + }, + kinds.SelectionSet: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + return visitor.ActionSkip, nil + }, + }, + kinds.FragmentDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + return visitor.ActionSkip, nil }, }, }, @@ -913,46 +919,24 @@ func NoUndefinedVariablesRule(context *ValidationContext) *ValidationRuleInstanc func NoUnusedFragmentsRule(context *ValidationContext) *ValidationRuleInstance { var fragmentDefs = []*ast.FragmentDefinition{} - var spreadsWithinOperation = []map[string]bool{} - var fragAdjacencies = map[string]map[string]bool{} - var spreadNames = map[string]bool{} + var spreadsWithinOperation = [][]*ast.FragmentSpread{} visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ kinds.OperationDefinition: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.OperationDefinition); ok && node != nil { - spreadNames = map[string]bool{} - spreadsWithinOperation = append(spreadsWithinOperation, spreadNames) + spreadsWithinOperation = append(spreadsWithinOperation, context.FragmentSpreads(node)) } - return visitor.ActionNoChange, nil + return visitor.ActionSkip, nil }, }, kinds.FragmentDefinition: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { if def, ok := p.Node.(*ast.FragmentDefinition); ok && def != nil { - defName := "" - if def.Name != nil { - defName = def.Name.Value - } - fragmentDefs = append(fragmentDefs, def) - spreadNames = map[string]bool{} - fragAdjacencies[defName] = spreadNames - } - return visitor.ActionNoChange, nil - }, - }, - kinds.FragmentSpread: visitor.NamedVisitFuncs{ - Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - if spread, ok := p.Node.(*ast.FragmentSpread); ok && spread != nil { - spreadName := "" - if spread.Name != nil { - spreadName = spread.Name.Value - } - spreadNames[spreadName] = true } - return visitor.ActionNoChange, nil + return visitor.ActionSkip, nil }, }, kinds.Document: visitor.NamedVisitFuncs{ @@ -960,14 +944,18 @@ func NoUnusedFragmentsRule(context *ValidationContext) *ValidationRuleInstance { fragmentNameUsed := map[string]interface{}{} - var reduceSpreadFragments func(spreads map[string]bool) - reduceSpreadFragments = func(spreads map[string]bool) { - for fragName, _ := range spreads { + var reduceSpreadFragments func(spreads []*ast.FragmentSpread) + reduceSpreadFragments = func(spreads []*ast.FragmentSpread) { + for _, spread := range spreads { + fragName := "" + if spread.Name != nil { + fragName = spread.Name.Value + } if isFragNameUsed, _ := fragmentNameUsed[fragName]; isFragNameUsed != true { fragmentNameUsed[fragName] = true - - if adjacencies, ok := fragAdjacencies[fragName]; ok { - reduceSpreadFragments(adjacencies) + fragment := context.Fragment(fragName) + if fragment != nil { + reduceSpreadFragments(context.FragmentSpreads(fragment)) } } } @@ -1802,6 +1790,11 @@ func UniqueFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.OperationDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + return visitor.ActionSkip, nil + }, + }, kinds.FragmentDefinition: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.FragmentDefinition); ok && node != nil { @@ -1818,7 +1811,7 @@ func UniqueFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance } knownFragmentNames[fragmentName] = node.Name } - return visitor.ActionNoChange, nil + return visitor.ActionSkip, nil }, }, }, @@ -1854,8 +1847,6 @@ func UniqueInputFieldNamesRule(context *ValidationContext) *ValidationRuleInstan }, kinds.ObjectField: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - var action = visitor.ActionNoChange - var result interface{} if node, ok := p.Node.(*ast.ObjectField); ok { fieldName := "" if node.Name != nil { @@ -1872,7 +1863,7 @@ func UniqueInputFieldNamesRule(context *ValidationContext) *ValidationRuleInstan } } - return action, result + return visitor.ActionSkip, nil }, }, }, @@ -1909,7 +1900,12 @@ func UniqueOperationNamesRule(context *ValidationContext) *ValidationRuleInstanc } knownOperationNames[operationName] = node.Name } - return visitor.ActionNoChange, nil + return visitor.ActionSkip, nil + }, + }, + kinds.FragmentDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + return visitor.ActionSkip, nil }, }, }, From 1620a889758fd225f0166af0f3aa356038a6be84 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 13 Apr 2016 15:44:01 +0800 Subject: [PATCH 60/69] Merge branch 'better-error-messages-for-inputs' of https://github.com/freiksenet/graphql-js into freiksenet-better-error-messages-for-inputs Commit: 284415e00e905e9d3ff24ad931d7aa9ea849a312 [284415e] Parents: 2f10ad44e7, ab13f32c1c Author: Lee Byron Date: 18 November 2015 at 3:48:47 AM SGT --- rules.go | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/rules.go b/rules.go index caa76a8a..5fbe11c3 100644 --- a/rules.go +++ b/rules.go @@ -77,23 +77,26 @@ func ArgumentsOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInsta if argAST, ok := p.Node.(*ast.Argument); ok { value := argAST.Value argDef := context.Argument() - isValid, messages := isValidLiteralValue(argDef.Type, value) - if argDef != nil && !isValid { - argNameValue := "" - if argAST.Name != nil { - argNameValue = argAST.Name.Value - } + if argDef != nil { + isValid, messages := isValidLiteralValue(argDef.Type, value) + if !isValid { + argNameValue := "" + if argAST.Name != nil { + argNameValue = argAST.Name.Value + } - messagesStr := "" - if len(messages) > 0 { - messagesStr = "\n" + strings.Join(messages, "\n") + messagesStr := "" + if len(messages) > 0 { + messagesStr = "\n" + strings.Join(messages, "\n") + } + return reportError( + context, + fmt.Sprintf(`Argument "%v" has invalid value %v.%v`, + argNameValue, printer.Print(value), messagesStr), + []ast.Node{value}, + ) } - return reportError( - context, - fmt.Sprintf(`Argument "%v" has invalid value %v.%v`, - argNameValue, printer.Print(value), messagesStr), - []ast.Node{value}, - ) + } } return visitor.ActionSkip, nil From 6d0e34100ec0c58e036222b7f97f89d002d12cf3 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 13 Apr 2016 15:45:10 +0800 Subject: [PATCH 61/69] Replaced validator kind strings with constants There were a few places in the validation rules that were not using the constants found in `src/language/kinds.js`. This commit replaces those strings with their respective constants. This can be considered a cleanup. Commit: 17998532216b520a899a196f54bcb59c1faa512a [1799853] Parents: 662e316f04 Author: Brandon Wood Date: 17 September 2015 at 9:52:56 AM SGT --- rules.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rules.go b/rules.go index 5fbe11c3..71b05f14 100644 --- a/rules.go +++ b/rules.go @@ -420,7 +420,7 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance if argumentOf == nil { return action, result } - if argumentOf.GetKind() == "Field" { + if argumentOf.GetKind() == kinds.Field { fieldDef := context.FieldDef() if fieldDef == nil { return action, result @@ -447,7 +447,7 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance []ast.Node{node}, ) } - } else if argumentOf.GetKind() == "Directive" { + } else if argumentOf.GetKind() == kinds.Directive { directive := context.Directive() if directive == nil { return action, result From 3f777a5c90c5e683083695a4c643b59ad45c499c Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 13 Apr 2016 17:33:59 +0800 Subject: [PATCH 62/69] [Validation] Factor out and memoize recursively referenced fragments. This adds a new method to the validation context which when given an Operation definition, returns a list of all Fragment definitions recursively referenced via fragment spreads. This new method is then used to ensure no fragments are unused. The implementation of this method is the same in principle as the one which used to be inline in the validation rule, but has been unfolded from recursion to use a while loop. Commit: eef8d97f64b5b5fa0df79435c4fe237976867573 [eef8d97] Parents: 568dc52a4f Author: Lee Byron Date: 17 November 2015 at 9:31:46 AM SGT --- rules.go | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/rules.go b/rules.go index 71b05f14..8bcb1067 100644 --- a/rules.go +++ b/rules.go @@ -922,50 +922,40 @@ func NoUndefinedVariablesRule(context *ValidationContext) *ValidationRuleInstanc func NoUnusedFragmentsRule(context *ValidationContext) *ValidationRuleInstance { var fragmentDefs = []*ast.FragmentDefinition{} - var spreadsWithinOperation = [][]*ast.FragmentSpread{} + var operationDefs = []*ast.OperationDefinition{} visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ kinds.OperationDefinition: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.OperationDefinition); ok && node != nil { - spreadsWithinOperation = append(spreadsWithinOperation, context.FragmentSpreads(node)) + operationDefs = append(operationDefs, node) } return visitor.ActionSkip, nil }, }, kinds.FragmentDefinition: visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { - if def, ok := p.Node.(*ast.FragmentDefinition); ok && def != nil { - fragmentDefs = append(fragmentDefs, def) + if node, ok := p.Node.(*ast.FragmentDefinition); ok && node != nil { + fragmentDefs = append(fragmentDefs, node) } return visitor.ActionSkip, nil }, }, kinds.Document: visitor.NamedVisitFuncs{ Leave: func(p visitor.VisitFuncParams) (string, interface{}) { - - fragmentNameUsed := map[string]interface{}{} - - var reduceSpreadFragments func(spreads []*ast.FragmentSpread) - reduceSpreadFragments = func(spreads []*ast.FragmentSpread) { - for _, spread := range spreads { + fragmentNameUsed := map[string]bool{} + for _, operation := range operationDefs { + fragments := context.RecursivelyReferencedFragments(operation) + for _, fragment := range fragments { fragName := "" - if spread.Name != nil { - fragName = spread.Name.Value - } - if isFragNameUsed, _ := fragmentNameUsed[fragName]; isFragNameUsed != true { - fragmentNameUsed[fragName] = true - fragment := context.Fragment(fragName) - if fragment != nil { - reduceSpreadFragments(context.FragmentSpreads(fragment)) - } + if fragment.Name != nil { + fragName = fragment.Name.Value } + fragmentNameUsed[fragName] = true } } - for _, spreadWithinOperation := range spreadsWithinOperation { - reduceSpreadFragments(spreadWithinOperation) - } + for _, def := range fragmentDefs { defName := "" if def.Name != nil { From 463deee98356864a636dd88ceb36822254b774f7 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 13 Apr 2016 17:38:30 +0800 Subject: [PATCH 63/69] Perf improvement for comparing two types Commit: f010e86c3d3057dee36d6096ed2600492c41f4e7 [f010e86] Parents: 6a3eac753d Author: Lee Byron Date: 13 November 2015 at 10:11:42 AM SGT --- rules.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/rules.go b/rules.go index 8bcb1067..71f181b5 100644 --- a/rules.go +++ b/rules.go @@ -1240,6 +1240,23 @@ func sameValue(value1 ast.Value, value2 ast.Value) bool { func sameType(typeA, typeB Type) bool { return fmt.Sprintf("%v", typeA) == fmt.Sprintf("%v", typeB) + + if typeA == typeB { + return true + } + + if typeA, ok := typeA.(*List); ok { + if typeB, ok := typeB.(*List); ok { + return sameType(typeA.OfType, typeB.OfType) + } + } + if typeA, ok := typeA.(*NonNull); ok { + if typeB, ok := typeB.(*NonNull); ok { + return sameType(typeA.OfType, typeB.OfType) + } + } + + return false } /** From c1006579ecf08824dcbb36ff80ffe165eac562e6 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Wed, 13 Apr 2016 17:45:23 +0800 Subject: [PATCH 64/69] [Validation] Performance improvements Fairly dramatic improvement of some validation rules by short-circuiting branches which do not need to be checked and memoizing the process of collecting fragment spreads from a given context - which is necessary by multiple rules. Commit: 0bc9088187b9902ab19c0ec34e0e9f036dc9d9ea [0bc9088] Parents: 7278b7c92c Author: Lee Byron Date: 13 November 2015 at 4:26:05 PM SGT --- rules.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules.go b/rules.go index 71f181b5..5414b88b 100644 --- a/rules.go +++ b/rules.go @@ -1779,7 +1779,7 @@ func UniqueArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance } knownArgNames[argName] = node.Name } - return visitor.ActionNoChange, nil + return visitor.ActionSkip, nil }, }, }, From 88f865388e8ea406921b1c16fc65bcd7728b13a5 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Thu, 14 Apr 2016 17:12:21 +0800 Subject: [PATCH 65/69] [Validation] Report errors rather than return them This replaces the mechanism of returning errors or lists of errors from a validator step to instead report those errors via calling a function on the context. This simplifies the implementation of the visitor mechanism, but also opens the doors for future rules being specified as warnings instead of errors. Commit: 5e545cce0104708a4ac6e994dd5f837d1d30a09b [5e545cc] Parents: ef7c755c58 Author: Lee Byron Date: 17 November 2015 at 11:32:13 AM SGT --- rules.go | 60 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/rules.go b/rules.go index 5414b88b..bdfeb8b0 100644 --- a/rules.go +++ b/rules.go @@ -89,7 +89,7 @@ func ArgumentsOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInsta if len(messages) > 0 { messagesStr = "\n" + strings.Join(messages, "\n") } - return reportError( + reportError( context, fmt.Sprintf(`Argument "%v" has invalid value %v.%v`, argNameValue, printer.Print(value), messagesStr), @@ -130,7 +130,7 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI ttype := context.InputType() if ttype, ok := ttype.(*NonNull); ok && defaultValue != nil { - return reportError( + reportError( context, fmt.Sprintf(`Variable "$%v" of type "%v" is required and will not use the default value. Perhaps you meant to use type "%v".`, name, ttype, ttype.OfType), @@ -143,7 +143,7 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI if len(messages) > 0 { messagesStr = "\n" + strings.Join(messages, "\n") } - return reportError( + reportError( context, fmt.Sprintf(`Variable "$%v" has invalid default value: %v.%v`, name, printer.Print(defaultValue), messagesStr), @@ -245,7 +245,7 @@ func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance message := UndefinedFieldMessage(nodeName, ttype.Name(), suggestedTypes) - return reportError( + reportError( context, message, []ast.Node{node}, @@ -362,7 +362,7 @@ func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleIn if node, ok := p.Node.(*ast.InlineFragment); ok { ttype := context.Type() if node.TypeCondition != nil && ttype != nil && !IsCompositeType(ttype) { - return reportError( + reportError( context, fmt.Sprintf(`Fragment cannot condition on non composite type "%v".`, ttype), []ast.Node{node.TypeCondition}, @@ -381,7 +381,7 @@ func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleIn if node.Name != nil { nodeName = node.Name.Value } - return reportError( + reportError( context, fmt.Sprintf(`Fragment "%v" cannot condition on non composite type "%v".`, nodeName, printer.Print(node.TypeCondition)), []ast.Node{node.TypeCondition}, @@ -441,7 +441,7 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance if parentType != nil { parentTypeName = parentType.Name() } - return reportError( + reportError( context, fmt.Sprintf(`Unknown argument "%v" on field "%v" of type "%v".`, nodeName, fieldDef.Name, parentTypeName), []ast.Node{node}, @@ -463,7 +463,7 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance } } if directiveArgDef == nil { - return reportError( + reportError( context, fmt.Sprintf(`Unknown argument "%v" on directive "@%v".`, nodeName, directive.Name), []ast.Node{node}, @@ -525,14 +525,14 @@ func KnownDirectivesRule(context *ValidationContext) *ValidationRuleInstance { } if appliedTo.GetKind() == kinds.OperationDefinition && directiveDef.OnOperation == false { - return reportError( + reportError( context, fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "operation"), []ast.Node{node}, ) } if appliedTo.GetKind() == kinds.Field && directiveDef.OnField == false { - return reportError( + reportError( context, fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "field"), []ast.Node{node}, @@ -541,7 +541,7 @@ func KnownDirectivesRule(context *ValidationContext) *ValidationRuleInstance { if (appliedTo.GetKind() == kinds.FragmentSpread || appliedTo.GetKind() == kinds.InlineFragment || appliedTo.GetKind() == kinds.FragmentDefinition) && directiveDef.OnFragment == false { - return reportError( + reportError( context, fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "fragment"), []ast.Node{node}, @@ -582,7 +582,7 @@ func KnownFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance fragment := context.Fragment(fragmentName) if fragment == nil { - return reportError( + reportError( context, fmt.Sprintf(`Unknown fragment "%v".`, fragmentName), []ast.Node{node.Name}, @@ -639,7 +639,7 @@ func KnownTypeNamesRule(context *ValidationContext) *ValidationRuleInstance { } ttype := context.Schema().Type(typeNameValue) if ttype == nil { - return reportError( + reportError( context, fmt.Sprintf(`Unknown type "%v".`, typeNameValue), []ast.Node{node}, @@ -684,7 +684,7 @@ func LoneAnonymousOperationRule(context *ValidationContext) *ValidationRuleInsta Kind: func(p visitor.VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(*ast.OperationDefinition); ok { if node.Name == nil && operationCount > 1 { - return reportError( + reportError( context, `This anonymous operation must be the only defined operation.`, []ast.Node{node}, @@ -1554,7 +1554,7 @@ func PossibleFragmentSpreadsRule(context *ValidationContext) *ValidationRuleInst parentType, _ := context.ParentType().(Type) if fragType != nil && parentType != nil && !doTypesOverlap(fragType, parentType) { - return reportError( + reportError( context, fmt.Sprintf(`Fragment cannot be spread here as objects of `+ `type "%v" can never be of type "%v".`, parentType, fragType), @@ -1575,7 +1575,7 @@ func PossibleFragmentSpreadsRule(context *ValidationContext) *ValidationRuleInst fragType := getFragmentType(context, fragName) parentType, _ := context.ParentType().(Type) if fragType != nil && parentType != nil && !doTypesOverlap(fragType, parentType) { - return reportError( + reportError( context, fmt.Sprintf(`Fragment "%v" cannot be spread here as objects of `+ `type "%v" can never be of type "%v".`, fragName, parentType, fragType), @@ -1714,14 +1714,14 @@ func ScalarLeafsRule(context *ValidationContext) *ValidationRuleInstance { if ttype != nil { if IsLeafType(ttype) { if node.SelectionSet != nil { - return reportError( + reportError( context, fmt.Sprintf(`Field "%v" of type "%v" must not have a sub selection.`, nodeName, ttype), []ast.Node{node.SelectionSet}, ) } } else if node.SelectionSet == nil { - return reportError( + reportError( context, fmt.Sprintf(`Field "%v" of type "%v" must have a sub selection.`, nodeName, ttype), []ast.Node{node}, @@ -1771,13 +1771,14 @@ func UniqueArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance argName = node.Name.Value } if nameAST, ok := knownArgNames[argName]; ok { - return reportError( + reportError( context, fmt.Sprintf(`There can be only one argument named "%v".`, argName), []ast.Node{nameAST, node.Name}, ) + } else { + knownArgNames[argName] = node.Name } - knownArgNames[argName] = node.Name } return visitor.ActionSkip, nil }, @@ -1813,13 +1814,14 @@ func UniqueFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance fragmentName = node.Name.Value } if nameAST, ok := knownFragmentNames[fragmentName]; ok { - return reportError( + reportError( context, fmt.Sprintf(`There can only be one fragment named "%v".`, fragmentName), []ast.Node{nameAST, node.Name}, ) + } else { + knownFragmentNames[fragmentName] = node.Name } - knownFragmentNames[fragmentName] = node.Name } return visitor.ActionSkip, nil }, @@ -1863,7 +1865,7 @@ func UniqueInputFieldNamesRule(context *ValidationContext) *ValidationRuleInstan fieldName = node.Name.Value } if knownNameAST, ok := knownNames[fieldName]; ok { - return reportError( + reportError( context, fmt.Sprintf(`There can be only one input field named "%v".`, fieldName), []ast.Node{knownNameAST, node.Name}, @@ -1902,13 +1904,14 @@ func UniqueOperationNamesRule(context *ValidationContext) *ValidationRuleInstanc operationName = node.Name.Value } if nameAST, ok := knownOperationNames[operationName]; ok { - return reportError( + reportError( context, fmt.Sprintf(`There can only be one operation named "%v".`, operationName), []ast.Node{nameAST, node.Name}, ) + } else { + knownOperationNames[operationName] = node.Name } - knownOperationNames[operationName] = node.Name } return visitor.ActionSkip, nil }, @@ -1953,13 +1956,12 @@ func UniqueVariableNamesRule(context *ValidationContext) *ValidationRuleInstance variableName = node.Variable.Name.Value } if nameAST, ok := knownVariableNames[variableName]; ok { - return reportError( + reportError( context, fmt.Sprintf(`There can only be one variable named "%v".`, variableName), []ast.Node{nameAST, variableNameAST}, ) - } - if variableNameAST != nil { + } else { knownVariableNames[variableName] = variableNameAST } } @@ -1995,7 +1997,7 @@ func VariablesAreInputTypesRule(context *ValidationContext) *ValidationRuleInsta if node.Variable != nil && node.Variable.Name != nil { variableName = node.Variable.Name.Value } - return reportError( + reportError( context, fmt.Sprintf(`Variable "$%v" cannot be non-input type "%v".`, variableName, printer.Print(node.Type)), From ca6839e64d69b7bb325eab9b48978d490d6e514e Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Thu, 14 Apr 2016 17:36:25 +0800 Subject: [PATCH 66/69] Fixed `go vet` error --- rules.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/rules.go b/rules.go index bdfeb8b0..331da8be 100644 --- a/rules.go +++ b/rules.go @@ -1239,8 +1239,6 @@ func sameValue(value1 ast.Value, value2 ast.Value) bool { } func sameType(typeA, typeB Type) bool { - return fmt.Sprintf("%v", typeA) == fmt.Sprintf("%v", typeB) - if typeA == typeB { return true } From 9b137fd0bc127d604d16ba674e4458309a726164 Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Fri, 15 Apr 2016 18:02:57 +0800 Subject: [PATCH 67/69] Fix `golint` warnings - The ones left are "uncommented exported func/var/methods etc" --- definition.go | 370 ++++++++++++++++---------------- directives.go | 18 +- executor.go | 33 ++- introspection.go | 122 +++++------ rules.go | 294 ++++++++++--------------- scalars.go | 10 +- schema.go | 29 ++- testutil/introspection_query.go | 4 +- type_info.go | 10 +- validator.go | 14 +- values.go | 11 +- 11 files changed, 405 insertions(+), 510 deletions(-) diff --git a/definition.go b/definition.go index bc5a3a3d..bf42b530 100644 --- a/definition.go +++ b/definition.go @@ -1,7 +1,6 @@ package graphql import ( - "errors" "fmt" "reflect" "regexp" @@ -10,7 +9,7 @@ import ( "golang.org/x/net/context" ) -// These are all of the possible kinds of +// Type interface for all of the possible kinds of GraphQL types type Type interface { Name() string Description() string @@ -28,7 +27,7 @@ var _ Type = (*List)(nil) var _ Type = (*NonNull)(nil) var _ Type = (*Argument)(nil) -// These types may be used as input types for arguments and directives. +// Input interface for types that may be used as input types for arguments and directives. type Input interface { Name() string Description() string @@ -42,6 +41,7 @@ var _ Input = (*InputObject)(nil) var _ Input = (*List)(nil) var _ Input = (*NonNull)(nil) +// IsInputType determines if given type is a GraphQLInputType func IsInputType(ttype Type) bool { named := GetNamed(ttype) if _, ok := named.(*Scalar); ok { @@ -56,6 +56,7 @@ func IsInputType(ttype Type) bool { return false } +// IsOutputType determines if given type is a GraphQLOutputType func IsOutputType(ttype Type) bool { name := GetNamed(ttype) if _, ok := name.(*Scalar); ok { @@ -76,6 +77,7 @@ func IsOutputType(ttype Type) bool { return false } +// IsLeafType determines if given type is a leaf value func IsLeafType(ttype Type) bool { named := GetNamed(ttype) if _, ok := named.(*Scalar); ok { @@ -87,7 +89,7 @@ func IsLeafType(ttype Type) bool { return false } -// These types may be used as output types as the result of fields. +// Output interface for types that may be used as output types as the result of fields. type Output interface { Name() string Description() string @@ -103,7 +105,7 @@ var _ Output = (*Enum)(nil) var _ Output = (*List)(nil) var _ Output = (*NonNull)(nil) -// These types may describe the parent context of a selection set. +// Composite interface for types that may describe the parent context of a selection set. type Composite interface { Name() string } @@ -112,6 +114,7 @@ var _ Composite = (*Object)(nil) var _ Composite = (*Interface)(nil) var _ Composite = (*Union)(nil) +// IsCompositeType determines if given type is a GraphQLComposite type func IsCompositeType(ttype interface{}) bool { if _, ok := ttype.(*Object); ok { return true @@ -125,7 +128,7 @@ func IsCompositeType(ttype interface{}) bool { return false } -// These types may describe the parent context of a selection set. +// Abstract interface for types that may describe the parent context of a selection set. type Abstract interface { Name() string ObjectType(value interface{}, info ResolveInfo) *Object @@ -136,6 +139,7 @@ type Abstract interface { var _ Abstract = (*Interface)(nil) var _ Abstract = (*Union)(nil) +// Nullable interface for types that can accept null as a value. type Nullable interface { } @@ -147,6 +151,7 @@ var _ Nullable = (*Enum)(nil) var _ Nullable = (*InputObject)(nil) var _ Nullable = (*List)(nil) +// GetNullable returns the Nullable type of the given GraphQL type func GetNullable(ttype Type) Nullable { if ttype, ok := ttype.(*NonNull); ok { return ttype.OfType @@ -154,7 +159,7 @@ func GetNullable(ttype Type) Nullable { return ttype } -// These named types do not include modifiers like List or NonNull. +// Named interface for types that do not include modifiers like List or NonNull. type Named interface { String() string } @@ -166,6 +171,7 @@ var _ Named = (*Union)(nil) var _ Named = (*Enum)(nil) var _ Named = (*InputObject)(nil) +// GetNamed returns the Named type of the given GraphQL type func GetNamed(ttype Type) Named { unmodifiedType := ttype for { @@ -182,23 +188,21 @@ func GetNamed(ttype Type) Named { return unmodifiedType } -/** - * Scalar Type Definition - * - * The leaf values of any request and input values to arguments are - * Scalars (or Enums) and are defined with a name and a series of functions - * used to parse input from ast or variables and to ensure validity. - * - * Example: - * - * var OddType = new Scalar({ - * name: 'Odd', - * serialize(value) { - * return value % 2 === 1 ? value : null; - * } - * }); - * - */ +// Scalar Type Definition +// +// The leaf values of any request and input values to arguments are +// Scalars (or Enums) and are defined with a name and a series of functions +// used to parse input from ast or variables and to ensure validity. +// +// Example: +// +// var OddType = new Scalar({ +// name: 'Odd', +// serialize(value) { +// return value % 2 === 1 ? value : null; +// } +// }); +// type Scalar struct { PrivateName string `json:"name"` PrivateDescription string `json:"description"` @@ -206,9 +210,17 @@ type Scalar struct { scalarConfig ScalarConfig err error } + +// SerializeFn is a function type for serializing a GraphQLScalar type value type SerializeFn func(value interface{}) interface{} + +// ParseValueFn is a function type for parsing the value of a GraphQLScalar type type ParseValueFn func(value interface{}) interface{} + +// ParseLiteralFn is a function type for parsing the literal value of a GraphQLScalar type type ParseLiteralFn func(valueAST ast.Value) interface{} + +// ScalarConfig options for creating a new GraphQLScalar type ScalarConfig struct { Name string `json:"name"` Description string `json:"description"` @@ -217,6 +229,7 @@ type ScalarConfig struct { ParseLiteral ParseLiteralFn } +// NewScalar creates a new GraphQLScalar func NewScalar(config ScalarConfig) *Scalar { st := &Scalar{} err := invariant(config.Name != "", "Type must be named.") @@ -290,43 +303,41 @@ func (st *Scalar) Error() error { return st.err } -/** - * Object Type Definition - * - * Almost all of the GraphQL types you define will be object Object types - * have a name, but most importantly describe their fields. - * - * Example: - * - * var AddressType = new Object({ - * name: 'Address', - * fields: { - * street: { type: String }, - * number: { type: Int }, - * formatted: { - * type: String, - * resolve(obj) { - * return obj.number + ' ' + obj.street - * } - * } - * } - * }); - * - * When two types need to refer to each other, or a type needs to refer to - * itself in a field, you can use a function expression (aka a closure or a - * thunk) to supply the fields lazily. - * - * Example: - * - * var PersonType = new Object({ - * name: 'Person', - * fields: () => ({ - * name: { type: String }, - * bestFriend: { type: PersonType }, - * }) - * }); - * - */ +// Object Type Definition +// +// Almost all of the GraphQL types you define will be object Object types +// have a name, but most importantly describe their fields. +// Example: +// +// var AddressType = new Object({ +// name: 'Address', +// fields: { +// street: { type: String }, +// number: { type: Int }, +// formatted: { +// type: String, +// resolve(obj) { +// return obj.number + ' ' + obj.street +// } +// } +// } +// }); +// +// When two types need to refer to each other, or a type needs to refer to +// itself in a field, you can use a function expression (aka a closure or a +// thunk) to supply the fields lazily. +// +// Example: +// +// var PersonType = new Object({ +// name: 'Person', +// fields: () => ({ +// name: { type: String }, +// bestFriend: { type: PersonType }, +// }) +// }); +// +// / type Object struct { PrivateName string `json:"name"` PrivateDescription string `json:"description"` @@ -429,7 +440,7 @@ func (gt *Object) Interfaces() []*Interface { configInterfaces = gt.typeConfig.Interfaces.([]*Interface) case nil: default: - gt.err = errors.New(fmt.Sprintf("Unknown Object.Interfaces type: %v", reflect.TypeOf(gt.typeConfig.Interfaces))) + gt.err = fmt.Errorf("Unknown Object.Interfaces type: %v", reflect.TypeOf(gt.typeConfig.Interfaces)) return nil } interfaces, err := defineInterfaces(gt, configInterfaces) @@ -544,6 +555,7 @@ func defineFieldMap(ttype Named, fields Fields) (FieldDefinitionMap, error) { return resultFieldMap, nil } +// ResolveParams Params for FieldResolveFn() // TODO: clean up GQLFRParams fields type ResolveParams struct { Source interface{} @@ -555,7 +567,6 @@ type ResolveParams struct { Context context.Context } -// TODO: relook at FieldResolveFn params type FieldResolveFn func(p ResolveParams) (interface{}, error) type ResolveInfo struct { @@ -627,24 +638,23 @@ func (st *Argument) Error() error { return nil } -/** - * Interface Type Definition - * - * When a field can return one of a heterogeneous set of types, a Interface type - * is used to describe what types are possible, what fields are in common across - * all types, as well as a function to determine which type is actually used - * when the field is resolved. - * - * Example: - * - * var EntityType = new Interface({ - * name: 'Entity', - * fields: { - * name: { type: String } - * } - * }); - * - */ +// Interface Type Definition +// +// When a field can return one of a heterogeneous set of types, a Interface type +// is used to describe what types are possible, what fields are in common across +// all types, as well as a function to determine which type is actually used +// when the field is resolved. +// +// Example: +// +// var EntityType = new Interface({ +// name: 'Entity', +// fields: { +// name: { type: String } +// } +// }); +// +// type Interface struct { PrivateName string `json:"name"` PrivateDescription string `json:"description"` @@ -764,29 +774,26 @@ func getTypeOf(value interface{}, info ResolveInfo, abstractType Abstract) *Obje return nil } -/** - * Union Type Definition - * - * When a field can return one of a heterogeneous set of types, a Union type - * is used to describe what types are possible as well as providing a function - * to determine which type is actually used when the field is resolved. - * - * Example: - * - * var PetType = new Union({ - * name: 'Pet', - * types: [ DogType, CatType ], - * resolveType(value) { - * if (value instanceof Dog) { - * return DogType; - * } - * if (value instanceof Cat) { - * return CatType; - * } - * } - * }); - * - */ +// Union Type Definition +// +// When a field can return one of a heterogeneous set of types, a Union type +// is used to describe what types are possible as well as providing a function +// to determine which type is actually used when the field is resolved. +// +// Example: +// +// var PetType = new Union({ +// name: 'Pet', +// types: [ DogType, CatType ], +// resolveType(value) { +// if (value instanceof Dog) { +// return DogType; +// } +// if (value instanceof Cat) { +// return CatType; +// } +// } +// }); type Union struct { PrivateName string `json:"name"` PrivateDescription string `json:"description"` @@ -901,27 +908,26 @@ func (ut *Union) Error() error { return ut.err } -/** - * Enum Type Definition - * - * Some leaf values of requests and input values are Enums. GraphQL serializes - * Enum values as strings, however internally Enums can be represented by any - * kind of type, often integers. - * - * Example: - * - * var RGBType = new Enum({ - * name: 'RGB', - * values: { - * RED: { value: 0 }, - * GREEN: { value: 1 }, - * BLUE: { value: 2 } - * } - * }); - * - * Note: If a value is not provided in a definition, the name of the enum value - * will be used as its internal value. - */ +// Enum Type Definition +// +// Some leaf values of requests and input values are Enums. GraphQL serializes +// Enum values as strings, however internally Enums can be represented by any +// kind of type, often integers. +// +// Example: +// +// var RGBType = new Enum({ +// name: 'RGB', +// values: { +// RED: { value: 0 }, +// GREEN: { value: 1 }, +// BLUE: { value: 2 } +// } +// }); +// +// Note: If a value is not provided in a definition, the name of the enum value +// will be used as its internal value. + type Enum struct { PrivateName string `json:"name"` PrivateDescription string `json:"description"` @@ -1071,26 +1077,23 @@ func (gt *Enum) getNameLookup() map[string]*EnumValueDefinition { return gt.nameLookup } -/** - * Input Object Type Definition - * - * An input object defines a structured collection of fields which may be - * supplied to a field argument. - * - * Using `NonNull` will ensure that a value must be provided by the query - * - * Example: - * - * var GeoPoint = new InputObject({ - * name: 'GeoPoint', - * fields: { - * lat: { type: new NonNull(Float) }, - * lon: { type: new NonNull(Float) }, - * alt: { type: Float, defaultValue: 0 }, - * } - * }); - * - */ +// InputObject Type Definition +// +// An input object defines a structured collection of fields which may be +// supplied to a field argument. +// +// Using `NonNull` will ensure that a value must be provided by the query +// +// Example: +// +// var GeoPoint = new InputObject({ +// name: 'GeoPoint', +// fields: { +// lat: { type: new NonNull(Float) }, +// lon: { type: new NonNull(Float) }, +// alt: { type: Float, defaultValue: 0 }, +// } +// }); type InputObject struct { PrivateName string `json:"name"` PrivateDescription string `json:"description"` @@ -1135,7 +1138,6 @@ type InputObjectConfig struct { Description string `json:"description"` } -// TODO: rename InputObjectConfig to GraphQLInputObjecTypeConfig for consistency? func NewInputObject(config InputObjectConfig) *InputObject { gt := &InputObject{} err := invariant(config.Name != "", "Type must be named.") @@ -1211,24 +1213,22 @@ func (gt *InputObject) Error() error { return gt.err } -/** - * List Modifier - * - * A list is a kind of type marker, a wrapping type which points to another - * type. Lists are often created within the context of defining the fields of - * an object type. - * - * Example: - * - * var PersonType = new Object({ - * name: 'Person', - * fields: () => ({ - * parents: { type: new List(Person) }, - * children: { type: new List(Person) }, - * }) - * }) - * - */ +// List Modifier +// +// A list is a kind of type marker, a wrapping type which points to another +// type. Lists are often created within the context of defining the fields of +// an object type. +// +// Example: +// +// var PersonType = new Object({ +// name: 'Person', +// fields: () => ({ +// parents: { type: new List(Person) }, +// children: { type: new List(Person) }, +// }) +// }) +// type List struct { OfType Type `json:"ofType"` @@ -1263,26 +1263,24 @@ func (gl *List) Error() error { return gl.err } -/** - * Non-Null Modifier - * - * A non-null is a kind of type marker, a wrapping type which points to another - * type. Non-null types enforce that their values are never null and can ensure - * an error is raised if this ever occurs during a request. It is useful for - * fields which you can make a strong guarantee on non-nullability, for example - * usually the id field of a database row will never be null. - * - * Example: - * - * var RowType = new Object({ - * name: 'Row', - * fields: () => ({ - * id: { type: new NonNull(String) }, - * }) - * }) - * - * Note: the enforcement of non-nullability occurs within the executor. - */ +// NonNull Modifier +// +// A non-null is a kind of type marker, a wrapping type which points to another +// type. Non-null types enforce that their values are never null and can ensure +// an error is raised if this ever occurs during a request. It is useful for +// fields which you can make a strong guarantee on non-nullability, for example +// usually the id field of a database row will never be null. +// +// Example: +// +// var RowType = new Object({ +// name: 'Row', +// fields: () => ({ +// id: { type: new NonNull(String) }, +// }) +// }) +// +// Note: the enforcement of non-nullability occurs within the executor. type NonNull struct { PrivateName string `json:"name"` // added to conform with introspection for NonNull.Name = nil OfType Type `json:"ofType"` @@ -1318,11 +1316,11 @@ func (gl *NonNull) Error() error { return gl.err } -var NAME_REGEXP, _ = regexp.Compile("^[_a-zA-Z][_a-zA-Z0-9]*$") +var NameRegExp, _ = regexp.Compile("^[_a-zA-Z][_a-zA-Z0-9]*$") func assertValidName(name string) error { return invariant( - NAME_REGEXP.MatchString(name), + NameRegExp.MatchString(name), fmt.Sprintf(`Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "%v" does not.`, name), ) } diff --git a/directives.go b/directives.go index 67a1aa0d..63411104 100644 --- a/directives.go +++ b/directives.go @@ -1,5 +1,7 @@ package graphql +// Directive structs are used by the GraphQL runtime as a way of modifying execution +// behavior. Type system creators will usually not create these directly. type Directive struct { Name string `json:"name"` Description string `json:"description"` @@ -9,10 +11,6 @@ type Directive struct { OnField bool `json:"onField"` } -/** - * Directives are used by the GraphQL runtime as a way of modifying execution - * behavior. Type system creators will usually not create these directly. - */ func NewDirective(config *Directive) *Directive { if config == nil { config = &Directive{} @@ -27,10 +25,8 @@ func NewDirective(config *Directive) *Directive { } } -/** - * Used to conditionally include fields or fragments - */ -var IncludeDirective *Directive = NewDirective(&Directive{ +// IncludeDirective is used to conditionally include fields or fragments +var IncludeDirective = NewDirective(&Directive{ Name: "include", Description: "Directs the executor to include this field or fragment only when " + "the `if` argument is true.", @@ -46,10 +42,8 @@ var IncludeDirective *Directive = NewDirective(&Directive{ OnField: true, }) -/** - * Used to conditionally skip (exclude) fields or fragments - */ -var SkipDirective *Directive = NewDirective(&Directive{ +// SkipDirective Used to conditionally skip (exclude) fields or fragments +var SkipDirective = NewDirective(&Directive{ Name: "skip", Description: "Directs the executor to skip this field or fragment when the `if` " + "argument is true.", diff --git a/executor.go b/executor.go index 3d31f8a4..2a787e7d 100644 --- a/executor.go +++ b/executor.go @@ -109,9 +109,8 @@ func buildExecutionContext(p BuildExecutionCtxParams) (*ExecutionContext, error) if operation == nil { if p.OperationName == "" { return nil, fmt.Errorf(`Unknown operation named "%v".`, p.OperationName) - } else { - return nil, fmt.Errorf(`Must provide an operation`) } + return nil, fmt.Errorf(`Must provide an operation`) } variableValues, err := getVariableValues(p.Schema, operation.GetVariableDefinitions(), p.Args) @@ -156,9 +155,9 @@ func executeOperation(p ExecuteOperationParams) *Result { if p.Operation.GetOperation() == "mutation" { return executeFieldsSerially(executeFieldsParams) - } else { - return executeFields(executeFieldsParams) } + return executeFields(executeFieldsParams) + } // Extracts the root type of the operation from the schema. @@ -470,12 +469,10 @@ type resolveFieldResultState struct { hasNoFieldDefs bool } -/** - * Resolves the field on the given source object. In particular, this - * figures out the value that the field returns by calling its resolve function, - * then calls completeValue to complete promises, serialize scalars, or execute - * the sub-selection-set for objects. - */ +// Resolves the field on the given source object. In particular, this +// figures out the value that the field returns by calling its resolve function, +// then calls completeValue to complete promises, serialize scalars, or execute +// the sub-selection-set for objects. func resolveField(eCtx *ExecutionContext, parentType *Object, source interface{}, fieldASTs []*ast.Field) (result interface{}, resultState resolveFieldResultState) { // catch panic from resolveFn var returnType Output @@ -771,15 +768,13 @@ func defaultResolveFn(p ResolveParams) (interface{}, error) { return nil, nil } -/** - * This method looks up the field on the given type defintion. - * It has special casing for the two introspection fields, __schema - * and __typename. __typename is special because it can always be - * queried as a field, even in situations where no other fields - * are allowed, like on a Union. __schema could get automatically - * added to the query type, but that would require mutating type - * definitions, which would cause issues. - */ +// This method looks up the field on the given type defintion. +// It has special casing for the two introspection fields, __schema +// and __typename. __typename is special because it can always be +// queried as a field, even in situations where no other fields +// are allowed, like on a Union. __schema could get automatically +// added to the query type, but that would require mutating type +// definitions, which would cause issues. func getFieldDef(schema Schema, parentType *Object, fieldName string) *FieldDefinition { if parentType == nil { diff --git a/introspection.go b/introspection.go index a59d6c90..99e7c04e 100644 --- a/introspection.go +++ b/introspection.go @@ -19,14 +19,14 @@ const ( TypeKindNonNull = "NON_NULL" ) -var __Directive *Object -var __Schema *Object -var __Type *Object -var __Field *Object -var __InputValue *Object -var __EnumValue *Object +var directiveType *Object +var schemaType *Object +var typeType *Object +var fieldType *Object +var inputValueType *Object +var enumValueType *Object -var __TypeKind *Enum +var typeKindEnum *Enum var SchemaMetaFieldDef *FieldDefinition var TypeMetaFieldDef *FieldDefinition @@ -34,7 +34,7 @@ var TypeNameMetaFieldDef *FieldDefinition func init() { - __TypeKind = NewEnum(EnumConfig{ + typeKindEnum = NewEnum(EnumConfig{ Name: "__TypeKind", Description: "An enum describing what kind of type a given `__Type` is", Values: EnumValueConfigMap{ @@ -81,7 +81,7 @@ func init() { }) // Note: some fields (for e.g "fields", "interfaces") are defined later due to cyclic reference - __Type = NewObject(ObjectConfig{ + typeType = NewObject(ObjectConfig{ Name: "__Type", Description: "The fundamental unit of any GraphQL Schema is the type. There are " + "many kinds of types in GraphQL as represented by the `__TypeKind` enum." + @@ -94,7 +94,7 @@ func init() { Fields: Fields{ "kind": &Field{ - Type: NewNonNull(__TypeKind), + Type: NewNonNull(typeKindEnum), Resolve: func(p ResolveParams) (interface{}, error) { switch p.Source.(type) { case *Scalar: @@ -132,7 +132,7 @@ func init() { }, }) - __InputValue = NewObject(ObjectConfig{ + inputValueType = NewObject(ObjectConfig{ Name: "__InputValue", Description: "Arguments provided to Fields or Directives and the input fields of an " + "InputObject are represented as Input Values which describe their type " + @@ -145,7 +145,7 @@ func init() { Type: String, }, "type": &Field{ - Type: NewNonNull(__Type), + Type: NewNonNull(typeType), }, "defaultValue": &Field{ Type: String, @@ -175,7 +175,7 @@ func init() { }, }) - __Field = NewObject(ObjectConfig{ + fieldType = NewObject(ObjectConfig{ Name: "__Field", Description: "Object and Interface types are described by a list of Fields, each of " + "which has a name, potentially a list of arguments, and a return type.", @@ -187,7 +187,7 @@ func init() { Type: String, }, "args": &Field{ - Type: NewNonNull(NewList(NewNonNull(__InputValue))), + Type: NewNonNull(NewList(NewNonNull(inputValueType))), Resolve: func(p ResolveParams) (interface{}, error) { if field, ok := p.Source.(*FieldDefinition); ok { return field.Args, nil @@ -196,7 +196,7 @@ func init() { }, }, "type": &Field{ - Type: NewNonNull(__Type), + Type: NewNonNull(typeType), }, "isDeprecated": &Field{ Type: NewNonNull(Boolean), @@ -213,7 +213,7 @@ func init() { }, }) - __Directive = NewObject(ObjectConfig{ + directiveType = NewObject(ObjectConfig{ Name: "__Directive", Description: "A Directive provides a way to describe alternate runtime execution and " + "type validation behavior in a GraphQL document. " + @@ -230,7 +230,7 @@ func init() { }, "args": &Field{ Type: NewNonNull(NewList( - NewNonNull(__InputValue), + NewNonNull(inputValueType), )), }, "onOperation": &Field{ @@ -245,7 +245,7 @@ func init() { }, }) - __Schema = NewObject(ObjectConfig{ + schemaType = NewObject(ObjectConfig{ Name: "__Schema", Description: `A GraphQL Schema defines the capabilities of a GraphQL server. ` + `It exposes all available types and directives on the server, as well as ` + @@ -254,7 +254,7 @@ func init() { "types": &Field{ Description: "A list of all types supported by this server.", Type: NewNonNull(NewList( - NewNonNull(__Type), + NewNonNull(typeType), )), Resolve: func(p ResolveParams) (interface{}, error) { if schema, ok := p.Source.(Schema); ok { @@ -269,7 +269,7 @@ func init() { }, "queryType": &Field{ Description: "The type that query operations will be rooted at.", - Type: NewNonNull(__Type), + Type: NewNonNull(typeType), Resolve: func(p ResolveParams) (interface{}, error) { if schema, ok := p.Source.(Schema); ok { return schema.QueryType(), nil @@ -280,7 +280,7 @@ func init() { "mutationType": &Field{ Description: `If this server supports mutation, the type that ` + `mutation operations will be rooted at.`, - Type: __Type, + Type: typeType, Resolve: func(p ResolveParams) (interface{}, error) { if schema, ok := p.Source.(Schema); ok { if schema.MutationType() != nil { @@ -293,7 +293,7 @@ func init() { "subscriptionType": &Field{ Description: `If this server supports subscription, the type that ` + `subscription operations will be rooted at.`, - Type: __Type, + Type: typeType, Resolve: func(p ResolveParams) (interface{}, error) { if schema, ok := p.Source.(Schema); ok { if schema.SubscriptionType() != nil { @@ -306,7 +306,7 @@ func init() { "directives": &Field{ Description: `A list of all directives supported by this server.`, Type: NewNonNull(NewList( - NewNonNull(__Directive), + NewNonNull(directiveType), )), Resolve: func(p ResolveParams) (interface{}, error) { if schema, ok := p.Source.(Schema); ok { @@ -318,7 +318,7 @@ func init() { }, }) - __EnumValue = NewObject(ObjectConfig{ + enumValueType = NewObject(ObjectConfig{ Name: "__EnumValue", Description: "One possible value for a given Enum. Enum values are unique values, not " + "a placeholder for a string or numeric value. However an Enum value is " + @@ -347,8 +347,8 @@ func init() { // Again, adding field configs to __Type that have cyclic reference here // because golang don't like them too much during init/compile-time - __Type.AddFieldConfig("fields", &Field{ - Type: NewList(NewNonNull(__Field)), + typeType.AddFieldConfig("fields", &Field{ + Type: NewList(NewNonNull(fieldType)), Args: FieldConfigArgument{ "includeDeprecated": &ArgumentConfig{ Type: Boolean, @@ -386,8 +386,8 @@ func init() { return nil, nil }, }) - __Type.AddFieldConfig("interfaces", &Field{ - Type: NewList(NewNonNull(__Type)), + typeType.AddFieldConfig("interfaces", &Field{ + Type: NewList(NewNonNull(typeType)), Resolve: func(p ResolveParams) (interface{}, error) { switch ttype := p.Source.(type) { case *Object: @@ -396,8 +396,8 @@ func init() { return nil, nil }, }) - __Type.AddFieldConfig("possibleTypes", &Field{ - Type: NewList(NewNonNull(__Type)), + typeType.AddFieldConfig("possibleTypes", &Field{ + Type: NewList(NewNonNull(typeType)), Resolve: func(p ResolveParams) (interface{}, error) { switch ttype := p.Source.(type) { case *Interface: @@ -408,8 +408,8 @@ func init() { return nil, nil }, }) - __Type.AddFieldConfig("enumValues", &Field{ - Type: NewList(NewNonNull(__EnumValue)), + typeType.AddFieldConfig("enumValues", &Field{ + Type: NewList(NewNonNull(enumValueType)), Args: FieldConfigArgument{ "includeDeprecated": &ArgumentConfig{ Type: Boolean, @@ -435,8 +435,8 @@ func init() { return nil, nil }, }) - __Type.AddFieldConfig("inputFields", &Field{ - Type: NewList(NewNonNull(__InputValue)), + typeType.AddFieldConfig("inputFields", &Field{ + Type: NewList(NewNonNull(inputValueType)), Resolve: func(p ResolveParams) (interface{}, error) { switch ttype := p.Source.(type) { case *InputObject: @@ -449,18 +449,15 @@ func init() { return nil, nil }, }) - __Type.AddFieldConfig("ofType", &Field{ - Type: __Type, + typeType.AddFieldConfig("ofType", &Field{ + Type: typeType, }) - /** - * Note that these are FieldDefinition and not FieldConfig, - * so the format for args is different. - */ - + // Note that these are FieldDefinition and not FieldConfig, + // so the format for args is different. d SchemaMetaFieldDef = &FieldDefinition{ Name: "__schema", - Type: NewNonNull(__Schema), + Type: NewNonNull(schemaType), Description: "Access the current type schema of this server.", Args: []*Argument{}, Resolve: func(p ResolveParams) (interface{}, error) { @@ -469,7 +466,7 @@ func init() { } TypeMetaFieldDef = &FieldDefinition{ Name: "__type", - Type: __Type, + Type: typeType, Description: "Request the type information of a single type.", Args: []*Argument{ &Argument{ @@ -498,21 +495,19 @@ func init() { } -/** - * Produces a GraphQL Value AST given a Golang value. - * - * Optionally, a GraphQL type may be provided, which will be used to - * disambiguate between value primitives. - * - * | JSON Value | GraphQL Value | - * | ------------- | -------------------- | - * | Object | Input Object | - * | Array | List | - * | Boolean | Boolean | - * | String | String / Enum Value | - * | Number | Int / Float | - * - */ +// Produces a GraphQL Value AST given a Golang value. +// +// Optionally, a GraphQL type may be provided, which will be used to +// disambiguate between value primitives. +// +// | JSON Value | GraphQL Value | +// | ------------- | -------------------- | +// | Object | Input Object | +// | Array | List | +// | Boolean | Boolean | +// | String | String / Enum Value | +// | Number | Int / Float | + func astFromValue(value interface{}, ttype Type) ast.Value { if ttype, ok := ttype.(*NonNull); ok { @@ -551,13 +546,12 @@ func astFromValue(value interface{}, ttype Type) ast.Value { return ast.NewListValue(&ast.ListValue{ Values: values, }) - } else { - // Because GraphQL will accept single values as a "list of one" when - // expecting a list, if there's a non-array value and an expected list type, - // create an AST using the list's item type. - val := astFromValue(value, ttype.OfType) - return val } + // Because GraphQL will accept single values as a "list of one" when + // expecting a list, if there's a non-array value and an expected list type, + // create an AST using the list's item type. + val := astFromValue(value, ttype.OfType) + return val } if valueVal.Type().Kind() == reflect.Map { diff --git a/rules.go b/rules.go index 331da8be..1b518846 100644 --- a/rules.go +++ b/rules.go @@ -11,9 +11,7 @@ import ( "strings" ) -/** - * SpecifiedRules set includes all validation rules defined by the GraphQL spec. - */ +// SpecifiedRules set includes all validation rules defined by the GraphQL spec. var SpecifiedRules = []ValidationRuleFn{ ArgumentsOfCorrectTypeRule, DefaultValuesOfCorrectTypeRule, @@ -62,13 +60,10 @@ func reportError(context *ValidationContext, message string, nodes []ast.Node) ( return visitor.ActionNoChange, nil } -/** - * ArgumentsOfCorrectTypeRule - * Argument values of correct type - * - * A GraphQL document is only valid if all field argument literal values are - * of the type expected by their position. - */ +// ArgumentsOfCorrectTypeRule Argument values of correct type +// +// A GraphQL document is only valid if all field argument literal values are +// of the type expected by their position. func ArgumentsOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ @@ -109,13 +104,10 @@ func ArgumentsOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInsta } } -/** - * DefaultValuesOfCorrectTypeRule - * Variable default values of correct type - * - * A GraphQL document is only valid if all variable default values are of the - * type expected by their definition. - */ +// DefaultValuesOfCorrectTypeRule Variable default values of correct type +// +// A GraphQL document is only valid if all variable default values are of the +// type expected by their definition. func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ @@ -184,11 +176,11 @@ func UndefinedFieldMessage(fieldName string, ttypeName string, suggestedTypes [] // construct helpful (but long) message message := fmt.Sprintf(`Cannot query field "%v" on type "%v".`, fieldName, ttypeName) suggestions := strings.Join(quoteStrings(suggestedTypes), ", ") - const MAX_LENGTH = 5 + const MaxLength = 5 if len(suggestedTypes) > 0 { - if len(suggestedTypes) > MAX_LENGTH { - suggestions = strings.Join(quoteStrings(suggestedTypes[0:MAX_LENGTH]), ", ") + - fmt.Sprintf(`, and %v other types`, len(suggestedTypes)-MAX_LENGTH) + if len(suggestedTypes) > MaxLength { + suggestions = strings.Join(quoteStrings(suggestedTypes[0:MaxLength]), ", ") + + fmt.Sprintf(`, and %v other types`, len(suggestedTypes)-MaxLength) } message = message + fmt.Sprintf(` However, this field exists on %v.`, suggestions) message = message + ` Perhaps you meant to use an inline fragment?` @@ -197,13 +189,10 @@ func UndefinedFieldMessage(fieldName string, ttypeName string, suggestedTypes [] return message } -/** - * FieldsOnCorrectTypeRule - * Fields on correct type - * - * A GraphQL document is only valid if all fields selected are defined by the - * parent type, or are an allowed meta field such as __typenamme - */ +// FieldsOnCorrectTypeRule Fields on correct type +// +// A GraphQL document is only valid if all fields selected are defined by the +// parent type, or are an allowed meta field such as __typenamme func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ @@ -263,9 +252,7 @@ func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance } } -/** - * Return implementations of `type` that include `fieldName` as a valid field. - */ +// Return implementations of `type` that include `fieldName` as a valid field. func getImplementationsIncludingField(ttype Abstract, fieldName string) []string { result := []string{} @@ -280,12 +267,10 @@ func getImplementationsIncludingField(ttype Abstract, fieldName string) []string return result } -/** - * Go through all of the implementations of type, and find other interaces - * that they implement. If those interfaces include `field` as a valid field, - * return them, sorted by how often the implementations include the other - * interface. - */ +// Go through all of the implementations of type, and find other interaces +// that they implement. If those interfaces include `field` as a valid field, +// return them, sorted by how often the implementations include the other +// interface. func getSiblingInterfacesIncludingField(ttype Abstract, fieldName string) []string { implementingObjects := ttype.PossibleTypes() @@ -346,14 +331,11 @@ func (s suggestedInterfaceSortedSlice) Less(i, j int) bool { return s[i].count < s[j].count } -/** - * FragmentsOnCompositeTypesRule - * Fragments on composite type - * - * Fragments use a type condition to determine if they apply, since fragments - * can only be spread into a composite type (object, interface, or union), the - * type condition must also be a composite type. - */ +// FragmentsOnCompositeTypesRule Fragments on composite type +// +// Fragments use a type condition to determine if they apply, since fragments +// can only be spread into a composite type (object, interface, or union), the +// type condition must also be a composite type. func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ @@ -398,13 +380,10 @@ func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleIn } } -/** - * KnownArgumentNamesRule - * Known argument names - * - * A GraphQL field is only valid if all supplied arguments are defined by - * that field. - */ +// KnownArgumentNamesRule Known argument names +// +// A GraphQL field is only valid if all supplied arguments are defined by +// that field. func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ @@ -482,12 +461,10 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance } } -/** - * Known directives - * - * A GraphQL document is only valid if all `@directives` are known by the - * schema and legally positioned. - */ +// KnownDirectivesRule Known directives +// +// A GraphQL document is only valid if all `@directives` are known by the +// schema and legally positioned. func KnownDirectivesRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ @@ -559,13 +536,10 @@ func KnownDirectivesRule(context *ValidationContext) *ValidationRuleInstance { } } -/** - * KnownFragmentNamesRule - * Known fragment names - * - * A GraphQL document is only valid if all `...Fragment` fragment spreads refer - * to fragments defined in the same document. - */ +// KnownFragmentNamesRule Known fragment names +// +// A GraphQL document is only valid if all `...Fragment` fragment spreads refer +// to fragments defined in the same document. func KnownFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ @@ -599,13 +573,10 @@ func KnownFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance } } -/** - * KnownTypeNamesRule - * Known type names - * - * A GraphQL document is only valid if referenced types (specifically - * variable definitions and fragment conditions) are defined by the type schema. - */ +// KnownTypeNamesRule Known type names +// +// A GraphQL document is only valid if referenced types (specifically +// variable definitions and fragment conditions) are defined by the type schema. func KnownTypeNamesRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ KindFuncMap: map[string]visitor.NamedVisitFuncs{ @@ -656,13 +627,10 @@ func KnownTypeNamesRule(context *ValidationContext) *ValidationRuleInstance { } } -/** - * LoneAnonymousOperationRule - * Lone anonymous operation - * - * A GraphQL document is only valid if when it contains an anonymous operation - * (the query short-hand) that it contains only that one operation definition. - */ +// LoneAnonymousOperationRule Lone anonymous operation +// +// A GraphQL document is only valid if when it contains an anonymous operation +// (the query short-hand) that it contains only that one operation definition. func LoneAnonymousOperationRule(context *ValidationContext) *ValidationRuleInstance { var operationCount = 0 visitorOpts := &visitor.VisitorOptions{ @@ -730,9 +698,7 @@ func CycleErrorMessage(fragName string, spreadNames []string) string { return fmt.Sprintf(`Cannot spread fragment "%v" within itself%v.`, fragName, via) } -/** - * NoFragmentCyclesRule - */ +// NoFragmentCyclesRule No fragment cycles func NoFragmentCyclesRule(context *ValidationContext) *ValidationRuleInstance { // Tracks already visited fragments to maintain O(N) and to ensure that cycles @@ -845,13 +811,10 @@ func UndefinedVarMessage(varName string, opName string) string { return fmt.Sprintf(`Variable "$%v" is not defined.`, varName) } -/** - * NoUndefinedVariables - * No undefined variables - * - * A GraphQL operation is only valid if all variables encountered, both directly - * and via fragment spreads, are defined by that operation. - */ +// NoUndefinedVariablesRule No undefined variables +// +// A GraphQL operation is only valid if all variables encountered, both directly +// and via fragment spreads, are defined by that operation. func NoUndefinedVariablesRule(context *ValidationContext) *ValidationRuleInstance { var variableNameDefined = map[string]bool{} @@ -912,13 +875,10 @@ func NoUndefinedVariablesRule(context *ValidationContext) *ValidationRuleInstanc } } -/** - * NoUnusedFragmentsRule - * No unused fragments - * - * A GraphQL document is only valid if all fragment definitions are spread - * within operations, or spread within other fragments spread within operations. - */ +// NoUnusedFragmentsRule No unused fragments +// +// A GraphQL document is only valid if all fragment definitions are spread +// within operations, or spread within other fragments spread within operations. func NoUnusedFragmentsRule(context *ValidationContext) *ValidationRuleInstance { var fragmentDefs = []*ast.FragmentDefinition{} @@ -988,13 +948,10 @@ func UnusedVariableMessage(varName string, opName string) string { return fmt.Sprintf(`Variable "$%v" is never used.`, varName) } -/** - * NoUnusedVariablesRule - * No unused variables - * - * A GraphQL operation is only valid if all variables defined by an operation - * are used, either directly or within a spread fragment. - */ +// NoUnusedVariablesRule No unused variables +// +// A GraphQL operation is only valid if all variables defined by an operation +// are used, either directly or within a spread fragment. func NoUnusedVariablesRule(context *ValidationContext) *ValidationRuleInstance { var variableDefs = []*ast.VariableDefinition{} @@ -1149,10 +1106,8 @@ func collectFieldASTsAndDefs(context *ValidationContext, parentType Named, selec return astAndDefs } -/** - * pairSet A way to keep track of pairs of things when the ordering of the pair does - * not matter. We do this by maintaining a sort of double adjacency sets. - */ +// pairSet A way to keep track of pairs of things when the ordering of the pair does +// not matter. We do this by maintaining a sort of double adjacency sets. type pairSet struct { data map[ast.Node]*nodeSet } @@ -1257,14 +1212,11 @@ func sameType(typeA, typeB Type) bool { return false } -/** - * OverlappingFieldsCanBeMergedRule - * Overlapping fields can be merged - * - * A selection set is only valid if all fields (including spreading any - * fragments) either correspond to distinct response names or can be merged - * without ambiguity. - */ +// OverlappingFieldsCanBeMergedRule Overlapping fields can be merged +// +// A selection set is only valid if all fields (including spreading any +// fragments) either correspond to distinct response names or can be merged +// without ambiguity. func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRuleInstance { comparedSet := newPairSet() @@ -1401,7 +1353,7 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul // ensure field traversal orderedName := sort.StringSlice{} - for responseName, _ := range fieldMap { + for responseName := range fieldMap { orderedName = append(orderedName, responseName) } orderedName.Sort() @@ -1533,14 +1485,11 @@ func doTypesOverlap(t1 Type, t2 Type) bool { return false } -/** - * PossibleFragmentSpreadsRule - * Possible fragment spread - * - * A fragment spread is only valid if the type condition could ever possibly - * be true: if there is a non-empty intersection of the possible parent types, - * and possible types which pass the type condition. - */ +// PossibleFragmentSpreadsRule Possible fragment spread +// +// A fragment spread is only valid if the type condition could ever possibly +// be true: if there is a non-empty intersection of the possible parent types, +// and possible types which pass the type condition. func PossibleFragmentSpreadsRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ @@ -1591,13 +1540,10 @@ func PossibleFragmentSpreadsRule(context *ValidationContext) *ValidationRuleInst } } -/** - * ProvidedNonNullArgumentsRule - * Provided required arguments - * - * A field or directive is only valid if all required (non-null) field arguments - * have been provided. - */ +// ProvidedNonNullArgumentsRule Provided required arguments +// +// A field or directive is only valid if all required (non-null) field arguments +// have been provided. func ProvidedNonNullArgumentsRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ @@ -1690,13 +1636,10 @@ func ProvidedNonNullArgumentsRule(context *ValidationContext) *ValidationRuleIns } } -/** - * ScalarLeafsRule - * Scalar leafs - * - * A GraphQL document is valid only if all leaf fields (fields without - * sub selections) are of scalar or enum types. - */ +// ScalarLeafsRule Scalar leafs +// +// A GraphQL document is valid only if all leaf fields (fields without +// sub selections) are of scalar or enum types. func ScalarLeafsRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ @@ -1737,13 +1680,10 @@ func ScalarLeafsRule(context *ValidationContext) *ValidationRuleInstance { } } -/** - * UniqueArgumentNamesRule - * Unique argument names - * - * A GraphQL field or directive is only valid if all supplied arguments are - * uniquely named. - */ +// UniqueArgumentNamesRule Unique argument names +// +// A GraphQL field or directive is only valid if all supplied arguments are +// uniquely named. func UniqueArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance { knownArgNames := map[string]*ast.Name{} @@ -1788,12 +1728,9 @@ func UniqueArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance } } -/** - * UniqueFragmentNamesRule - * Unique fragment names - * - * A GraphQL document is only valid if all defined fragments have unique names. - */ +// UniqueFragmentNamesRule Unique fragment names +// +// A GraphQL document is only valid if all defined fragments have unique names. func UniqueFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance { knownFragmentNames := map[string]*ast.Name{} @@ -1831,12 +1768,10 @@ func UniqueFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance } } -/** - * UniqueInputFieldNamesRule - * - * A GraphQL input object value is only valid if all supplied fields are - * uniquely named. - */ +// UniqueInputFieldNamesRule Unique input field names +// +// A GraphQL input object value is only valid if all supplied fields are +// uniquely named. func UniqueInputFieldNamesRule(context *ValidationContext) *ValidationRuleInstance { knownNameStack := []map[string]*ast.Name{} knownNames := map[string]*ast.Name{} @@ -1883,12 +1818,9 @@ func UniqueInputFieldNamesRule(context *ValidationContext) *ValidationRuleInstan } } -/** - * UniqueOperationNamesRule - * Unique operation names - * - * A GraphQL document is only valid if all defined operations have unique names. - */ +// UniqueOperationNamesRule Unique operation names +// +// A GraphQL document is only valid if all defined operations have unique names. func UniqueOperationNamesRule(context *ValidationContext) *ValidationRuleInstance { knownOperationNames := map[string]*ast.Name{} @@ -1926,11 +1858,9 @@ func UniqueOperationNamesRule(context *ValidationContext) *ValidationRuleInstanc } } -/** - * Unique variable names - * - * A GraphQL operation is only valid if all its variables are uniquely named. - */ +// UniqueVariableNamesRule Unique variable names +// +// A GraphQL operation is only valid if all its variables are uniquely named. func UniqueVariableNamesRule(context *ValidationContext) *ValidationRuleInstance { knownVariableNames := map[string]*ast.Name{} @@ -1973,13 +1903,10 @@ func UniqueVariableNamesRule(context *ValidationContext) *ValidationRuleInstance } } -/** - * VariablesAreInputTypesRule - * Variables are input types - * - * A GraphQL operation is only valid if all the variables it defines are of - * input types (scalar, enum, or input object). - */ +// VariablesAreInputTypesRule Variables are input types +// +// A GraphQL operation is only valid if all the variables it defines are of +// input types (scalar, enum, or input object). func VariablesAreInputTypesRule(context *ValidationContext) *ValidationRuleInstance { visitorOpts := &visitor.VisitorOptions{ @@ -2024,10 +1951,7 @@ func effectiveType(varType Type, varDef *ast.VariableDefinition) Type { return NewNonNull(varType) } -/** - * VariablesInAllowedPositionRule - * Variables passed to field arguments conform to type - */ +// VariablesInAllowedPositionRule Variables passed to field arguments conform to type func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleInstance { varDefMap := map[string]*ast.VariableDefinition{} @@ -2090,13 +2014,11 @@ func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleI } } -/** - * Utility for validators which determines if a value literal AST is valid given - * an input type. - * - * Note that this only validates literal values, variables are assumed to - * provide values of the correct type. - */ +// Utility for validators which determines if a value literal AST is valid given +// an input type. +// +// Note that this only validates literal values, variables are assumed to +// provide values of the correct type. func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) { // A value must be provided if the type is non-null. if ttype, ok := ttype.(*NonNull); ok { diff --git a/scalars.go b/scalars.go index ea04e3a5..77842a1f 100644 --- a/scalars.go +++ b/scalars.go @@ -79,7 +79,7 @@ func coerceInt(value interface{}) interface{} { } // Int is the GraphQL Integer type definition. -var Int *Scalar = NewScalar(ScalarConfig{ +var Int = NewScalar(ScalarConfig{ Name: "Int", Description: "The `Int` scalar type represents non-fractional signed whole numeric " + "values. Int can represent values between -(2^31) and 2^31 - 1. ", @@ -120,7 +120,7 @@ func coerceFloat32(value interface{}) interface{} { } // Float is the GraphQL float type definition. -var Float *Scalar = NewScalar(ScalarConfig{ +var Float = NewScalar(ScalarConfig{ Name: "Float", Description: "The `Float` scalar type represents signed double-precision fractional " + "values as specified by " + @@ -147,7 +147,7 @@ func coerceString(value interface{}) interface{} { } // String is the GraphQL string type definition -var String *Scalar = NewScalar(ScalarConfig{ +var String = NewScalar(ScalarConfig{ Name: "String", Description: "The `String` scalar type represents textual data, represented as UTF-8 " + "character sequences. The String type is most often used by GraphQL to " + @@ -193,7 +193,7 @@ func coerceBool(value interface{}) interface{} { } // Boolean is the GraphQL boolean type definition -var Boolean *Scalar = NewScalar(ScalarConfig{ +var Boolean = NewScalar(ScalarConfig{ Name: "Boolean", Description: "The `Boolean` scalar type represents `true` or `false`.", Serialize: coerceBool, @@ -208,7 +208,7 @@ var Boolean *Scalar = NewScalar(ScalarConfig{ }) // ID is the GraphQL id type definition -var ID *Scalar = NewScalar(ScalarConfig{ +var ID = NewScalar(ScalarConfig{ Name: "ID", Description: "The `ID` scalar type represents a unique identifier, often used to " + "refetch an object or as key for a cache. The ID type appears in a JSON " + diff --git a/schema.go b/schema.go index a65b788a..108cdbac 100644 --- a/schema.go +++ b/schema.go @@ -4,18 +4,6 @@ import ( "fmt" ) -/** -Schema Definition -A Schema is created by supplying the root types of each type of operation, -query, mutation (optional) and subscription (optional). A schema definition is then supplied to the -validator and executor. -Example: - myAppSchema, err := NewSchema(SchemaConfig({ - Query: MyAppQueryRootType, - Mutation: MyAppMutationRootType, - Subscription: MyAppSubscriptionRootType, - }); -*/ type SchemaConfig struct { Query *Object Mutation *Object @@ -23,9 +11,18 @@ type SchemaConfig struct { Directives []*Directive } -// chose to name as TypeMap instead of TypeMap type TypeMap map[string]Type +//Schema Definition +//A Schema is created by supplying the root types of each type of operation, +//query, mutation (optional) and subscription (optional). A schema definition is then supplied to the +//validator and executor. +//Example: +// myAppSchema, err := NewSchema(SchemaConfig({ +// Query: MyAppQueryRootType, +// Mutation: MyAppMutationRootType, +// Subscription: MyAppSubscriptionRootType, +// }); type Schema struct { typeMap TypeMap directives []*Directive @@ -72,8 +69,8 @@ func NewSchema(config SchemaConfig) (Schema, error) { schema.QueryType(), schema.MutationType(), schema.SubscriptionType(), - __Type, - __Schema, + typeType, + schemaType, } for _, objectType := range objectTypes { if objectType == nil { @@ -274,7 +271,7 @@ func assertObjectImplementsInterface(object *Object, iface *Interface) error { ifaceFieldMap := iface.Fields() // Assert each interface field is implemented. - for fieldName, _ := range ifaceFieldMap { + for fieldName := range ifaceFieldMap { objectField := objectFieldMap[fieldName] ifaceField := ifaceFieldMap[fieldName] diff --git a/testutil/introspection_query.go b/testutil/introspection_query.go index ce908fdb..555ad9df 100644 --- a/testutil/introspection_query.go +++ b/testutil/introspection_query.go @@ -26,7 +26,7 @@ var IntrospectionQuery = ` kind name description - fields { + fields(includeDeprecated: true) { name description args { @@ -44,7 +44,7 @@ var IntrospectionQuery = ` interfaces { ...TypeRef } - enumValues { + enumValues(includeDeprecated: true) { name description isDeprecated diff --git a/type_info.go b/type_info.go index 16ce9f93..3c06c29b 100644 --- a/type_info.go +++ b/type_info.go @@ -86,7 +86,7 @@ func (ti *TypeInfo) Enter(node ast.Node) { switch node := node.(type) { case *ast.SelectionSet: namedType := GetNamed(ti.Type()) - var compositeType Composite = nil + var compositeType Composite if IsCompositeType(namedType) { compositeType, _ = namedType.(Composite) } @@ -236,11 +236,9 @@ func (ti *TypeInfo) Leave(node ast.Node) { } } -/** - * Not exactly the same as the executor's definition of FieldDef, in this - * statically evaluated environment we do not always have an Object type, - * and need to handle Interface and Union types. - */ +// DefaultTypeInfoFieldDef Not exactly the same as the executor's definition of FieldDef, in this +// statically evaluated environment we do not always have an Object type, +// and need to handle Interface and Union types. func DefaultTypeInfoFieldDef(schema *Schema, parentType Type, fieldAST *ast.Field) *FieldDefinition { name := "" if fieldAST.Name != nil { diff --git a/validator.go b/validator.go index 3d3a1ac2..1e057935 100644 --- a/validator.go +++ b/validator.go @@ -51,14 +51,12 @@ func ValidateDocument(schema *Schema, astDoc *ast.Document, rules []ValidationRu return vr } -/** - * VisitUsingRules This uses a specialized visitor which runs multiple visitors in parallel, - * while maintaining the visitor skip and break API. - * - * @internal - * Had to expose it to unit test experimental customizable validation feature, - * but not meant for public consumption - */ +// VisitUsingRules This uses a specialized visitor which runs multiple visitors in parallel, +// while maintaining the visitor skip and break API. +// +// @internal +// Had to expose it to unit test experimental customizable validation feature, +// but not meant for public consumption func VisitUsingRules(schema *Schema, typeInfo *TypeInfo, astDoc *ast.Document, rules []ValidationRuleFn) []gqlerrors.FormattedError { context := NewValidationContext(schema, astDoc, typeInfo) diff --git a/values.go b/values.go index 15c650ae..8ade746c 100644 --- a/values.go +++ b/values.go @@ -268,12 +268,12 @@ func isValidInputValue(value interface{}, ttype Input) (bool, []string) { fieldNames := []string{} valueMapFieldNames := []string{} - for fieldName, _ := range fields { + for fieldName := range fields { fieldNames = append(fieldNames, fieldName) } sort.Strings(fieldNames) - for fieldName, _ := range valueMap { + for fieldName := range valueMap { valueMapFieldNames = append(valueMapFieldNames, fieldName) } sort.Strings(valueMapFieldNames) @@ -302,16 +302,15 @@ func isValidInputValue(value interface{}, ttype Input) (bool, []string) { parsedVal := ttype.ParseValue(value) if isNullish(parsedVal) { return false, []string{fmt.Sprintf(`Expected type "%v", found "%v".`, ttype.Name(), value)} - } else { - return true, nil } + return true, nil + case *Enum: parsedVal := ttype.ParseValue(value) if isNullish(parsedVal) { return false, []string{fmt.Sprintf(`Expected type "%v", found "%v".`, ttype.Name(), value)} - } else { - return true, nil } + return true, nil } return true, nil } From ee2f8c6749df3177b9a3e89b586044bc4ef5197a Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Sun, 17 Apr 2016 14:32:08 +0800 Subject: [PATCH 68/69] Fix further `go lint ./...` warnings --- examples/todo/main.go | 4 +- gqlerrors/located.go | 2 +- language/lexer/lexer.go | 40 ++++----- language/location/location.go | 2 +- language/parser/parser.go | 6 +- language/{type_info => typeInfo}/type_info.go | 7 +- language/visitor/visitor.go | 89 ++++++++----------- testutil/testutil.go | 29 +++--- 8 files changed, 80 insertions(+), 99 deletions(-) rename language/{type_info => typeInfo}/type_info.go (58%) diff --git a/examples/todo/main.go b/examples/todo/main.go index 6a3f3c98..f5b69629 100644 --- a/examples/todo/main.go +++ b/examples/todo/main.go @@ -75,12 +75,12 @@ var rootMutation = graphql.NewObject(graphql.ObjectConfig{ text, _ := params.Args["text"].(string) // figure out new id - newId := RandStringRunes(8) + newID := RandStringRunes(8) // perform mutation operation here // for e.g. create a Todo and save to DB. newTodo := Todo{ - ID: newId, + ID: newID, Text: text, Done: false, } diff --git a/gqlerrors/located.go b/gqlerrors/located.go index 8bc8272e..b02fcd8a 100644 --- a/gqlerrors/located.go +++ b/gqlerrors/located.go @@ -5,7 +5,7 @@ import ( "github.com/graphql-go/graphql/language/ast" ) -// NewLocatedError +// NewLocatedError creates a graphql.Error with location info // @deprecated 0.4.18 // Already exists in `graphql.NewLocatedError()` func NewLocatedError(err interface{}, nodes []ast.Node) *Error { diff --git a/language/lexer/lexer.go b/language/lexer/lexer.go index 7458035a..4c149c34 100644 --- a/language/lexer/lexer.go +++ b/language/lexer/lexer.go @@ -121,7 +121,7 @@ func readName(source *source.Source, position int) Token { code >= 48 && code <= 57 || // 0-9 code >= 65 && code <= 90 || // A-Z code >= 97 && code <= 122) { // a-z - end += 1 + end++ continue } else { break @@ -140,11 +140,11 @@ func readNumber(s *source.Source, start int, firstCode rune) (Token, error) { position := start isFloat := false if code == 45 { // - - position += 1 + position++ code = charCodeAt(body, position) } if code == 48 { // 0 - position += 1 + position++ code = charCodeAt(body, position) if code >= 48 && code <= 57 { description := fmt.Sprintf("Invalid number, unexpected digit after 0: %v.", printCharCode(code)) @@ -160,7 +160,7 @@ func readNumber(s *source.Source, start int, firstCode rune) (Token, error) { } if code == 46 { // . isFloat = true - position += 1 + position++ code = charCodeAt(body, position) p, err := readDigits(s, position, code) if err != nil { @@ -171,10 +171,10 @@ func readNumber(s *source.Source, start int, firstCode rune) (Token, error) { } if code == 69 || code == 101 { // E e isFloat = true - position += 1 + position++ code = charCodeAt(body, position) if code == 43 || code == 45 { // + - - position += 1 + position++ code = charCodeAt(body, position) } p, err := readDigits(s, position, code) @@ -198,7 +198,7 @@ func readDigits(s *source.Source, start int, firstCode rune) (int, error) { if code >= 48 && code <= 57 { // 0 - 9 for { if code >= 48 && code <= 57 { // 0 - 9 - position += 1 + position++ code = charCodeAt(body, position) continue } else { @@ -230,7 +230,7 @@ func readString(s *source.Source, start int) (Token, error) { if code < 0x0020 && code != 0x0009 { return Token{}, gqlerrors.NewSyntaxError(s, position, fmt.Sprintf(`Invalid character within String: %v.`, printCharCode(code))) } - position += 1 + position++ if code == 92 { // \ value += body[chunkStart : position-1] code = charCodeAt(body, position) @@ -278,7 +278,7 @@ func readString(s *source.Source, start int) (Token, error) { return Token{}, gqlerrors.NewSyntaxError(s, position, fmt.Sprintf(`Invalid character escape sequence: \\%c.`, code)) } - position += 1 + position++ chunkStart = position } continue @@ -313,11 +313,11 @@ func char2hex(a rune) int { return int(a) - 48 } else if a >= 65 && a <= 70 { // A-F return int(a) - 55 - } else if a >= 97 && a <= 102 { // a-f + } else if a >= 97 && a <= 102 { + // a-f return int(a) - 87 - } else { - return -1 } + return -1 } func makeToken(kind int, start int, end int, value string) Token { @@ -409,9 +409,8 @@ func readToken(s *source.Source, fromPosition int) (Token, error) { token, err := readNumber(s, position, code) if err != nil { return token, err - } else { - return token, nil } + return token, nil // " case 34: token, err := readString(s, position) @@ -428,9 +427,9 @@ func charCodeAt(body string, position int) rune { r := []rune(body) if len(r) > position { return r[position] - } else { - return -1 } + return -1 + } // Reads from body starting at startPosition until it finds a non-whitespace @@ -453,16 +452,16 @@ func positionAfterWhitespace(body string, startPosition int) int { code == 0x000D || // carriage return // Comma code == 0x002C { - position += 1 + position++ } else if code == 35 { // # - position += 1 + position++ for { code := charCodeAt(body, position) if position < bodyLength && code != 0 && // SourceCharacter but not LineTerminator (code > 0x001F || code == 0x0009) && code != 0x000A && code != 0x000D { - position += 1 + position++ continue } else { break @@ -482,9 +481,8 @@ func positionAfterWhitespace(body string, startPosition int) int { func GetTokenDesc(token Token) string { if token.Value == "" { return GetTokenKindDesc(token.Kind) - } else { - return fmt.Sprintf("%s \"%s\"", GetTokenKindDesc(token.Kind), token.Value) } + return fmt.Sprintf("%s \"%s\"", GetTokenKindDesc(token.Kind), token.Value) } func GetTokenKindDesc(kind int) string { diff --git a/language/location/location.go b/language/location/location.go index 12ffff87..ec667caa 100644 --- a/language/location/location.go +++ b/language/location/location.go @@ -23,7 +23,7 @@ func GetLocation(s *source.Source, position int) SourceLocation { for _, match := range matches { matchIndex := match[0] if matchIndex < position { - line += 1 + line++ l := len(s.Body[match[0]:match[1]]) column = position + 1 - (matchIndex + l) continue diff --git a/language/parser/parser.go b/language/parser/parser.go index 705c0ae3..7b480c3d 100644 --- a/language/parser/parser.go +++ b/language/parser/parser.go @@ -363,9 +363,8 @@ func parseSelection(parser *Parser) (interface{}, error) { if peek(parser, lexer.TokenKind[lexer.SPREAD]) { r, err := parseFragment(parser) return r, err - } else { - return parseField(parser) } + return parseField(parser) } /** @@ -1241,9 +1240,8 @@ func skip(parser *Parser, Kind int) (bool, error) { if parser.Token.Kind == Kind { err := advance(parser) return true, err - } else { - return false, nil } + return false, nil } // If the next token is of the given kind, return that token after advancing diff --git a/language/type_info/type_info.go b/language/typeInfo/type_info.go similarity index 58% rename from language/type_info/type_info.go rename to language/typeInfo/type_info.go index 02b7b04f..e012ee02 100644 --- a/language/type_info/type_info.go +++ b/language/typeInfo/type_info.go @@ -1,13 +1,10 @@ -package type_info +package typeInfo import ( "github.com/graphql-go/graphql/language/ast" ) -/** - * TypeInfoI defines the interface for TypeInfo - * Implementation - */ +// TypeInfoI defines the interface for TypeInfo Implementation type TypeInfoI interface { Enter(node ast.Node) Leave(node ast.Node) diff --git a/language/visitor/visitor.go b/language/visitor/visitor.go index a88369c5..62c50149 100644 --- a/language/visitor/visitor.go +++ b/language/visitor/visitor.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/type_info" + "github.com/graphql-go/graphql/language/typeInfo" "reflect" ) @@ -18,7 +18,7 @@ const ( type KeyMap map[string][]string // note that the keys are in Capital letters, equivalent to the ast.Node field Names -var QueryDocumentKeys KeyMap = KeyMap{ +var QueryDocumentKeys = KeyMap{ "Name": []string{}, "Document": []string{"Definitions"}, "OperationDefinition": []string{ @@ -621,10 +621,9 @@ func updateNodeField(value interface{}, fieldName string, fieldValue interface{} if isPtr == true { retVal = val.Addr().Interface() return retVal - } else { - retVal = val.Interface() - return retVal } + retVal = val.Interface() + return retVal } } @@ -710,12 +709,10 @@ func isNilNode(node interface{}) bool { return val.Interface() == nil } -/** - * Creates a new visitor instance which delegates to many visitors to run in - * parallel. Each visitor will be visited for each node before moving on. - * - * If a prior visitor edits a node, no following visitors will see that node. - */ +// VisitInParallel Creates a new visitor instance which delegates to many visitors to run in +// parallel. Each visitor will be visited for each node before moving on. +// +// If a prior visitor edits a node, no following visitors will see that node. func VisitInParallel(visitorOptsSlice ...*VisitorOptions) *VisitorOptions { skipping := map[int]interface{}{} @@ -768,23 +765,21 @@ func VisitInParallel(visitorOptsSlice ...*VisitorOptions) *VisitorOptions { } } -/** - * Creates a new visitor instance which maintains a provided TypeInfo instance - * along with visiting visitor. - */ -func VisitWithTypeInfo(typeInfo type_info.TypeInfoI, visitorOpts *VisitorOptions) *VisitorOptions { +// VisitWithTypeInfo Creates a new visitor instance which maintains a provided TypeInfo instance +// along with visiting visitor. +func VisitWithTypeInfo(ttypeInfo typeInfo.TypeInfoI, visitorOpts *VisitorOptions) *VisitorOptions { return &VisitorOptions{ Enter: func(p VisitFuncParams) (string, interface{}) { if node, ok := p.Node.(ast.Node); ok { - typeInfo.Enter(node) + ttypeInfo.Enter(node) fn := GetVisitFn(visitorOpts, node.GetKind(), false) if fn != nil { action, result := fn(p) if action == ActionUpdate { - typeInfo.Leave(node) + ttypeInfo.Leave(node) if isNode(result) { if result, ok := result.(ast.Node); ok { - typeInfo.Enter(result) + ttypeInfo.Enter(result) } } } @@ -801,17 +796,15 @@ func VisitWithTypeInfo(typeInfo type_info.TypeInfoI, visitorOpts *VisitorOptions if fn != nil { action, result = fn(p) } - typeInfo.Leave(node) + ttypeInfo.Leave(node) } return action, result }, } } -/** - * Given a visitor instance, if it is leaving or not, and a node kind, return - * the function the visitor runtime should call. - */ +// GetVisitFn Given a visitor instance, if it is leaving or not, and a node kind, return +// the function the visitor runtime should call. func GetVisitFn(visitorOpts *VisitorOptions, kind string, isLeaving bool) VisitFunc { if visitorOpts == nil { return nil @@ -825,35 +818,31 @@ func GetVisitFn(visitorOpts *VisitorOptions, kind string, isLeaving bool) VisitF if isLeaving { // { Kind: { leave() {} } } return kindVisitor.Leave - } else { - // { Kind: { enter() {} } } - return kindVisitor.Enter } - } else { - - if isLeaving { - // { enter() {} } - specificVisitor := visitorOpts.Leave - if specificVisitor != nil { - return specificVisitor - } - if specificKindVisitor, ok := visitorOpts.LeaveKindMap[kind]; ok { - // { leave: { Kind() {} } } - return specificKindVisitor - } + // { Kind: { enter() {} } } + return kindVisitor.Enter - } else { - // { leave() {} } - specificVisitor := visitorOpts.Enter - if specificVisitor != nil { - return specificVisitor - } - if specificKindVisitor, ok := visitorOpts.EnterKindMap[kind]; ok { - // { enter: { Kind() {} } } - return specificKindVisitor - } - } } + if isLeaving { + // { enter() {} } + specificVisitor := visitorOpts.Leave + if specificVisitor != nil { + return specificVisitor + } + if specificKindVisitor, ok := visitorOpts.LeaveKindMap[kind]; ok { + // { leave: { Kind() {} } } + return specificKindVisitor + } + } + // { leave() {} } + specificVisitor := visitorOpts.Enter + if specificVisitor != nil { + return specificVisitor + } + if specificKindVisitor, ok := visitorOpts.EnterKindMap[kind]; ok { + // { enter: { Kind() {} } } + return specificKindVisitor + } return nil } diff --git a/testutil/testutil.go b/testutil/testutil.go index 9077a154..9951d8f3 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -29,7 +29,7 @@ var ( ) type StarWarsChar struct { - Id string + ID string Name string Friends []StarWarsChar AppearsIn []int @@ -39,41 +39,41 @@ type StarWarsChar struct { func init() { Luke = StarWarsChar{ - Id: "1000", + ID: "1000", Name: "Luke Skywalker", AppearsIn: []int{4, 5, 6}, HomePlanet: "Tatooine", } Vader = StarWarsChar{ - Id: "1001", + ID: "1001", Name: "Darth Vader", AppearsIn: []int{4, 5, 6}, HomePlanet: "Tatooine", } Han = StarWarsChar{ - Id: "1002", + ID: "1002", Name: "Han Solo", AppearsIn: []int{4, 5, 6}, } Leia = StarWarsChar{ - Id: "1003", + ID: "1003", Name: "Leia Organa", AppearsIn: []int{4, 5, 6}, HomePlanet: "Alderaa", } Tarkin = StarWarsChar{ - Id: "1004", + ID: "1004", Name: "Wilhuff Tarkin", AppearsIn: []int{4}, } Threepio = StarWarsChar{ - Id: "2000", + ID: "2000", Name: "C-3PO", AppearsIn: []int{4, 5, 6}, PrimaryFunction: "Protocol", } Artoo = StarWarsChar{ - Id: "2001", + ID: "2001", Name: "R2-D2", AppearsIn: []int{4, 5, 6}, PrimaryFunction: "Astromech", @@ -135,9 +135,9 @@ func init() { }, ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { if character, ok := value.(StarWarsChar); ok { - id, _ := strconv.Atoi(character.Id) + id, _ := strconv.Atoi(character.ID) human := GetHuman(id) - if human.Id != "" { + if human.ID != "" { return humanType } } @@ -158,7 +158,7 @@ func init() { Description: "The id of the human.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if human, ok := p.Source.(StarWarsChar); ok { - return human.Id, nil + return human.ID, nil } return nil, nil }, @@ -217,7 +217,7 @@ func init() { Description: "The id of the droid.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if droid, ok := p.Source.(StarWarsChar); ok { - return droid.Id, nil + return droid.ID, nil } return nil, nil }, @@ -241,7 +241,7 @@ func init() { for _, friend := range droid.Friends { friends = append(friends, map[string]interface{}{ "name": friend.Name, - "id": friend.Id, + "id": friend.ID, }) } return droid.Friends, nil @@ -418,9 +418,8 @@ subLoop: } if !found { return false - } else { - continue subLoop } + continue subLoop } return true } From c4394ae0505d26b7e7c3f548f345984f6db4373e Mon Sep 17 00:00:00 2001 From: Hafiz Ismail Date: Thu, 5 May 2016 12:06:03 +0800 Subject: [PATCH 69/69] Minor code clean up - removed commented out unused variables --- executor.go | 1 - union_interface_test.go | 5 ----- 2 files changed, 6 deletions(-) diff --git a/executor.go b/executor.go index 2a787e7d..b810f358 100644 --- a/executor.go +++ b/executor.go @@ -82,7 +82,6 @@ type ExecutionContext struct { func buildExecutionContext(p BuildExecutionCtxParams) (*ExecutionContext, error) { eCtx := &ExecutionContext{} - // operations := map[string]ast.Definition{} var operation *ast.OperationDefinition fragments := map[string]ast.Definition{} diff --git a/union_interface_test.go b/union_interface_test.go index ce8ac8c2..2da90f40 100644 --- a/union_interface_test.go +++ b/union_interface_test.go @@ -497,9 +497,6 @@ func TestUnionIntersectionTypes_AllowsFragmentConditionsToBeAbstractTypes(t *tes } func TestUnionIntersectionTypes_GetsExecutionInfoInResolver(t *testing.T) { - //var encounteredSchema *graphql.Schema - //var encounteredRootValue interface{} - var personType2 *graphql.Object namedType2 := graphql.NewInterface(graphql.InterfaceConfig{ @@ -510,8 +507,6 @@ func TestUnionIntersectionTypes_GetsExecutionInfoInResolver(t *testing.T) { }, }, ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { - //encounteredSchema = &info.Schema - //encounteredRootValue = info.RootValue return personType2 }, })