Skip to content

Commit

Permalink
Merge pull request #13 from checkout/feature/async-wrapper-no-data
Browse files Browse the repository at this point in the history
Adding an async wrapper for no data success case
  • Loading branch information
okhan-okbay-cko authored Feb 2, 2024
2 parents 4bfb354 + 4281105 commit 0439704
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 36 deletions.
18 changes: 14 additions & 4 deletions Sources/CheckoutNetwork/CheckoutClientInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,31 @@ import Foundation
/// Interface for a network client that can support Checkout networking requirements
public protocol CheckoutClientInterface {

// MARK: Type Assignments

/// Completion handler that will return a result containing a decodable object or an error
typealias CompletionHandler<T> = ((Result<T, Error>) -> Void)

/// Completion handler that will return errors if it fails or nothing if it completes as expected
typealias NoDataResponseCompletionHandler = ((Error?) -> Void)


// MARK: Traditional Base Methods

/// Create, customise and run a request with the given configuration, calling the completion handler once completed
func runRequest<T: Decodable>(with configuration: RequestConfiguration,
completionHandler: @escaping CompletionHandler<T>)

/// Async wrapper of func runRequest(_:_:) with CompletionHandler<T>
func runRequest<T: Decodable>(with configuration: RequestConfiguration) async throws -> T

/// Create, customise and run a request with the given configuration, calling the completion handler once completed
func runRequest(with configuration: RequestConfiguration,
completionHandler: @escaping NoDataResponseCompletionHandler)


// MARK: Async Wrappers

/// Async wrapper of func runRequest(_:_:) with CompletionHandler<T>
func runRequest<T: Decodable>(with configuration: RequestConfiguration) async throws -> T

/// Async wrapper of func runRequest(_:_:) with NoDataResponseCompletionHandler
func runRequest(with configuration: RequestConfiguration) async throws
}

46 changes: 32 additions & 14 deletions Sources/CheckoutNetwork/CheckoutNetworkClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,6 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
}
}

