Skip to content

Commit

Permalink
fix: Fix ExecutionResult Decoder to Conform to GraphQL Spec (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
maticzav authored Jul 4, 2023
1 parent 1b369f1 commit b457761
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 3 deletions.
16 changes: 16 additions & 0 deletions Sources/GraphQL/Execution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ extension ExecutionArgs: Hashable {
public struct ExecutionResult: Equatable, Encodable, Decodable {

/// Result of a successfull execution of a query.
/// - NOTE: AnyCodable is represented as a non-nullable value because it's easier to handle results if we represent `nil` value as `AnyCodable(nil)` value.
/// Because GraphQL Specification allows the possibility of missing `data` field, we manually decode execution result.
public var data: AnyCodable

/// Any errors that occurred during the GraphQL execution of the server.
Expand All @@ -65,6 +67,20 @@ public struct ExecutionResult: Equatable, Encodable, Decodable {
self.hasNext = hasNext
self.extensions = extensions
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

// NOTE: GraphQL Specification allows the possibility of missing `data` field, but the
// code in the library assumes that AnyCodable value is always present.
// As a workaround, we manually construct a nil literal using AnyCodable to simplify further processing.
let data = try container.decodeIfPresent(AnyCodable.self, forKey: .data)

self.data = data ?? AnyCodable.init(nilLiteral: ())
self.errors = try container.decodeIfPresent([GraphQLError].self, forKey: .errors)
self.hasNext = try container.decodeIfPresent(Bool.self, forKey: .hasNext)
self.extensions = try container.decodeIfPresent([String : AnyCodable].self, forKey: .extensions)
}
}

// MARK: - Extra
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftGraphQL/Selection/Selection+Transform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public extension Selection {
switch fields.__state {
case let .decoding(data):
switch data.value {
case is Void:
case is Void, is NSNull:
throw ObjectDecodingError.unexpectedNilValue
default:
return try self.__decode(data: data)
Expand Down
145 changes: 145 additions & 0 deletions Tests/GraphQLTests/ExecutionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import GraphQL
import XCTest

/// A suite of tests that check all edge cases of the response format as described in [GraphQL Spec Response Format](http://spec.graphql.org/October2021/#sec-Response-Format) section.
final class ExecutionTests: XCTestCase {

func testExecutionWithDataAndErrors() throws {
let result: ExecutionResult = """
{
"data": "Hello World!",
"errors": [
{
"message": "Message.",
"locations": [ { "line": 6, "column": 7 } ],
"path": [ "hero", "heroFriends", 1, "name" ]
}
]
}
""".decode()

XCTAssertEqual(
result,
ExecutionResult(
data: AnyCodable("Hello World!"),
errors: [
GraphQL.GraphQLError(
message: "Message.",
locations: [
GraphQL.GraphQLError.Location(line: 6, column: 7)
],
path: [
GraphQL.GraphQLError.PathLink.path("hero"),
GraphQL.GraphQLError.PathLink.path("heroFriends"),
GraphQL.GraphQLError.PathLink.index(1),
GraphQL.GraphQLError.PathLink.path("name")
],
extensions: nil
)
],
hasNext: nil,
extensions: nil
)
)
}

func testExecutionWithErrorsField() throws {
let result: ExecutionResult = """
{
"errors": [
{
"message": "Message.",
"locations": [ { "line": 6, "column": 7 } ],
"path": [ "hero", "heroFriends", 1, "name" ]
}
]
}
""".decode()

XCTAssertEqual(
result,
GraphQL.ExecutionResult(
data: nil,
errors: [
GraphQL.GraphQLError(
message: "Message.",
locations: [
GraphQL.GraphQLError.Location(line: 6, column: 7)
],
path: [
GraphQL.GraphQLError.PathLink.path("hero"),
GraphQL.GraphQLError.PathLink.path("heroFriends"),
GraphQL.GraphQLError.PathLink.index(1),
GraphQL.GraphQLError.PathLink.path("name")
],
extensions: nil
)
],
hasNext: nil,
extensions: nil
)
)
}

func testExecutionWithoutErrorsField() throws {
let result: ExecutionResult = """
{
"data": "Hello World!"
}
""".decode()

XCTAssertEqual(
result,
GraphQL.ExecutionResult(
data: AnyCodable("Hello World!"),
errors: nil,
hasNext: nil,
extensions: nil
)
)
}

func testExecutionWithErrorsWithExtensions() throws {
let result: ExecutionResult = """
{
"errors": [
{
"message": "Bad Request Exception",
"extensions": {
"code": "BAD_USER_INPUT",
}
}
],
"data": null
}
""".decode()

XCTAssertEqual(
result,
GraphQL.ExecutionResult(
data: nil,
errors: [
GraphQL.GraphQLError(
message: "Bad Request Exception",
locations: nil,
path: nil,
extensions: [
"code": AnyCodable("BAD_USER_INPUT")
]
)
],
hasNext: nil,
extensions: nil
)
)
}
}


extension String {
/// Converts a string representation of a GraphQL result into the execution result if possible.
fileprivate func decode() -> ExecutionResult {
let decoder = JSONDecoder()
return try! decoder.decode(ExecutionResult.self, from: self.data(using: .utf8)!)
}
}
49 changes: 47 additions & 2 deletions Tests/SwiftGraphQLTests/Selection/SelectionDecodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ final class SelectionDecodingTests: XCTestCase {
XCTAssertEqual(decoded, nil)
XCTAssertEqual(result.errors, nil)
}

func testMissingDataField() throws {
let result: ExecutionResult = """
{}
""".execution()

let selection = Selection<String?, String?> {
switch $0.__state {
case let .decoding(data):
return try String?(from: data)
case .selecting:
return "wrong"
}
}

let decoded = try selection.decode(raw: result.data)
XCTAssertEqual(decoded, nil)
XCTAssertEqual(result.errors, nil)
}

func testNullableNSNull() throws {
let result: ExecutionResult = """
Expand Down Expand Up @@ -194,13 +213,39 @@ final class SelectionDecodingTests: XCTestCase {

XCTAssertThrowsError(try selection.nonNullOrFail.decode(raw: result.data)) { (error) -> Void in
switch error {
case let ScalarDecodingError.unexpectedScalarType(expected: expected, received: _):
XCTAssertEqual(expected, "String")
case ObjectDecodingError.unexpectedNilValue:
()
default:
XCTFail()
}
}
}

func testInvalidScalarError() throws {
let result: ExecutionResult = """
{
"data": 1
}
""".execution()

let selection = Selection<String, String> {
switch $0.__state {
case let .decoding(data):
return try String(from: data)
case .selecting:
return "wrong"
}
}

XCTAssertThrowsError(try selection.nonNullOrFail.decode(raw: result.data)) { (error) -> Void in
switch error {
case let ScalarDecodingError.unexpectedScalarType(expected: expected, received: _):
XCTAssertEqual(expected, "String")
default:
XCTFail()
}
}
}

func testCustomError() throws {
let result: ExecutionResult = """
Expand Down

1 comment on commit b457761

@vercel
Copy link

@vercel vercel bot commented on b457761 Jul 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.