Skip to content

feature: Enum parsing throws instead of null fallback #107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Sources/GraphQL/SwiftUtilities/DidYouMean.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
private let MAX_SUGGESTIONS = 5

func didYouMean(_ submessage: String? = nil, suggestions: [String]) -> String {
guard !suggestions.isEmpty else {
return ""
}

var message = " Did you mean "
if let submessage = submessage {
message.append("\(submessage) ")
}

let suggestionList = suggestions[0 ... min(suggestions.count - 1, MAX_SUGGESTIONS - 1)]
.map { "\"\($0)\"" }.orList()
return message + "\(suggestionList)?"
}
27 changes: 27 additions & 0 deletions Sources/GraphQL/SwiftUtilities/FormatList.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
extension Collection where Element == String, Index == Int {
/// Given ["A", "B", "C"] return "A, B, or C".
func orList() -> String {
return formatList("or")
}

/// Given ["A", "B", "C"] return "A, B, and C".
func andList() -> String {
return formatList("and")
}

private func formatList(_ conjunction: String) -> String {
switch count {
case 0:
return ""
case 1:
return self[0]
case 2:
return joined(separator: " \(conjunction) ")
default:
let allButLast = self[0 ... count - 2]
let lastItem = self[count - 1]

return allButLast.joined(separator: ", ") + ", \(conjunction) \(lastItem)"
}
}
}
16 changes: 0 additions & 16 deletions Sources/GraphQL/SwiftUtilities/QuotedOrList.swift

This file was deleted.

48 changes: 39 additions & 9 deletions Sources/GraphQL/Type/Definition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1012,23 +1012,53 @@ public final class GraphQLEnumType {
}

public func serialize(value: Any) throws -> Map {
return try valueLookup[map(from: value)].map { .string($0.name) } ?? .null
let mapValue = try map(from: value)
guard let enumValue = valueLookup[mapValue] else {
throw GraphQLError(
message: "Enum '\(name)' cannot represent value '\(mapValue)'."
)
}
return .string(enumValue.name)
}

public func parseValue(value: Map) throws -> Map {
if case let .string(value) = value {
return nameLookup[value]?.value ?? .null
guard let valueStr = value.string else {
throw GraphQLError(
message: "Enum '\(name)' cannot represent non-string value '\(value)'." +
didYouMeanEnumValue(unknownValueStr: value.description)
)
}

return .null
guard let enumValue = nameLookup[valueStr] else {
throw GraphQLError(
message: "Value '\(valueStr)' does not exist in '\(name)' enum." +
didYouMeanEnumValue(unknownValueStr: valueStr)
)
}
return enumValue.value
}

public func parseLiteral(valueAST: Value) -> Map {
if let enumValue = valueAST as? EnumValue {
return nameLookup[enumValue.value]?.value ?? .null
public func parseLiteral(valueAST: Value) throws -> Map {
guard let enumNode = valueAST as? EnumValue else {
throw GraphQLError(
message: "Enum '\(name)' cannot represent non-enum value '\(valueAST)'." +
didYouMeanEnumValue(unknownValueStr: "\(valueAST)"),
nodes: [valueAST]
)
}
guard let enumValue = nameLookup[enumNode.value] else {
throw GraphQLError(
message: "Value '\(enumNode)' does not exist in '\(name)' enum." +
didYouMeanEnumValue(unknownValueStr: enumNode.value),
nodes: [valueAST]
)
}
return enumValue.value
}

return .null
private func didYouMeanEnumValue(unknownValueStr: String) -> String {
let allNames = values.map { $0.name }
let suggestedValues = suggestionList(input: unknownValueStr, options: allNames)
return didYouMean("the enum value", suggestions: suggestedValues)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ func undefinedFieldMessage(
var message = "Cannot query field \"\(fieldName)\" on type \"\(type)\"."

if !suggestedTypeNames.isEmpty {
let suggestions = quotedOrList(items: suggestedTypeNames)
message += " Did you mean to use an inline fragment on \(suggestions)?"
message += didYouMean("to use an inline fragment on", suggestions: suggestedTypeNames)
} else if !suggestedFieldNames.isEmpty {
let suggestions = quotedOrList(items: suggestedFieldNames)
message += " Did you mean \(suggestions)?"
message += didYouMean(suggestions: suggestedFieldNames)
}

return message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ func undefinedArgumentMessage(
"Field \"\(fieldName)\" on type \"\(type)\" does not have argument \"\(argumentName)\"."

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

return message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ func missingArgumentsMessage(
type: String,
missingArguments: [String]
) -> String {
let arguments = quotedOrList(items: missingArguments)
let arguments = missingArguments.andList()
return "Field \"\(fieldName)\" on type \"\(type)\" is missing required arguments \(arguments)."
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/GraphQL/Validation/Rules/ScalarLeafsRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ func noSubselectionAllowedMessage(fieldName: String, type: GraphQLType) -> Strin

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

/**
Expand Down
46 changes: 46 additions & 0 deletions Tests/GraphQLTests/SwiftUtilitiesTests/DidYouMeanTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@testable import GraphQL
import XCTest

class DidYouMeanTests: XCTestCase {
func testEmptyList() {
XCTAssertEqual(
didYouMean(suggestions: []),
""
)
}

func testSingleSuggestion() {
XCTAssertEqual(
didYouMean(suggestions: ["A"]),
#" Did you mean "A"?"#
)
}

func testTwoSuggestions() {
XCTAssertEqual(
didYouMean(suggestions: ["A", "B"]),
#" Did you mean "A" or "B"?"#
)
}

func testMultipleSuggestions() {
XCTAssertEqual(
didYouMean(suggestions: ["A", "B", "C"]),
#" Did you mean "A", "B", or "C"?"#
)
}

func testLimitsToFiveSuggestions() {
XCTAssertEqual(
didYouMean(suggestions: ["A", "B", "C", "D", "E", "F"]),
#" Did you mean "A", "B", "C", "D", or "E"?"#
)
}

func testAddsSubmessage() {
XCTAssertEqual(
didYouMean("the letter", suggestions: ["A"]),
#" Did you mean the letter "A"?"#
)
}
}