public func runRequest<T: Decodable>(with configuration: RequestConfiguration) async throws -> T {
return try await withCheckedThrowingContinuation { continuation in
runRequest(with: configuration) { (result: Result<T, Error>) in
switch result {
case .success(let response):
continuation.resume(returning: response)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}

public func runRequest(with configuration: RequestConfiguration,
completionHandler: @escaping NoDataResponseCompletionHandler) {
taskQueue.sync {
Expand Down Expand Up @@ -112,7 +99,7 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
let errorReason = try JSONDecoder().decode(ErrorReason.self, from: data ?? Data())
return CheckoutNetworkError.invalidData(reason: errorReason)
} catch {
return CheckoutNetworkError.invalidDataResponseReceivedWithNoData
return CheckoutNetworkError.noDataResponseReceived
}
}

Expand All @@ -123,3 +110,34 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
return nil
}
}

// MARK: Async Wrappers
public extension CheckoutNetworkClient {

func runRequest<T: Decodable>(with configuration: RequestConfiguration) async throws -> T {
return try await withCheckedThrowingContinuation { continuation in
runRequest(with: configuration) { (result: Result<T, Error>) in
switch result {
case .success(let response):
continuation.resume(returning: response)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}

func runRequest(with configuration: RequestConfiguration) async throws {
return try await withCheckedThrowingContinuation { continuation in
runRequest(with: configuration) { (error: Error?) in

guard let error = error else {
continuation.resume(returning: Void())
return
}

continuation.resume(throwing: error)
}
}
}
}
6 changes: 1 addition & 5 deletions Sources/CheckoutNetwork/CheckoutNetworkError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,9 @@ public enum CheckoutNetworkError: Error, Equatable {
/// Network response was not in the 200 range
case unexpectedResponseCode(code: Int)

/// Network call and completion appear valid but no data was returned making the parsing impossible. Use different call if no data is expected
/// Network call and completion appear valid but no data was returned making the parsing impossible. Use runRequest method with NoDataResponseCompletionHandler if no data is expected (HTTP 204 is a success case with no content being returned)
case noDataResponseReceived

/// Network response returned with HTTP Code 422
case invalidData(reason: ErrorReason)


/// HTTP code 422 received with no meaningful data alongside
case invalidDataResponseReceivedWithNoData
}
16 changes: 11 additions & 5 deletions Sources/CheckoutNetworkFakeClient/CheckoutNetworkFakeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ final public class CheckoutNetworkFakeClient: CheckoutClientInterface {
calledRequests.append((config: configuration, completion: completionHandler))
}

public func runRequest<T: Decodable>(with configuration: CheckoutNetwork.RequestConfiguration) async throws -> T {
calledAsyncRequests.append(configuration)
return dataToBeReturned as! T
}

public func runRequest(with configuration: RequestConfiguration,
completionHandler: @escaping NoDataResponseCompletionHandler) {
calledRequests.append((configuration, completionHandler))
}
}

extension CheckoutNetworkFakeClient {
public func runRequest<T: Decodable>(with configuration: CheckoutNetwork.RequestConfiguration) async throws -> T {
calledAsyncRequests.append(configuration)
return dataToBeReturned as! T
}

public func runRequest(with configuration: RequestConfiguration) async throws {
calledAsyncRequests.append(configuration)
}
}
57 changes: 51 additions & 6 deletions Tests/CheckoutNetworkTests/AsyncWrapperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,24 @@
@testable import CheckoutNetwork
import XCTest

final class AsyncWrapperTests: XCTestCase {
final class AsyncWrapperTests: XCTestCase {}

// MARK: Run Request whilst expecting a data response
extension AsyncWrapperTests {
func test_whenRunRequestReturnsData_ThenAsyncRunRequestPropagatesIt() async throws {
let fakeSession = FakeSession()
let fakeDataTask = FakeDataTask()
fakeSession.calledDataTasksReturn = fakeDataTask
let client = CheckoutNetworkClientSpy(session: fakeSession)
let testConfig = try! RequestConfiguration(path: FakePath.testServices)

let expectedResult = FakeObject(id: "some response")
client.expectedResult = expectedResult
let expectedResponseBody = FakeObject(id: "some response")
client.expectedResponseBody = expectedResponseBody
client.expectedError = nil
let result: FakeObject = try await client.runRequest(with: testConfig)
let responseBody: FakeObject = try await client.runRequest(with: testConfig)
XCTAssertEqual(client.configuration.request, testConfig.request)
XCTAssertEqual(client.runRequestCallCount, 1)
XCTAssertEqual(result, expectedResult)
XCTAssertEqual(responseBody, expectedResponseBody)
}

func test_whenRunRequestReturnsError_ThenAsyncRunRequestPropagatesIt() async throws {
Expand All @@ -34,7 +36,7 @@ final class AsyncWrapperTests: XCTestCase {
let testConfig = try! RequestConfiguration(path: FakePath.testServices)

let expectedError = FakeError.someError
client.expectedResult = nil
client.expectedResponseBody = nil
client.expectedError = expectedError

do {
Expand All @@ -47,3 +49,46 @@ final class AsyncWrapperTests: XCTestCase {
}
}
}

// MARK: Run Request whilst expecting no data response (HTTP 204)

extension AsyncWrapperTests {
func test_whenRunRequestWithNoDataReturnsNoError_ThenAsyncRunRequestPropagatesIt() async throws {
let fakeSession = FakeSession()
let fakeDataTask = FakeDataTask()
fakeSession.calledDataTasksReturn = fakeDataTask
let client = CheckoutNetworkClientSpy(session: fakeSession)
let testConfig = try! RequestConfiguration(path: FakePath.testServices)

client.expectedResponseBody = nil
client.expectedError = nil
do {
try await client.runRequest(with: testConfig)
XCTAssertEqual(client.configuration.request, testConfig.request)
XCTAssertEqual(client.runRequestCallCount, 1)
} catch {
XCTFail("Should have not thrown an error \(error)")
}
}

func test_whenRunRequestWithNoDataReturnsError_ThenAsyncRunRequestPropagatesIt() async throws {
let fakeSession = FakeSession()
let fakeDataTask = FakeDataTask()
fakeSession.calledDataTasksReturn = fakeDataTask
let client = CheckoutNetworkClientSpy(session: fakeSession)
let testConfig = try! RequestConfiguration(path: FakePath.testServices)

let expectedError = FakeError.someError
client.expectedResponseBody = nil
client.expectedError = expectedError

do {
try await client.runRequest(with: testConfig)
XCTFail("An error was expected to be thrown")
} catch let error as FakeError {
XCTAssertEqual(client.configuration.request, testConfig.request)
XCTAssertEqual(client.runRequestCallCount, 1)
XCTAssertEqual(error, expectedError)
}
}
}
16 changes: 14 additions & 2 deletions Tests/CheckoutNetworkTests/Helpers/CheckoutNetworkClientSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,29 @@ class CheckoutNetworkClientSpy: CheckoutNetworkClient {
private(set) var runRequestCallCount: Int = 0
private(set) var configuration: RequestConfiguration!

var expectedResult: FakeObject?
var expectedResponseBody: FakeObject?
var expectedError: Error?

override func runRequest<T>(with configuration: RequestConfiguration, completionHandler: @escaping CheckoutNetworkClient.CompletionHandler<T>) where T : Decodable {
runRequestCallCount += 1
self.configuration = configuration

if let result = expectedResult {
if let result = expectedResponseBody {
completionHandler(.success(result as! T))
} else if let error = expectedError {
completionHandler(.failure(error))
}
}

override func runRequest(with configuration: RequestConfiguration, completionHandler: @escaping CheckoutNetworkClient.NoDataResponseCompletionHandler) {
runRequestCallCount += 1
self.configuration = configuration

guard let error = expectedError else {
completionHandler(nil)
return
}

completionHandler(error)
}
}

0 comments on commit 0439704

Please sign in to comment.