Skip to content

Commit

Permalink
[Chore] Allow custom error handling on requests (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
gentilijuanmanuel authored Aug 23, 2024
1 parent 0c45531 commit 329f7e6
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 6 deletions.
69 changes: 69 additions & 0 deletions Sources/CoreNetworking/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,73 @@ public class HTTPClient {
throw Request.RequestError.unexpectedStatusCode
}
}

/// Executes a request asynchronously and returns a result or throws an error.
public func execute<SuccessResponse: Decodable, ErrorResponse: Decodable>(
_ request: Request,
successResponseType: SuccessResponse.Type,
errorResponseType: ErrorResponse.Type
) async throws -> Result<SuccessResponse, ErrorResponse> {
networkLogger.logRequest(request)
let (data, response) = try await URLSession.shared.data(
for: request.urlRequest,
delegate: nil
)

guard let response = response as? HTTPURLResponse else {
throw Request.RequestError.noResponse
}
networkLogger.logResponse(response, request: request)

switch response.statusCode {
case 200...299:
do {
let decodedResponse = try jsonDecoder.decode(
successResponseType,
from: data
)

networkLogger.logDecodingSuccessResponse(
model: decodedResponse,
for: successResponseType,
data: data
)
return .success(decodedResponse)
} catch {
networkLogger.logDecodingErrorResponse(
with: error,
for: successResponseType,
data: data
)

guard let decodingError = error as? DecodingError else {
throw Request.RequestError.decode()
}

throw Request.RequestError.decode(decodingError)
}
case 401:
do {
let decodedResponse = try jsonDecoder.decode(
errorResponseType,
from: data
)

return .failure(decodedResponse)
} catch {
throw Request.RequestError.unexpectedStatusCode
}
default:
do {
let decodedResponse = try jsonDecoder.decode(
errorResponseType,
from: data
)

return .failure(decodedResponse)
} catch {
throw Request.RequestError.unexpectedStatusCode
}
}
}
}
111 changes: 105 additions & 6 deletions Tests/CoreNetworkingTests/CoreNetworkingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import XCTest
@testable import CoreNetworking

final class CoreNetworkingTests: XCTestCase {
func testCorrectDecoding() async throws {
func test_successfulSimpleRequest_shouldReturnResponse() async throws {
let fetchFact = {
try await HTTPClient.shared
.execute(
Expand All @@ -19,7 +19,61 @@ final class CoreNetworkingTests: XCTestCase {
XCTAssertNotNil(fact)
}

func testErrorDecoding() async throws {
func test_successfulRequestWithCustomErrorHandling_shouldReturnResultWithResponse() async throws {
let fetchFact = {
try await HTTPClient.shared
.execute(
.init(
urlString: "https://catfact.ninja/fact/",
method: .get([]),
headers: [:]
),
successResponseType: CatFact.self,
errorResponseType: GenericError.self
)
}
let service = CatFactsService(
dependencies: .init(
fetchFactWithErrorHandling: fetchFact
)
)
let result = try await service.fetchCatFactWithErrorHandling()
switch result {
case let .success(response):
XCTAssertNotNil(response)
case .failure, .none:
XCTFail()
}
}

func test_wrongRequestWithCustomErrorHandling_shouldReturnResultWithError() async throws {
let fetchFact = {
try await HTTPClient.shared
.execute(
.init(
urlString: "https://catfact.ninja/factsss/",
method: .get([]),
headers: [:]
),
successResponseType: CatFact.self,
errorResponseType: GenericError.self
)
}
let service = CatFactsService(
dependencies: .init(
fetchFactWithErrorHandling: fetchFact
)
)
let result = try await service.fetchCatFactWithErrorHandling()
switch result {
case .success, .none:
XCTFail()
case let .failure(error):
XCTAssertNotNil(error)
}
}

func test_simpleRequest_withWrongRequestType_shouldReturnDecodingError() async throws {
let fetchFact = {
try await HTTPClient.shared
.execute(
Expand Down Expand Up @@ -49,28 +103,73 @@ final class CoreNetworkingTests: XCTestCase {
XCTFail("Should have thrown RequestError.decode error")
}
}

func test_requestWithCustomErrorHandling_withWrongRequestType_shouldReturnDecodingError() async throws {
let fetchFact = {
try await HTTPClient.shared
.execute(
.init(
urlString: "https://catfact.ninja/facts/",
method: .get([]),
headers: [:]
),
successResponseType: CatFact.self,
errorResponseType: GenericError.self
)
}
let service = CatFactsService(
dependencies: .init(
fetchFactWithErrorHandling: fetchFact
)
)

do {
_ = try await service.fetchCatFactWithErrorHandling()
XCTFail("Should have thrown")
} catch let Request.RequestError.decode(DecodingError.keyNotFound(key, context)?) {
XCTAssertEqual(key.intValue, nil)
XCTAssertEqual(key.stringValue, "fact")
XCTAssertEqual(context.codingPath.count, 0)
XCTAssertEqual(
context.debugDescription,
"No value associated with key CodingKeys(stringValue: \"fact\", intValue: nil) (\"fact\")."
)
XCTAssertNil(context.underlyingError)
} catch {
XCTFail("Should have thrown RequestError.decode error")
}
}
}

final class CatFactsService {
private let dependencies: Dependencies
var fact: CatFact?

init(dependencies: Dependencies) {
self.dependencies = dependencies
}

func fetchCatFact() async throws -> CatFact {
return try await dependencies.fetchFact()
func fetchCatFact() async throws -> CatFact? {
try await dependencies.fetchFact?()
}

func fetchCatFactWithErrorHandling() async throws -> Result<CatFact, GenericError>? {
try await dependencies.fetchFactWithErrorHandling?()
}
}

extension CatFactsService {
struct Dependencies {
var fetchFact: () async throws -> CatFact
var fetchFact: (() async throws -> CatFact)? = nil
var fetchFactWithErrorHandling: (() async throws -> Result<CatFact, GenericError>)? = nil
}
}

struct CatFact: Decodable {
let fact: String
let length: Int
}

struct GenericError: Error, Decodable {
let code: Int
let message: String
}

0 comments on commit 329f7e6

Please sign in to comment.