Skip to content

Commit

Permalink
Map URLSession errors into CheckoutNetworkError
Browse files Browse the repository at this point in the history
This will allow clients to handle them easily.
  • Loading branch information
okhan-okbay-cko committed Feb 20, 2024
1 parent b7aa490 commit a292a7f
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 73 deletions.
2 changes: 0 additions & 2 deletions Sources/CheckoutNetwork/CheckoutClientInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ 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)
Expand Down
38 changes: 38 additions & 0 deletions Sources/CheckoutNetwork/CheckoutNetworkClient+AsyncWrappers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// CheckoutNetworkClient+AsyncWrappers.swift
//
//
// Created by Okhan Okbay on 20/02/2024.
//

import Foundation

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)
}
}
}
}
50 changes: 50 additions & 0 deletions Sources/CheckoutNetwork/CheckoutNetworkClient+ErrorHandling.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// CheckoutNetworkClient+ErrorHandling.swift
//
//
// Created by Okhan Okbay on 20/02/2024.
//

import Foundation

extension CheckoutNetworkClient {
func getErrorFromResponse(_ response: URLResponse?, data: Data?) -> Error? {
guard let response = response as? HTTPURLResponse else {
return CheckoutNetworkError.invalidURLResponse
}

guard response.statusCode != 422 else {
do {
let errorReason = try JSONDecoder().decode(ErrorReason.self, from: data ?? Data())
return CheckoutNetworkError.unprocessableContent(reason: errorReason)
} catch {
return CheckoutNetworkError.noDataResponseReceived
}
}

guard response.statusCode >= 200,
response.statusCode < 300 else {
return CheckoutNetworkError.unexpectedHTTPResponse(code: response.statusCode)
}
return nil
}

func convertDataTaskErrorsToCheckoutNetworkError(error: Error) -> CheckoutNetworkError {
let error = error as NSError

switch error.code {
case NSURLErrorNotConnectedToInternet,
NSURLErrorTimedOut,
NSURLErrorNetworkConnectionLost,
NSURLErrorInternationalRoamingOff,
NSURLErrorCannotConnectToHost,
NSURLErrorServerCertificateUntrusted:

return CheckoutNetworkError.connectivity

default:
return CheckoutNetworkError.other(underlyingError: error)
}

}
}
54 changes: 1 addition & 53 deletions Sources/CheckoutNetwork/CheckoutNetworkClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
self?.tasks.removeValue(forKey: taskID)
guard let self = self else { return }
if let error = error {
completionHandler(.failure(error))
completionHandler(.failure(convertDataTaskErrorsToCheckoutNetworkError(error: error)))
return
}

Expand Down Expand Up @@ -88,56 +88,4 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
}
return taskID
}

private func getErrorFromResponse(_ response: URLResponse?, data: Data?) -> Error? {
guard let response = response as? HTTPURLResponse else {
return CheckoutNetworkError.unexpectedResponseCode(code: 0)
}

guard response.statusCode != 422 else {
do {
let errorReason = try JSONDecoder().decode(ErrorReason.self, from: data ?? Data())
return CheckoutNetworkError.invalidData(reason: errorReason)
} catch {
return CheckoutNetworkError.noDataResponseReceived
}
}

guard response.statusCode >= 200,
response.statusCode < 300 else {
return CheckoutNetworkError.unexpectedResponseCode(code: response.statusCode)
}
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)
}
}
}
}
72 changes: 58 additions & 14 deletions Sources/CheckoutNetwork/CheckoutNetworkError.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,67 @@
//
// CheckoutNetworkError.swift
//
//
//
// Created by Alex Ioja-Yang on 20/06/2022.
//

import Foundation

