Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/space-code/atomic", exact: "1.1.1"),
.package(url: "https://github.com/space-code/typhoon", exact: "1.4.0"),
.package(url: "https://github.com/space-code/typhoon", exact: "3.0.0"),
.package(url: "https://github.com/WeTransfer/Mocker", exact: "3.0.2"),
],
targets: [
Expand Down
2 changes: 1 addition & 1 deletion Package@swift-5.10.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/space-code/atomic", exact: "1.1.0"),
.package(url: "https://github.com/space-code/typhoon", exact: "1.4.0"),
.package(url: "https://github.com/space-code/typhoon", exact: "3.0.0"),
.package(url: "https://github.com/WeTransfer/Mocker", exact: "3.0.1"),
],
targets: [
Expand Down
2 changes: 1 addition & 1 deletion Package@swift-6.0.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/space-code/atomic", exact: "1.1.0"),
.package(url: "https://github.com/space-code/typhoon", exact: "1.4.0"),
.package(url: "https://github.com/space-code/typhoon", exact: "3.0.0"),
.package(url: "https://github.com/WeTransfer/Mocker", exact: "3.0.1"),
],
targets: [
Expand Down

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly {
private let jsonEncoder: JSONEncoder
/// A global evaluator to determine if a retry should be attempted based on the error.
/// This applies to all requests processed by this instance.
private let retryEvaluator: (@Sendable (Error) -> Bool)?
private let retryEvaluator: (@Sendable (Error) -> RetryAction)?

// MARK: Initialization

Expand All @@ -47,7 +47,7 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly {
delegate: RequestProcessorDelegate? = nil,
interceptor: IAuthenticationInterceptor? = nil,
jsonEncoder: JSONEncoder = JSONEncoder(),
retryEvaluator: (@Sendable (Error) -> Bool)? = nil
retryEvaluator: (@Sendable (Error) -> RetryAction)? = nil
) {
self.configure = configure
self.delegate = SafeRequestProcessorDelegate(delegate: delegate)
Expand All @@ -59,7 +59,7 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly {
case .none:
retryPolicyStrategy = nil
case .default:
retryPolicyStrategy = .constant(retry: 5, duration: .seconds(1))
retryPolicyStrategy = .constant(retry: 5, dispatchDuration: .seconds(1))
case let .custom(strategy):
retryPolicyStrategy = strategy
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ public struct Response<T> {
}
}

// MARK: @unchecked Sendable
// MARK: Sendable

extension Response: @unchecked Sendable where T: Sendable {}
extension Response: Sendable where T: Sendable {}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,25 @@ public protocol IRequestProcessor {
strategy: RetryPolicyStrategy?,
delegate: URLSessionDelegate?,
configure: (@Sendable (inout URLRequest) throws -> Void)?,
shouldRetry: (@Sendable (Error) -> Bool)?
shouldRetry: (@Sendable (Error) -> RetryAction)?
) async throws -> Response<M>

/// Sends a network request and returns the result along with retry information.
///
/// - Parameters:
/// - request: The request object conforming to the `IRequest` protocol.
/// - strategy: An optional override for the retry policy strategy.
/// - delegate: An optional `URLSessionDelegate`.
/// - configure: An optional closure to modify the `URLRequest`.
/// - shouldRetry: An optional closure to determine if a retry should be attempted.
/// - Returns: A retry result containing the response.
func sendWithResult<M: Decodable>(
_ request: some IRequest,
strategy: RetryPolicyStrategy?,
delegate: URLSessionDelegate?,
configure: (@Sendable (inout URLRequest) throws -> Void)?,
shouldRetry: (@Sendable (Error) -> RetryAction)?
) async throws -> RetryResult<Response<M>>
}

extension IRequestProcessor {
Expand All @@ -46,4 +63,23 @@ extension IRequestProcessor {
func send<M: Decodable>(_ request: some IRequest) async throws -> Response<M> {
try await send(request, strategy: nil, delegate: nil, configure: nil, shouldRetry: nil)
}

/// Sends a network request with result with default parameters.
///
/// - Parameters:
/// - request: The request object conforming to the `IRequest` protocol.
func sendWithResult<M: Decodable>(
_ request: some IRequest,
strategy: RetryPolicyStrategy?
) async throws -> RetryResult<Response<M>> {
try await sendWithResult(request, strategy: strategy, delegate: nil, configure: nil, shouldRetry: nil)
}

/// Sends a network request with result with default parameters.
///
/// - Parameters:
/// - request: The request object conforming to the `IRequest` protocol.
func sendWithResult<M: Decodable>(_ request: some IRequest) async throws -> RetryResult<Response<M>> {
try await sendWithResult(request, strategy: nil, delegate: nil, configure: nil, shouldRetry: nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//

import Foundation
import enum Typhoon.RetryAction
import enum Typhoon.RetryPolicyStrategy

// MARK: - INetworkLayerAssembly
Expand All @@ -28,7 +29,7 @@ public protocol INetworkLayerAssembly {
delegate: RequestProcessorDelegate?,
interceptor: IAuthenticationInterceptor?,
jsonEncoder: JSONEncoder,
retryEvaluator: (@Sendable (Error) -> Bool)?
retryEvaluator: (@Sendable (Error) -> RetryAction)?
)

/// Construct and link all internal components to create a request processor.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extension RequestProcessor {
queryFormatter: QueryParametersFormatter()
),
dataRequestHandler: DataRequestHandler(),
retryPolicyService: RetryPolicyService(strategy: .constant(retry: 1, duration: .seconds(0))),
retryPolicyService: RetryPolicyService(strategy: .constant(retry: 1, dispatchDuration: .seconds(0))),
delegate: SafeRequestProcessorDelegate(delegate: requestProcessorDelegate),
interceptor: interceptor,
retryEvaluator: { _ in true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,28 @@ final class AuthentificatorInterceptorMock: IAuthenticationInterceptor, @uncheck
var invokedRefreshCount = 0
var invokedRefreshParameters: (request: URLRequest, response: HTTPURLResponse, session: URLSession)?
var invokedRefreshParametersList = [(request: URLRequest, response: HTTPURLResponse, session: URLSession)]()
var refreshClosure: (() async throws -> Void)?

func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) {
func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) async throws {
invokedRefresh = true
invokedRefreshCount += 1
invokedRefreshParameters = (request, response, session)
invokedRefreshParametersList.append((request, response, session))
try await refreshClosure?()
}

var invokedIsRequireRefresh = false
var invokedIsRequireRefreshCount = 0
var invokedIsRequireRefreshParameters: (request: URLRequest, response: HTTPURLResponse)?
var invokedIsRequireRefreshParametersList = [(request: URLRequest, response: HTTPURLResponse)]()
var stubbedIsRequireRefreshResult: Bool! = false
var isRequireRefreshClosure: (() -> Bool)?

func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool {
invokedIsRequireRefresh = true
invokedIsRequireRefreshCount += 1
invokedIsRequireRefreshParameters = (request, response)
invokedIsRequireRefreshParametersList.append((request, response))
return stubbedIsRequireRefreshResult
return isRequireRefreshClosure?() ?? stubbedIsRequireRefreshResult
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ final class DataRequestHandlerMock: NSObject, IDataRequestHandler, @unchecked Se
var invokedStartDataTaskParametersList = [(task: URLSessionDataTask, delegate: URLSessionDelegate?)]()
var stubbedStartDataTask: Response<Data>!
var startDataTaskThrowError: Error?
var startDataTaskClosure: ((URLSessionDataTask, URLSessionDelegate?) async throws -> Response<Data>)?

func startDataTask(
_ task: URLSessionDataTask,
Expand All @@ -39,6 +40,11 @@ final class DataRequestHandlerMock: NSObject, IDataRequestHandler, @unchecked Se
invokedStartDataTaskCount += 1
invokedStartDataTaskParameters = (task, delegate)
invokedStartDataTaskParametersList.append((task, delegate))

if let startDataTaskClosure {
return try await startDataTaskClosure(task, delegate)
}

if let error = startDataTaskThrowError {
throw error
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,23 @@ final class RequestProcessorAuthenicationTests: XCTestCase {
try await test_failAuthentication(
adaptError: URLError(.unknown),
refreshError: nil,
expectedError: RetryPolicyError.retryLimitExceeded
expectedUnderlyingError: URLError(.unknown)
)
}

func test_thatRequestProcessorThrowsAnError_whenInterceptorRefreshDidFail() async throws {
try await test_failAuthentication(
adaptError: nil,
refreshError: URLError(.unknown),
expectedError: RetryPolicyError.retryLimitExceeded
expectedUnderlyingError: URLError(.unknown)
)
}

// MARK: Private

private func test_failAuthentication(adaptError: Error?, refreshError: Error?, expectedError: Error) async throws {
private func test_failAuthentication(
adaptError: Error?,
refreshError: Error?,
expectedUnderlyingError: Error
) async throws {
class FailInterceptor: IAuthenticationInterceptor, @unchecked Sendable {
let adaptError: Error?
let refreshError: Error?
Expand Down Expand Up @@ -105,17 +107,25 @@ final class RequestProcessorAuthenicationTests: XCTestCase {
// given
let interceptor = FailInterceptor(adaptError: adaptError, refreshError: refreshError)
let sut = RequestProcessor.mock(interceptor: interceptor)

let request = makeRequest(.user)

DynamicStubs.register(stubs: [.user], statusCode: 200)

// when
var thrownError: Error?
do {
let _: Response<User> = try await sut.send(request)
} catch {
XCTAssertEqual(error as NSError, expectedError as NSError)
thrownError = error
}

// then
guard case let .retryLimitExceeded(errors) = thrownError as? RetryPolicyError else {
XCTFail("Expected RetryPolicyError.retryLimitExceeded, got \(String(describing: thrownError))")
return
}
XCTAssertFalse(errors.isEmpty, "Collected errors should not be empty")
XCTAssertEqual(errors.last as? NSError, expectedUnderlyingError as NSError)
}

private func makeRequest(_ path: String) -> IRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ final class RequestProcessorRequestTests: XCTestCase {
XCTAssertNotNil(user.data.avatarUrl)
}

func test_thatRequestProcessorThrowsRretryLimitExceededError_whenRequestDidFail() async {
func test_thatRequestProcessorThrowsRetryLimitExceededError_whenRequestDidFail() async {
// given
DynamicStubs.register(stubs: [.user], statusCode: 500)

Expand All @@ -39,11 +39,19 @@ final class RequestProcessorRequestTests: XCTestCase {
let request = makeRequest(.user)

// when
var thrownError: Error?
do {
let _: Response<User> = try await sut.send(request)
} catch {
XCTAssertEqual(error as NSError, RetryPolicyError.retryLimitExceeded as NSError)
thrownError = error
}

// then
guard case let .retryLimitExceeded(errors) = thrownError as? RetryPolicyError else {
XCTFail("Expected RetryPolicyError.retryLimitExceeded, got \(String(describing: thrownError))")
return
}
XCTAssertFalse(errors.isEmpty, "Collected errors should not be empty")
}

func test_thatRequestProcessorConfigureARequest() async throws {
Expand Down
Loading
Loading