From ab305a8ff4baaedc5b080afe3e85056e1b7aac45 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 15 Feb 2024 10:47:29 +0100 Subject: [PATCH 1/3] feat: Allow returning complete api response --- .../Networking/ApiFetcher.swift | 47 ++++++++++++++----- .../Networking/ValidApiResponse.swift | 34 ++++++++++++++ 2 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 Sources/InfomaniakCore/Networking/ValidApiResponse.swift diff --git a/Sources/InfomaniakCore/Networking/ApiFetcher.swift b/Sources/InfomaniakCore/Networking/ApiFetcher.swift index 48b46a1..55c9576 100644 --- a/Sources/InfomaniakCore/Networking/ApiFetcher.swift +++ b/Sources/InfomaniakCore/Networking/ApiFetcher.swift @@ -29,6 +29,10 @@ public protocol RefreshTokenDelegate: AnyObject { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) open class ApiFetcher { + enum ErrorDomain: Error { + case noServerResponse + } + public typealias RequestModifier = (inout URLRequest) throws -> Void /// All status except 401 are handled by our code, 401 status is handled by Alamofire's Authenticator code @@ -138,22 +142,43 @@ open class ApiFetcher { open func perform(request: DataRequest, decoder: JSONDecoder = ApiFetcher.decoder) async throws -> (data: T, responseAt: Int?) { + let validServerResponse: ValidServerResponse = try await perform(request: request, decoder: decoder) + return (validServerResponse.validApiResponse.data, validServerResponse.validApiResponse.responseAt) + } + + open func perform(request: DataRequest, + decoder: JSONDecoder = ApiFetcher.decoder) async throws -> ValidServerResponse { let validatedRequest = request.validate(statusCode: ApiFetcher.handledHttpStatus) - let response = await validatedRequest.serializingDecodable(ApiResponse.self, - automaticallyCancelling: true, - decoder: decoder).response - let apiResponse = try response.result.get() - return try handleApiResponse(apiResponse, responseStatusCode: response.response?.statusCode ?? -1) + let dataResponse = await validatedRequest.serializingDecodable(ApiResponse.self, + automaticallyCancelling: true, + decoder: decoder).response + return try handleApiResponse(dataResponse) } - open func handleApiResponse(_ response: ApiResponse, - responseStatusCode: Int) throws -> (data: T, responseAt: Int?) { - if let responseData = response.data { - return (responseData, response.responseAt) - } else if let apiError = response.error { + open func handleApiResponse(_ dataResponse: DataResponse, AFError>) throws + -> ValidServerResponse { + let apiResponse = try dataResponse.result.get() + + // This value should not be null because dataResponse.result.get should throw before + guard let serverResponse = dataResponse.response else { + throw ErrorDomain.noServerResponse + } + + if let responseData = apiResponse.data { + let validApiResponse = ValidApiResponse( + result: apiResponse.result, + data: responseData, + total: apiResponse.total, + pages: apiResponse.pages, + page: apiResponse.page, + itemsPerPage: apiResponse.itemsPerPage, + responseAt: apiResponse.responseAt + ) + return ValidServerResponse(responseHeaders: serverResponse.headers, validApiResponse: validApiResponse) + } else if let apiError = apiResponse.error { throw InfomaniakError.apiError(apiError) } else { - throw InfomaniakError.serverError(statusCode: responseStatusCode) + throw InfomaniakError.serverError(statusCode: serverResponse.statusCode) } } diff --git a/Sources/InfomaniakCore/Networking/ValidApiResponse.swift b/Sources/InfomaniakCore/Networking/ValidApiResponse.swift new file mode 100644 index 0000000..c1b4711 --- /dev/null +++ b/Sources/InfomaniakCore/Networking/ValidApiResponse.swift @@ -0,0 +1,34 @@ +/* + Infomaniak Core - iOS + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Alamofire + +public struct ValidServerResponse { + public let responseHeaders: HTTPHeaders + public let validApiResponse: ValidApiResponse +} + +public struct ValidApiResponse { + public let result: ApiResult + public let data: ResponseContent + public let total: Int? + public let pages: Int? + public let page: Int? + public let itemsPerPage: Int? + public let responseAt: Int? +} From e1e75881ff5a26a376fcf0e53bf7979dfdf170bd Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 15 Feb 2024 10:52:28 +0100 Subject: [PATCH 2/3] test: Fix test to adopt new handleApiResponse signature --- .../UTDecodeApiResponse.swift | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/Tests/InfomaniakCoreTests/UTDecodeApiResponse.swift b/Tests/InfomaniakCoreTests/UTDecodeApiResponse.swift index 951e4f4..f501398 100644 --- a/Tests/InfomaniakCoreTests/UTDecodeApiResponse.swift +++ b/Tests/InfomaniakCoreTests/UTDecodeApiResponse.swift @@ -16,11 +16,23 @@ along with this program. If not, see . */ +@testable import Alamofire @testable import InfomaniakCore import XCTest @available(iOS 13.0, *) final class UTDecodeApiResponse: XCTestCase { + func fakeDataResponse(decodedResponse: ApiResponse) -> DataResponse, AFError> { + DataResponse( + request: nil, + response: HTTPURLResponse(), + data: nil, + metrics: nil, + serializationDuration: 0, + result: .success(decodedResponse) + ) + } + func testDecodeNullDataResponse() throws { // GIVEN let apiFetcher = ApiFetcher() @@ -33,12 +45,12 @@ final class UTDecodeApiResponse: XCTestCase { // WHEN let decodedResponse = try? JSONDecoder().decode(ApiResponse.self, from: jsonData) + let dataResponse = fakeDataResponse(decodedResponse: decodedResponse!) // THEN XCTAssertNotNil(decodedResponse, "Response shouldn't be nil") XCTAssertNoThrow( - try apiFetcher.handleApiResponse(decodedResponse!, responseStatusCode: 0), - "handleApiResponse shouldn't throw" + try apiFetcher.handleApiResponse(dataResponse), "handleApiResponse shouldn't throw" ) } @@ -54,13 +66,11 @@ final class UTDecodeApiResponse: XCTestCase { // WHEN let decodedResponse = try? JSONDecoder().decode(ApiResponse.self, from: jsonData) + let dataResponse = fakeDataResponse(decodedResponse: decodedResponse!) // THEN XCTAssertNotNil(decodedResponse, "Response shouldn't be nil") - XCTAssertNoThrow( - try apiFetcher.handleApiResponse(decodedResponse!, responseStatusCode: 0), - "handleApiResponse shouldn't throw" - ) + XCTAssertNoThrow(try apiFetcher.handleApiResponse(dataResponse), "handleApiResponse shouldn't throw") } func testDecodeErrorNullApiResponse() throws { @@ -74,11 +84,12 @@ final class UTDecodeApiResponse: XCTestCase { // WHEN let decodedResponse = try? JSONDecoder().decode(ApiResponse.self, from: jsonData) + let dataResponse = fakeDataResponse(decodedResponse: decodedResponse!) // THEN XCTAssertNotNil(decodedResponse, "Response shouldn't be nil") do { - let _ = try apiFetcher.handleApiResponse(decodedResponse!, responseStatusCode: 0) + let _ = try apiFetcher.handleApiResponse(dataResponse) } catch { let ikError = error as? InfomaniakError XCTAssertNotNil(ikError, "Error should be InfomaniakError") @@ -97,11 +108,12 @@ final class UTDecodeApiResponse: XCTestCase { // WHEN let decodedResponse = try? JSONDecoder().decode(ApiResponse.self, from: jsonData) + let dataResponse = fakeDataResponse(decodedResponse: decodedResponse!) // THEN XCTAssertNotNil(decodedResponse, "Response shouldn't be nil") do { - let _ = try apiFetcher.handleApiResponse(decodedResponse!, responseStatusCode: 0) + let _ = try apiFetcher.handleApiResponse(dataResponse) } catch { let ikError = error as? InfomaniakError XCTAssertNotNil(ikError, "Error should be InfomaniakError") @@ -123,11 +135,12 @@ final class UTDecodeApiResponse: XCTestCase { // WHEN let decodedResponse = try? JSONDecoder().decode(ApiResponse.self, from: jsonData) + let dataResponse = fakeDataResponse(decodedResponse: decodedResponse!) // THEN XCTAssertNotNil(decodedResponse, "Response shouldn't be nil") do { - let _ = try apiFetcher.handleApiResponse(decodedResponse!, responseStatusCode: 0) + let _ = try apiFetcher.handleApiResponse(dataResponse) } catch { let ikError = error as? InfomaniakError XCTAssertNotNil(ikError, "Error should be InfomaniakError") From bdd1534403c2a6310edf7451c97f67c37a44b781 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 15 Feb 2024 11:05:57 +0100 Subject: [PATCH 3/3] feat: Mark deprecated old perform --- Sources/InfomaniakCore/Networking/ApiFetcher.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/InfomaniakCore/Networking/ApiFetcher.swift b/Sources/InfomaniakCore/Networking/ApiFetcher.swift index 55c9576..c0d1a0c 100644 --- a/Sources/InfomaniakCore/Networking/ApiFetcher.swift +++ b/Sources/InfomaniakCore/Networking/ApiFetcher.swift @@ -32,7 +32,7 @@ open class ApiFetcher { enum ErrorDomain: Error { case noServerResponse } - + public typealias RequestModifier = (inout URLRequest) throws -> Void /// All status except 401 are handled by our code, 401 status is handled by Alamofire's Authenticator code @@ -140,12 +140,18 @@ open class ApiFetcher { ) } + @available(*, deprecated, message: "Use perform with ValidServerResponse instead") open func perform(request: DataRequest, decoder: JSONDecoder = ApiFetcher.decoder) async throws -> (data: T, responseAt: Int?) { let validServerResponse: ValidServerResponse = try await perform(request: request, decoder: decoder) return (validServerResponse.validApiResponse.data, validServerResponse.validApiResponse.responseAt) } + open func perform(request: DataRequest, + decoder: JSONDecoder = ApiFetcher.decoder) async throws -> T { + return try await perform(request: request, decoder: decoder).validApiResponse.data + } + open func perform(request: DataRequest, decoder: JSONDecoder = ApiFetcher.decoder) async throws -> ValidServerResponse { let validatedRequest = request.validate(statusCode: ApiFetcher.handledHttpStatus)