diff --git a/GoodNetworking-Sample/GoodNetworking-Sample.xcodeproj/xcshareddata/xcschemes/GoodNetworking-Sample.xcscheme b/GoodNetworking-Sample/GoodNetworking-Sample.xcodeproj/xcshareddata/xcschemes/GoodNetworking-Sample.xcscheme
new file mode 100644
index 0000000..8be713f
--- /dev/null
+++ b/GoodNetworking-Sample/GoodNetworking-Sample.xcodeproj/xcshareddata/xcschemes/GoodNetworking-Sample.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GoodNetworking-Sample/GoodNetworking-Sample/Managers/RequestManager/RequestManager.swift b/GoodNetworking-Sample/GoodNetworking-Sample/Managers/RequestManager/RequestManager.swift
index 2f5d3ad..8f492de 100644
--- a/GoodNetworking-Sample/GoodNetworking-Sample/Managers/RequestManager/RequestManager.swift
+++ b/GoodNetworking-Sample/GoodNetworking-Sample/Managers/RequestManager/RequestManager.swift
@@ -24,8 +24,7 @@ final class RequestManager: RequestManagerType {
}
func fetchHero(heroId: Int) -> RequestPublisher {
- return session.request(endpoint: Endpoint.hero(id: heroId))
- .goodify()
+ return session.execute(endpoint: Endpoint.hero(id: heroId))
.eraseToAnyPublisher()
}
diff --git a/GoodNetworking-Sample/GoodNetworking-Sample/Screens/Home/HomeViewModel.swift b/GoodNetworking-Sample/GoodNetworking-Sample/Screens/Home/HomeViewModel.swift
index 170dc40..ef1b614 100644
--- a/GoodNetworking-Sample/GoodNetworking-Sample/Screens/Home/HomeViewModel.swift
+++ b/GoodNetworking-Sample/GoodNetworking-Sample/Screens/Home/HomeViewModel.swift
@@ -49,13 +49,18 @@ final class HomeViewModel {
extension HomeViewModel {
func fetchHero() {
- di.requestManager.fetchHero(heroId: Int.random(in: Constants.Hero.range))
- .map{ HeroFetchingResultState.success($0) }
- .catch { Just(HeroFetchingResultState.error($0)) }
- .prepend(HeroFetchingResultState.loading)
- .eraseToAnyPublisher()
- .sink { [weak self] result in self?.heroResult.send(result) }
- .store(in: &cancellables)
+// di.requestManager.fetchHero(heroId: Int.random(in: Constants.Hero.range))
+ let heroID = Int.random(in: Constants.Hero.range)
+ return Publishers.Merge(
+ di.requestManager.fetchHero(heroId: heroID),
+ di.requestManager.fetchHero(heroId: heroID)
+ )
+ .map{ HeroFetchingResultState.success($0) }
+ .catch { Just(HeroFetchingResultState.error($0)) }
+ .prepend(HeroFetchingResultState.loading)
+ .eraseToAnyPublisher()
+ .sink { [weak self] result in self?.heroResult.send(result) }
+ .store(in: &cancellables)
}
func goToAbout() {
diff --git a/Sources/GoodNetworking/Executor/ExecutorTask.swift b/Sources/GoodNetworking/Executor/ExecutorTask.swift
new file mode 100644
index 0000000..546aa00
--- /dev/null
+++ b/Sources/GoodNetworking/Executor/ExecutorTask.swift
@@ -0,0 +1,30 @@
+//
+// ExecutorTask.swift
+//
+//
+// Created by Matus Klasovity on 06/08/2024.
+//
+
+import Foundation
+import Alamofire
+
+final class ExecutorTask {
+
+ var finishDate: Date?
+ let taskID: String
+ let task: Task, Never>
+
+ private let cacheTimeout: TimeInterval
+
+ var exceedsTimeout: Bool {
+ guard let finishDate else { return false }
+ return Date().timeIntervalSince(finishDate) > cacheTimeout
+ }
+
+ init(taskID: String, task: Task, Never>, cacheTimeout: TimeInterval) {
+ self.taskID = taskID
+ self.task = task
+ self.cacheTimeout = cacheTimeout
+ }
+
+}
diff --git a/Sources/GoodNetworking/Executor/RequestExecutor.swift b/Sources/GoodNetworking/Executor/RequestExecutor.swift
new file mode 100644
index 0000000..4f8a73c
--- /dev/null
+++ b/Sources/GoodNetworking/Executor/RequestExecutor.swift
@@ -0,0 +1,96 @@
+//
+// RequestExecutor.swift
+//
+//
+// Created by Matus Klasovity on 06/08/2024.
+//
+
+import Foundation
+import Alamofire
+
+actor RequestExecutor {
+
+ private let logger: SessionLogger = {
+ if #available(iOS 14, *) {
+ return OSLogLogger()
+ } else {
+ return PrintLogger()
+ }
+ }()
+
+ private var runningRequestTasks: [String: ExecutorTask] = [:]
+
+ func execute(
+ _ request: DataRequest,
+ taskID: String,
+ deduplicate: Bool,
+ validResponseCodes: Set,
+ emptyResponseCodes: Set,
+ emptyResponseMethods: Set,
+ cacheTimeout: TimeInterval
+ ) async -> DataResponse {
+ let randomUUID = UUID().uuidString
+ return await execute(
+ request,
+ taskID: deduplicate ? taskID : randomUUID,
+ validResponseCodes: validResponseCodes,
+ emptyResponseCodes: emptyResponseCodes,
+ emptyResponseMethods: emptyResponseMethods,
+ cacheTimeout: cacheTimeout
+ )
+ }
+
+ private func execute(
+ _ request: DataRequest,
+ taskID: String,
+ validResponseCodes: Set,
+ emptyResponseCodes: Set,
+ emptyResponseMethods: Set,
+ cacheTimeout: TimeInterval
+ ) async -> DataResponse {
+ runningRequestTasks = runningRequestTasks.filter { !$0.value.exceedsTimeout }
+
+ if let runningTask = runningRequestTasks[taskID] {
+ logger.log(level: .info, message: "🚀 taskID: \(taskID) Cached value used")
+ return await runningTask.task.value.map { $0 as! SuccessType }
+ } else {
+ let requestTask = Task, Never> {
+ let result: DataResponse = await request.goodifyAsync(
+ validResponseCodes: validResponseCodes,
+ emptyResponseCodes: emptyResponseCodes,
+ emptyResponseMethods: emptyResponseMethods
+ )
+
+ return result.map { $0 as Decodable }
+ }
+
+ logger.log(level: .info, message: "🚀 taskID: \(taskID): Task created")
+ let executorTask: ExecutorTask = ExecutorTask(
+ taskID: taskID,
+ task: requestTask,
+ cacheTimeout: cacheTimeout
+ )
+
+ runningRequestTasks[taskID] = executorTask
+
+ let dataResponse = await requestTask.value
+ switch dataResponse.result {
+ case .success:
+ logger.log(level: .info, message: "🚀 taskID: \(taskID): Task finished successfully")
+ if cacheTimeout > 0 {
+ runningRequestTasks[taskID]?.finishDate = Date()
+ } else {
+ runningRequestTasks[taskID] = nil
+ }
+
+ case .failure:
+ logger.log(level: .error, message: "🚀 taskID: \(taskID): Task finished with error")
+ runningRequestTasks[taskID] = nil
+ }
+
+ return dataResponse.map { $0 as! SuccessType }
+ }
+ }
+
+}
+
diff --git a/Sources/GoodNetworking/Extensions/DataRequestExtensions.swift b/Sources/GoodNetworking/Extensions/DataRequestExtensions.swift
index 34a4f69..b90c1c2 100644
--- a/Sources/GoodNetworking/Extensions/DataRequestExtensions.swift
+++ b/Sources/GoodNetworking/Extensions/DataRequestExtensions.swift
@@ -81,6 +81,81 @@ public extension DataRequest {
.value()
}
+ /// Returns a `T` response or thrown an error for this instance and uses a ``serializingDecodable`` method to serialize the
+ /// response.
+ ///
+ /// - Parameters:
+ /// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by default.
+ /// - validResponseCodes: `Set` of acceptable HTTP status code in the default acceptable range of 200…299.
+ /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by
+ /// default.
+ /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of
+ /// status code. `[.head]` by default.
+ /// - decoder: `JSONDecoder` instance used to decode response `Data`. For `Decodable` `JSONDecoder()` by default.
+ /// For `Decodable & WithCustomDecoder` custom `decoder` used by default.
+ ///
+ /// - Returns: The `DataResponsePublisher`.
+ func goodifyAsync(
+ type: T.Type = T.self,
+ validResponseCodes: Set = Set(200..<299),
+ emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes,
+ emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods,
+ decoder: JSONDecoder = (T.self as? WithCustomDecoder.Type)?.decoder ?? JSONDecoder()
+ ) async throws -> T {
+ do {
+ return try await self
+ .validate(statusCode: validResponseCodes)
+ .serializingDecodable(
+ T.self,
+ automaticallyCancelling: true,
+ decoder: decoder,
+ emptyResponseCodes: emptyResponseCodes,
+ emptyRequestMethods: emptyResponseMethods
+ )
+ .value
+ } catch let afError as AFError {
+ // potentionally the place for alamofire non fatal crashes logger
+ throw afError
+ } catch {
+ throw error
+ }
+ }
+
+
+ /// Returns a `DataResponse` for this instance and uses a ``serializingDecodable`` method to serialize the
+ /// response.
+ ///
+ /// - Parameters:
+ /// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by default.
+ /// - validResponseCodes: `Set` of acceptable HTTP status code in the default acceptable range of 200…299.
+ /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by
+ /// default.
+ /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of
+ /// status code. `[.head]` by default.
+ /// - decoder: `JSONDecoder` instance used to decode response `Data`. For `Decodable` `JSONDecoder()` by default.
+ /// For `Decodable & WithCustomDecoder` custom `decoder` used by default.
+ ///
+ /// - Returns: The `DataResponsePublisher`.
+ func goodifyAsync(
+ type: T.Type = T.self,
+ validResponseCodes: Set = Set(200..<299),
+ emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes,
+ emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods,
+ decoder: JSONDecoder = (T.self as? WithCustomDecoder.Type)?.decoder ?? JSONDecoder()
+ ) async -> DataResponse {
+ await self
+ .validate(statusCode: validResponseCodes)
+ .serializingDecodable(
+ T.self,
+ automaticallyCancelling: true,
+ decoder: decoder,
+ emptyResponseCodes: emptyResponseCodes,
+ emptyRequestMethods: emptyResponseMethods
+ )
+ .response
+ }
+
+
}
// MARK: - Private
diff --git a/Sources/GoodNetworking/Extensions/FutureExtensions.swift b/Sources/GoodNetworking/Extensions/FutureExtensions.swift
new file mode 100644
index 0000000..344e472
--- /dev/null
+++ b/Sources/GoodNetworking/Extensions/FutureExtensions.swift
@@ -0,0 +1,47 @@
+//
+// FutureExtensions.swift
+//
+//
+// Created by Matus Klasovity on 06/08/2024.
+//
+
+import Foundation
+import Combine
+
+extension Future where Failure == Error {
+
+ static func create(asyncThrowableFunc: @Sendable @escaping () async throws -> Output) -> Self {
+ Self.init { promise in
+ nonisolated(unsafe) let promise = promise
+ Task {
+ do {
+ let result = try await asyncThrowableFunc()
+ await MainActor.run {
+ promise(.success(result))
+ }
+ } catch {
+ await MainActor.run {
+ promise(.failure(error))
+ }
+ }
+ }
+ }
+ }
+
+}
+
+extension Future where Failure == Never {
+
+ static func create(asyncFunc: @Sendable @escaping () async -> Output) -> Self {
+ Self.init { promise in
+ nonisolated(unsafe) let promise = promise
+ Task {
+ let result = await asyncFunc()
+ await MainActor.run {
+ promise(.success(result))
+ }
+ }
+ }
+ }
+
+}
diff --git a/Sources/GoodNetworking/Session/NetworkSession.swift b/Sources/GoodNetworking/Session/NetworkSession.swift
index d050216..e68cdfb 100644
--- a/Sources/GoodNetworking/Session/NetworkSession.swift
+++ b/Sources/GoodNetworking/Session/NetworkSession.swift
@@ -7,6 +7,7 @@
import Alamofire
import Foundation
+import Combine
/// Executes network requests for the client app.
public class NetworkSession {
@@ -22,6 +23,8 @@ public class NetworkSession {
public let baseUrl: String?
+ private let requestExecutor = RequestExecutor()
+
// MARK: - Initialization
/// A public initializer that sets the baseURL and configuration properties, and initializes the underlying `Session` object.
@@ -42,16 +45,17 @@ public class NetworkSession {
}
-// MARK: - Request
+// MARK: - Build Request
public extension NetworkSession {
/// Builds a DataRequest object by constructing URL and Body parameters.
///
/// - Parameters:
- /// - endpoint: A GREndpoint instance representing the endpoint.
+ /// - endpoint: A Endpoint instance representing the endpoint.
/// - base: An optional BaseURL instance representing the base URL. If not provided, the default `baseUrl` property will be used.
/// - Returns: A DataRequest object that is ready to be executed.
+ @available(*, deprecated, renamed: "buildRequest", message: "Request method is deprecated, use buildRequest instead.")
func request(endpoint: Endpoint, base: String? = nil) -> DataRequest {
let baseUrl = base ?? baseUrl ?? ""
@@ -64,6 +68,318 @@ public extension NetworkSession {
)
}
+ /// Builds a DataRequest object by constructing URL and Body parameters.
+ ///
+ /// - Parameters:
+ /// - endpoint: A Endpoint instance representing the endpoint.
+ /// - base: An optional BaseURL instance representing the base URL. If not provided, the default `baseUrl` property will be used.
+ /// - Returns: A DataRequest object that is ready to be executed.
+ func buildRequest(endpoint: Endpoint, base: String? = nil) -> DataRequest {
+ let baseUrl = base ?? baseUrl ?? ""
+
+ return session.request(
+ try? endpoint.url(on: baseUrl),
+ method: endpoint.method,
+ parameters: endpoint.parameters?.dictionary,
+ encoding: endpoint.encoding,
+ headers: endpoint.headers
+ )
+ }
+
+}
+
+// MARK: - Execute request - Async
+
+public extension NetworkSession {
+
+ /// Executes a data request and returns a `DataResponse` containing the result of the request.
+ ///
+ /// This method allows you to specify various parameters to control the request execution,
+ /// including deduplication, valid response codes, and empty response handling.
+ ///
+ /// - Parameters:
+ /// - request: The `DataRequest` to be executed.
+ /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`.
+ /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299.
+ /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`.
+ /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`.
+ /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache).
+ /// - Returns: A `DataResponse` containing the result of the request, which includes either the decoded result of type `ResultType` or an `AFError`.
+ func execute(
+ request: DataRequest,
+ deduplicate: Bool = true,
+ validResponseCodes: Set = Set(200..<300),
+ emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes,
+ emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods,
+ cacheTimeout: TimeInterval = 0
+ ) async -> DataResponse {
+ let taskID = request.convertible.urlRequest?.url?.absoluteString ?? UUID().uuidString
+
+ return await requestExecutor.execute(
+ request,
+ taskID: taskID,
+ deduplicate: deduplicate,
+ validResponseCodes: validResponseCodes,
+ emptyResponseCodes: emptyResponseCodes,
+ emptyResponseMethods: emptyResponseMethods,
+ cacheTimeout: cacheTimeout
+ )
+ }
+
+ /// Executes a request to the specified endpoint and returns a `DataResponse` containing the result.
+ ///
+ /// This method allows you to specify various parameters to control the request execution,
+ /// including the base URL, deduplication, valid response codes, and empty response handling.
+ ///
+ /// - Parameters:
+ /// - endpoint: The `Endpoint` representing the API endpoint to be requested.
+ /// - base: An optional base URL to be used for the request. If not provided, the default base URL is used.
+ /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`.
+ /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299.
+ /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`.
+ /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`.
+ /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache).
+ /// - Returns: A `DataResponse` containing the result of the request, which includes either the decoded result of type `ResultType` or an `AFError`.
+ func execute(
+ endpoint: Endpoint,
+ base: String? = nil,
+ deduplicate: Bool = true,
+ validResponseCodes: Set = Set(200..<300),
+ emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes,
+ emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods,
+ cacheTimeout: TimeInterval = 0
+ ) async -> DataResponse {
+ let baseUrl = base ?? baseUrl ?? ""
+ let taskID = (try? endpoint.url(on: baseUrl).absoluteString) ?? UUID().uuidString
+ let request = self.buildRequest(endpoint: endpoint, base: base)
+
+ return await requestExecutor.execute(
+ request,
+ taskID: taskID,
+ deduplicate: deduplicate,
+ validResponseCodes: validResponseCodes,
+ emptyResponseCodes: emptyResponseCodes,
+ emptyResponseMethods: emptyResponseMethods,
+ cacheTimeout: cacheTimeout
+ )
+ }
+
+}
+
+// MARK: - Execute Request - Publisher DataResponse
+
+public extension NetworkSession {
+
+ /// Executes a request to the specified endpoint and returns a `DataResponse` containing the result.
+ ///
+ /// This method allows you to specify various parameters to control the request execution,
+ /// including the base URL, deduplication, valid response codes, and empty response handling.
+ ///
+ /// - Parameters:
+ /// - endpoint: The `Endpoint` representing the API endpoint to be requested.
+ /// - base: An optional base URL to be used for the request. If not provided, the default base URL is used.
+ /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`.
+ /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299.
+ /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`.
+ /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`.
+ /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache).
+ /// - Returns: A Publisher of `DataResponse` containing the result of the request, which includes either the decoded result of type `ResultType` or an `AFError`.
+ func execute(
+ request: DataRequest,
+ deduplicate: Bool = true,
+ validResponseCodes: Set = Set(200..<300),
+ emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes,
+ emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods,
+ cacheTimeout: TimeInterval = 0
+ ) -> AnyPublisher, Never> {
+ let taskID = request.convertible.urlRequest?.url?.absoluteString ?? UUID().uuidString
+
+ return Future.create { [weak self] in
+ guard let self else {
+ return DataResponse(
+ request: nil,
+ response: nil,
+ data: nil,
+ metrics: nil,
+ serializationDuration: .nan,
+ result: .failure(AFError.sessionDeinitialized)
+ )
+ }
+
+ return await requestExecutor.execute(
+ request,
+ taskID: taskID,
+ deduplicate: deduplicate,
+ validResponseCodes: validResponseCodes,
+ emptyResponseCodes: emptyResponseCodes,
+ emptyResponseMethods: emptyResponseMethods,
+ cacheTimeout: cacheTimeout
+ )
+ }
+ .eraseToAnyPublisher()
+ }
+
+ /// Executes a request to the specified endpoint and returns a `DataResponse` containing the result.
+ ///
+ /// This method allows you to specify various parameters to control the request execution,
+ /// including the base URL, deduplication, valid response codes, and empty response handling.
+ ///
+ /// - Parameters:
+ /// - endpoint: The `Endpoint` representing the API endpoint to be requested.
+ /// - base: An optional base URL to be used for the request. If not provided, the default base URL is used.
+ /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`.
+ /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299.
+ /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`.
+ /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`.
+ /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache).
+ /// - Returns: A Publisher of `DataResponse` containing the result of the request, which includes either the decoded result of type `ResultType` or an `AFError`.
+ func execute(
+ endpoint: Endpoint,
+ base: String? = nil,
+ deduplicate: Bool = true,
+ validResponseCodes: Set = Set(200..<300),
+ emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes,
+ emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods,
+ cacheTimeout: TimeInterval = 0
+ ) -> AnyPublisher, Never> {
+ let baseUrl = base ?? baseUrl ?? ""
+ let taskID = (try? endpoint.url(on: baseUrl).absoluteString) ?? UUID().uuidString
+ let request = self.buildRequest(endpoint: endpoint, base: base)
+
+ return Future.create { [weak self] in
+ guard let self else {
+ return DataResponse(
+ request: nil,
+ response: nil,
+ data: nil,
+ metrics: nil,
+ serializationDuration: .nan,
+ result: .failure(AFError.sessionDeinitialized)
+ )
+ }
+
+ return await requestExecutor.execute(
+ request,
+ taskID: taskID,
+ deduplicate: deduplicate,
+ validResponseCodes: validResponseCodes,
+ emptyResponseCodes: emptyResponseCodes,
+ emptyResponseMethods: emptyResponseMethods,
+ cacheTimeout: cacheTimeout
+ )
+ }
+ .eraseToAnyPublisher()
+ }
+
+}
+
+// MARK: - Execute Request - Publisher Response
+
+public extension NetworkSession {
+
+ /// Executes a request to the specified endpoint and returns a `DataResponse` containing the result.
+ ///
+ /// This method allows you to specify various parameters to control the request execution,
+ /// including the base URL, deduplication, valid response codes, and empty response handling.
+ ///
+ /// - Parameters:
+ /// - endpoint: The `Endpoint` representing the API endpoint to be requested.
+ /// - base: An optional base URL to be used for the request. If not provided, the default base URL is used.
+ /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`.
+ /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299.
+ /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`.
+ /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`.
+ /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache).
+ /// - Returns: A Publisher of `ResultType` or an `AFError`.
+ func execute(
+ endpoint: Endpoint,
+ base: String? = nil,
+ deduplicate: Bool = true,
+ validResponseCodes: Set = Set(200..<300),
+ emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes,
+ emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods,
+ cacheTimeout: TimeInterval = 0
+ ) -> AnyPublisher {
+ let baseUrl = base ?? baseUrl ?? ""
+ let taskID = (try? endpoint.url(on: baseUrl).absoluteString) ?? UUID().uuidString
+ let request = self.buildRequest(endpoint: endpoint, base: base)
+
+ return Future.create { [weak self] in
+ guard let self else {
+ throw AFError.sessionDeinitialized
+ }
+
+ let dataResponse: DataResponse = await requestExecutor.execute(
+ request,
+ taskID: taskID,
+ deduplicate: deduplicate,
+ validResponseCodes: validResponseCodes,
+ emptyResponseCodes: emptyResponseCodes,
+ emptyResponseMethods: emptyResponseMethods,
+ cacheTimeout: cacheTimeout
+ )
+
+ switch dataResponse.result {
+ case .success(let success):
+ return success
+ case .failure(let failure):
+ throw failure
+ }
+ }
+ .mapError { $0.asAFError(orFailWith: "") }
+ .eraseToAnyPublisher()
+ }
+
+ /// Executes a request to the specified endpoint and returns a `DataResponse` containing the result.
+ ///
+ /// This method allows you to specify various parameters to control the request execution,
+ /// including the base URL, deduplication, valid response codes, and empty response handling.
+ ///
+ /// - Parameters:
+ /// - endpoint: The `Endpoint` representing the API endpoint to be requested.
+ /// - base: An optional base URL to be used for the request. If not provided, the default base URL is used.
+ /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`.
+ /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299.
+ /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`.
+ /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`.
+ /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache).
+ /// - Returns: A Publisher of `ResultType` or an `AFError`.
+ func execute(
+ request: DataRequest,
+ deduplicate: Bool = true,
+ validResponseCodes: Set = Set(200..<300),
+ emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes,
+ emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods,
+ cacheTimeout: TimeInterval = 0
+ ) -> AnyPublisher {
+ let taskID = request.convertible.urlRequest?.url?.absoluteString ?? UUID().uuidString
+
+ return Future.create { [weak self] in
+ guard let self else {
+ throw AFError.sessionDeinitialized
+ }
+
+ let dataResponse: DataResponse = await requestExecutor.execute(
+ request,
+ taskID: taskID,
+ deduplicate: deduplicate,
+ validResponseCodes: validResponseCodes,
+ emptyResponseCodes: emptyResponseCodes,
+ emptyResponseMethods: emptyResponseMethods,
+ cacheTimeout: cacheTimeout
+ )
+
+ switch dataResponse.result {
+ case .success(let success):
+ return success
+ case .failure(let failure):
+ throw failure
+ }
+ }
+ .mapError { $0.asAFError(orFailWith: "") }
+ .eraseToAnyPublisher()
+ }
+
}
// MARK: - Download