From fafb85881c0c2e2dffdd522436a91d6352b05b4a Mon Sep 17 00:00:00 2001 From: Nicholas Lythall Date: Thu, 5 Nov 2020 11:04:55 +1000 Subject: [PATCH] Support the '&' character for types implementing multiple interfaces --- Sources/GraphQL/Language/AST.swift | 1 + Sources/GraphQL/Language/Lexer.swift | 10 +++++++ Sources/GraphQL/Language/Parser.swift | 25 ++++++++++++++-- .../LanguageTests/ParserTests.swift | 29 +++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index 3a730e0e..a138f16b 100644 --- a/Sources/GraphQL/Language/AST.swift +++ b/Sources/GraphQL/Language/AST.swift @@ -40,6 +40,7 @@ final public class Token { case eof = "" case bang = "!" case dollar = "$" + case amp = "&" case openingParenthesis = "(" case closingParenthesis = ")" case spread = "..." diff --git a/Sources/GraphQL/Language/Lexer.swift b/Sources/GraphQL/Language/Lexer.swift index 0501aa74..9c8d4708 100644 --- a/Sources/GraphQL/Language/Lexer.swift +++ b/Sources/GraphQL/Language/Lexer.swift @@ -236,6 +236,16 @@ func readToken(lexer: Lexer, prev: Token) throws -> Token { column: col, prev: prev ) + // & + case 38: + return Token( + kind: .amp, + start: position, + end: position + 1, + line: line, + column: col, + prev: prev + ) // ( case 40: return Token( diff --git a/Sources/GraphQL/Language/Parser.swift b/Sources/GraphQL/Language/Parser.swift index 04ec50aa..d35de999 100644 --- a/Sources/GraphQL/Language/Parser.swift +++ b/Sources/GraphQL/Language/Parser.swift @@ -761,17 +761,22 @@ func parseObjectTypeDefinition(lexer: Lexer) throws -> ObjectTypeDefinition { } /** - * ImplementsInterfaces : implements NamedType+ + * ImplementsInterfaces : + * - implements &? NamedType + * - ImplementsInterfaces & NamedType */ func parseImplementsInterfaces(lexer: Lexer) throws -> [NamedType] { var types: [NamedType] = [] if lexer.token.value == "implements" { try lexer.advance() - + // Optional leading ampersand + try expectOptional(lexer: lexer, kind: .amp) repeat { types.append(try parseNamedType(lexer: lexer)) - } while peek(lexer: lexer, kind: .name) + } while try expectOptional(lexer: lexer, kind: .amp) != nil + // Legacy support for the SDL? + || peek(lexer: lexer, kind: .name) } return types @@ -1079,6 +1084,20 @@ func expect(lexer: Lexer, kind: Token.Kind) throws -> Token { return token } +/** + * If the next token is of the given kind, return that token after advancing + * the lexer. Otherwise, do not change the parser state and return nil. + */ +@discardableResult +func expectOptional(lexer: Lexer, kind: Token.Kind) throws -> Token? { + let token = lexer.token + if token.kind == kind { + try lexer.advance() + return token + } + return nil +} + /** * If the next token is a keyword with the given value, return that token after * advancing the lexer. Otherwise, do not change the parser state and return diff --git a/Tests/GraphQLTests/LanguageTests/ParserTests.swift b/Tests/GraphQLTests/LanguageTests/ParserTests.swift index 87feb914..9d7ad3b8 100644 --- a/Tests/GraphQLTests/LanguageTests/ParserTests.swift +++ b/Tests/GraphQLTests/LanguageTests/ParserTests.swift @@ -112,12 +112,41 @@ class ParserTests : XCTestCase { "Syntax Error GraphQL (1:39) Unexpected Name \"null\"" )) } + + XCTAssertThrowsError(try parse(source: "type WithImplementsButNoTypes implements {}")) { error in + guard let error = error as? GraphQLError else { + return XCTFail() + } + + XCTAssert(error.message.contains( + "Syntax Error GraphQL (1:42) Expected Name, found {" + )) + } + + XCTAssertThrowsError(try parse(source: "type WithImplementsWithTrailingAmp implements AInterface & {}")) { error in + guard let error = error as? GraphQLError else { + return XCTFail() + } + + XCTAssert(error.message.contains( + "Syntax Error GraphQL (1:60) Expected Name, found {" + )) + } } func testVariableInlineValues() throws { _ = try parse(source: "{ field(complex: { a: { b: [ $var ] } }) }") } + func testImplementsInterface() throws { + _ = try parse(source: "type Swallow implements Animal {}") + _ = try parse(source: "type Swallow implements Animal & Bird {}") + _ = try parse(source: "type Swallow implements & Animal & Bird {}") + _ = try parse(source: "interface Bird implements Animal {}") + _ = try parse(source: "interface Bird implements Animal & Lifeform {}") + _ = try parse(source: "interface Bird implements & Animal {}") + } + // it('parses multi-byte characters', async () => { // // Note: \u0A0A could be naively interpretted as two line-feed chars. // expect(