2021-07-30 23:03:25 +02:00
|
|
|
//
|
|
|
|
|
// Copyright RevenueCat Inc. All Rights Reserved.
|
|
|
|
|
//
|
|
|
|
|
// Licensed under the MIT License (the "License");
|
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
|
//
|
|
|
|
|
// https://opensource.org/licenses/MIT
|
|
|
|
|
//
|
|
|
|
|
// HTTPClient.swift
|
|
|
|
|
//
|
|
|
|
|
// Created by César de la Vega on 7/22/21.
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
|
2022-08-31 13:30:38 -07:00
|
|
|
// swiftlint:disable file_length
|
|
|
|
|
|
2021-08-23 11:20:55 -07:00
|
|
|
class HTTPClient {
|
2021-07-30 23:03:25 +02:00
|
|
|
|
2022-03-04 13:46:24 -08:00
|
|
|
typealias RequestHeaders = [String: String]
|
2022-04-12 15:11:22 -07:00
|
|
|
typealias Completion<Value: HTTPResponseBody> = (HTTPResponse<Value>.Result) -> Void
|
2022-03-04 13:46:24 -08:00
|
|
|
|
2022-06-14 15:12:46 -07:00
|
|
|
let systemInfo: SystemInfo
|
|
|
|
|
let timeout: TimeInterval
|
|
|
|
|
let apiKey: String
|
|
|
|
|
let authHeaders: RequestHeaders
|
2022-05-12 15:43:10 -07:00
|
|
|
|
|
|
|
|
private let session: URLSession
|
2022-02-23 18:05:59 -05:00
|
|
|
private let state: Atomic<State> = .init(.initial)
|
2022-02-19 10:11:52 -07:00
|
|
|
private let eTagManager: ETagManager
|
2022-01-13 16:24:37 +01:00
|
|
|
private let dnsChecker: DNSCheckerType.Type
|
2021-07-30 23:03:25 +02:00
|
|
|
|
2022-06-14 15:12:46 -07:00
|
|
|
init(apiKey: String,
|
|
|
|
|
systemInfo: SystemInfo,
|
|
|
|
|
eTagManager: ETagManager,
|
|
|
|
|
dnsChecker: DNSCheckerType.Type = DNSChecker.self,
|
|
|
|
|
requestTimeout: TimeInterval = Configuration.networkTimeoutDefault) {
|
2021-07-30 23:03:25 +02:00
|
|
|
let config = URLSessionConfiguration.ephemeral
|
|
|
|
|
config.httpMaximumConnectionsPerHost = 1
|
2022-05-12 15:43:10 -07:00
|
|
|
config.timeoutIntervalForRequest = requestTimeout
|
|
|
|
|
config.timeoutIntervalForResource = requestTimeout
|
2021-07-30 23:03:25 +02:00
|
|
|
self.session = URLSession(configuration: config)
|
|
|
|
|
self.systemInfo = systemInfo
|
|
|
|
|
self.eTagManager = eTagManager
|
2022-01-13 16:24:37 +01:00
|
|
|
self.dnsChecker = dnsChecker
|
2022-05-12 15:43:10 -07:00
|
|
|
self.timeout = requestTimeout
|
2022-06-14 15:12:46 -07:00
|
|
|
self.apiKey = apiKey
|
|
|
|
|
self.authHeaders = HTTPClient.authorizationHeader(withAPIKey: apiKey)
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2022-06-14 15:12:46 -07:00
|
|
|
func perform<Value: HTTPResponseBody>(_ request: HTTPRequest, completionHandler: Completion<Value>?) {
|
2022-03-04 13:42:23 -08:00
|
|
|
perform(request: .init(httpRequest: request,
|
2022-06-14 15:12:46 -07:00
|
|
|
headers: self.authHeaders,
|
2022-02-23 18:09:50 -05:00
|
|
|
completionHandler: completionHandler))
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2021-08-23 11:20:55 -07:00
|
|
|
func clearCaches() {
|
2021-09-01 14:09:41 -07:00
|
|
|
eTagManager.clearCaches()
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-04 13:46:24 -08:00
|
|
|
extension HTTPClient {
|
|
|
|
|
|
|
|
|
|
static func authorizationHeader(withAPIKey apiKey: String) -> RequestHeaders {
|
|
|
|
|
return ["Authorization": "Bearer \(apiKey)"]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-31 13:30:38 -07:00
|
|
|
// @unchecked because:
|
|
|
|
|
// - Class is not `final` (it's mocked). This implicitly makes subclasses `Sendable` even if they're not thread-safe.
|
|
|
|
|
extension HTTPClient: @unchecked Sendable {}
|
|
|
|
|
|
|
|
|
|
// MARK: - Private
|
|
|
|
|
|
2022-02-23 18:05:59 -05:00
|
|
|
private extension HTTPClient {
|
2022-04-01 14:22:29 -07:00
|
|
|
|
2022-02-23 18:05:59 -05:00
|
|
|
struct State {
|
|
|
|
|
var queuedRequests: [Request]
|
|
|
|
|
var currentSerialRequest: Request?
|
|
|
|
|
|
2022-06-14 15:12:46 -07:00
|
|
|
static let initial: Self = .init(queuedRequests: [], currentSerialRequest: nil)
|
2022-02-23 18:05:59 -05:00
|
|
|
}
|
2022-04-01 14:22:29 -07:00
|
|
|
|
2022-02-16 13:41:28 -08:00
|
|
|
struct Request: CustomStringConvertible {
|
|
|
|
|
|
2022-03-04 13:42:23 -08:00
|
|
|
var httpRequest: HTTPRequest
|
2022-03-04 13:46:24 -08:00
|
|
|
var headers: HTTPClient.RequestHeaders
|
2022-04-12 15:11:22 -07:00
|
|
|
var completionHandler: HTTPClient.Completion<Data>?
|
2022-02-16 13:41:28 -08:00
|
|
|
var retried: Bool = false
|
|
|
|
|
|
2022-06-14 15:12:46 -07:00
|
|
|
init<Value: HTTPResponseBody>(httpRequest: HTTPRequest,
|
|
|
|
|
headers: HTTPClient.RequestHeaders,
|
|
|
|
|
completionHandler: HTTPClient.Completion<Value>?) {
|
2022-04-12 15:11:22 -07:00
|
|
|
self.httpRequest = httpRequest
|
|
|
|
|
self.headers = headers
|
|
|
|
|
|
|
|
|
|
if let completionHandler = completionHandler {
|
|
|
|
|
self.completionHandler = { result in
|
|
|
|
|
completionHandler(result.parseResponse())
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.completionHandler = nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-04 13:42:23 -08:00
|
|
|
var method: HTTPRequest.Method { self.httpRequest.method }
|
|
|
|
|
var path: String { self.httpRequest.path.description }
|
2022-02-16 13:41:28 -08:00
|
|
|
|
2022-03-04 13:46:24 -08:00
|
|
|
func adding(defaultHeaders: HTTPClient.RequestHeaders) -> Self {
|
2022-02-16 13:41:28 -08:00
|
|
|
var copy = self
|
|
|
|
|
copy.headers = defaultHeaders.merging(self.headers)
|
|
|
|
|
|
|
|
|
|
return copy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func retriedRequest() -> Self {
|
|
|
|
|
var copy = self
|
|
|
|
|
copy.retried = true
|
|
|
|
|
|
|
|
|
|
return copy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var description: String {
|
|
|
|
|
"""
|
|
|
|
|
<\(type(of: self)): httpMethod=\(self.method.httpMethod)
|
|
|
|
|
path=\(self.path)
|
|
|
|
|
headers=\(self.headers.description )
|
|
|
|
|
retried=\(self.retried)
|
|
|
|
|
>
|
|
|
|
|
"""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-30 23:03:25 +02:00
|
|
|
private extension HTTPClient {
|
|
|
|
|
|
|
|
|
|
var defaultHeaders: [String: String] {
|
2022-05-20 14:51:48 -07:00
|
|
|
let observerMode = !self.systemInfo.finishTransactions
|
|
|
|
|
|
2021-07-30 23:03:25 +02:00
|
|
|
var headers: [String: String] = [
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
"X-Version": SystemInfo.frameworkVersion,
|
|
|
|
|
"X-Platform": SystemInfo.platformHeader,
|
|
|
|
|
"X-Platform-Version": SystemInfo.systemVersion,
|
|
|
|
|
"X-Platform-Flavor": systemInfo.platformFlavor,
|
|
|
|
|
"X-Client-Version": SystemInfo.appVersion,
|
|
|
|
|
"X-Client-Build-Version": SystemInfo.buildVersion,
|
2022-04-11 11:41:47 -07:00
|
|
|
"X-StoreKit2-Setting": "\(self.systemInfo.storeKit2Setting.debugDescription)",
|
2022-05-20 14:51:48 -07:00
|
|
|
"X-Observer-Mode-Enabled": "\(observerMode)",
|
|
|
|
|
"X-Is-Sandbox": "\(self.systemInfo.isSandbox)"
|
2021-07-30 23:03:25 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
if let platformFlavorVersion = self.systemInfo.platformFlavorVersion {
|
|
|
|
|
headers["X-Platform-Flavor-Version"] = platformFlavorVersion
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-01 20:25:04 +01:00
|
|
|
if let idfv = systemInfo.identifierForVendor {
|
2021-07-30 23:03:25 +02:00
|
|
|
headers["X-Apple-Device-Identifier"] = idfv
|
|
|
|
|
}
|
|
|
|
|
return headers
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-23 18:09:50 -05:00
|
|
|
func perform(request: Request) {
|
|
|
|
|
if !request.retried {
|
2022-02-23 18:05:59 -05:00
|
|
|
let requestEnqueued: Bool = self.state.modify {
|
|
|
|
|
if $0.currentSerialRequest != nil {
|
|
|
|
|
Logger.debug(Strings.network.serial_request_queued(httpMethod: request.method.httpMethod,
|
|
|
|
|
path: request.path,
|
|
|
|
|
queuedRequestsCount: $0.queuedRequests.count))
|
|
|
|
|
|
|
|
|
|
$0.queuedRequests.append(request)
|
|
|
|
|
return true
|
|
|
|
|
} else {
|
|
|
|
|
Logger.debug(Strings.network.starting_request(httpMethod: request.method.httpMethod,
|
|
|
|
|
path: request.path))
|
|
|
|
|
$0.currentSerialRequest = request
|
|
|
|
|
return false
|
|
|
|
|
}
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
2022-02-23 18:05:59 -05:00
|
|
|
|
|
|
|
|
guard !requestEnqueued else { return }
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2022-02-23 18:09:50 -05:00
|
|
|
self.start(request: request)
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2022-04-12 15:11:22 -07:00
|
|
|
/// - Returns: `nil` if the request must be retried
|
2022-04-01 14:22:29 -07:00
|
|
|
func parse(urlResponse: URLResponse?,
|
|
|
|
|
request: Request,
|
|
|
|
|
urlRequest: URLRequest,
|
|
|
|
|
data: Data?,
|
2022-06-14 15:12:46 -07:00
|
|
|
error networkError: Error?) -> HTTPResponse<Data>.Result? {
|
2022-04-01 14:22:29 -07:00
|
|
|
if let networkError = networkError {
|
2022-04-21 09:31:20 -07:00
|
|
|
return .failure(NetworkError(networkError, dnsChecker: self.dnsChecker))
|
2022-04-01 14:22:29 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let httpURLResponse = urlResponse as? HTTPURLResponse else {
|
2022-04-12 15:11:22 -07:00
|
|
|
return .failure(.unexpectedResponse(urlResponse))
|
2022-04-01 14:22:29 -07:00
|
|
|
}
|
|
|
|
|
|
2022-04-12 15:11:22 -07:00
|
|
|
/// - Returns `nil` if status code is 304, since the response will be empty
|
|
|
|
|
/// and fetched from the eTag.
|
|
|
|
|
func dataIfAvailable(_ statusCode: HTTPStatusCode) -> Data? {
|
|
|
|
|
if statusCode == .notModified {
|
|
|
|
|
return nil
|
2022-04-01 14:22:29 -07:00
|
|
|
} else {
|
2022-04-12 15:11:22 -07:00
|
|
|
return data
|
2022-04-01 14:22:29 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let statusCode = HTTPStatusCode(rawValue: httpURLResponse.statusCode)
|
|
|
|
|
|
|
|
|
|
Logger.debug(Strings.network.api_request_completed(request.httpRequest,
|
|
|
|
|
httpCode: statusCode))
|
|
|
|
|
|
2022-04-12 15:11:22 -07:00
|
|
|
return Result
|
|
|
|
|
.success(dataIfAvailable(statusCode))
|
|
|
|
|
.mapSuccessToOptionalHTTPResult(statusCode)
|
2022-04-01 14:22:29 -07:00
|
|
|
.map {
|
2022-04-12 15:11:22 -07:00
|
|
|
self.eTagManager.httpResultFromCacheOrBackend(with: httpURLResponse,
|
|
|
|
|
data: $0.body,
|
|
|
|
|
request: urlRequest,
|
|
|
|
|
retried: request.retried)
|
2022-04-01 14:22:29 -07:00
|
|
|
}
|
2022-04-05 11:39:49 -07:00
|
|
|
.asOptionalResult?
|
2022-04-12 15:11:22 -07:00
|
|
|
.convertUnsuccessfulResponseToError()
|
2022-04-01 14:22:29 -07:00
|
|
|
}
|
|
|
|
|
|
2022-02-16 13:41:28 -08:00
|
|
|
func handle(urlResponse: URLResponse?,
|
|
|
|
|
request: Request,
|
|
|
|
|
urlRequest: URLRequest,
|
|
|
|
|
data: Data?,
|
2022-02-23 18:09:50 -05:00
|
|
|
error networkError: Error?) {
|
2022-04-05 11:39:49 -07:00
|
|
|
let response = self.parse(
|
2022-04-01 14:22:29 -07:00
|
|
|
urlResponse: urlResponse,
|
|
|
|
|
request: request,
|
|
|
|
|
urlRequest: urlRequest,
|
|
|
|
|
data: data,
|
|
|
|
|
error: networkError
|
|
|
|
|
)
|
2021-07-30 23:03:25 +02:00
|
|
|
|
2022-04-05 11:39:49 -07:00
|
|
|
if let response = response {
|
|
|
|
|
request.completionHandler?(response)
|
|
|
|
|
} else {
|
2022-04-01 14:22:29 -07:00
|
|
|
Logger.debug(Strings.network.retrying_request(httpMethod: request.method.httpMethod,
|
|
|
|
|
path: request.path))
|
2021-07-30 23:03:25 +02:00
|
|
|
|
2022-04-01 14:22:29 -07:00
|
|
|
self.state.modify {
|
|
|
|
|
$0.queuedRequests.insert(request.retriedRequest(), at: 0)
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-23 18:09:50 -05:00
|
|
|
self.beginNextRequest()
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2022-02-23 18:05:59 -05:00
|
|
|
func beginNextRequest() {
|
|
|
|
|
let nextRequest: Request? = self.state.modify {
|
|
|
|
|
Logger.debug(Strings.network.serial_request_done(httpMethod: $0.currentSerialRequest?.method.httpMethod,
|
|
|
|
|
path: $0.currentSerialRequest?.path,
|
|
|
|
|
queuedRequestsCount: $0.queuedRequests.count))
|
|
|
|
|
$0.currentSerialRequest = $0.queuedRequests.popFirst()
|
|
|
|
|
|
|
|
|
|
return $0.currentSerialRequest
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let nextRequest = nextRequest {
|
|
|
|
|
Logger.debug(Strings.network.starting_next_request(request: nextRequest.description))
|
2022-02-23 18:09:50 -05:00
|
|
|
self.start(request: nextRequest)
|
2022-02-23 18:05:59 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-23 18:09:50 -05:00
|
|
|
func start(request: Request) {
|
2022-02-23 18:05:59 -05:00
|
|
|
let urlRequest = self.convert(request: request.adding(defaultHeaders: self.defaultHeaders))
|
|
|
|
|
|
|
|
|
|
guard let urlRequest = urlRequest else {
|
2022-03-08 12:56:13 -08:00
|
|
|
Logger.error("Could not create request to \(request.path)")
|
2022-02-23 18:05:59 -05:00
|
|
|
|
2022-03-15 15:17:04 -05:00
|
|
|
request.completionHandler?(
|
2022-04-12 15:11:22 -07:00
|
|
|
.failure(.unableToCreateRequest(request.httpRequest.path))
|
2022-03-15 15:17:04 -05:00
|
|
|
)
|
2022-02-23 18:05:59 -05:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-15 14:32:54 -05:00
|
|
|
Logger.debug(Strings.network.api_request_started(request.httpRequest))
|
2022-02-23 18:05:59 -05:00
|
|
|
|
|
|
|
|
let task = session.dataTask(with: urlRequest) { (data, urlResponse, error) -> Void in
|
|
|
|
|
self.handle(urlResponse: urlResponse,
|
|
|
|
|
request: request,
|
|
|
|
|
urlRequest: urlRequest,
|
|
|
|
|
data: data,
|
2022-02-23 18:09:50 -05:00
|
|
|
error: error)
|
2022-02-23 18:05:59 -05:00
|
|
|
}
|
|
|
|
|
task.resume()
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 13:41:28 -08:00
|
|
|
func convert(request: Request) -> URLRequest? {
|
2022-03-04 13:42:23 -08:00
|
|
|
guard let requestURL = request.httpRequest.path.url else {
|
2021-07-30 23:03:25 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var urlRequest = URLRequest(url: requestURL)
|
2022-02-16 13:41:28 -08:00
|
|
|
urlRequest.httpMethod = request.method.httpMethod
|
2021-07-30 23:03:25 +02:00
|
|
|
|
2022-02-16 13:41:28 -08:00
|
|
|
let eTagHeader = eTagManager.eTagHeader(for: urlRequest, refreshETag: request.retried)
|
|
|
|
|
let headersWithETag = request.headers.merging(eTagHeader)
|
2021-07-30 23:03:25 +02:00
|
|
|
|
|
|
|
|
urlRequest.allHTTPHeaderFields = headersWithETag
|
2022-03-08 12:56:13 -08:00
|
|
|
do {
|
|
|
|
|
urlRequest.httpBody = try request.httpRequest.requestBody?.asData()
|
|
|
|
|
} catch {
|
|
|
|
|
Logger.error(Strings.network.creating_json_error(error: error.localizedDescription))
|
|
|
|
|
return nil
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
2022-02-16 13:41:28 -08:00
|
|
|
|
2021-07-30 23:03:25 +02:00
|
|
|
return urlRequest
|
|
|
|
|
}
|
2021-08-24 12:56:37 -07:00
|
|
|
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
2022-03-04 13:42:23 -08:00
|
|
|
|
2022-08-31 13:30:38 -07:00
|
|
|
// MARK: - Extensions
|
|
|
|
|
|
2022-03-04 13:42:23 -08:00
|
|
|
extension HTTPRequest.Path {
|
|
|
|
|
|
|
|
|
|
var url: URL? {
|
2022-06-14 15:12:46 -07:00
|
|
|
return URL(string: self.relativePath, relativeTo: SystemInfo.serverHostURL)
|
2022-03-04 13:42:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var relativePath: String {
|
|
|
|
|
return "\(Self.pathPrefix)/\(self.description)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static let pathPrefix: String = "/v1"
|
|
|
|
|
|
|
|
|
|
}
|
2022-03-08 12:56:13 -08:00
|
|
|
|
|
|
|
|
private extension Encodable {
|
|
|
|
|
|
|
|
|
|
func asData() throws -> Data {
|
2022-04-01 14:22:29 -07:00
|
|
|
return try JSONEncoder.default.encode(self)
|
2022-03-08 12:56:13 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2022-04-12 15:11:22 -07:00
|
|
|
|
2022-04-21 09:31:20 -07:00
|
|
|
private extension NetworkError {
|
|
|
|
|
|
|
|
|
|
/// Creates a `NetworkError` from any request `Error`.
|
|
|
|
|
init(_ error: Error, dnsChecker: DNSCheckerType.Type) {
|
|
|
|
|
if dnsChecker.isBlockedAPIError(error),
|
|
|
|
|
let blockedError = dnsChecker.errorWithBlockedHostFromError(error) {
|
|
|
|
|
Logger.error(blockedError.description)
|
|
|
|
|
self = blockedError
|
|
|
|
|
} else {
|
|
|
|
|
let nsError = error as NSError
|
|
|
|
|
|
|
|
|
|
switch (nsError.domain, nsError.code) {
|
|
|
|
|
case (NSURLErrorDomain, NSURLErrorNotConnectedToInternet):
|
|
|
|
|
self = .offlineConnection()
|
|
|
|
|
default:
|
|
|
|
|
self = .networkError(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-12 15:11:22 -07:00
|
|
|
extension Result where Success == HTTPResponse<Data>, Failure == NetworkError {
|
|
|
|
|
|
|
|
|
|
// Converts an unsuccessful response into a `Result.failure`
|
|
|
|
|
fileprivate func convertUnsuccessfulResponseToError() -> Self {
|
|
|
|
|
return self.flatMap { response in
|
|
|
|
|
response.statusCode.isSuccessfulResponse
|
|
|
|
|
? .success(response)
|
|
|
|
|
: .failure(
|
|
|
|
|
.errorResponse(
|
|
|
|
|
ErrorResponse.from(response.body),
|
|
|
|
|
response.statusCode
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parses a `Result<HTTPResponse<Data>>` to `Result<HTTPResponse<Value>>`
|
|
|
|
|
func parseResponse<Value: HTTPResponseBody>() -> HTTPResponse<Value>.Result {
|
|
|
|
|
return self.flatMap { response in // Convert the `Result` type
|
|
|
|
|
Result<HTTPResponse<Value>, Error> { // Create a new `Result<Value>`
|
|
|
|
|
try response.mapBody { data in // Convert the body of `HTTPResponse<Data>` from `Data` -> `Value`
|
|
|
|
|
try Value.create(with: data) // Decode `Data` into `Value`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Convert decoding errors into `NetworkError.decoding`
|
|
|
|
|
.mapError { NetworkError.decoding($0, response.body) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension Result where Success == Data? {
|
|
|
|
|
|
|
|
|
|
/// Converts a `Result<Data?, Error>` into `Result<HTTPResponse<Data?>, Failure>`
|
|
|
|
|
func mapSuccessToOptionalHTTPResult(_ statusCode: HTTPStatusCode) -> Result<HTTPResponse<Data?>, Failure> {
|
|
|
|
|
return self.map { HTTPResponse(statusCode: statusCode, body: $0) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|