public enum CheckoutNetworkError: Error, Equatable {

/// Review the url you provided
case invalidURL

/// 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 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)
public enum CheckoutNetworkError: LocalizedError, Equatable {

/// Review the url you provided
case invalidURL

/// When a response could not be bound to an instance of HTTPURLResponse
case invalidURLResponse

/// Network response was not in the 200 range
case unexpectedHTTPResponse(code: Int)

/// 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 unprocessableContent(reason: ErrorReason)

/// Connectivity errors mapped from URLSession.dataTask()'s error
///
/// Only the following are mapped into this error case:
/// NSURLErrorNotConnectedToInternet
/// NSURLErrorTimedOut
/// NSURLErrorNetworkConnectionLost
/// NSURLErrorInternationalRoamingOff
/// NSURLErrorCannotConnectToHost
/// NSURLErrorServerCertificateUntrusted
case connectivity

/// All the other errors that can be received from URLSession.
/// Use the underlying error if you need more granular error handling.
/// Underlying errors here are in NSURLErrorDomain.
case other(underlyingError: NSError)

public var errorDescription: String? {
switch self {
case .invalidURL:
return "Could not instantiate a URL with the provided String value"

case .invalidURLResponse:
return "Could not instantiate an HTTPURLResponse with the received response value"

case .unexpectedHTTPResponse(let code):
return "Received an unexpected HTTP response: \(code)"

case .noDataResponseReceived:
return "No data is received in the response body. Use the method with NoDataResponseCompletionHandler if no data was expected"

case .unprocessableContent(let reason):
return "HTTP response 422 is received with: \(reason)"

case .connectivity:
return "There is a problem with the internet connection"

case .other(let error):
return "An unhandled error is produced: \(error)"
}
}
}
6 changes: 6 additions & 0 deletions Sources/CheckoutNetwork/Models/ErrorReason.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ public struct ErrorReason: Decodable, Equatable {
case errorCodes = "error_codes"
}
}

extension ErrorReason: CustomStringConvertible {
public var description: String {
"\(requestID) \(errorType) \(errorCodes.joined(separator: ", "))"
}
}
8 changes: 4 additions & 4 deletions Tests/CheckoutNetworkTests/CheckoutNetworkClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ final class CheckoutNetworkClientTests: XCTestCase {
case .success(_):
XCTFail("Test expects a specific error to be returned")
case .failure(let failure):
XCTAssertEqual(failure as? CheckoutNetworkError, CheckoutNetworkError.unexpectedResponseCode(code: testResponseCode))
XCTAssertEqual(failure as? CheckoutNetworkError, CheckoutNetworkError.unexpectedHTTPResponse(code: testResponseCode))
}
}

Expand All @@ -126,7 +126,7 @@ final class CheckoutNetworkClientTests: XCTestCase {
case .success(_):
XCTFail("Test expects a specific error to be returned")
case .failure(let failure):
XCTAssertEqual(failure as? CheckoutNetworkError, CheckoutNetworkError.unexpectedResponseCode(code: 0))
XCTAssertEqual(failure as? CheckoutNetworkError, CheckoutNetworkError.unexpectedHTTPResponse(code: 0))
}
}

Expand Down Expand Up @@ -297,7 +297,7 @@ final class CheckoutNetworkClientTests: XCTestCase {
let expect = expectation(description: "Ensure completion handler is called")
client.runRequest(with: testConfig) {
expect.fulfill()
XCTAssertEqual($0 as? CheckoutNetworkError, CheckoutNetworkError.unexpectedResponseCode(code: testResponseCode))
XCTAssertEqual($0 as? CheckoutNetworkError, CheckoutNetworkError.unexpectedHTTPResponse(code: testResponseCode))
}

XCTAssertFalse(client.tasks.isEmpty)
Expand All @@ -318,7 +318,7 @@ final class CheckoutNetworkClientTests: XCTestCase {
let expect = expectation(description: "Ensure completion handler is called")
client.runRequest(with: testConfig) {
expect.fulfill()
XCTAssertEqual($0 as? CheckoutNetworkError, CheckoutNetworkError.unexpectedResponseCode(code: 0))
XCTAssertEqual($0 as? CheckoutNetworkError, CheckoutNetworkError.unexpectedHTTPResponse(code: 0))
}

XCTAssertFalse(client.tasks.isEmpty)
Expand Down

0 comments on commit a292a7f

Please sign in to comment.