Skip to content

Commit

Permalink
Merge pull request #101 from Infomaniak/server-response-api-fetcher
Browse files Browse the repository at this point in the history
feat: Allow returning complete api response
  • Loading branch information
PhilippeWeidmann committed Feb 27, 2024
2 parents a1689a3 + bdd1534 commit 5e65d5c
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 20 deletions.
53 changes: 42 additions & 11 deletions Sources/InfomaniakCore/Networking/ApiFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -136,24 +140,51 @@ open class ApiFetcher {
)
}

@available(*, deprecated, message: "Use perform with ValidServerResponse instead")
open func perform<T: Decodable>(request: DataRequest,
decoder: JSONDecoder = ApiFetcher.decoder) async throws -> (data: T, responseAt: Int?) {
let validServerResponse: ValidServerResponse<T> = try await perform(request: request, decoder: decoder)
return (validServerResponse.validApiResponse.data, validServerResponse.validApiResponse.responseAt)
}

open func perform<T: Decodable>(request: DataRequest,
decoder: JSONDecoder = ApiFetcher.decoder) async throws -> T {
return try await perform(request: request, decoder: decoder).validApiResponse.data
}

open func perform<T: Decodable>(request: DataRequest,
decoder: JSONDecoder = ApiFetcher.decoder) async throws -> ValidServerResponse<T> {
let validatedRequest = request.validate(statusCode: ApiFetcher.handledHttpStatus)
let response = await validatedRequest.serializingDecodable(ApiResponse<T>.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<T>.self,
automaticallyCancelling: true,
decoder: decoder).response
return try handleApiResponse(dataResponse)
}

open func handleApiResponse<T: Decodable>(_ response: ApiResponse<T>,
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<T: Decodable>(_ dataResponse: DataResponse<ApiResponse<T>, AFError>) throws
-> ValidServerResponse<T> {
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)
}
}

Expand Down
34 changes: 34 additions & 0 deletions Sources/InfomaniakCore/Networking/ValidApiResponse.swift
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

import Alamofire

public struct ValidServerResponse<ValidApiResponseContent> {
public let responseHeaders: HTTPHeaders
public let validApiResponse: ValidApiResponse<ValidApiResponseContent>
}

public struct ValidApiResponse<ResponseContent> {
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?
}
31 changes: 22 additions & 9 deletions Tests/InfomaniakCoreTests/UTDecodeApiResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,23 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

@testable import Alamofire
@testable import InfomaniakCore
import XCTest

@available(iOS 13.0, *)
final class UTDecodeApiResponse: XCTestCase {
func fakeDataResponse<T: Decodable>(decodedResponse: ApiResponse<T>) -> DataResponse<ApiResponse<T>, AFError> {
DataResponse(
request: nil,
response: HTTPURLResponse(),
data: nil,
metrics: nil,
serializationDuration: 0,
result: .success(decodedResponse)
)
}

func testDecodeNullDataResponse() throws {
// GIVEN
let apiFetcher = ApiFetcher()
Expand All @@ -33,12 +45,12 @@ final class UTDecodeApiResponse: XCTestCase {

// WHEN
let decodedResponse = try? JSONDecoder().decode(ApiResponse<NullableResponse>.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"
)
}

Expand All @@ -54,13 +66,11 @@ final class UTDecodeApiResponse: XCTestCase {

// WHEN
let decodedResponse = try? JSONDecoder().decode(ApiResponse<Int>.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 {
Expand All @@ -74,11 +84,12 @@ final class UTDecodeApiResponse: XCTestCase {

// WHEN
let decodedResponse = try? JSONDecoder().decode(ApiResponse<Int>.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")
Expand All @@ -97,11 +108,12 @@ final class UTDecodeApiResponse: XCTestCase {

// WHEN
let decodedResponse = try? JSONDecoder().decode(ApiResponse<Int>.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")
Expand All @@ -123,11 +135,12 @@ final class UTDecodeApiResponse: XCTestCase {

// WHEN
let decodedResponse = try? JSONDecoder().decode(ApiResponse<Int>.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")
Expand Down

0 comments on commit 5e65d5c

Please sign in to comment.