Skip to content

Commit c7a26f7

Browse files
Merge pull request #107 from NeedleInAJayStack/feature/enum-parsing-errors
feature: Enum parsing throws instead of null fallback
2 parents c0c9664 + 953a4a0 commit c7a26f7

File tree

9 files changed

+133
-33
lines changed

9 files changed

+133
-33
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
private let MAX_SUGGESTIONS = 5
2+
3+
func didYouMean(_ submessage: String? = nil, suggestions: [String]) -> String {
4+
guard !suggestions.isEmpty else {
5+
return ""
6+
}
7+
8+
var message = " Did you mean "
9+
if let submessage = submessage {
10+
message.append("\(submessage) ")
11+
}
12+
13+
let suggestionList = suggestions[0 ... min(suggestions.count - 1, MAX_SUGGESTIONS - 1)]
14+
.map { "\"\($0)\"" }.orList()
15+
return message + "\(suggestionList)?"
16+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
extension Collection where Element == String, Index == Int {
2+
/// Given ["A", "B", "C"] return "A, B, or C".
3+
func orList() -> String {
4+
return formatList("or")
5+
}
6+
7+
/// Given ["A", "B", "C"] return "A, B, and C".
8+
func andList() -> String {
9+
return formatList("and")
10+
}
11+
12+
private func formatList(_ conjunction: String) -> String {
13+
switch count {
14+
case 0:
15+
return ""
16+
case 1:
17+
return self[0]
18+
case 2:
19+
return joined(separator: " \(conjunction) ")
20+
default:
21+
let allButLast = self[0 ... count - 2]
22+
let lastItem = self[count - 1]
23+
24+
return allButLast.joined(separator: ", ") + ", \(conjunction) \(lastItem)"
25+
}
26+
}
27+
}

Sources/GraphQL/SwiftUtilities/QuotedOrList.swift

Lines changed: 0 additions & 16 deletions
This file was deleted.

Sources/GraphQL/Type/Definition.swift

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,23 +1012,53 @@ public final class GraphQLEnumType {
10121012
}
10131013

10141014
public func serialize(value: Any) throws -> Map {
1015-
return try valueLookup[map(from: value)].map { .string($0.name) } ?? .null
1015+
let mapValue = try map(from: value)
1016+
guard let enumValue = valueLookup[mapValue] else {
1017+
throw GraphQLError(
1018+
message: "Enum '\(name)' cannot represent value '\(mapValue)'."
1019+
)
1020+
}
1021+
return .string(enumValue.name)
10161022
}
10171023

10181024
public func parseValue(value: Map) throws -> Map {
1019-
if case let .string(value) = value {
1020-
return nameLookup[value]?.value ?? .null
1025+
guard let valueStr = value.string else {
1026+
throw GraphQLError(
1027+
message: "Enum '\(name)' cannot represent non-string value '\(value)'." +
1028+
didYouMeanEnumValue(unknownValueStr: value.description)
1029+
)
10211030
}
1022-
1023-
return .null
1031+
guard let enumValue = nameLookup[valueStr] else {
1032+
throw GraphQLError(
1033+
message: "Value '\(valueStr)' does not exist in '\(name)' enum." +
1034+
didYouMeanEnumValue(unknownValueStr: valueStr)
1035+
)
1036+
}
1037+
return enumValue.value
10241038
}
10251039

1026-
public func parseLiteral(valueAST: Value) -> Map {
1027-
if let enumValue = valueAST as? EnumValue {
1028-
return nameLookup[enumValue.value]?.value ?? .null
1040+
public func parseLiteral(valueAST: Value) throws -> Map {
1041+
guard let enumNode = valueAST as? EnumValue else {
1042+
throw GraphQLError(
1043+
message: "Enum '\(name)' cannot represent non-enum value '\(valueAST)'." +
1044+
didYouMeanEnumValue(unknownValueStr: "\(valueAST)"),
1045+
nodes: [valueAST]
1046+
)
10291047
}
1048+
guard let enumValue = nameLookup[enumNode.value] else {
1049+
throw GraphQLError(
1050+
message: "Value '\(enumNode)' does not exist in '\(name)' enum." +
1051+
didYouMeanEnumValue(unknownValueStr: enumNode.value),
1052+
nodes: [valueAST]
1053+
)
1054+
}
1055+
return enumValue.value
1056+
}
10301057

1031-
return .null
1058+
private func didYouMeanEnumValue(unknownValueStr: String) -> String {
1059+
let allNames = values.map { $0.name }
1060+
let suggestedValues = suggestionList(input: unknownValueStr, options: allNames)
1061+
return didYouMean("the enum value", suggestions: suggestedValues)
10321062
}
10331063
}
10341064

Sources/GraphQL/Validation/Rules/FieldsOnCorrectTypeRule.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ func undefinedFieldMessage(
77
var message = "Cannot query field \"\(fieldName)\" on type \"\(type)\"."
88

99
if !suggestedTypeNames.isEmpty {
10-
let suggestions = quotedOrList(items: suggestedTypeNames)
11-
message += " Did you mean to use an inline fragment on \(suggestions)?"
10+
message += didYouMean("to use an inline fragment on", suggestions: suggestedTypeNames)
1211
} else if !suggestedFieldNames.isEmpty {
13-
let suggestions = quotedOrList(items: suggestedFieldNames)
14-
message += " Did you mean \(suggestions)?"
12+
message += didYouMean(suggestions: suggestedFieldNames)
1513
}
1614

1715
return message

Sources/GraphQL/Validation/Rules/KnownArgumentNamesRule.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ func undefinedArgumentMessage(
1010
"Field \"\(fieldName)\" on type \"\(type)\" does not have argument \"\(argumentName)\"."
1111

1212
if !suggestedArgumentNames.isEmpty {
13-
let suggestions = quotedOrList(items: suggestedArgumentNames)
14-
message += " Did you mean \(suggestions)?"
13+
message += didYouMean(suggestions: suggestedArgumentNames)
1514
}
1615

1716
return message

Sources/GraphQL/Validation/Rules/ProvidedNonNullArgumentsRule.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ func missingArgumentsMessage(
55
type: String,
66
missingArguments: [String]
77
) -> String {
8-
let arguments = quotedOrList(items: missingArguments)
8+
let arguments = missingArguments.andList()
99
return "Field \"\(fieldName)\" on type \"\(type)\" is missing required arguments \(arguments)."
1010
}
1111

Sources/GraphQL/Validation/Rules/ScalarLeafsRule.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ func noSubselectionAllowedMessage(fieldName: String, type: GraphQLType) -> Strin
55

66
func requiredSubselectionMessage(fieldName: String, type: GraphQLType) -> String {
77
return "Field \"\(fieldName)\" of type \"\(type)\" must have a " +
8-
"selection of subfields. Did you mean \"\(fieldName) { ... }\"?"
8+
"selection of subfields." + didYouMean(suggestions: ["\(fieldName) { ... }"])
99
}
1010

1111
/**
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
@testable import GraphQL
2+
import XCTest
3+
4+
class DidYouMeanTests: XCTestCase {
5+
func testEmptyList() {
6+
XCTAssertEqual(
7+
didYouMean(suggestions: []),
8+
""
9+
)
10+
}
11+
12+
func testSingleSuggestion() {
13+
XCTAssertEqual(
14+
didYouMean(suggestions: ["A"]),
15+
#" Did you mean "A"?"#
16+
)
17+
}
18+
19+
func testTwoSuggestions() {
20+
XCTAssertEqual(
21+
didYouMean(suggestions: ["A", "B"]),
22+
#" Did you mean "A" or "B"?"#
23+
)
24+
}
25+
26+
func testMultipleSuggestions() {
27+
XCTAssertEqual(
28+
didYouMean(suggestions: ["A", "B", "C"]),
29+
#" Did you mean "A", "B", or "C"?"#
30+
)
31+
}
32+
33+
func testLimitsToFiveSuggestions() {
34+
XCTAssertEqual(
35+
didYouMean(suggestions: ["A", "B", "C", "D", "E", "F"]),
36+
#" Did you mean "A", "B", "C", "D", or "E"?"#
37+
)
38+
}
39+
40+
func testAddsSubmessage() {
41+
XCTAssertEqual(
42+
didYouMean("the letter", suggestions: ["A"]),
43+
#" Did you mean the letter "A"?"#
44+
)
45+
}
46+
}

0 commit comments

Comments
 (0)