From 3120bbb5d618b969a4c3575ccd0113c0aa547355 Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Wed, 6 Sep 2023 14:47:02 +0100 Subject: [PATCH 01/15] Clean up async aweit --- .../contents.xcworkspacedata | 7 + Package.swift | 8 +- Source/ContainerNetworkTask.swift | 72 ----- Source/NetworkAccess.swift | 2 +- Source/NetworkError.swift | 6 +- Source/NetworkResponseProcessor.swift | 95 ------ Source/NetworkService+Async.swift | 77 ----- Source/NetworkService+ResourceWithError.swift | 120 ++----- Source/NetworkService+Result.swift | 82 ----- Source/NetworkService.swift | 30 +- Source/NetworkServiceMock.swift | 205 ------------ .../BasicNetworkService.swift | 37 ++- .../ModifyRequestNetworkService.swift | 5 +- .../NetworkServices/NetworkServiceMock.swift | 184 +++++++++++ .../RetryNetworkService.swift | 90 ++---- Source/NetworkTask.swift | 44 --- Source/NetworkTaskMock.swift | 54 ---- Source/URLSession+NetworkAccess.swift | 18 +- Source/URLSessionDataTask+NetworkTask.swift | 27 -- Tests/ContainerNetworkTaskTest.swift | 49 --- Tests/ModifyRequestNetworkService.swift | 19 +- Tests/NetworkAccessMock.swift | 28 +- Tests/NetworkErrorTest.swift | 41 ++- Tests/NetworkResponseProcessorTest.swift | 111 ------- Tests/NetworkServiceMockTest.swift | 306 ++++-------------- Tests/NetworkServiceTest.swift | 268 +++------------ Tests/NetworkServiceWithErrorTest.swift | 151 +-------- Tests/NetworkTaskMockTests.swift | 61 ---- Tests/RetryNetworkserviceTest.swift | 160 +++++---- Tests/TrainModel.swift | 2 +- Tests/URLSession+NetworkAccessTest.swift | 80 ----- Tests/URLSessionMock.swift | 55 ---- 32 files changed, 520 insertions(+), 1974 deletions(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata delete mode 100644 Source/ContainerNetworkTask.swift delete mode 100644 Source/NetworkResponseProcessor.swift delete mode 100644 Source/NetworkService+Async.swift delete mode 100644 Source/NetworkService+Result.swift delete mode 100644 Source/NetworkServiceMock.swift rename Source/{ => NetworkServices}/BasicNetworkService.swift (76%) rename Source/{ => NetworkServices}/ModifyRequestNetworkService.swift (90%) create mode 100644 Source/NetworkServices/NetworkServiceMock.swift rename Source/{ => NetworkServices}/RetryNetworkService.swift (53%) delete mode 100644 Source/NetworkTask.swift delete mode 100644 Source/NetworkTaskMock.swift delete mode 100644 Source/URLSessionDataTask+NetworkTask.swift delete mode 100644 Tests/ContainerNetworkTaskTest.swift delete mode 100644 Tests/NetworkResponseProcessorTest.swift delete mode 100644 Tests/NetworkTaskMockTests.swift delete mode 100644 Tests/URLSession+NetworkAccessTest.swift delete mode 100644 Tests/URLSessionMock.swift diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Package.swift b/Package.swift index edc0cc5..5bff3ff 100644 --- a/Package.swift +++ b/Package.swift @@ -30,10 +30,10 @@ import PackageDescription let package = Package( name: "DBNetworkStack", platforms: [ - .iOS(.v9), - .tvOS(.v9), - .watchOS(.v2), - .macOS(.v10_10) + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6), + .macOS(.v10_15) ], products: [ .library( diff --git a/Source/ContainerNetworkTask.swift b/Source/ContainerNetworkTask.swift deleted file mode 100644 index 59b4eef..0000000 --- a/Source/ContainerNetworkTask.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// Copyright (C) 2018 DB Systel GmbH. -// DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -// - -import Foundation - -/// A task which contains another task which can be updated in fligh. -/// Use this task to compose a chain of requests during the original request. -/// An oAuth Flow would be a good example for this. -/// -/// - Note: Take look at `RetryNetworkService` to see how to use it in detail. -public final class ContainerNetworkTask: NetworkTask { - - // MARK: - Init - - /// Creates a `ContainerNetworkTask` instance. - public init() { } - - // MARK: - Override - - // MARK: - Protocol NetworkTask - - /** - Resumes a task. - */ - public func resume() { - underlyingTask?.resume() - } - - /** - Cancels the underlying task. - */ - public func cancel() { - isCanceled = true - underlyingTask?.cancel() - } - - /** - Suspends a task. - */ - public func suspend() { - underlyingTask?.suspend() - } - - // MARK: - Public - - /// The underlying task - public var underlyingTask: NetworkTask? - - /// Indicates if the request has been canceled. - /// When composing multiple requests this flag must be respected. - public private(set) var isCanceled = false -} diff --git a/Source/NetworkAccess.swift b/Source/NetworkAccess.swift index 7f8df17..c61f12d 100644 --- a/Source/NetworkAccess.swift +++ b/Source/NetworkAccess.swift @@ -32,6 +32,6 @@ public protocol NetworkAccess { /// - request: The request one wants to fetch. /// - callback: Callback which gets called when the request finishes. /// - Returns: the running network task - func load(request: URLRequest, callback: @escaping (Data?, HTTPURLResponse?, Error?) -> Void) -> NetworkTask + func load(request: URLRequest) async throws -> (Data, URLResponse) } diff --git a/Source/NetworkError.swift b/Source/NetworkError.swift index eb48cb8..3544a54 100644 --- a/Source/NetworkError.swift +++ b/Source/NetworkError.swift @@ -40,11 +40,7 @@ public enum NetworkError: Error { /// Complete request failed. case requestError(error: Error) - public init?(response: HTTPURLResponse?, data: Data?) { - guard let response = response else { - return nil - } - + public init?(response: HTTPURLResponse, data: Data) { switch response.statusCode { case 200..<300: return nil case 401: diff --git a/Source/NetworkResponseProcessor.swift b/Source/NetworkResponseProcessor.swift deleted file mode 100644 index 998be4a..0000000 --- a/Source/NetworkResponseProcessor.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// Copyright (C) 2017 DB Systel GmbH. -// DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -// - -import Foundation -import Dispatch - -final class NetworkResponseProcessor { - /** - Processes the results of an HTTPRequest and parses the result the matching Model type of the given resource. - - Great error handling should be implemented here as well. - - - parameter response: response from the server. Could be nil - - parameter resource: The resource matching the response. - - parameter data: Returned data. Could be nil. - - parameter error: the return error. Could be nil. - - - returns: the parsed model object. - */ - func process(response: HTTPURLResponse?, resource: Resource, data: Data?, error: Error?) throws -> Result { - if let error = error { - if case URLError.cancelled = error { - throw NetworkError.cancelled - } - - throw NetworkError.requestError(error: error) - } - if let responseError = NetworkError(response: response, data: data) { - throw responseError - } - guard let data = data else { - throw NetworkError.serverError(response: response, data: nil) - } - do { - return try resource.parse(data) - } catch let error { - throw NetworkError.serializationError(error: error, data: data) - } - } - - /// This parseses a `HTTPURLResponse` with a given resource into the result type of the resource or errors. - /// The result will be return via a blocks onCompletion/onError. - /// - /// - Parameters: - /// - queue: The `DispatchQueue` to execute the completion and error block on. - /// - response: the HTTPURLResponse one wants to parse. - /// - resource: the resource. - /// - data: the payload of the response. - /// - error: optional error from net network. - /// - onCompletion: completion block which gets called on the given `queue`. - /// - onError: error block which gets called on the given `queue`. - func processAsyncResponse(queue: DispatchQueue, response: HTTPURLResponse?, resource: Resource, data: Data?, - error: Error?, onCompletion: @escaping (Result, HTTPURLResponse) -> Void, onError: @escaping (NetworkError) -> Void) { - do { - let parsed = try process( - response: response, - resource: resource, - data: data, - error: error - ) - queue.async { - if let response = response { - onCompletion(parsed, response) - } else { - onError(NetworkError.unknownError) - } - } - } catch let genericError { - let dbNetworkError: NetworkError! = genericError as? NetworkError - queue.async { - return onError(dbNetworkError) - } - } - } -} diff --git a/Source/NetworkService+Async.swift b/Source/NetworkService+Async.swift deleted file mode 100644 index df9ad99..0000000 --- a/Source/NetworkService+Async.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// File.swift -// -// -// Created by Lukas Schmidt on 19.12.21. -// - -import Foundation - -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -public extension NetworkService { - - /** - Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. - - **Example**: - ```swift - let networkService: NetworkService = // - let resource: Resource = // - - let (result, response) = try await networkService.request(resource) - ``` - - - parameter resource: The resource you want to fetch. - - - returns: a touple containing the parsed result and the HTTP response - - Throws: A `NetworkError` - */ - @discardableResult - func request(_ resource: Resource) async throws -> (Result, HTTPURLResponse) { - var task: NetworkTask? - let cancel = { task?.cancel() } - return try await withTaskCancellationHandler(operation: { - try Task.checkCancellation() - return try await withCheckedThrowingContinuation({ coninuation in - task = request(resource: resource, onCompletionWithResponse: { - coninuation.resume(with: $0) - }) - }) - }, onCancel: { - cancel() - }) - } - - /** - Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. - - **Example**: - ```swift - let networkService: NetworkService = // - let resource: ResourceWithError = // - - let (result, response) = try await networkService.request(resource) - ``` - - - parameter resource: The resource you want to fetch. - - - returns: a touple containing the parsed result and the HTTP response - - Throws: Custom Error provided by ResourceWithError - */ - @discardableResult - func request(_ resource: ResourceWithError) async throws -> (Result, HTTPURLResponse) { - var task: NetworkTask? - let cancel = { task?.cancel() } - return try await withTaskCancellationHandler(operation: { - try Task.checkCancellation() - return try await withCheckedThrowingContinuation({ coninuation in - task = request(resource: resource, onCompletionWithResponse: { - coninuation.resume(with: $0) - }) - }) - }, onCancel: { - cancel() - }) - } - -} diff --git a/Source/NetworkService+ResourceWithError.swift b/Source/NetworkService+ResourceWithError.swift index 0842392..48b2acd 100644 --- a/Source/NetworkService+ResourceWithError.swift +++ b/Source/NetworkService+ResourceWithError.swift @@ -51,16 +51,12 @@ extension NetworkService { - returns: a running network task */ @discardableResult - public func request( - queue: DispatchQueue, - resource: ResourceWithError, - onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, - onError: @escaping (E) -> Void - ) -> NetworkTask { + public func requestResultWithResponse( + for resource: ResourceWithError + ) async -> Result<(Success, HTTPURLResponse), E> { let resourceWithoutError = Resource(request: resource.request, parse: resource.parse) - return request(queue: queue, resource: resourceWithoutError, onCompletionWithResponse: onCompletionWithResponse) { networkError in - onError(resource.mapError(networkError)) - } + return await self.requestResultWithResponse(for: resourceWithoutError) + .mapError(resource.mapError) } /** @@ -88,17 +84,13 @@ extension NetworkService { - returns: a running network task */ @discardableResult - public func request( - _ resource: ResourceWithError, - onCompletion: @escaping (Result) -> Void, - onError: @escaping (E) -> Void - ) -> NetworkTask { - return request( - queue: .main, - resource: resource, - onCompletionWithResponse: { model, _ in onCompletion(model) }, - onError: onError - ) + public func requestResult( + for resource: ResourceWithError + ) async -> Result { + let resourceWithoutError = Resource(request: resource.request, parse: resource.parse) + return await requestResultWithResponse(for: resourceWithoutError) + .mapError(resource.mapError) + .map({ $0.0 }) } /** @@ -127,86 +119,14 @@ extension NetworkService { - returns: a running network task */ @discardableResult - func request( - queue: DispatchQueue = .main, - resource: ResourceWithError, - onCompletionWithResponse: @escaping (Swift.Result<(Result, HTTPURLResponse), E>) -> Void - ) -> NetworkTask { - return request( - queue: queue, - resource: resource, - onCompletionWithResponse: { result, response in - onCompletionWithResponse(.success((result, response))) - }, onError: { error in - onCompletionWithResponse(.failure(error)) - } - ) - } - - /** - Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. - Execution happens on no specific queue. It dependes on the network access which queue is used. - Once execution is finished either the completion block or the error block gets called. - These blocks are called on the main queue. - - **Example**: - ```swift - let networkService: NetworkService = // - let resource: ResourceWithError = // - - networkService.request(resource, onCompletion: { htmlText in - print(htmlText) - }, onError: { error in - // Handle errors - }) - ``` - - - parameter resource: The resource you want to fetch. - - parameter onComplition: Callback which gets called when fetching and transforming into model succeeds. - - parameter onError: Callback which gets called with an custom error. - - - returns: a running network task - */ - @discardableResult - public func request( - _ resource: ResourceWithError, - onCompletion: @escaping (Swift.Result) -> Void - ) -> NetworkTask { - return request( - queue: .main, - resource: resource, - onCompletionWithResponse: { model, _ in onCompletion(.success(model)) }, - onError: { onCompletion(.failure($0))} - ) + public func request( + _ resource: ResourceWithError + ) async throws -> Success { + let resourceWithoutError = Resource(request: resource.request, parse: resource.parse) + return try await requestResultWithResponse(for: resourceWithoutError) + .mapError(resource.mapError) + .map({ $0.0 }) + .get() } - /** - Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. - Execution happens on no specific queue. It dependes on the network access which queue is used. - Once execution is finished either the completion block or the error block gets called. - These blocks are called on the main queue. - - **Example**: - ```swift - let networkService: NetworkService = // - let resource: Resource = // - - networkService.request(resource, onCompletionWithResponse: { htmlText, httpResponse in - print(htmlText, httpResponse) - }, onError: { error in - // Handle errors - }) - ``` - - - parameter resource: The resource you want to fetch. - - parameter onCompletion: Callback which gets called when fetching and transforming into model succeeds. - - parameter onError: Callback which gets called when fetching or transforming fails. - - - returns: a running network task - */ - @discardableResult - func request(_ resource: ResourceWithError, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, - onError: @escaping (E) -> Void) -> NetworkTask { - return request(queue: .main, resource: resource, onCompletionWithResponse: onCompletionWithResponse, onError: onError) - } } diff --git a/Source/NetworkService+Result.swift b/Source/NetworkService+Result.swift deleted file mode 100644 index f361c33..0000000 --- a/Source/NetworkService+Result.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// NetworkService+Result.swift -// DBNetworkStack -// -// Created by Lukas Schmidt on 03.01.19. -// Copyright © 2019 DBSystel. All rights reserved. -// - -import Foundation - -public extension NetworkService { - - /** - Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. - Execution happens on no specific queue. It dependes on the network access which queue is used. - Once execution is finished the completion block gets called. - You decide on which queue completion gets executed. Defaults to `main`. - - **Example**: - ```swift - let networkService: NetworkService = // - let resource: Resource = // - - networkService.request(resource: resource, onCompletionWithResponse: { result in - print(result) - }) - ``` - - - parameter queue: The `DispatchQueue` to execute the completion block on. Defaults to `main`. - - parameter resource: The resource you want to fetch. - - parameter onCompletionWithResponse: Callback which gets called when request completes. - - - returns: a running network task - */ - @discardableResult - func request(queue: DispatchQueue = .main, - resource: Resource, - onCompletionWithResponse: @escaping (Swift.Result<(Result, HTTPURLResponse), NetworkError>) -> Void) -> NetworkTask { - return request(queue: queue, - resource: resource, - onCompletionWithResponse: { result, response in - onCompletionWithResponse(.success((result, response))) - }, onError: { error in - onCompletionWithResponse(.failure(error)) - }) - } - - /** - Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. - Execution happens on no specific queue. It dependes on the network access which queue is used. - Once execution is finished the completion block gets called. - Completion gets executed on `main` queue. - - **Example**: - ```swift - let networkService: NetworkService = // - let resource: Resource = // - - networkService.request(resource, onCompletion: { result in - print(result) - }) - ``` - - - parameter resource: The resource you want to fetch. - - parameter onComplition: Callback which gets called when request completes. - - - returns: a running network task - */ - @discardableResult - func request(_ resource: Resource, - onCompletion: @escaping (Swift.Result) -> Void) -> NetworkTask { - return request(resource: resource, - onCompletionWithResponse: { result in - switch result { - case .success(let response): - onCompletion(.success(response.0)) - case .failure(let error): - onCompletion(.failure(error)) - } - }) - } -} diff --git a/Source/NetworkService.swift b/Source/NetworkService.swift index 3c3d379..8142879 100644 --- a/Source/NetworkService.swift +++ b/Source/NetworkService.swift @@ -57,11 +57,11 @@ public protocol NetworkService { - returns: a running network task */ @discardableResult - func request(queue: DispatchQueue, resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, - onError: @escaping (NetworkError) -> Void) -> NetworkTask + func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> } public extension NetworkService { + /** Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. Execution happens on no specific queue. It dependes on the network access which queue is used. @@ -73,52 +73,50 @@ public extension NetworkService { let networkService: NetworkService = // let resource: Resource = // - networkService.request(resource, onCompletion: { htmlText in - print(htmlText) + networkService.request(resource, onCompletionWithResponse: { htmlText, httpResponse in + print(htmlText, httpResponse) }, onError: { error in // Handle errors }) ``` - parameter resource: The resource you want to fetch. - - parameter onComplition: Callback which gets called when fetching and transforming into model succeeds. + - parameter onCompletion: Callback which gets called when fetching and transforming into model succeeds. - parameter onError: Callback which gets called when fetching or transforming fails. - returns: a running network task */ @discardableResult - func request(_ resource: Resource, onCompletion: @escaping (Result) -> Void, - onError: @escaping (NetworkError) -> Void) -> NetworkTask { - return request(queue: .main, resource: resource, onCompletionWithResponse: { model, _ in onCompletion(model) }, onError: onError) + func requestResult(for resource: Resource) async -> Result { + return await requestResultWithResponse(for: resource).map({ $0.0 }) } - + /** Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. Execution happens on no specific queue. It dependes on the network access which queue is used. Once execution is finished either the completion block or the error block gets called. These blocks are called on the main queue. - + **Example**: ```swift let networkService: NetworkService = // let resource: Resource = // - + networkService.request(resource, onCompletionWithResponse: { htmlText, httpResponse in print(htmlText, httpResponse) }, onError: { error in // Handle errors }) ``` - + - parameter resource: The resource you want to fetch. - parameter onCompletion: Callback which gets called when fetching and transforming into model succeeds. - parameter onError: Callback which gets called when fetching or transforming fails. - + - returns: a running network task */ @discardableResult - func request(_ resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, - onError: @escaping (NetworkError) -> Void) -> NetworkTask { - return request(queue: .main, resource: resource, onCompletionWithResponse: onCompletionWithResponse, onError: onError) + func request(_ resource: Resource) async throws -> Success { + return try await requestResultWithResponse(for: resource).get().0 } } diff --git a/Source/NetworkServiceMock.swift b/Source/NetworkServiceMock.swift deleted file mode 100644 index 11fe5ba..0000000 --- a/Source/NetworkServiceMock.swift +++ /dev/null @@ -1,205 +0,0 @@ -// -// Copyright (C) 2017 DB Systel GmbH. -// DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -// - -import Foundation -import Dispatch - -struct NetworkServiceMockCallback { - let onErrorCallback: (NetworkError) -> Void - let onTypedSuccess: (Any, HTTPURLResponse) throws -> Void - - init(resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, onError: @escaping (NetworkError) -> Void) { - onTypedSuccess = { anyResult, response in - guard let typedResult = anyResult as? Result else { - throw NetworkServiceMock.Error.typeMismatch - } - onCompletionWithResponse(typedResult, response) - } - onErrorCallback = { error in - onError(error) - } - } -} - -/** - Mocks a `NetworkService`. - You can configure expected results or errors to have a fully functional mock. - - **Example**: - ```swift - //Given - let networkServiceMock = NetworkServiceMock() - let resource: Resource = // - - //When - networkService.request( - resource, - onCompletion: { string in /*...*/ }, - onError: { error in /*...*/ } - ) - networkService.returnSuccess(with: "Sucess") - - //Then - //Test your expectations - - ``` - - It is possible to start multiple requests at a time. - All requests and responses (or errors) are processed - in order they have been called. So, everything is serial. - - **Example**: - ```swift - //Given - let networkServiceMock = NetworkServiceMock() - let resource: Resource = // - - //When - networkService.request( - resource, - onCompletion: { string in /* Success */ }, - onError: { error in /*...*/ } - ) - networkService.request( - resource, - onCompletion: { string in /*...*/ }, - onError: { error in /*. cancel error .*/ } - ) - - networkService.returnSuccess(with: "Sucess") - networkService.returnError(with: .cancelled) - - //Then - //Test your expectations - - ``` - - - seealso: `NetworkService` - */ -public final class NetworkServiceMock: NetworkService { - - public enum Error: Swift.Error, CustomDebugStringConvertible { - case missingRequest - case typeMismatch - - public var debugDescription: String { - switch self { - case .missingRequest: - return "Could not return because no request" - case .typeMismatch: - return "Return type does not match requested type" - } - } - } - - /// Count of all started requests - public var requestCount: Int { - return lastRequests.count - } - - /// Last executed request - public var lastRequest: URLRequest? { - return lastRequests.last - } - - public var pendingRequestCount: Int { - return callbacks.count - } - - /// All executed requests. - public private(set) var lastRequests: [URLRequest] = [] - - /// Set this to hava a custom networktask returned by the mock - public var nextNetworkTask: NetworkTask? - - private var callbacks: [NetworkServiceMockCallback] = [] - - /// Creates an instace of `NetworkServiceMock` - public init() {} - - /** - Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. - Execution happens on no specific queue. It dependes on the network access which queue is used. - Once execution is finished either the completion block or the error block gets called. - You decide on which queue these blocks get executed. - - **Example**: - ```swift - let networkService: NetworkService = // - let resource: Resource = // - - networkService.request(queue: .main, resource: resource, onCompletionWithResponse: { htmlText, response in - print(htmlText, response) - }, onError: { error in - // Handle errors - }) - ``` - - - parameter queue: The `DispatchQueue` to execute the completion and error block on. - - parameter resource: The resource you want to fetch. - - parameter onCompletionWithResponse: Callback which gets called when fetching and transforming into model succeeds. - - parameter onError: Callback which gets called when fetching or transforming fails. - - - returns: a running network task - */ - @discardableResult - public func request(queue: DispatchQueue, resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, - onError: @escaping (NetworkError) -> Void) -> NetworkTask { - lastRequests.append(resource.request) - callbacks.append(NetworkServiceMockCallback( - resource: resource, - onCompletionWithResponse: onCompletionWithResponse, - onError: onError - )) - - return nextNetworkTask ?? NetworkTaskMock() - } - - /// Will return an error to the current waiting request. - /// - /// - Parameters: - /// - error: the error which gets passed to the caller - /// - /// - Throws: An error of type `NetworkServiceMock.Error` - public func returnError(with error: NetworkError) throws { - guard !callbacks.isEmpty else { - throw Error.missingRequest - } - callbacks.removeFirst().onErrorCallback(error) - } - - /// Will return a successful request, by using the given type `T` as serialized result of a request. - /// - /// - Parameters: - /// - data: the mock response from the server. `Data()` by default - /// - httpResponse: the mock `HTTPURLResponse` from the server. `HTTPURLResponse()` by default - /// - /// - Throws: An error of type `NetworkServiceMock.Error` - public func returnSuccess(with serializedResponse: T, httpResponse: HTTPURLResponse = HTTPURLResponse()) throws { - guard !callbacks.isEmpty else { - throw Error.missingRequest - } - try callbacks.removeFirst().onTypedSuccess(serializedResponse, httpResponse) - } - -} diff --git a/Source/BasicNetworkService.swift b/Source/NetworkServices/BasicNetworkService.swift similarity index 76% rename from Source/BasicNetworkService.swift rename to Source/NetworkServices/BasicNetworkService.swift index 71ed881..be7bc2b 100644 --- a/Source/BasicNetworkService.swift +++ b/Source/NetworkServices/BasicNetworkService.swift @@ -36,9 +36,8 @@ import Dispatch - seealso: `NetworkService` */ public final class BasicNetworkService: NetworkService { - let networkAccess: NetworkAccess - let networkResponseProcessor: NetworkResponseProcessor - + private let networkAccess: NetworkAccess + /** Creates an `BasicNetworkService` instance with a given network access to execute requests on. @@ -46,7 +45,6 @@ public final class BasicNetworkService: NetworkService { */ public init(networkAccess: NetworkAccess) { self.networkAccess = networkAccess - self.networkResponseProcessor = NetworkResponseProcessor() } /** @@ -75,14 +73,29 @@ public final class BasicNetworkService: NetworkService { - returns: a running network task */ @discardableResult - public func request(queue: DispatchQueue, resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, - onError: @escaping (NetworkError) -> Void) -> NetworkTask { - let request = resource.request - let dataTask = networkAccess.load(request: request, callback: { data, response, error in - self.networkResponseProcessor.processAsyncResponse(queue: queue, response: response, resource: resource, data: data, - error: error, onCompletion: onCompletionWithResponse, onError: onError) - }) - return dataTask + public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { + do { + let (data, response) = try await networkAccess.load(request: resource.request) + guard let response = response as? HTTPURLResponse else { + return .failure(.unknownError) + } + if let responseError = NetworkError(response: response, data: data) { + return .failure(responseError) + } + + do { + return .success((try resource.parse(data), response)) + } catch let error { + return .failure(.serializationError(error: error, data: data)) + } + } catch let error { + if case URLError.cancelled = error { + return .failure(.cancelled) + } + + return .failure(.requestError(error: error)) + } + } } diff --git a/Source/ModifyRequestNetworkService.swift b/Source/NetworkServices/ModifyRequestNetworkService.swift similarity index 90% rename from Source/ModifyRequestNetworkService.swift rename to Source/NetworkServices/ModifyRequestNetworkService.swift index 3d3da04..198d027 100644 --- a/Source/ModifyRequestNetworkService.swift +++ b/Source/NetworkServices/ModifyRequestNetworkService.swift @@ -80,12 +80,11 @@ public final class ModifyRequestNetworkService: NetworkService { - returns: a running network task */ @discardableResult - public func request(queue: DispatchQueue, resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, - onError: @escaping (NetworkError) -> Void) -> NetworkTask { + public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { let request = requestModifications.reduce(resource.request, { request, modify in return modify(request) }) let newResource = Resource(request: request, parse: resource.parse) - return networkService.request(queue: queue, resource: newResource, onCompletionWithResponse: onCompletionWithResponse, onError: onError) + return await networkService.requestResultWithResponse(for: newResource) } } diff --git a/Source/NetworkServices/NetworkServiceMock.swift b/Source/NetworkServices/NetworkServiceMock.swift new file mode 100644 index 0000000..9666016 --- /dev/null +++ b/Source/NetworkServices/NetworkServiceMock.swift @@ -0,0 +1,184 @@ +// +// Created by Lukas Schmidt on 27.01.22. +// + +import Foundation + + +/** + Mocks a `NetworkService`. + You can configure expected results or errors to have a fully functional mock. + + **Example**: + ```swift + //Given + let networkServiceMock = NetworkServiceMock() + let resource: Resource = // + + //When + networkService.request( + resource, + onCompletion: { string in /*...*/ }, + onError: { error in /*...*/ } + ) + networkService.returnSuccess(with: "Sucess") + + //Then + //Test your expectations + + ``` + + It is possible to start multiple requests at a time. + All requests and responses (or errors) are processed + in order they have been called. So, everything is serial. + + **Example**: + ```swift + //Given + let networkServiceMock = NetworkServiceMock() + let resource: Resource = // + + //When + networkService.request( + resource, + onCompletion: { string in /* Success */ }, + onError: { error in /*...*/ } + ) + networkService.request( + resource, + onCompletion: { string in /*...*/ }, + onError: { error in /*. cancel error .*/ } + ) + + networkService.returnSuccess(with: "Sucess") + networkService.returnError(with: .cancelled) + + //Then + //Test your expectations + + ``` + + - seealso: `NetworkService` + */ +public final actor NetworkServiceMock: NetworkService { + + public enum Error: Swift.Error, CustomDebugStringConvertible { + case missingRequest + case typeMismatch + + public var debugDescription: String { + switch self { + case .missingRequest: + return "Could not return because no request" + case .typeMismatch: + return "Return type does not match requested type" + } + } + } + + /// Count of all started requests + public var requestCount: Int { + return lastRequests.count + } + + /// Last executed request + public var lastRequest: URLRequest? { + return lastRequests.last + } + + /// All executed requests. + public private(set) var lastRequests: [URLRequest] = [] + + private var scheduledResponses: [Result<(Data, HTTPURLResponse), NetworkError>] + + private let encoder: JSONEncoder + + /// Creates an instace of `NetworkServiceMock` + public init( + scheduledResponses: [Result<(Data, HTTPURLResponse), NetworkError>] = [], + encoder: JSONEncoder = JSONEncoder() + ) { + self.encoder = encoder + self.scheduledResponses = scheduledResponses + } + + /** + Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. + Execution happens on no specific queue. It dependes on the network access which queue is used. + Once execution is finished either the completion block or the error block gets called. + You decide on which queue these blocks get executed. + + **Example**: + ```swift + let networkService: NetworkService = // + let resource: Resource = // + + networkService.request(queue: .main, resource: resource, onCompletionWithResponse: { htmlText, response in + print(htmlText, response) + }, onError: { error in + // Handle errors + }) + ``` + + - parameter queue: The `DispatchQueue` to execute the completion and error block on. + - parameter resource: The resource you want to fetch. + - parameter onCompletionWithResponse: Callback which gets called when fetching and transforming into model succeeds. + - parameter onError: Callback which gets called when fetching or transforming fails. + + */ + public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { + lastRequests.append(resource.request) + if !scheduledResponses.isEmpty { + let scheduled = scheduledResponses.removeFirst() + switch scheduled { + case .success((let data, let httpURLResponse)): + do { + let result = try resource.parse(data) + return .success((result, httpURLResponse)) + } catch { + fatalError("Not able to parse data. Error: \(error)") + } + case .failure(let error): + return .failure(error) + } + } else { + return .failure(.serverError(response: nil, data: nil)) + + } + } + + public func schedule(result: Result<(T, HTTPURLResponse), NetworkError>) { + let scheduled: Result<(Data, HTTPURLResponse), NetworkError> + switch result { + case .failure(let error): + scheduled = .failure(error) + case .success((let object, let httpUrlResponse)): + guard let data = try? encoder.encode(object) else { + fatalError("Not able to encode object") + } + print(String(data: data, encoding: .utf8)) + scheduled = .success((data, httpUrlResponse)) + } + scheduledResponses.append(scheduled) + } + + public func schedule(success: Void) { + schedule(result: .success(("", HTTPURLResponse()))) + } + + public func schedule(success: (Void, HTTPURLResponse)) { + schedule(result: .success(("", success.1))) + } + + public func schedule(success: T) { + schedule(result: .success((success, HTTPURLResponse()))) + } + + public func schedule(success: (T, HTTPURLResponse)) { + schedule(result: .success(success)) + } + + public func schedule(failure: NetworkError) { + scheduledResponses.append(.failure(failure)) + } +} diff --git a/Source/RetryNetworkService.swift b/Source/NetworkServices/RetryNetworkService.swift similarity index 53% rename from Source/RetryNetworkService.swift rename to Source/NetworkServices/RetryNetworkService.swift index f8fb301..ae5a311 100644 --- a/Source/RetryNetworkService.swift +++ b/Source/NetworkServices/RetryNetworkService.swift @@ -31,10 +31,10 @@ import Dispatch - seealso: `NetworkService` */ public final class RetryNetworkService: NetworkService { + private let networkService: NetworkService private let numberOfRetries: Int private let idleTimeInterval: TimeInterval - private let dispatchRetry: (_ deadline: DispatchTime, _ execute: @escaping () -> Void ) -> Void private let shouldRetry: (NetworkError) -> Bool /// Creates an instance of `RetryNetworkService` @@ -44,17 +44,15 @@ public final class RetryNetworkService: NetworkService { /// - numberOfRetries: the number of retrys before final error /// - idleTimeInterval: time between error and retry /// - shouldRetry: closure which evaluated if error should be retry - /// - dispatchRetry: closure where to dispatch the waiting - public init(networkService: NetworkService, numberOfRetries: Int, - idleTimeInterval: TimeInterval, shouldRetry: @escaping (NetworkError) -> Bool, - dispatchRetry: @escaping (_ deadline: DispatchTime, _ execute: @escaping () -> Void ) -> Void = { deadline, execute in - DispatchQueue.global(qos: .utility).asyncAfter(deadline: deadline, execute: execute) - }) { + public init( + networkService: NetworkService, + numberOfRetries: Int, + idleTimeInterval: TimeInterval, shouldRetry: @escaping (NetworkError) -> Bool + ) { self.networkService = networkService self.numberOfRetries = numberOfRetries self.idleTimeInterval = idleTimeInterval self.shouldRetry = shouldRetry - self.dispatchRetry = dispatchRetry } /** @@ -82,59 +80,35 @@ public final class RetryNetworkService: NetworkService { - returns: a running network task */ - @discardableResult - public func request(queue: DispatchQueue, resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, - onError: @escaping (NetworkError) -> Void) -> NetworkTask { - let containerTask = ContainerNetworkTask() - let retryOnError = customOnError( - containerTask: containerTask, - numberOfRetriesLeft: numberOfRetries, - queue: queue, - resource: resource, - onCompletionWithResponse: onCompletionWithResponse, - onError: onError - ) - containerTask.underlyingTask = networkService.request( - queue: queue, - resource: resource, - onCompletionWithResponse: onCompletionWithResponse, - onError: retryOnError - ) - - return containerTask + public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { + let result = await networkService.requestResultWithResponse(for: resource) + switch result { + case .success: + return result + case .failure(let failure): + return await requestResultWithResponseOnError(error: failure, numberOfRetriesLeft: numberOfRetries, resource: resource) + } } - private func customOnError(containerTask: ContainerNetworkTask, - numberOfRetriesLeft: Int, - queue: DispatchQueue, - resource: Resource, - onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, - onError: @escaping (NetworkError) -> Void) -> (NetworkError) -> Void { - return { error in - if self.shouldRetry(error), numberOfRetriesLeft > 0 { - guard !containerTask.isCanceled else { - return - } - self.dispatchRetry(.now() + self.idleTimeInterval, { - let newOnError = self.customOnError( - containerTask: containerTask, - numberOfRetriesLeft: numberOfRetriesLeft - 1, - queue: queue, - resource: resource, - onCompletionWithResponse: onCompletionWithResponse, - onError: onError - ) - - containerTask.underlyingTask = self.networkService.request( - queue: queue, - resource: resource, - onCompletionWithResponse: onCompletionWithResponse, - onError: newOnError - ) - }) - } else { - onError(error) + private func requestResultWithResponseOnError( + error: NetworkError, + numberOfRetriesLeft: Int, + resource: Resource + ) async -> Result<(Success, HTTPURLResponse), NetworkError> { + if self.shouldRetry(error), numberOfRetriesLeft > 0 { + let duration = UInt64(idleTimeInterval * 1_000_000_000) + try? await Task.sleep(nanoseconds: duration) + #warning("check for cancellation") + + let result = await networkService.requestResultWithResponse(for: resource) + switch result { + case .success: + return result + case .failure(let failure): + return await requestResultWithResponseOnError(error: failure, numberOfRetriesLeft: numberOfRetriesLeft - 1, resource: resource) } + } else { + return .failure(error) } } diff --git a/Source/NetworkTask.swift b/Source/NetworkTask.swift deleted file mode 100644 index ef12d7b..0000000 --- a/Source/NetworkTask.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright (C) 2017 DB Systel GmbH. -// DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -// - -import Foundation - -/** - `NetworkTaskRepresenting` is a task which runs async to fetch data. - */ -public protocol NetworkTask: AnyObject { - /** - Cancels a task. - */ - func cancel() - - /** - Resumes a task. - */ - func resume() - - /** - Suspends a task. - */ - func suspend() -} diff --git a/Source/NetworkTaskMock.swift b/Source/NetworkTaskMock.swift deleted file mode 100644 index 39afc7a..0000000 --- a/Source/NetworkTaskMock.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright (C) 2017 DB Systel GmbH. -// DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -// - -import Foundation - -/// Mock implementation for `NetworkTask`. -public class NetworkTaskMock: NetworkTask { - - /// Mock state of the network task - public enum State { - case canceled, resumed, suspended - } - - /// Creates an `NetworkTaskMock` instance - public init() {} - - /// State of the network taks. Can be used to assert. - public private(set) var state: State? - - /// Cancel the request. Sets state to cancled. - public func cancel() { - state = .canceled - } - - /// Resumes the request. Sets state to resumed. - public func resume() { - state = .resumed - } - - /// Suspends the request. Sets state to suspended. - public func suspend() { - state = .suspended - } -} diff --git a/Source/URLSession+NetworkAccess.swift b/Source/URLSession+NetworkAccess.swift index 7a52ad8..c7fc912 100644 --- a/Source/URLSession+NetworkAccess.swift +++ b/Source/URLSession+NetworkAccess.swift @@ -25,21 +25,7 @@ import Foundation /// Adds conformens to `NetworkAccess`. `URLSession` can be used as a network access. extension URLSession: NetworkAccess { - /** - Fetches a request asynchrony from remote location. - - - parameter request: The request you want to fetch. - - parameter callback: Callback which gets called when the request finishes. - - - returns: the running network task - */ - public func load(request: URLRequest, callback: @escaping (Data?, HTTPURLResponse?, Error?) -> Void) -> NetworkTask { - let task = dataTask(with: request, completionHandler: { data, response, error in - callback(data, response as? HTTPURLResponse, error) - }) - - task.resume() - - return task + public func load(request: URLRequest) async throws -> (Data, URLResponse) { + return try await data(for: request) } } diff --git a/Source/URLSessionDataTask+NetworkTask.swift b/Source/URLSessionDataTask+NetworkTask.swift deleted file mode 100644 index 496b3c2..0000000 --- a/Source/URLSessionDataTask+NetworkTask.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (C) 2017 DB Systel GmbH. -// DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -// - -import Foundation - -/// URLSessionDataTask conforms to NetworkTask -extension URLSessionDataTask: NetworkTask { } diff --git a/Tests/ContainerNetworkTaskTest.swift b/Tests/ContainerNetworkTaskTest.swift deleted file mode 100644 index e9e6cf0..0000000 --- a/Tests/ContainerNetworkTaskTest.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Created by Christian Himmelsbach on 23.07.18. -// - -import XCTest -import DBNetworkStack - -class ContainerNetworkTaskTest: XCTestCase { - - func testGIVEN_AuthenticatorNetworkTask_WHEN_ResumeTask_THEN_UnderlyingTaskShouldBeResumed() { - //Given - let taskMock = NetworkTaskMock() - let task = ContainerNetworkTask() - task.underlyingTask = taskMock - - //When - task.resume() - - //Then - XCTAssert(taskMock.state == .resumed) - } - - func testGIVEN_AuthenticatorNetworkTask_WHEN_SuspendTask_THEN_UnderlyingTaskShouldBeSuspened() { - //Given - let taskMock = NetworkTaskMock() - let task = ContainerNetworkTask() - task.underlyingTask = taskMock - - //When - task.suspend() - - //Then - XCTAssert(taskMock.state == .suspended) - } - - func testGIVEN_AuthenticatorNetworkTask_WHEN_CancelTask_THEN_UnderlyingTaskShouldBeCanceled() { - // Given - let taskMock = NetworkTaskMock() - let task = ContainerNetworkTask() - task.underlyingTask = taskMock - - // When - task.cancel() - - // Then - XCTAssert(taskMock.state == .canceled) - } - -} diff --git a/Tests/ModifyRequestNetworkService.swift b/Tests/ModifyRequestNetworkService.swift index 1ef9297..3361cc4 100644 --- a/Tests/ModifyRequestNetworkService.swift +++ b/Tests/ModifyRequestNetworkService.swift @@ -27,27 +27,22 @@ import Foundation class ModifyRequestNetworkServiceTest: XCTestCase { - var networkServiceMock: NetworkServiceMock! - - override func setUp() { - super.setUp() - networkServiceMock = NetworkServiceMock() - } - - func testRequest_withModifedRequest() { + func testRequest_withModifedRequest() async { //Given + let networkServiceMock = NetworkServiceMock() let modification: [(URLRequest) -> URLRequest] = [ { request in return request.appending(queryParameters: ["key": "1"]) } ] - let networkService: NetworkService = ModifyRequestNetworkService(networkService: networkServiceMock, requestModifications: modification) + let networkService = ModifyRequestNetworkService(networkService: networkServiceMock, requestModifications: modification) let request = URLRequest(path: "/trains", baseURL: .defaultMock) let resource = Resource(request: request, parse: { _ in return 1 }) //When - networkService.request(resource, onCompletion: { _ in }, onError: { _ in }) - + let response = await networkService.requestResult(for: resource) + //Then - XCTAssert(networkServiceMock.lastRequest?.url?.absoluteString.contains("key=1") ?? false) + let lastRequest = await networkServiceMock.lastRequest + XCTAssert(lastRequest?.url?.absoluteString.contains("key=1") ?? false) } func testAddHTTPHeaderToRequest() { diff --git a/Tests/NetworkAccessMock.swift b/Tests/NetworkAccessMock.swift index 1c3d0de..44ac56b 100644 --- a/Tests/NetworkAccessMock.swift +++ b/Tests/NetworkAccessMock.swift @@ -25,23 +25,21 @@ import Foundation import DBNetworkStack class NetworkAccessMock: NetworkAccess { - fileprivate(set) var data: Data? - fileprivate(set) var response: HTTPURLResponse? - fileprivate(set) var error: NSError? - + + init(result: Result<(Data, URLResponse), NSError>) { + self.result = result + } + + private let result: Result<(Data, URLResponse), NSError> fileprivate(set) var request: URLRequest? - func load(request: URLRequest, callback: @escaping (Data?, HTTPURLResponse?, Error?) -> Void) -> NetworkTask { + func load(request: URLRequest) async throws -> (Data, URLResponse) { self.request = request - - callback(data, response, error) - - return NetworkTaskMock() - } - - func changeMock(data: Data?, response: HTTPURLResponse?, error: NSError?) { - self.data = data - self.response = response - self.error = error + switch result { + case .success(let success): + return success + case .failure(let failure): + throw failure + } } } diff --git a/Tests/NetworkErrorTest.swift b/Tests/NetworkErrorTest.swift index 849c978..8f0af49 100644 --- a/Tests/NetworkErrorTest.swift +++ b/Tests/NetworkErrorTest.swift @@ -26,17 +26,14 @@ import XCTest @testable import DBNetworkStack class NetworkErrorTest: XCTestCase { - - func urlResponseWith(statusCode: Int) -> HTTPURLResponse? { - return HTTPURLResponse(url: .defaultMock, statusCode: statusCode, httpVersion: nil, headerFields: nil) - } + private let testData: Data! = "test_string".data(using: .utf8) - func testInit_WithHTTPStatusCode400() { + func testInit_WithHTTPStatusCode400() throws { //Given - let expectedResponse = urlResponseWith(statusCode: 400) - + let expectedResponse = try XCTUnwrap(HTTPURLResponse(url: .defaultMock, statusCode: 400, httpVersion: nil, headerFields: nil)) + //When let error = NetworkError(response: expectedResponse, data: testData) @@ -50,10 +47,10 @@ class NetworkErrorTest: XCTestCase { } } - func testInit_WithHTTPStatusCode401() { + func testInit_WithHTTPStatusCode401() throws { //Given - let expectedResponse = urlResponseWith(statusCode: 401) - + let expectedResponse = try XCTUnwrap(HTTPURLResponse(url: .defaultMock, statusCode: 401, httpVersion: nil, headerFields: nil)) + //When let error = NetworkError(response: expectedResponse, data: testData) @@ -67,21 +64,21 @@ class NetworkErrorTest: XCTestCase { } } - func testInit_WithHTTPStatusCode200() { + func testInit_WithHTTPStatusCode200() throws { //Given - let response = urlResponseWith(statusCode: 200) - + let response = try XCTUnwrap(HTTPURLResponse(url: .defaultMock, statusCode: 200, httpVersion: nil, headerFields: nil)) + //When - let error = NetworkError(response: response, data: nil) - + let error = NetworkError(response: response, data: testData) + //Then XCTAssertNil(error) } - func testInit_WithHTTPStatusCode511() { + func testInit_WithHTTPStatusCode511() throws { //Given - let expectedResponse = urlResponseWith(statusCode: 511) - + let expectedResponse = try XCTUnwrap(HTTPURLResponse(url: .defaultMock, statusCode: 511, httpVersion: nil, headerFields: nil)) + //When let error = NetworkError(response: expectedResponse, data: testData) @@ -95,12 +92,12 @@ class NetworkErrorTest: XCTestCase { } } - func testInit_WithInvalidHTTPStatusCode900() { + func testInit_WithInvalidHTTPStatusCode900() throws { //Given - let response = urlResponseWith(statusCode: 900) - + let response = try XCTUnwrap(HTTPURLResponse(url: .defaultMock, statusCode: 900, httpVersion: nil, headerFields: nil)) + //When - let error = NetworkError(response: response, data: nil) + let error = NetworkError(response: response, data: testData) //Then XCTAssertNil(error) diff --git a/Tests/NetworkResponseProcessorTest.swift b/Tests/NetworkResponseProcessorTest.swift deleted file mode 100644 index 7c872b0..0000000 --- a/Tests/NetworkResponseProcessorTest.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// Copyright (C) 2017 DB Systel GmbH. -// DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -// - -import XCTest -@testable import DBNetworkStack - -class NetworkResponseProcessingTests: XCTestCase { - - var processor: NetworkResponseProcessor! - - override func setUp() { - super.setUp() - processor = NetworkResponseProcessor() - } - - func testCancelError() { - // Given - let resource = Resource(request: URLRequest.defaultMock, parse: { _ in return 0 }) - let cancelledError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil)) - - // When - var result: Int? - do { - result = try processor.process(response: nil, resource: resource, data: nil, error: cancelledError) - } catch let error as NetworkError { - // Then - switch error { - case .cancelled: // Excpected - break - default: - XCTFail("Expected cancelled error (got \(error)") - } - } catch let error { - XCTFail("Expected NetworkError (got \(type(of: error)))") - } - - XCTAssertNil(result, "Expected processing to fail") - } - - struct UnknownError: Error {} - - func testParseThrowsUnknownError() { - // Given - let resource = Resource(request: URLRequest.defaultMock, parse: { _ -> Int in - throw NetworkError.unknownError }) - let data: Data! = "Data".data(using: .utf8) - - // When - do { - _ = try processor.process(response: .defaultMock, resource: resource, data: data, error: nil) - } catch let error as NetworkError { - // Then - switch error { - case .serializationError(let error, let recievedData): // Excpected - switch error as? NetworkError { - case .unknownError?: - XCTAssert(true) - default: - XCTFail("Expects unknownError") - } - - XCTAssertEqual(recievedData, data) - default: - XCTFail("Expected cancelled error (got \(error)") - } - } catch let error { - XCTFail("Expected NetworkError (got \(type(of: error)))") - } - } - - func testParseSucessFullWithNilResponse() { - //Given - let resource = Resource(request: URLRequest.defaultMock, parse: { _ in return 0 }) - - //When - do { - _ = try processor.process(response: nil, resource: resource, data: Data(), error: nil) - } catch let error as NetworkError { - // Then - switch error { - case .unknownError: // Excpected - break - default: - XCTFail("Expected cancelled error (got \(error)") - } - } catch let error { - XCTFail("Expected NetworkError (got \(type(of: error)))") - } - - } -} diff --git a/Tests/NetworkServiceMockTest.swift b/Tests/NetworkServiceMockTest.swift index 0b71b2e..b6cb2f6 100644 --- a/Tests/NetworkServiceMockTest.swift +++ b/Tests/NetworkServiceMockTest.swift @@ -26,303 +26,113 @@ import DBNetworkStack class NetworkServiceMockTest: XCTestCase { - var networkServiceMock: NetworkServiceMock! - let resource = Resource(request: URLRequest(path: "/trains", baseURL: .defaultMock), parse: { _ in return 1 }) - - override func setUp() { - networkServiceMock = NetworkServiceMock() - } - - func testRequestCount() { - //When - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) - - //Then - XCTAssertEqual(networkServiceMock.requestCount, 2) - } - - func testLastRequests() { - //When - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) - - //Then - XCTAssertEqual(networkServiceMock.lastRequests, [resource.request, resource.request]) - } - - func testReturnSuccessWithData() throws { - //Given - var capturedResult: Int? - var executionCount: Int = 0 - - //When - networkServiceMock.request(resource, onCompletion: { result in - capturedResult = result - executionCount += 1 - }, onError: { _ in }) - try networkServiceMock.returnSuccess(with: 1) - - //Then - XCTAssertEqual(capturedResult, 1) - XCTAssertEqual(executionCount, 1) - } + let resource: Resource = Resource(request: URLRequest(path: "train", baseURL: .defaultMock), decoder: JSONDecoder()) - func testCorrectOrderOfReturnSuccessWithDataForMultipleRequests() throws { - //Given - var called1First = false - var called2First = false + func testRequestCount() async throws { + let networkServiceMock = NetworkServiceMock() + await networkServiceMock.schedule(success: Train(name: "1")) + await networkServiceMock.schedule(success: Train(name: "2")) //When - networkServiceMock.request(resource, onCompletion: { _ in - if !called2First { - called1First = true - } - }, onError: { _ in }) - networkServiceMock.request(resource, onCompletion: { _ in - if !called1First { - called2First = true - } - }, onError: { _ in }) - try networkServiceMock.returnSuccess(with: 0) - try networkServiceMock.returnSuccess(with: 0) + try await networkServiceMock.request(resource) + try await networkServiceMock.request(resource) //Then - XCTAssertTrue(called1First) - XCTAssertFalse(called2First) + let requestCount = await networkServiceMock.requestCount + XCTAssertEqual(requestCount, 2) } - - func testRequestSuccessWithDataChaining() throws { + + func testLastRequests() async throws { //Given - var executionCount1: Int = 0 - var executionCount2: Int = 0 + let networkServiceMock = NetworkServiceMock() + await networkServiceMock.schedule(success: Train(name: "1")) + await networkServiceMock.schedule(success: Train(name: "2")) //When - networkServiceMock.request(resource, onCompletion: { _ in - executionCount1 += 1 - self.networkServiceMock.request(self.resource, onCompletion: { _ in - executionCount2 += 1 - }, onError: { _ in }) - }, onError: { _ in }) - try networkServiceMock.returnSuccess(with: 0) - try networkServiceMock.returnSuccess(with: 0) + try await networkServiceMock.request(resource) + try await networkServiceMock.request(resource) //Then - XCTAssertEqual(executionCount1, 1) - XCTAssertEqual(executionCount2, 1) + let lastRequests = await networkServiceMock.lastRequests + XCTAssertEqual(lastRequests, [resource.request, resource.request]) } - - func testReturnSuccessWithDataForAllRequests() throws { + + func testReturnSuccessWithData() async throws { //Given - var executionCount1: Int = 0 - var executionCount2: Int = 0 + let networkServiceMock = NetworkServiceMock() + await networkServiceMock.schedule(success: Train(name: "1")) //When - networkServiceMock.request(resource, onCompletion: { _ in - executionCount1 += 1 - }, onError: { _ in }) - networkServiceMock.request(resource, onCompletion: { _ in - executionCount2 += 1 - }, onError: { _ in }) - try networkServiceMock.returnSuccess(with: 0) - try networkServiceMock.returnSuccess(with: 0) + let result = try await networkServiceMock.request(resource) - //Then - XCTAssertEqual(executionCount1, 1) - XCTAssertEqual(executionCount2, 1) - } - - func testReturnSuccessWithSerializedData() throws { - //Given - var capturedResult: Int? - var executionCount: Int = 0 - - //When - networkServiceMock.request(resource, onCompletion: { result in - capturedResult = result - executionCount += 1 - }, onError: { _ in }) - try networkServiceMock.returnSuccess(with: 10) //Then - XCTAssertEqual(capturedResult, 10) - XCTAssertEqual(executionCount, 1) + XCTAssertEqual(result, Train(name: "1")) } - - func testCorrectOrderOfReturnSuccessWithSerializedDataForMultipleRequests() throws { - //Given - var capturedResult1: Int? - var capturedResult2: Int? - //When - networkServiceMock.request(resource, onCompletion: { result in - capturedResult1 = result - }, onError: { _ in }) - networkServiceMock.request(resource, onCompletion: { result in - capturedResult2 = result - }, onError: { _ in }) - try networkServiceMock.returnSuccess(with: 10) - try networkServiceMock.returnSuccess(with: 20) - - //Then - XCTAssertEqual(capturedResult1, 10) - XCTAssertEqual(capturedResult2, 20) - } - - func testRequestSuccessWithSerializedDataChaining() throws { - //Given - var executionCount1: Int = 0 - var executionCount2: Int = 0 - - //When - networkServiceMock.request(resource, onCompletion: { _ in - executionCount1 += 1 - self.networkServiceMock.request(self.resource, onCompletion: { _ in - executionCount2 += 1 - }, onError: { _ in }) - }, onError: { _ in }) - try networkServiceMock.returnSuccess(with: 10) - try networkServiceMock.returnSuccess(with: 20) - - //Then - XCTAssertEqual(executionCount1, 1) - XCTAssertEqual(executionCount2, 1) - } - - func testReturnSuccessWithSerializedDataForAllRequests() throws { + func testCorrectOrderOfReturnSuccessWithDataForMultipleRequests() async throws { //Given - var executionCount1: Int = 0 - var executionCount2: Int = 0 - + let networkServiceMock = NetworkServiceMock() + await networkServiceMock.schedule(success: Train(name: "1")) + await networkServiceMock.schedule(success: Train(name: "2")) + //When - networkServiceMock.request(resource, onCompletion: { _ in - executionCount1 += 1 - }, onError: { _ in }) - networkServiceMock.request(resource, onCompletion: { _ in - executionCount2 += 1 - }, onError: { _ in }) - try networkServiceMock.returnSuccess(with: 10) - try networkServiceMock.returnSuccess(with: 10) - + let result1 = try await networkServiceMock.request(resource) + let result2 = try await networkServiceMock.request(resource) + //Then - XCTAssertEqual(executionCount1, 1) - XCTAssertEqual(executionCount2, 1) + XCTAssertEqual(result1, Train(name: "1")) + XCTAssertEqual(result2, Train(name: "2")) } - func testReturnError() throws { + func testReturnError() async throws { //Given - var capturedError: NetworkError? - var executionCount: Int = 0 - + let networkServiceMock = NetworkServiceMock() + await networkServiceMock.schedule(failure: .unknownError) + //When - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { error in - capturedError = error - executionCount += 1 - }) - try networkServiceMock.returnError(with: .unknownError) - + let result = await networkServiceMock.requestResult(for: resource) + //Then - if let error = capturedError, case .unknownError = error { - + if case .failure(.unknownError) = result { + } else { - XCTFail("Wrong error type") + XCTFail() } - XCTAssertEqual(executionCount, 1) } - func testCorrectOrderOfReturnErrorForMultipleRequests() throws { + func testCorrectOrderOfReturnErrorForMultipleRequests() async throws { //Given - var capturedError1: NetworkError? - var capturedError2: NetworkError? + let networkServiceMock = NetworkServiceMock() + await networkServiceMock.schedule(failure: .unknownError) + await networkServiceMock.schedule(failure: .cancelled) //When - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { error in - capturedError1 = error - }) - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { error in - capturedError2 = error - }) - try networkServiceMock.returnError(with: .unknownError) - try networkServiceMock.returnError(with: .cancelled) + let result1 = await networkServiceMock.requestResult(for: resource) + let result2 = await networkServiceMock.requestResult(for: resource) //Then - if case .unknownError? = capturedError1, case .cancelled? = capturedError2 { - + if case .failure(.unknownError) = result1, case .failure(.cancelled) = result2 { + } else { XCTFail("Wrong order of error responses") } } - - func testRequestErrorChaining() throws { - //Given - var executionCount1: Int = 0 - var executionCount2: Int = 0 - - //When - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in - executionCount1 += 1 - self.networkServiceMock.request(self.resource, onCompletion: { _ in }, onError: { _ in - executionCount2 += 1 - }) - }) - - try networkServiceMock.returnError(with: .unknownError) - try networkServiceMock.returnError(with: .unknownError) - - //Then - XCTAssertEqual(executionCount1, 1) - XCTAssertEqual(executionCount2, 1) - } - - func testReturnErrorsForAllRequests() throws { - //Given - var executionCount1: Int = 0 - var executionCount2: Int = 0 + func testReturnSuccessMismatchType() async { //When - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in - executionCount1 += 1 - }) - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in - executionCount2 += 1 - }) - try networkServiceMock.returnError(with: .unknownError) - try networkServiceMock.returnError(with: .unknownError) - - //Then - XCTAssertEqual(executionCount1, 1) - XCTAssertEqual(executionCount2, 1) - } + let networkServiceMock = NetworkServiceMock() - func testReturnSuccessMismatchType() { //When - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) - - //Then - XCTAssertThrowsError(try networkServiceMock.returnSuccess(with: "Mismatch Type")) - } - - func testReturnSuccessMissingRequest() { - //Then - XCTAssertThrowsError(try networkServiceMock.returnSuccess(with: 1)) - } + let result1 = await networkServiceMock.requestResult(for: resource) - func testReturnErrorMissingRequest() { //Then - XCTAssertThrowsError(try networkServiceMock.returnError(with: .unknownError)) - } - - func testPendingRequestCountEmpty() { - XCTAssertEqual(networkServiceMock.pendingRequestCount, 0) - } - - func testPendingRequestCountNotEmpty() { - //When - networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) + if case .failure(.serverError) = result1 { - //Then - XCTAssertEqual(networkServiceMock.pendingRequestCount, 1) + } else { + XCTFail("Wrong order of error responses") + } } } diff --git a/Tests/NetworkServiceTest.swift b/Tests/NetworkServiceTest.swift index 7809b67..b09c470 100644 --- a/Tests/NetworkServiceTest.swift +++ b/Tests/NetworkServiceTest.swift @@ -26,265 +26,79 @@ import XCTest @testable import DBNetworkStack class NetworkServiceTest: XCTestCase { - - var networkService: NetworkService! - - var networkAccess = NetworkAccessMock() - + let trainName = "ICE" var resource: Resource { let request = URLRequest(path: "train", baseURL: .defaultMock) return Resource(request: request, decoder: JSONDecoder()) } + - override func setUp() { - networkService = BasicNetworkService(networkAccess: networkAccess) - } - - func testRequest_withValidResponse() { + func testRequest_withValidResponse() async throws { //Given - networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) - let expection = expectation(description: "loadValidRequest") - - //When - networkService.request(resource, onCompletionWithResponse: { train, response in - XCTAssertEqual(train.name, self.trainName) - XCTAssertEqual(response, .defaultMock) - expection.fulfill() - }, onError: { _ in - XCTFail("Should not call error block") - }) - - waitForExpectations(timeout: 1, handler: nil) - - //Then - XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") - } + let networkAccess = NetworkAccessMock(result: .success((Train.validJSONData, HTTPURLResponse.defaultMock))) + let networkService = BasicNetworkService(networkAccess: networkAccess) - func testRequest_withNoDataResponse() { - //Given - networkAccess.changeMock(data: nil, response: nil, error: nil) - let expection = expectation(description: "testNoData") - //When - var capturedError: NetworkError? - networkService.request(resource, onCompletion: { _ in - XCTFail("Should not call success block") - }, onError: { error in - capturedError = error - expection.fulfill() - }) - + let (train, response) = try await networkService.requestResultWithResponse(for: resource).get() + //Then - waitForExpectations(timeout: 1, handler: nil) - - switch capturedError { - case .serverError(let response, let data)?: - XCTAssertNil(response) - XCTAssertNil(data) - default: - XCTFail("Expect serverError") - } - } - - func testRequest_withFailingSerialization() { - //Given - networkAccess.changeMock(data: Train.JSONDataWithInvalidKey, response: nil, error: nil) - let expection = expectation(description: "testRequest_withFailingSerialization") - - //When - networkService.request(resource, onCompletion: { _ in - XCTFail("Should not call success block") - }, onError: { (error: NetworkError) in - if case .serializationError(_, _) = error { - expection.fulfill() - } else { - XCTFail("Expects serializationError") - } - }) - - waitForExpectations(timeout: 1, handler: nil) - } - - func testRequest_withErrorResponse() { - //Given - let error = NSError(domain: "", code: 0, userInfo: nil) - networkAccess.changeMock(data: nil, response: nil, error: error) - let expection = expectation(description: "testOnError") - - //When - networkService.request(resource, onCompletion: { _ in - }, onError: { resultError in - //Then - switch resultError { - case .requestError: - expection.fulfill() - default: - XCTFail("Expects requestError") - } - }) - - waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(train.name, self.trainName) + XCTAssertEqual(response, .defaultMock) + XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") } - private lazy var testData: Data! = { - return "test_string".data(using: .utf8) - }() - - func testRequest_withStatusCode401Response() { + func testRequest_withFailingSerialization() async { //Given - let expectedResponse = HTTPURLResponse(url: .defaultMock, statusCode: 401, httpVersion: nil, headerFields: nil) - networkAccess.changeMock(data: testData, response: expectedResponse, error: nil) - let expection = expectation(description: "testOnError") + let networkAccess = NetworkAccessMock(result: .success((Train.JSONDataWithInvalidKey, HTTPURLResponse.defaultMock))) + let networkService = BasicNetworkService(networkAccess: networkAccess) //When - networkService.request(resource, onCompletion: { _ in - }, onError: { resultError in - //Then - switch resultError { - case .unauthorized(let response, let data): - XCTAssertEqual(response, expectedResponse) - XCTAssertEqual(data, self.testData) - expection.fulfill() - default: - XCTFail("Expects unauthorized") - } - }) - - waitForExpectations(timeout: 1, handler: nil) - } - - func testGIVEN_aRequest_WHEN_requestWithResultResponse_THEN_ShouldRespond() { - // GIVEN - - networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) - let expection = expectation(description: "loadValidRequest") - var expectedResult: Result? - - //When - networkService.request(resource, onCompletion: { result in - expectedResult = result - expection.fulfill() - }) - - waitForExpectations(timeout: 1, handler: nil) - + let result = await networkService.requestResult(for: resource) + //Then - switch expectedResult { - case .success(let train)?: - XCTAssertEqual(train.name, self.trainName) - case .failure?: - XCTFail("Should be an error") - case nil: - XCTFail("Result should not be nil") + if case .failure(.serializationError(_, let data)) = result { + XCTAssertEqual(data, Train.JSONDataWithInvalidKey) + } else { + XCTFail("Expects serializationError") } - XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") } - func testGIVEN_aRequest_WHEN_requestWithResultErrorResponse_THEN_ShouldError() { + func testRequest_withErrorResponse() async { //Given - networkAccess.changeMock(data: nil, response: nil, error: nil) - var expectedResult: Result? - let expection = expectation(description: "testNoData") - + let error = NSError(domain: "", code: 0, userInfo: nil) + let networkAccess = NetworkAccessMock(result: .failure(error)) + let networkService = BasicNetworkService(networkAccess: networkAccess) + //When - - networkService.request(resource, onCompletion: { result in - expectedResult = result - expection.fulfill() - }) - + let result = await networkService.requestResult(for: resource) + //Then - waitForExpectations(timeout: 1, handler: nil) - - switch expectedResult { - case .failure(let error)?: - if case .serverError(let response, let data) = error { - XCTAssertNil(response) - XCTAssertNil(data) - } else { - XCTFail("Expect serverError") - } + switch result { + case .failure(.requestError): + return default: - XCTFail("Expect serverError") + XCTFail("Expects requestError") } } - - func testGIVEN_aRequest_WHEN_requestWithResultAndResponse_THEN_ShouldRespond() { - // GIVEN - - networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) - let expection = expectation(description: "loadValidRequest") - var expectedResult: Result<(Train, HTTPURLResponse), NetworkError>? - - //When - networkService.request(resource: resource) { (result) in - expectedResult = result - expection.fulfill() - } - - waitForExpectations(timeout: 1, handler: nil) - - //Then - switch expectedResult { - case .success(let result)?: - XCTAssertEqual(result.0.name, self.trainName) - XCTAssertEqual(result.1, .defaultMock) - case .failure?: - XCTFail("Should be an error") - case nil: - XCTFail("Result should not be nil") - } - XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") - } - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - func testGIVEN_aRequest_WHEN_requestWithAsyncResultAndResponse_THEN_ShouldRespond() async throws { - // GIVEN - networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) + func testRequest_withStatusCode401Response() async throws { + //Given + let testData: Data! = "test_string".data(using: .utf8) + let expectedResponse = try XCTUnwrap(HTTPURLResponse(url: .defaultMock, statusCode: 401, httpVersion: nil, headerFields: nil)) + let networkAccess = NetworkAccessMock(result: .success((testData, expectedResponse))) + let networkService = BasicNetworkService(networkAccess: networkAccess) //When - let (result, response) = try await networkService.request(resource) - + let result = await networkService.requestResult(for: resource) //Then - XCTAssertEqual(result.name, self.trainName) - XCTAssertEqual(response, .defaultMock) - XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") - } - - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - func testGIVEN_aRequest_WHEN_requestWithAsyncResultAndResponse_THEN_ShouldThwo() async { - // GIVEN - let error = NSError(domain: "", code: 0, userInfo: nil) - networkAccess.changeMock(data: nil, response: nil, error: error) - - //When - do { - try await networkService.request(resource) - XCTFail("Schould throw") - } catch let error { - XCTAssertTrue(error is NetworkError) - } - } - - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - func testGIVEN_aRequest_WHEN_requestWithAsyncResultAndResponseAndCancel_THEN_ShouldThwo() async { - // GIVEN - let error = NSError(domain: "", code: 0, userInfo: nil) - networkAccess.changeMock(data: nil, response: nil, error: error) - - //When - let task = Task { - try await networkService.request(resource) - } - task.cancel() - let result = await task.result - if case .failure(let error) = result, let networkError = error as? CancellationError { - + if case .failure(.unauthorized(response: expectedResponse, data: testData)) = result { + return } else { - XCTFail("Schould throw") + XCTFail() } } + } diff --git a/Tests/NetworkServiceWithErrorTest.swift b/Tests/NetworkServiceWithErrorTest.swift index 3f71e3c..d4775e2 100644 --- a/Tests/NetworkServiceWithErrorTest.swift +++ b/Tests/NetworkServiceWithErrorTest.swift @@ -35,163 +35,38 @@ enum CustomError: Error { class NetworkServiceWithErrorTest: XCTestCase { - var networkService: NetworkService! - - var networkAccess = NetworkAccessMock() - let trainName = "ICE" var resource: ResourceWithError { let request = URLRequest(path: "train", baseURL: .defaultMock) return ResourceWithError(request: request, decoder: JSONDecoder(), mapError: { CustomError(networkError: $0) }) } - - override func setUp() { - networkService = BasicNetworkService(networkAccess: networkAccess) - } - - func testRequest_withValidResponse() { - //Given - networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) - let expection = expectation(description: "loadValidRequest") - - //When - networkService.request(resource, onCompletionWithResponse: { train, response in - XCTAssertEqual(train.name, self.trainName) - XCTAssertEqual(response, .defaultMock) - expection.fulfill() - }, onError: { _ in - XCTFail("Should not call error block") - }) - - waitForExpectations(timeout: 1, handler: nil) - - //Then - XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") - } - - func testRequest_withError() { - //Given - networkAccess.changeMock(data: nil, response: nil, error: nil) - let expection = expectation(description: "testNoData") - - //When - var capturedError: CustomError? - networkService.request(resource, onCompletion: { _ in - XCTFail("Should not call success block") - }, onError: { error in - capturedError = error - expection.fulfill() - }) - - //Then - waitForExpectations(timeout: 1, handler: nil) - - XCTAssertEqual(capturedError, .error) - } - func testGIVEN_aRequest_WHEN_requestWithResultResponse_THEN_ShouldRespond() { - // GIVEN - - networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) - let expection = expectation(description: "loadValidRequest") - var expectedResult: Result? - - //When - networkService.request(resource, onCompletion: { result in - expectedResult = result - expection.fulfill() - }) - - waitForExpectations(timeout: 1, handler: nil) - - //Then - switch expectedResult { - case .success(let train)?: - XCTAssertEqual(train.name, self.trainName) - case .failure?: - XCTFail("Should be an error") - case nil: - XCTFail("Result should not be nil") - } - XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") - } - - func testGIVEN_aRequest_WHEN_requestWithResultErrorResponse_THEN_ShouldError() { + func testRequest_withValidResponse() async throws { //Given - networkAccess.changeMock(data: nil, response: nil, error: nil) - var expectedResult: Result? - let expection = expectation(description: "testNoData") - - //When - - networkService.request(resource, onCompletion: { result in - expectedResult = result - expection.fulfill() - }) - - //Then - waitForExpectations(timeout: 1, handler: nil) + let networkAccess = NetworkAccessMock(result: .success((Train.validJSONData, HTTPURLResponse.defaultMock))) + let networkService = BasicNetworkService(networkAccess: networkAccess) - XCTAssertEqual(expectedResult, .failure(.error)) - } - - func testGIVEN_aRequest_WHEN_requestWithResultAndResponse_THEN_ShouldRespond() { - // GIVEN - - networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) - let expection = expectation(description: "loadValidRequest") - var expectedResult: Result<(Train, HTTPURLResponse), CustomError>? - //When - networkService.request(resource: resource) { (result) in - expectedResult = result - expection.fulfill() - } - - waitForExpectations(timeout: 1, handler: nil) - - //Then - switch expectedResult { - case .success(let result)?: - XCTAssertEqual(result.0.name, self.trainName) - XCTAssertEqual(result.1, .defaultMock) - case .failure?: - XCTFail("Should be an error") - case nil: - XCTFail("Result should not be nil") - } - XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") - } - - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - func testGIVEN_aRequest_WHEN_requestWithAsyncResultAndResponse_THEN_ShouldRespond() async throws { - // GIVEN - networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) - - //When - let (result, response) = try await networkService.request(resource) - + let (train, response) = try await networkService.requestResultWithResponse(for: resource).get() //Then - XCTAssertEqual(result.name, self.trainName) + XCTAssertEqual(train.name, self.trainName) XCTAssertEqual(response, .defaultMock) XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") } - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - func testGIVEN_aRequest_WHEN_requestWithAsyncResultAndResponse_THEN_ShouldThwo() async { - // GIVEN + func testRequest_withError() async { let error = NSError(domain: "", code: 0, userInfo: nil) - networkAccess.changeMock(data: nil, response: nil, error: error) + let networkAccess = NetworkAccessMock(result: .failure(error)) + let networkService = BasicNetworkService(networkAccess: networkAccess) //When - do { - try await networkService.request(resource) - XCTFail("Schould throw") - } catch let error { - XCTAssertTrue(error is CustomError) - } + let result = await networkService.requestResult(for: resource) + + //Then + XCTAssertEqual(result, .failure(.error)) } + } diff --git a/Tests/NetworkTaskMockTests.swift b/Tests/NetworkTaskMockTests.swift deleted file mode 100644 index 11eee34..0000000 --- a/Tests/NetworkTaskMockTests.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// Copyright (C) 2017 DB Systel GmbH. -// DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -// - -import XCTest -import DBNetworkStack - -class NetworkTaskMockTests: XCTestCase { - - func testCancled() { - //Given - let task = NetworkTaskMock() - - //When - task.cancel() - - //Then - XCTAssertEqual(task.state, .canceled) - } - - func testResumed() { - //Given - let task = NetworkTaskMock() - - //When - task.resume() - - //Then - XCTAssertEqual(task.state, .resumed) - } - - func testSuspended() { - //Given - let task = NetworkTaskMock() - - //When - task.suspend() - - //Then - XCTAssertEqual(task.state, .suspended) - } -} diff --git a/Tests/RetryNetworkserviceTest.swift b/Tests/RetryNetworkserviceTest.swift index 3722e2f..7a8835a 100644 --- a/Tests/RetryNetworkserviceTest.swift +++ b/Tests/RetryNetworkserviceTest.swift @@ -25,116 +25,108 @@ import XCTest @testable import DBNetworkStack class RetryNetworkserviceTest: XCTestCase { - var networkServiceMock: NetworkServiceMock! var resource: Resource { let request = URLRequest(path: "/train", baseURL: .defaultMock) return Resource(request: request, parse: { _ in return 1}) } - override func setUp() { - super.setUp() - networkServiceMock = NetworkServiceMock() - } - - override func tearDown() { - networkServiceMock = nil - super.tearDown() - } - - func testRetryRequest_shouldRetry() throws { + func testRetryRequest_shouldRetry() async throws { //Given let errorCount = 2 let numberOfRetries = 2 var executedRetrys = 0 - - let retryService = RetryNetworkService(networkService: networkServiceMock, numberOfRetries: numberOfRetries, - idleTimeInterval: 0, shouldRetry: { _ in return true }, dispatchRetry: { _, block in - executedRetrys += 1 - block() - }) - - //When - weak var task = retryService.request(resource, onCompletion: { _ in - XCTAssertEqual(executedRetrys, numberOfRetries) - }, onError: { _ in - XCTFail("Expects to not call error block") - }) - try (0.. Void)? - - override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { - lastOutgoingRequest = request - currentDataTask = URLSessionDataTaskMock() - completeRequest = completionHandler - return currentDataTask - } - - func completeWith(data: Data?, response: URLResponse?, error: Error?) { - completeRequest?(data, response, error) - completeRequest = nil - } -} From 5f6c9fac60c5bf2197776751988bef367cd62108 Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Thu, 7 Sep 2023 12:00:07 +0100 Subject: [PATCH 02/15] Improves test --- .../NetworkServices/NetworkServiceMock.swift | 1 - Tests/ModifyRequestNetworkService.swift | 31 ++++++++-------- Tests/NetworkServiceTest.swift | 15 ++++++++ Tests/NetworkServiceWithErrorTest.swift | 16 +++++++++ Tests/ResourceTest.swift | 16 ++++++--- Tests/ResourceWithErrorTest.swift | 36 ++++++++++++++++--- 6 files changed, 92 insertions(+), 23 deletions(-) diff --git a/Source/NetworkServices/NetworkServiceMock.swift b/Source/NetworkServices/NetworkServiceMock.swift index 9666016..4f1dcd5 100644 --- a/Source/NetworkServices/NetworkServiceMock.swift +++ b/Source/NetworkServices/NetworkServiceMock.swift @@ -156,7 +156,6 @@ public final actor NetworkServiceMock: NetworkService { guard let data = try? encoder.encode(object) else { fatalError("Not able to encode object") } - print(String(data: data, encoding: .utf8)) scheduled = .success((data, httpUrlResponse)) } scheduledResponses.append(scheduled) diff --git a/Tests/ModifyRequestNetworkService.swift b/Tests/ModifyRequestNetworkService.swift index 3361cc4..9d321ef 100644 --- a/Tests/ModifyRequestNetworkService.swift +++ b/Tests/ModifyRequestNetworkService.swift @@ -27,7 +27,7 @@ import Foundation class ModifyRequestNetworkServiceTest: XCTestCase { - func testRequest_withModifedRequest() async { + func testRequest_withModifedRequest() async throws { //Given let networkServiceMock = NetworkServiceMock() let modification: [(URLRequest) -> URLRequest] = [ { request in @@ -38,11 +38,12 @@ class ModifyRequestNetworkServiceTest: XCTestCase { let resource = Resource(request: request, parse: { _ in return 1 }) //When - let response = await networkService.requestResult(for: resource) + await networkService.requestResult(for: resource) //Then let lastRequest = await networkServiceMock.lastRequest - XCTAssert(lastRequest?.url?.absoluteString.contains("key=1") ?? false) + let lastRequestURL = try XCTUnwrap(lastRequest?.url) + XCTAssert(lastRequestURL.absoluteString.contains("key=1")) } func testAddHTTPHeaderToRequest() { @@ -57,7 +58,7 @@ class ModifyRequestNetworkServiceTest: XCTestCase { XCTAssertEqual(newRequest.allHTTPHeaderFields?["header"], "head") } - func testAddDuplicatedQueryToRequest() { + func testAddDuplicatedQueryToRequest() throws { //Given let url = URL(staticString: "bahn.de?test=test&bool=true") let request = URLRequest(url: url) @@ -68,14 +69,15 @@ class ModifyRequestNetworkServiceTest: XCTestCase { let newRequest = request.appending(queryParameters: parameters) //Then - let newURL: URL! = newRequest.url - let query = URLComponents(url: newURL, resolvingAgainstBaseURL: true)?.queryItems - XCTAssertEqual(query?.count, 2) - XCTAssert(query?.contains(where: { $0.name == "test" && $0.value == "test2" }) ?? false) - XCTAssert(query?.contains(where: { $0.name == "bool" && $0.value == "true" }) ?? false) + let newURL = try XCTUnwrap(newRequest.url) + let urlComponents = URLComponents(url: newURL, resolvingAgainstBaseURL: true) + let query = try XCTUnwrap(urlComponents?.queryItems) + XCTAssertEqual(query.count, 2) + XCTAssert(query.contains(where: { $0.name == "test" && $0.value == "test2" })) + XCTAssert(query.contains(where: { $0.name == "bool" && $0.value == "true" })) } - func testReplaceAllQueryItemsFromRequest() { + func testReplaceAllQueryItemsFromRequest() throws { //Given let url = URL(staticString: "bahn.de?test=test&bool=true") let request = URLRequest(url: url) @@ -86,9 +88,10 @@ class ModifyRequestNetworkServiceTest: XCTestCase { let newRequest = request.replacingAllQueryItems(with: parameters) //Then - let newURL: URL! = newRequest.url - let query = URLComponents(url: newURL, resolvingAgainstBaseURL: true)?.queryItems - XCTAssertEqual(query?.count, 1) - XCTAssert(query?.contains(where: { $0.name == "test5" && $0.value == "test2" }) ?? false) + let newURL = try XCTUnwrap(newRequest.url) + let urlComponents = URLComponents(url: newURL, resolvingAgainstBaseURL: true) + let query = try XCTUnwrap(urlComponents?.queryItems) + XCTAssertEqual(query.count, 1) + XCTAssert(query.contains(where: { $0.name == "test5" && $0.value == "test2" })) } } diff --git a/Tests/NetworkServiceTest.swift b/Tests/NetworkServiceTest.swift index b09c470..9d87f2d 100644 --- a/Tests/NetworkServiceTest.swift +++ b/Tests/NetworkServiceTest.swift @@ -83,6 +83,21 @@ class NetworkServiceTest: XCTestCase { } } + func testRequest_withErrorThrow() async { + //Given + let error = NSError(domain: "", code: 0, userInfo: nil) + let networkAccess = NetworkAccessMock(result: .failure(error)) + let networkService = BasicNetworkService(networkAccess: networkAccess) + + //When + do { + try await networkService.request(resource) + XCTFail("Expects throws") + } catch { + return + } + } + func testRequest_withStatusCode401Response() async throws { //Given let testData: Data! = "test_string".data(using: .utf8) diff --git a/Tests/NetworkServiceWithErrorTest.swift b/Tests/NetworkServiceWithErrorTest.swift index d4775e2..0cd0485 100644 --- a/Tests/NetworkServiceWithErrorTest.swift +++ b/Tests/NetworkServiceWithErrorTest.swift @@ -68,5 +68,21 @@ class NetworkServiceWithErrorTest: XCTestCase { //Then XCTAssertEqual(result, .failure(.error)) } + + func testRequestThrows_withError() async { + let error = NSError(domain: "", code: 0, userInfo: nil) + let networkAccess = NetworkAccessMock(result: .failure(error)) + let networkService = BasicNetworkService(networkAccess: networkAccess) + + //When + do { + try await networkService.request(resource) + XCTFail() + } catch let error as CustomError { + XCTAssertEqual(error, .error) + } catch { + XCTFail() + } + } } diff --git a/Tests/ResourceTest.swift b/Tests/ResourceTest.swift index 32febc4..badc99a 100644 --- a/Tests/ResourceTest.swift +++ b/Tests/ResourceTest.swift @@ -27,17 +27,25 @@ import DBNetworkStack class ResourceTest: XCTestCase { - func testResource() { + func testResource() throws { //Given let validData: Data! = "ICE".data(using: .utf8) let resource = Resource(request: URLRequest.defaultMock, parse: { String(data: $0, encoding: .utf8) }) //When - let name = try? resource.parse(validData) + let name = try resource.parse(validData) //Then - XCTAssertEqual(name ?? nil, "ICE") + XCTAssertEqual(name, "ICE") } - + + func testResourceWithVoidResult() throws { + //Given + let resource = Resource(request: URLRequest.defaultMock) + + //When + try resource.parse(Data()) + } + } diff --git a/Tests/ResourceWithErrorTest.swift b/Tests/ResourceWithErrorTest.swift index 9ea0e08..d83bc41 100644 --- a/Tests/ResourceWithErrorTest.swift +++ b/Tests/ResourceWithErrorTest.swift @@ -27,7 +27,7 @@ import DBNetworkStack class ResourceWithErrorTest: XCTestCase { - func testResource() { + func testResource() throws { //Given let validData: Data! = "ICE".data(using: .utf8) @@ -38,10 +38,27 @@ class ResourceWithErrorTest: XCTestCase { ) //When - let name = try? resource.parse(validData) + let name = try resource.parse(validData) //Then - XCTAssertEqual(name ?? nil, "ICE") + XCTAssertEqual(name, "ICE") + } + + func testResourceMap() throws { + //Given + let validData: Data! = "ICE".data(using: .utf8) + + let resource = ResourceWithError( + request: URLRequest.defaultMock, + parse: { String(data: $0, encoding: .utf8) }, + mapError: { $0 } + ) + + //When + let numberOfCharacters = try resource.map(transform: { $0?.count }).parse(validData) + + //Then + XCTAssertEqual(numberOfCharacters, 3) } func testResourceMapError() { @@ -60,5 +77,16 @@ class ResourceWithErrorTest: XCTestCase { //Then XCTAssertEqual(mappedError, .error) } - + + func testResourceWithVoidResult() throws { + //Given + let resource = ResourceWithError( + request: URLRequest.defaultMock, + mapError: { _ in return .error } + ) + + //When + try resource.parse(Data()) + } + } From 64b59944e47d604e94bb528673e5b83d53718f8c Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Thu, 7 Sep 2023 12:14:09 +0100 Subject: [PATCH 03/15] Sendable Support --- Package.swift | 8 ++++++-- Source/NetworkAccess.swift | 2 +- Source/NetworkService.swift | 2 +- Source/NetworkServices/ModifyRequestNetworkService.swift | 4 ++-- Source/NetworkServices/RetryNetworkService.swift | 7 ++++--- Tests/ModifyRequestNetworkService.swift | 6 +++--- Tests/NetworkAccessMock.swift | 2 +- Tests/NetworkServiceTest.swift | 3 ++- Tests/NetworkServiceWithErrorTest.swift | 3 ++- Tests/RetryNetworkserviceTest.swift | 4 ---- 10 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Package.swift b/Package.swift index 5bff3ff..455072c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.9 // // Package.swift // @@ -44,7 +44,11 @@ let package = Package( .target( name: "DBNetworkStack", dependencies: [], - path: "Source"), + path: "Source", + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrencyComplete") + ] + ), .testTarget( name: "DBNetworkStackTests", dependencies: ["DBNetworkStack"], diff --git a/Source/NetworkAccess.swift b/Source/NetworkAccess.swift index c61f12d..539a033 100644 --- a/Source/NetworkAccess.swift +++ b/Source/NetworkAccess.swift @@ -24,7 +24,7 @@ import Foundation /// `NetworkAccess` provides access to the network. -public protocol NetworkAccess { +public protocol NetworkAccess: Sendable { /// Fetches a request asynchrony from remote location. /// diff --git a/Source/NetworkService.swift b/Source/NetworkService.swift index 8142879..69ddab6 100644 --- a/Source/NetworkService.swift +++ b/Source/NetworkService.swift @@ -30,7 +30,7 @@ import Dispatch - seealso: `BasicNetworkService` - seealso: `NetworkServiceMock` */ -public protocol NetworkService { +public protocol NetworkService: Sendable { /** Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. Execution happens on no specific queue. It dependes on the network access which queue is used. diff --git a/Source/NetworkServices/ModifyRequestNetworkService.swift b/Source/NetworkServices/ModifyRequestNetworkService.swift index 198d027..ea37366 100644 --- a/Source/NetworkServices/ModifyRequestNetworkService.swift +++ b/Source/NetworkServices/ModifyRequestNetworkService.swift @@ -41,7 +41,7 @@ import Dispatch */ public final class ModifyRequestNetworkService: NetworkService { - private let requestModifications: [(URLRequest) -> URLRequest] + private let requestModifications: [@Sendable (URLRequest) -> URLRequest] private let networkService: NetworkService /// Creates an insatcne of `ModifyRequestNetworkService`. @@ -49,7 +49,7 @@ public final class ModifyRequestNetworkService: NetworkService { /// - Parameters: /// - networkService: a networkservice. /// - requestModifications: array of modifications to modify requests. - public init(networkService: NetworkService, requestModifications: [(URLRequest) -> URLRequest]) { + public init(networkService: NetworkService, requestModifications: [@Sendable (URLRequest) -> URLRequest]) { self.networkService = networkService self.requestModifications = requestModifications } diff --git a/Source/NetworkServices/RetryNetworkService.swift b/Source/NetworkServices/RetryNetworkService.swift index ae5a311..f8fcd65 100644 --- a/Source/NetworkServices/RetryNetworkService.swift +++ b/Source/NetworkServices/RetryNetworkService.swift @@ -35,8 +35,8 @@ public final class RetryNetworkService: NetworkService { private let networkService: NetworkService private let numberOfRetries: Int private let idleTimeInterval: TimeInterval - private let shouldRetry: (NetworkError) -> Bool - + private let shouldRetry: @Sendable (NetworkError) -> Bool + /// Creates an instance of `RetryNetworkService` /// /// - Parameters: @@ -47,7 +47,8 @@ public final class RetryNetworkService: NetworkService { public init( networkService: NetworkService, numberOfRetries: Int, - idleTimeInterval: TimeInterval, shouldRetry: @escaping (NetworkError) -> Bool + idleTimeInterval: TimeInterval, + shouldRetry: @Sendable @escaping (NetworkError) -> Bool ) { self.networkService = networkService self.numberOfRetries = numberOfRetries diff --git a/Tests/ModifyRequestNetworkService.swift b/Tests/ModifyRequestNetworkService.swift index 9d321ef..c2bd3b8 100644 --- a/Tests/ModifyRequestNetworkService.swift +++ b/Tests/ModifyRequestNetworkService.swift @@ -30,9 +30,9 @@ class ModifyRequestNetworkServiceTest: XCTestCase { func testRequest_withModifedRequest() async throws { //Given let networkServiceMock = NetworkServiceMock() - let modification: [(URLRequest) -> URLRequest] = [ { request in - return request.appending(queryParameters: ["key": "1"]) - } ] + let modification: [@Sendable (URLRequest) -> URLRequest] = [ + { $0.appending(queryParameters: ["key": "1"]) } + ] let networkService = ModifyRequestNetworkService(networkService: networkServiceMock, requestModifications: modification) let request = URLRequest(path: "/trains", baseURL: .defaultMock) let resource = Resource(request: request, parse: { _ in return 1 }) diff --git a/Tests/NetworkAccessMock.swift b/Tests/NetworkAccessMock.swift index 44ac56b..d00a923 100644 --- a/Tests/NetworkAccessMock.swift +++ b/Tests/NetworkAccessMock.swift @@ -24,7 +24,7 @@ import Foundation import DBNetworkStack -class NetworkAccessMock: NetworkAccess { +actor NetworkAccessMock: NetworkAccess { init(result: Result<(Data, URLResponse), NSError>) { self.result = result diff --git a/Tests/NetworkServiceTest.swift b/Tests/NetworkServiceTest.swift index 9d87f2d..ddc9467 100644 --- a/Tests/NetworkServiceTest.swift +++ b/Tests/NetworkServiceTest.swift @@ -46,7 +46,8 @@ class NetworkServiceTest: XCTestCase { //Then XCTAssertEqual(train.name, self.trainName) XCTAssertEqual(response, .defaultMock) - XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") + let request = await networkAccess.request + XCTAssertEqual(request?.url?.absoluteString, "https://bahn.de/train") } func testRequest_withFailingSerialization() async { diff --git a/Tests/NetworkServiceWithErrorTest.swift b/Tests/NetworkServiceWithErrorTest.swift index 0cd0485..95b607e 100644 --- a/Tests/NetworkServiceWithErrorTest.swift +++ b/Tests/NetworkServiceWithErrorTest.swift @@ -54,7 +54,8 @@ class NetworkServiceWithErrorTest: XCTestCase { //Then XCTAssertEqual(train.name, self.trainName) XCTAssertEqual(response, .defaultMock) - XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") + let request = await networkAccess.request + XCTAssertEqual(request?.url?.absoluteString, "https://bahn.de/train") } func testRequest_withError() async { diff --git a/Tests/RetryNetworkserviceTest.swift b/Tests/RetryNetworkserviceTest.swift index 7a8835a..f8461de 100644 --- a/Tests/RetryNetworkserviceTest.swift +++ b/Tests/RetryNetworkserviceTest.swift @@ -34,7 +34,6 @@ class RetryNetworkserviceTest: XCTestCase { //Given let errorCount = 2 let numberOfRetries = 2 - var executedRetrys = 0 let networkServiceMock = NetworkServiceMock() for _ in (0.. Date: Thu, 7 Sep 2023 13:56:18 +0100 Subject: [PATCH 04/15] Improves Mocks --- .../NetworkServices/NetworkServiceMock.swift | 83 +++++++++++-------- Tests/NetworkServiceMockTest.swift | 39 +++++---- Tests/RetryNetworkserviceTest.swift | 33 ++++---- 3 files changed, 88 insertions(+), 67 deletions(-) diff --git a/Source/NetworkServices/NetworkServiceMock.swift b/Source/NetworkServices/NetworkServiceMock.swift index 4f1dcd5..044df68 100644 --- a/Source/NetworkServices/NetworkServiceMock.swift +++ b/Source/NetworkServices/NetworkServiceMock.swift @@ -89,17 +89,38 @@ public final actor NetworkServiceMock: NetworkService { /// All executed requests. public private(set) var lastRequests: [URLRequest] = [] - private var scheduledResponses: [Result<(Data, HTTPURLResponse), NetworkError>] - + private var responses: [Result<(Data, HTTPURLResponse), NetworkError>] private let encoder: JSONEncoder /// Creates an instace of `NetworkServiceMock` public init( - scheduledResponses: [Result<(Data, HTTPURLResponse), NetworkError>] = [], + responses: [Result<(Data, HTTPURLResponse), NetworkError>] = [], + encoder: JSONEncoder = JSONEncoder() + ) { + self.encoder = encoder + self.responses = responses + } + + /// Creates an instace of `NetworkServiceMock` + public init( + _ responses: repeat Result<(each T, HTTPURLResponse), NetworkError>, + encoder: JSONEncoder = JSONEncoder() + ) { + self.encoder = encoder + var encodedResponses: [Result<(Data, HTTPURLResponse), NetworkError>] = [] + repeat (each responses).decode(&encodedResponses, encoder: encoder) + self.responses = encodedResponses + } + + /// Creates an instace of `NetworkServiceMock` + public init( + _ responses: repeat Result, encoder: JSONEncoder = JSONEncoder() ) { self.encoder = encoder - self.scheduledResponses = scheduledResponses + var encodedResponses: [Result<(Data, HTTPURLResponse), NetworkError>] = [] + repeat (each responses).decode(&encodedResponses, encoder: encoder) + self.responses = encodedResponses } /** @@ -128,8 +149,8 @@ public final actor NetworkServiceMock: NetworkService { */ public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { lastRequests.append(resource.request) - if !scheduledResponses.isEmpty { - let scheduled = scheduledResponses.removeFirst() + if !responses.isEmpty { + let scheduled = responses.removeFirst() switch scheduled { case .success((let data, let httpURLResponse)): do { @@ -143,41 +164,37 @@ public final actor NetworkServiceMock: NetworkService { } } else { return .failure(.serverError(response: nil, data: nil)) - } } +} - public func schedule(result: Result<(T, HTTPURLResponse), NetworkError>) { - let scheduled: Result<(Data, HTTPURLResponse), NetworkError> - switch result { - case .failure(let error): - scheduled = .failure(error) - case .success((let object, let httpUrlResponse)): - guard let data = try? encoder.encode(object) else { - fatalError("Not able to encode object") - } - scheduled = .success((data, httpUrlResponse)) - } - scheduledResponses.append(scheduled) - } +fileprivate extension Result { - public func schedule(success: Void) { - schedule(result: .success(("", HTTPURLResponse()))) + func decode( + _ array: inout [Result<(Data, HTTPURLResponse), NetworkError>], + encoder: JSONEncoder + ) where Success == (T, HTTPURLResponse), Failure == NetworkError { + array.append(self.map({ (try! encoder.encode($0.0), $0.1) })) } - public func schedule(success: (Void, HTTPURLResponse)) { - schedule(result: .success(("", success.1))) - } +} - public func schedule(success: T) { - schedule(result: .success((success, HTTPURLResponse()))) - } +fileprivate extension Result where Success: Encodable, Failure == NetworkError { - public func schedule(success: (T, HTTPURLResponse)) { - schedule(result: .success(success)) + func decode( + _ array: inout [Result<(Data, HTTPURLResponse), NetworkError>], + encoder: JSONEncoder + ) { + let defaultResponse: HTTPURLResponse! = HTTPURLResponse( + url: URL(staticString: "bahn.de"), + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: nil + ) + array.append(self.map({ (try! encoder.encode($0), defaultResponse) })) } - public func schedule(failure: NetworkError) { - scheduledResponses.append(.failure(failure)) - } } + + + diff --git a/Tests/NetworkServiceMockTest.swift b/Tests/NetworkServiceMockTest.swift index b6cb2f6..7d10064 100644 --- a/Tests/NetworkServiceMockTest.swift +++ b/Tests/NetworkServiceMockTest.swift @@ -30,9 +30,11 @@ class NetworkServiceMockTest: XCTestCase { let resource: Resource = Resource(request: URLRequest(path: "train", baseURL: .defaultMock), decoder: JSONDecoder()) func testRequestCount() async throws { - let networkServiceMock = NetworkServiceMock() - await networkServiceMock.schedule(success: Train(name: "1")) - await networkServiceMock.schedule(success: Train(name: "2")) + //Given + let networkServiceMock = NetworkServiceMock( + .success(Train(name: "1")), + .success(Train(name: "2")) + ) //When try await networkServiceMock.request(resource) @@ -45,9 +47,10 @@ class NetworkServiceMockTest: XCTestCase { func testLastRequests() async throws { //Given - let networkServiceMock = NetworkServiceMock() - await networkServiceMock.schedule(success: Train(name: "1")) - await networkServiceMock.schedule(success: Train(name: "2")) + let networkServiceMock = NetworkServiceMock( + .success(Train(name: "1")), + .success(Train(name: "2")) + ) //When try await networkServiceMock.request(resource) @@ -60,8 +63,9 @@ class NetworkServiceMockTest: XCTestCase { func testReturnSuccessWithData() async throws { //Given - let networkServiceMock = NetworkServiceMock() - await networkServiceMock.schedule(success: Train(name: "1")) + let networkServiceMock = NetworkServiceMock( + .success(Train(name: "1")) + ) //When let result = try await networkServiceMock.request(resource) @@ -73,9 +77,10 @@ class NetworkServiceMockTest: XCTestCase { func testCorrectOrderOfReturnSuccessWithDataForMultipleRequests() async throws { //Given - let networkServiceMock = NetworkServiceMock() - await networkServiceMock.schedule(success: Train(name: "1")) - await networkServiceMock.schedule(success: Train(name: "2")) + let networkServiceMock = NetworkServiceMock( + .success(Train(name: "1")), + .success(Train(name: "2")) + ) //When let result1 = try await networkServiceMock.request(resource) @@ -88,8 +93,9 @@ class NetworkServiceMockTest: XCTestCase { func testReturnError() async throws { //Given - let networkServiceMock = NetworkServiceMock() - await networkServiceMock.schedule(failure: .unknownError) + let networkServiceMock = NetworkServiceMock( + Result.failure(.unknownError) + ) //When let result = await networkServiceMock.requestResult(for: resource) @@ -104,9 +110,10 @@ class NetworkServiceMockTest: XCTestCase { func testCorrectOrderOfReturnErrorForMultipleRequests() async throws { //Given - let networkServiceMock = NetworkServiceMock() - await networkServiceMock.schedule(failure: .unknownError) - await networkServiceMock.schedule(failure: .cancelled) + let networkServiceMock = NetworkServiceMock( + Result.failure(.unknownError), + Result.failure(.cancelled) + ) //When let result1 = await networkServiceMock.requestResult(for: resource) diff --git a/Tests/RetryNetworkserviceTest.swift b/Tests/RetryNetworkserviceTest.swift index f8461de..b606279 100644 --- a/Tests/RetryNetworkserviceTest.swift +++ b/Tests/RetryNetworkserviceTest.swift @@ -34,11 +34,11 @@ class RetryNetworkserviceTest: XCTestCase { //Given let errorCount = 2 let numberOfRetries = 2 - let networkServiceMock = NetworkServiceMock() - for _ in (0...failure(.unknownError), + Result.failure(.unknownError), + Result.success(1) + ) let retryService = RetryNetworkService( networkService: networkServiceMock, @@ -82,16 +82,18 @@ class RetryNetworkserviceTest: XCTestCase { func testRetryRequest_moreErrorsThenRetryAttemps() async throws { //Given - let networkServiceMock = NetworkServiceMock() + let networkServiceMock = NetworkServiceMock( + Result.failure(.unknownError), + Result.failure(.unknownError), + Result.failure(.unknownError), + Result.failure(.unknownError) + ) let retryService = RetryNetworkService( networkService: networkServiceMock, numberOfRetries: 3, idleTimeInterval: 0, shouldRetry: { _ in return true } ) - for _ in (0..<4) { - await networkServiceMock.schedule(failure: .unknownError) - } //When let result = await retryService.requestResult(for: resource) @@ -107,22 +109,17 @@ class RetryNetworkserviceTest: XCTestCase { func testRetryRequest_shouldNotRetry() async throws { //Given - var executedRetrys = 0 - let networkServiceMock = NetworkServiceMock() + let networkServiceMock = NetworkServiceMock( + Result.failure(.unknownError) + ) let retryService = RetryNetworkService( networkService: networkServiceMock, numberOfRetries: 3, idleTimeInterval: 0, shouldRetry: { _ in return true } ) - await networkServiceMock.schedule(failure: .unknownError) //When - let result = await retryService.requestResult(for: resource) - - - //Then -// XCTAssertNil(task) -// XCTAssertNotNil(capturedError) + await retryService.requestResult(for: resource) } } From 829f5945409cf3f0a1e8ec3a3f96084e42822d49 Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Thu, 21 Sep 2023 13:54:15 +0200 Subject: [PATCH 05/15] Support vision & catalyst native --- Package.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 455072c..1b748d7 100644 --- a/Package.swift +++ b/Package.swift @@ -33,7 +33,9 @@ let package = Package( .iOS(.v13), .tvOS(.v13), .watchOS(.v6), - .macOS(.v10_15) + .macOS(.v10_15), + .visionOS(.v1), + .macCatalyst(.v14) ], products: [ .library( From a0e2245fbb09cf85682a03c09df9506213391dc0 Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Fri, 13 Oct 2023 14:40:52 +0200 Subject: [PATCH 06/15] NetworkErrorConvertible + convinece init --- Source/NetworkError.swift | 8 ++++++++ Source/NetworkErrorConvertible.swift | 14 ++++++++++++++ ...urceWithError+NetworkErrorConvertible.swift | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 Source/NetworkErrorConvertible.swift create mode 100644 Source/ResourceWithError+NetworkErrorConvertible.swift diff --git a/Source/NetworkError.swift b/Source/NetworkError.swift index 3544a54..fc2052c 100644 --- a/Source/NetworkError.swift +++ b/Source/NetworkError.swift @@ -94,3 +94,11 @@ extension NetworkError: CustomDebugStringConvertible { } } } + +extension NetworkError: NetworkErrorConvertible { + + public init(networkError: NetworkError) { + self = networkError + } + +} diff --git a/Source/NetworkErrorConvertible.swift b/Source/NetworkErrorConvertible.swift new file mode 100644 index 0000000..3c8a115 --- /dev/null +++ b/Source/NetworkErrorConvertible.swift @@ -0,0 +1,14 @@ +// +// File.swift +// +// +// Created by Lukas Schmidt on 13.10.23. +// + +import Foundation + +public protocol NetworkErrorConvertible: Error { + + init(networkError: NetworkError) + +} diff --git a/Source/ResourceWithError+NetworkErrorConvertible.swift b/Source/ResourceWithError+NetworkErrorConvertible.swift new file mode 100644 index 0000000..1eff5fa --- /dev/null +++ b/Source/ResourceWithError+NetworkErrorConvertible.swift @@ -0,0 +1,18 @@ +// +// File.swift +// +// +// Created by Lukas Schmidt on 13.10.23. +// + +import Foundation + +public extension ResourceWithError where E: NetworkErrorConvertible { + + init(request: URLRequest, parse: @escaping (Data) throws -> Model) { + self.request = request + self.parse = parse + self.mapError = { E(networkError: $0) } + } + +} From e1f84a36604a0cc5e6a2d08a091b3e30b53036a2 Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Fri, 13 Oct 2023 15:10:35 +0200 Subject: [PATCH 07/15] Resource with generic Error as default --- Source/NetworkService+ResourceWithError.swift | 17 ++++++++----- Source/NetworkService.swift | 11 ++++++--- .../NetworkServices/BasicNetworkService.swift | 2 +- .../ModifyRequestNetworkService.swift | 4 ++-- .../NetworkServices/NetworkServiceMock.swift | 2 +- .../NetworkServices/RetryNetworkService.swift | 24 +++++++++++-------- Source/Resource+Decodable.swift | 11 +++++---- Source/Resource+Inspect.swift | 9 +++---- Source/Resource+Map.swift | 23 +++--------------- Source/Resource+Void.swift | 13 +++++----- Source/Resource.swift | 19 ++++++++------- ...rceWithError+NetworkErrorConvertible.swift | 2 +- Source/ResourceWithError.swift | 21 +--------------- Tests/DecodableResoureTest.swift | 4 ++-- Tests/ModifyRequestNetworkService.swift | 2 +- Tests/NetworkServiceMockTest.swift | 2 +- Tests/NetworkServiceTest.swift | 2 +- Tests/NetworkServiceWithErrorTest.swift | 4 ++-- Tests/ResourceInspectTest.swift | 2 +- Tests/ResourceTest.swift | 6 ++--- Tests/ResourceWithErrorTest.swift | 8 +++---- Tests/RetryNetworkserviceTest.swift | 2 +- 22 files changed, 87 insertions(+), 103 deletions(-) diff --git a/Source/NetworkService+ResourceWithError.swift b/Source/NetworkService+ResourceWithError.swift index 48b2acd..643c823 100644 --- a/Source/NetworkService+ResourceWithError.swift +++ b/Source/NetworkService+ResourceWithError.swift @@ -52,9 +52,9 @@ extension NetworkService { */ @discardableResult public func requestResultWithResponse( - for resource: ResourceWithError + for resource: Resource ) async -> Result<(Success, HTTPURLResponse), E> { - let resourceWithoutError = Resource(request: resource.request, parse: resource.parse) + let resourceWithoutError = Resource(request: resource.request, parse: resource.parse) return await self.requestResultWithResponse(for: resourceWithoutError) .mapError(resource.mapError) } @@ -85,9 +85,9 @@ extension NetworkService { */ @discardableResult public func requestResult( - for resource: ResourceWithError + for resource: Resource ) async -> Result { - let resourceWithoutError = Resource(request: resource.request, parse: resource.parse) + let resourceWithoutError = Resource(request: resource.request, parse: resource.parse) return await requestResultWithResponse(for: resourceWithoutError) .mapError(resource.mapError) .map({ $0.0 }) @@ -120,13 +120,18 @@ extension NetworkService { */ @discardableResult public func request( - _ resource: ResourceWithError + _ resource: Resource ) async throws -> Success { - let resourceWithoutError = Resource(request: resource.request, parse: resource.parse) + let resourceWithoutError = Resource(request: resource.request, parse: resource.parse) return try await requestResultWithResponse(for: resourceWithoutError) .mapError(resource.mapError) .map({ $0.0 }) .get() } + @discardableResult + func requestWithResponse(for resource: Resource) async throws -> (Success, HTTPURLResponse) { + return try await requestResultWithResponse(for: resource).get() + } + } diff --git a/Source/NetworkService.swift b/Source/NetworkService.swift index 69ddab6..9dbcdcf 100644 --- a/Source/NetworkService.swift +++ b/Source/NetworkService.swift @@ -57,7 +57,7 @@ public protocol NetworkService: Sendable { - returns: a running network task */ @discardableResult - func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> + func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> } public extension NetworkService { @@ -87,7 +87,7 @@ public extension NetworkService { - returns: a running network task */ @discardableResult - func requestResult(for resource: Resource) async -> Result { + func requestResult(for resource: Resource) async -> Result { return await requestResultWithResponse(for: resource).map({ $0.0 }) } @@ -116,7 +116,12 @@ public extension NetworkService { - returns: a running network task */ @discardableResult - func request(_ resource: Resource) async throws -> Success { + func request(_ resource: Resource) async throws -> Success { return try await requestResultWithResponse(for: resource).get().0 } + + @discardableResult + func requestWithResponse(for resource: Resource) async throws -> (Success, HTTPURLResponse) { + return try await requestResultWithResponse(for: resource).get() + } } diff --git a/Source/NetworkServices/BasicNetworkService.swift b/Source/NetworkServices/BasicNetworkService.swift index be7bc2b..ec91b10 100644 --- a/Source/NetworkServices/BasicNetworkService.swift +++ b/Source/NetworkServices/BasicNetworkService.swift @@ -73,7 +73,7 @@ public final class BasicNetworkService: NetworkService { - returns: a running network task */ @discardableResult - public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { + public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { do { let (data, response) = try await networkAccess.load(request: resource.request) guard let response = response as? HTTPURLResponse else { diff --git a/Source/NetworkServices/ModifyRequestNetworkService.swift b/Source/NetworkServices/ModifyRequestNetworkService.swift index ea37366..47b6763 100644 --- a/Source/NetworkServices/ModifyRequestNetworkService.swift +++ b/Source/NetworkServices/ModifyRequestNetworkService.swift @@ -80,11 +80,11 @@ public final class ModifyRequestNetworkService: NetworkService { - returns: a running network task */ @discardableResult - public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { + public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { let request = requestModifications.reduce(resource.request, { request, modify in return modify(request) }) - let newResource = Resource(request: request, parse: resource.parse) + let newResource = Resource(request: request, parse: resource.parse) return await networkService.requestResultWithResponse(for: newResource) } } diff --git a/Source/NetworkServices/NetworkServiceMock.swift b/Source/NetworkServices/NetworkServiceMock.swift index 044df68..46f57e2 100644 --- a/Source/NetworkServices/NetworkServiceMock.swift +++ b/Source/NetworkServices/NetworkServiceMock.swift @@ -147,7 +147,7 @@ public final actor NetworkServiceMock: NetworkService { - parameter onError: Callback which gets called when fetching or transforming fails. */ - public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { + public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { lastRequests.append(resource.request) if !responses.isEmpty { let scheduled = responses.removeFirst() diff --git a/Source/NetworkServices/RetryNetworkService.swift b/Source/NetworkServices/RetryNetworkService.swift index f8fcd65..7866dc7 100644 --- a/Source/NetworkServices/RetryNetworkService.swift +++ b/Source/NetworkServices/RetryNetworkService.swift @@ -81,7 +81,7 @@ public final class RetryNetworkService: NetworkService { - returns: a running network task */ - public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { + public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { let result = await networkService.requestResultWithResponse(for: resource) switch result { case .success: @@ -94,20 +94,24 @@ public final class RetryNetworkService: NetworkService { private func requestResultWithResponseOnError( error: NetworkError, numberOfRetriesLeft: Int, - resource: Resource + resource: Resource ) async -> Result<(Success, HTTPURLResponse), NetworkError> { if self.shouldRetry(error), numberOfRetriesLeft > 0 { let duration = UInt64(idleTimeInterval * 1_000_000_000) try? await Task.sleep(nanoseconds: duration) - #warning("check for cancellation") - - let result = await networkService.requestResultWithResponse(for: resource) - switch result { - case .success: - return result - case .failure(let failure): - return await requestResultWithResponseOnError(error: failure, numberOfRetriesLeft: numberOfRetriesLeft - 1, resource: resource) + do { + try Task.checkCancellation() + let result = await networkService.requestResultWithResponse(for: resource) + switch result { + case .success: + return result + case .failure(let failure): + return await requestResultWithResponseOnError(error: failure, numberOfRetriesLeft: numberOfRetriesLeft - 1, resource: resource) + } + } catch { + return .failure(.cancelled) } + } else { return .failure(error) } diff --git a/Source/Resource+Decodable.swift b/Source/Resource+Decodable.swift index ed738e8..abfde9f 100644 --- a/Source/Resource+Decodable.swift +++ b/Source/Resource+Decodable.swift @@ -31,12 +31,13 @@ extension Resource where Model: Decodable { /// - Parameters: /// - request: The request to get the remote data payload /// - decoder: a decoder which can decode the payload into the model type - public init(request: URLRequest, decoder: JSONDecoder) { - self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }) + /// - mapError: a closure which maps to Error + public init(request: URLRequest, decoder: JSONDecoder, mapError: @escaping (_ networkError: NetworkError) -> E) { + self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }, mapError: mapError) } } -extension ResourceWithError where Model: Decodable { +extension Resource where Model: Decodable, E: NetworkErrorConvertible { /// Creates an instace of Resource where the result type is `Decodable` and /// can be decoded with the given decoder @@ -45,7 +46,7 @@ extension ResourceWithError where Model: Decodable { /// - request: The request to get the remote data payload /// - decoder: a decoder which can decode the payload into the model type /// - mapError: a closure which maps to Error - public init(request: URLRequest, decoder: JSONDecoder, mapError: @escaping (_ networkError: NetworkError) -> E) { - self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }, mapError: mapError) + public init(request: URLRequest, decoder: JSONDecoder) { + self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }, mapError: { E(networkError: $0) }) } } diff --git a/Source/Resource+Inspect.swift b/Source/Resource+Inspect.swift index 335b484..38ac046 100644 --- a/Source/Resource+Inspect.swift +++ b/Source/Resource+Inspect.swift @@ -27,7 +27,7 @@ extension Resource { This lets one inspect the data payload before data gets parsed. ```swift - let resource: Resource = // + let resource: Resource = // resource.inspectData { data in print(String(bytes: data, encoding: .utf8)) } @@ -36,11 +36,12 @@ extension Resource { - parameter inspector: closure which gets passed the data - returns: a new resource which gets instepcted before parsing */ - public func inspectData(_ inspector: @escaping (Data) -> Void) -> Resource { - return Resource(request: request, parse: { data in + public func inspectData(_ inspector: @escaping (Data) -> Void) -> Resource { + let parse: (Data) throws -> Model = { data in inspector(data) return try self.parse(data) - }) + } + return Resource(request: request, parse: parse, mapError: mapError) } } diff --git a/Source/Resource+Map.swift b/Source/Resource+Map.swift index 5ef7ae3..21f99a7 100644 --- a/Source/Resource+Map.swift +++ b/Source/Resource+Map.swift @@ -27,27 +27,10 @@ extension Resource { /// /// - Parameter transform: transforms the original result of the resource /// - Returns: the transformed resource - public func map(transform: @escaping (Model) throws -> T) -> Resource { - return Resource(request: request, parse: { data in - return try transform(try self.parse(data)) - }) - } -} - -extension ResourceWithError { - - /// Maps a resource result to a different resource. This is useful when you have result of R which contains T and your API request a resource of T, - /// - /// Error parsing is not changed - /// - /// - Parameter transform: transforms the original result of the resource - /// - Returns: the transformed resource - public func map(transform: @escaping (Model) throws -> T) -> ResourceWithError { - return ResourceWithError( + public func map(transform: @escaping (Model) throws -> T) -> Resource { + return Resource( request: request, - parse: { data in - return try transform(try self.parse(data)) - }, + parse: { return try transform(try self.parse($0)) }, mapError: mapError ) } diff --git a/Source/Resource+Void.swift b/Source/Resource+Void.swift index 9c961c0..f4b2063 100644 --- a/Source/Resource+Void.swift +++ b/Source/Resource+Void.swift @@ -13,19 +13,20 @@ public extension Resource where Model == Void { /// /// - Parameters: /// - request: The request to get the remote data payload - init(request: URLRequest) { - self.init(request: request, parse: { _ in }) + /// - mapError: a closure which maps to Error + init(request: URLRequest, mapError: @escaping (_ networkError: NetworkError) -> E) { + self.init(request: request, parse: { _ in }, mapError: mapError) } } -extension ResourceWithError where Model == Void { +public extension Resource where Model == Void, E: NetworkErrorConvertible { /// Creates an instace of Resource where the result type is `Void` /// /// - Parameters: /// - request: The request to get the remote data payload - /// - mapError: a closure which maps to Error - public init(request: URLRequest, mapError: @escaping (_ networkError: NetworkError) -> E) { - self.init(request: request, parse: { _ in }, mapError: mapError) + init(request: URLRequest) { + self.init(request: request, parse: { _ in }, mapError: { E(networkError: $0) }) } + } diff --git a/Source/Resource.swift b/Source/Resource.swift index 9f8b9e5..aa1015d 100644 --- a/Source/Resource.swift +++ b/Source/Resource.swift @@ -30,25 +30,28 @@ import Foundation **Example**: ```swift let request: URLRequest = // - let resource: Resource = Resource(request: request, parse: { data in + let resource: Resource = Resource(request: request, parse: { data in String(data: data, encoding: .utf8) }) ``` */ -public struct Resource { +public struct Resource { /// The request to fetch the resource remote payload public let request: URLRequest - + /// Parses data into given model. public let parse: (_ data: Data) throws -> Model - - /// Creates a type safe resource, which can be used to fetch it with `NetworkService` + public let mapError: (_ networkError: NetworkError) -> E + + /// Creates a type safe resource, which can be used to fetch it with NetworkService /// /// - Parameters: - /// - request: The request to get the remote data payload - /// - parse: Parses data fetched with the request into given Model - public init(request: URLRequest, parse: @escaping (Data) throws -> Model) { + /// - request: The request to get the remote data payload + /// - parse: Parses data fetched with the request into given Model + + public init(request: URLRequest, parse: @escaping (Data) throws -> Model, mapError: @escaping (_ networkError: NetworkError) -> E) { self.request = request self.parse = parse + self.mapError = mapError } } diff --git a/Source/ResourceWithError+NetworkErrorConvertible.swift b/Source/ResourceWithError+NetworkErrorConvertible.swift index 1eff5fa..f230e84 100644 --- a/Source/ResourceWithError+NetworkErrorConvertible.swift +++ b/Source/ResourceWithError+NetworkErrorConvertible.swift @@ -7,7 +7,7 @@ import Foundation -public extension ResourceWithError where E: NetworkErrorConvertible { +public extension Resource where E: NetworkErrorConvertible { init(request: URLRequest, parse: @escaping (Data) throws -> Model) { self.request = request diff --git a/Source/ResourceWithError.swift b/Source/ResourceWithError.swift index 9cfca39..0fe9faa 100644 --- a/Source/ResourceWithError.swift +++ b/Source/ResourceWithError.swift @@ -37,23 +37,4 @@ import Foundation }) ``` */ -public struct ResourceWithError { - /// The request to fetch the resource remote payload - public let request: URLRequest - - /// Parses data into given model. - public let parse: (_ data: Data) throws -> Model - public let mapError: (_ networkError: NetworkError) -> E - - /// Creates a type safe resource, which can be used to fetch it with NetworkService - /// - /// - Parameters: - /// - request: The request to get the remote data payload - /// - parse: Parses data fetched with the request into given Model - - public init(request: URLRequest, parse: @escaping (Data) throws -> Model, mapError: @escaping (_ networkError: NetworkError) -> E) { - self.request = request - self.parse = parse - self.mapError = mapError - } -} + diff --git a/Tests/DecodableResoureTest.swift b/Tests/DecodableResoureTest.swift index e61538a..b70be21 100644 --- a/Tests/DecodableResoureTest.swift +++ b/Tests/DecodableResoureTest.swift @@ -26,9 +26,9 @@ import XCTest @testable import DBNetworkStack class DecodableResoureTest: XCTestCase { - var resource: Resource { + var resource: Resource { let request = URLRequest(path: "/train", baseURL: .defaultMock) - return Resource(request: request, decoder: JSONDecoder()) + return Resource(request: request, decoder: JSONDecoder()) } func testResource_withValidData() { diff --git a/Tests/ModifyRequestNetworkService.swift b/Tests/ModifyRequestNetworkService.swift index c2bd3b8..c25dde6 100644 --- a/Tests/ModifyRequestNetworkService.swift +++ b/Tests/ModifyRequestNetworkService.swift @@ -35,7 +35,7 @@ class ModifyRequestNetworkServiceTest: XCTestCase { ] let networkService = ModifyRequestNetworkService(networkService: networkServiceMock, requestModifications: modification) let request = URLRequest(path: "/trains", baseURL: .defaultMock) - let resource = Resource(request: request, parse: { _ in return 1 }) + let resource = Resource(request: request, parse: { _ in return 1 }) //When await networkService.requestResult(for: resource) diff --git a/Tests/NetworkServiceMockTest.swift b/Tests/NetworkServiceMockTest.swift index 7d10064..f6f777e 100644 --- a/Tests/NetworkServiceMockTest.swift +++ b/Tests/NetworkServiceMockTest.swift @@ -27,7 +27,7 @@ import DBNetworkStack class NetworkServiceMockTest: XCTestCase { - let resource: Resource = Resource(request: URLRequest(path: "train", baseURL: .defaultMock), decoder: JSONDecoder()) + let resource: Resource = Resource(request: URLRequest(path: "train", baseURL: .defaultMock), decoder: JSONDecoder()) func testRequestCount() async throws { //Given diff --git a/Tests/NetworkServiceTest.swift b/Tests/NetworkServiceTest.swift index ddc9467..338c204 100644 --- a/Tests/NetworkServiceTest.swift +++ b/Tests/NetworkServiceTest.swift @@ -29,7 +29,7 @@ class NetworkServiceTest: XCTestCase { let trainName = "ICE" - var resource: Resource { + var resource: Resource { let request = URLRequest(path: "train", baseURL: .defaultMock) return Resource(request: request, decoder: JSONDecoder()) } diff --git a/Tests/NetworkServiceWithErrorTest.swift b/Tests/NetworkServiceWithErrorTest.swift index 95b607e..e1567ea 100644 --- a/Tests/NetworkServiceWithErrorTest.swift +++ b/Tests/NetworkServiceWithErrorTest.swift @@ -37,9 +37,9 @@ class NetworkServiceWithErrorTest: XCTestCase { let trainName = "ICE" - var resource: ResourceWithError { + var resource: Resource { let request = URLRequest(path: "train", baseURL: .defaultMock) - return ResourceWithError(request: request, decoder: JSONDecoder(), mapError: { CustomError(networkError: $0) }) + return Resource(request: request, decoder: JSONDecoder(), mapError: { CustomError(networkError: $0) }) } diff --git a/Tests/ResourceInspectTest.swift b/Tests/ResourceInspectTest.swift index ba8540d..26b10bd 100644 --- a/Tests/ResourceInspectTest.swift +++ b/Tests/ResourceInspectTest.swift @@ -29,7 +29,7 @@ final class ResourceInspectTest: XCTestCase { let data = Data() var capuredParsingData: Data? var capturedInspectedData: Data? - let resource = Resource(request: URLRequest.defaultMock, parse: { data in + let resource = Resource(request: URLRequest.defaultMock, parse: { data in capuredParsingData = data return 1 }) diff --git a/Tests/ResourceTest.swift b/Tests/ResourceTest.swift index badc99a..ed9fc22 100644 --- a/Tests/ResourceTest.swift +++ b/Tests/ResourceTest.swift @@ -31,8 +31,8 @@ class ResourceTest: XCTestCase { //Given let validData: Data! = "ICE".data(using: .utf8) - let resource = Resource(request: URLRequest.defaultMock, parse: { String(data: $0, encoding: .utf8) }) - + let resource = Resource(request: URLRequest.defaultMock, parse: { String(data: $0, encoding: .utf8) }) + //When let name = try resource.parse(validData) @@ -42,7 +42,7 @@ class ResourceTest: XCTestCase { func testResourceWithVoidResult() throws { //Given - let resource = Resource(request: URLRequest.defaultMock) + let resource = Resource(request: URLRequest.defaultMock) //When try resource.parse(Data()) diff --git a/Tests/ResourceWithErrorTest.swift b/Tests/ResourceWithErrorTest.swift index d83bc41..ab81d4c 100644 --- a/Tests/ResourceWithErrorTest.swift +++ b/Tests/ResourceWithErrorTest.swift @@ -31,7 +31,7 @@ class ResourceWithErrorTest: XCTestCase { //Given let validData: Data! = "ICE".data(using: .utf8) - let resource = ResourceWithError( + let resource = Resource( request: URLRequest.defaultMock, parse: { String(data: $0, encoding: .utf8) }, mapError: { $0 } @@ -48,7 +48,7 @@ class ResourceWithErrorTest: XCTestCase { //Given let validData: Data! = "ICE".data(using: .utf8) - let resource = ResourceWithError( + let resource = Resource( request: URLRequest.defaultMock, parse: { String(data: $0, encoding: .utf8) }, mapError: { $0 } @@ -66,7 +66,7 @@ class ResourceWithErrorTest: XCTestCase { enum CustomError: Error{ case error } - let resource = ResourceWithError( + let resource = Resource( request: URLRequest.defaultMock, mapError: { _ in return .error } ) @@ -80,7 +80,7 @@ class ResourceWithErrorTest: XCTestCase { func testResourceWithVoidResult() throws { //Given - let resource = ResourceWithError( + let resource = Resource( request: URLRequest.defaultMock, mapError: { _ in return .error } ) diff --git a/Tests/RetryNetworkserviceTest.swift b/Tests/RetryNetworkserviceTest.swift index b606279..ad588f1 100644 --- a/Tests/RetryNetworkserviceTest.swift +++ b/Tests/RetryNetworkserviceTest.swift @@ -25,7 +25,7 @@ import XCTest @testable import DBNetworkStack class RetryNetworkserviceTest: XCTestCase { - var resource: Resource { + var resource: Resource { let request = URLRequest(path: "/train", baseURL: .defaultMock) return Resource(request: request, parse: { _ in return 1}) } From 94af22f9818e379f9c3e012c44cde4fbc9d5929e Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Tue, 31 Oct 2023 14:20:53 +0100 Subject: [PATCH 08/15] Adds back old mock methods --- .../NetworkServices/NetworkServiceMock.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Source/NetworkServices/NetworkServiceMock.swift b/Source/NetworkServices/NetworkServiceMock.swift index 46f57e2..ada0660 100644 --- a/Source/NetworkServices/NetworkServiceMock.swift +++ b/Source/NetworkServices/NetworkServiceMock.swift @@ -166,6 +166,40 @@ public final actor NetworkServiceMock: NetworkService { return .failure(.serverError(response: nil, data: nil)) } } + + public func schedule(result: Result<(T, HTTPURLResponse), NetworkError>) { + let scheduled: Result<(Data, HTTPURLResponse), NetworkError> + switch result { + case .failure(let error): + scheduled = .failure(error) + case .success((let object, let httpUrlResponse)): + guard let data = try? encoder.encode(object) else { + fatalError("Not able to encode object") + } + scheduled = .success((data, httpUrlResponse)) + } + responses.append(scheduled) + } + + public func schedule(success: Void) { + schedule(result: .success(("", HTTPURLResponse()))) + } + + public func schedule(success: (Void, HTTPURLResponse)) { + schedule(result: .success(("", success.1))) + } + + public func schedule(success: T) { + schedule(result: .success((success, HTTPURLResponse()))) + } + + public func schedule(success: (T, HTTPURLResponse)) { + schedule(result: .success(success)) + } + + public func schedule(failure: NetworkError) { + responses.append(.failure(failure)) + } } fileprivate extension Result { From 3bd1831262845ce75e2094b43929c02eeef155ef Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Sun, 12 Nov 2023 11:55:58 +0100 Subject: [PATCH 09/15] Try unchecked Sendable --- Source/NetworkServices/NetworkServiceMock.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/NetworkServices/NetworkServiceMock.swift b/Source/NetworkServices/NetworkServiceMock.swift index ada0660..9d6b7f8 100644 --- a/Source/NetworkServices/NetworkServiceMock.swift +++ b/Source/NetworkServices/NetworkServiceMock.swift @@ -60,7 +60,7 @@ import Foundation - seealso: `NetworkService` */ -public final actor NetworkServiceMock: NetworkService { +public final class NetworkServiceMock: NetworkService, @unchecked Sendable { public enum Error: Swift.Error, CustomDebugStringConvertible { case missingRequest From 485dafca607856e8f19b99b58adeea098e32e25a Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Wed, 7 Feb 2024 16:40:12 +0100 Subject: [PATCH 10/15] Sync NetworkServiceMock --- Source/NetworkServices/NetworkServiceMock.swift | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/Source/NetworkServices/NetworkServiceMock.swift b/Source/NetworkServices/NetworkServiceMock.swift index 9d6b7f8..068a3be 100644 --- a/Source/NetworkServices/NetworkServiceMock.swift +++ b/Source/NetworkServices/NetworkServiceMock.swift @@ -62,20 +62,6 @@ import Foundation */ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { - public enum Error: Swift.Error, CustomDebugStringConvertible { - case missingRequest - case typeMismatch - - public var debugDescription: String { - switch self { - case .missingRequest: - return "Could not return because no request" - case .typeMismatch: - return "Return type does not match requested type" - } - } - } - /// Count of all started requests public var requestCount: Int { return lastRequests.count @@ -147,6 +133,7 @@ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { - parameter onError: Callback which gets called when fetching or transforming fails. */ + @MainActor public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { lastRequests.append(resource.request) if !responses.isEmpty { From 801a125c95dac0aa8e3f6584b9671bb421652a27 Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Mon, 12 Feb 2024 10:24:45 +0100 Subject: [PATCH 11/15] Adds dynamic mapping to Mock --- .../NetworkServices/NetworkServiceMock.swift | 73 ++++++++++++++----- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/Source/NetworkServices/NetworkServiceMock.swift b/Source/NetworkServices/NetworkServiceMock.swift index 068a3be..f292186 100644 --- a/Source/NetworkServices/NetworkServiceMock.swift +++ b/Source/NetworkServices/NetworkServiceMock.swift @@ -75,7 +75,7 @@ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { /// All executed requests. public private(set) var lastRequests: [URLRequest] = [] - private var responses: [Result<(Data, HTTPURLResponse), NetworkError>] + private var responses: [(String, Result<(Data, HTTPURLResponse), NetworkError>)] private let encoder: JSONEncoder /// Creates an instace of `NetworkServiceMock` @@ -84,7 +84,7 @@ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { encoder: JSONEncoder = JSONEncoder() ) { self.encoder = encoder - self.responses = responses + self.responses = responses.map({ ("*", $0) }) } /// Creates an instace of `NetworkServiceMock` @@ -94,8 +94,8 @@ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { ) { self.encoder = encoder var encodedResponses: [Result<(Data, HTTPURLResponse), NetworkError>] = [] - repeat (each responses).decode(&encodedResponses, encoder: encoder) - self.responses = encodedResponses + repeat encodedResponses.append((each responses).encode(encoder: encoder)) + self.responses = encodedResponses.map({ ("*", $0) }) } /// Creates an instace of `NetworkServiceMock` @@ -105,8 +105,43 @@ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { ) { self.encoder = encoder var encodedResponses: [Result<(Data, HTTPURLResponse), NetworkError>] = [] - repeat (each responses).decode(&encodedResponses, encoder: encoder) - self.responses = encodedResponses + repeat encodedResponses.append((each responses).encode(encoder: encoder)) + self.responses = encodedResponses.map({ ("*", $0) }) + } + + /// Creates an instace of `NetworkServiceMock` + public init( + mappings responses: [(String, Result<(Data, HTTPURLResponse), NetworkError>)], + encoder: JSONEncoder = JSONEncoder() + ) { + self.encoder = encoder + self.responses = responses + } + + /// Creates an instace of `NetworkServiceMock` + public init( + mappings responses: repeat (String, Result<(each T, HTTPURLResponse), NetworkError>), + encoder: JSONEncoder = JSONEncoder() + ) { + self.encoder = encoder + var encodedResponses: [Result<(Data, HTTPURLResponse), NetworkError>] = [] + var paths: [String] = [] + repeat encodedResponses.append((each responses).1.encode(encoder: encoder)) + repeat paths.append((each responses).0) + self.responses = encodedResponses.enumerated().map({ (paths[$0.offset], $0.element) }) + } + + /// Creates an instace of `NetworkServiceMock` + public init( + mappings responses: repeat (String, Result), + encoder: JSONEncoder = JSONEncoder() + ) { + self.encoder = encoder + var encodedResponses: [Result<(Data, HTTPURLResponse), NetworkError>] = [] + var paths: [String] = [] + repeat encodedResponses.append((each responses).1.encode(encoder: encoder)) + repeat paths.append((each responses).0) + self.responses = encodedResponses.enumerated().map({ (paths[$0.offset], $0.element) }) } /** @@ -137,7 +172,13 @@ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { lastRequests.append(resource.request) if !responses.isEmpty { - let scheduled = responses.removeFirst() + let index = responses.firstIndex(where: { + return $0.0 == "*" || $0.0 == resource.request.url?.path + }) + guard let index else { + return .failure(.serverError(response: nil, data: nil)) + } + let scheduled = responses.remove(at: index).1 switch scheduled { case .success((let data, let httpURLResponse)): do { @@ -165,7 +206,7 @@ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { } scheduled = .success((data, httpUrlResponse)) } - responses.append(scheduled) + responses.append(("*", scheduled)) } public func schedule(success: Void) { @@ -185,34 +226,32 @@ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { } public func schedule(failure: NetworkError) { - responses.append(.failure(failure)) + responses.append(("*", .failure(failure))) } } fileprivate extension Result { - func decode( - _ array: inout [Result<(Data, HTTPURLResponse), NetworkError>], + func encode( encoder: JSONEncoder - ) where Success == (T, HTTPURLResponse), Failure == NetworkError { - array.append(self.map({ (try! encoder.encode($0.0), $0.1) })) + ) -> Result<(Data, HTTPURLResponse), NetworkError> where Success == (T, HTTPURLResponse), Failure == NetworkError { + return self.map({ ((try? encoder.encode($0.0)) ?? Data(), $0.1) }) } } fileprivate extension Result where Success: Encodable, Failure == NetworkError { - func decode( - _ array: inout [Result<(Data, HTTPURLResponse), NetworkError>], + func encode( encoder: JSONEncoder - ) { + ) -> Result<(Data, HTTPURLResponse), NetworkError> { let defaultResponse: HTTPURLResponse! = HTTPURLResponse( url: URL(staticString: "bahn.de"), statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil ) - array.append(self.map({ (try! encoder.encode($0), defaultResponse) })) + return self.map({ ((try? encoder.encode($0)) ?? Data(), defaultResponse) }) } } From d9c4dc498e947f57d773ac2c87444b5983bd3ef9 Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Tue, 5 Mar 2024 15:59:50 +0100 Subject: [PATCH 12/15] More Sendable types --- Package.swift | 2 +- Source/HTTPMethod.swift | 2 +- Source/NetworkError.swift | 2 +- .../NetworkServices/NetworkServiceMock.swift | 2 +- Source/Resource+Decodable.swift | 2 +- Source/Resource+Inspect.swift | 4 +- Source/Resource+Map.swift | 2 +- Source/Resource+Void.swift | 2 +- Source/Resource.swift | 8 ++-- ...rceWithError+NetworkErrorConvertible.swift | 2 +- Source/ResourceWithError.swift | 40 ------------------- 11 files changed, 14 insertions(+), 54 deletions(-) delete mode 100644 Source/ResourceWithError.swift diff --git a/Package.swift b/Package.swift index 1b748d7..01a251d 100644 --- a/Package.swift +++ b/Package.swift @@ -48,7 +48,7 @@ let package = Package( dependencies: [], path: "Source", swiftSettings: [ - .enableExperimentalFeature("StrictConcurrencyComplete") + .enableExperimentalFeature("StrictConcurrency") ] ), .testTarget( diff --git a/Source/HTTPMethod.swift b/Source/HTTPMethod.swift index dedec34..d5a3c53 100644 --- a/Source/HTTPMethod.swift +++ b/Source/HTTPMethod.swift @@ -29,7 +29,7 @@ import Foundation See [IETF document](https://tools.ietf.org/html/rfc7231#section-4.3) */ -public enum HTTPMethod: String { +public enum HTTPMethod: String, Sendable { case GET case POST case PUT diff --git a/Source/NetworkError.swift b/Source/NetworkError.swift index fc2052c..4e341a4 100644 --- a/Source/NetworkError.swift +++ b/Source/NetworkError.swift @@ -24,7 +24,7 @@ import Foundation /// `NetworkError` provides a collection of error types which can occur during execution. -public enum NetworkError: Error { +public enum NetworkError: Error, Sendable { /// The error is unkonw case unknownError /// The request was cancelled before it finished diff --git a/Source/NetworkServices/NetworkServiceMock.swift b/Source/NetworkServices/NetworkServiceMock.swift index f292186..1d8c1b3 100644 --- a/Source/NetworkServices/NetworkServiceMock.swift +++ b/Source/NetworkServices/NetworkServiceMock.swift @@ -169,7 +169,7 @@ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { */ @MainActor - public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { + public func requestResultWithResponse(for resource: Resource) async -> Result<(Success, HTTPURLResponse), NetworkError> { lastRequests.append(resource.request) if !responses.isEmpty { let index = responses.firstIndex(where: { diff --git a/Source/Resource+Decodable.swift b/Source/Resource+Decodable.swift index abfde9f..949fb43 100644 --- a/Source/Resource+Decodable.swift +++ b/Source/Resource+Decodable.swift @@ -32,7 +32,7 @@ extension Resource where Model: Decodable { /// - request: The request to get the remote data payload /// - decoder: a decoder which can decode the payload into the model type /// - mapError: a closure which maps to Error - public init(request: URLRequest, decoder: JSONDecoder, mapError: @escaping (_ networkError: NetworkError) -> E) { + public init(request: URLRequest, decoder: JSONDecoder, mapError: @escaping @Sendable (_ networkError: NetworkError) -> E) { self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }, mapError: mapError) } } diff --git a/Source/Resource+Inspect.swift b/Source/Resource+Inspect.swift index 38ac046..8c9a56e 100644 --- a/Source/Resource+Inspect.swift +++ b/Source/Resource+Inspect.swift @@ -36,8 +36,8 @@ extension Resource { - parameter inspector: closure which gets passed the data - returns: a new resource which gets instepcted before parsing */ - public func inspectData(_ inspector: @escaping (Data) -> Void) -> Resource { - let parse: (Data) throws -> Model = { data in + public func inspectData(_ inspector: @escaping @Sendable (Data) -> Void) -> Resource { + let parse: @Sendable (Data) throws -> Model = { data in inspector(data) return try self.parse(data) } diff --git a/Source/Resource+Map.swift b/Source/Resource+Map.swift index 21f99a7..f33df20 100644 --- a/Source/Resource+Map.swift +++ b/Source/Resource+Map.swift @@ -27,7 +27,7 @@ extension Resource { /// /// - Parameter transform: transforms the original result of the resource /// - Returns: the transformed resource - public func map(transform: @escaping (Model) throws -> T) -> Resource { + public func map(transform: @escaping @Sendable (Model) throws -> T) -> Resource { return Resource( request: request, parse: { return try transform(try self.parse($0)) }, diff --git a/Source/Resource+Void.swift b/Source/Resource+Void.swift index f4b2063..06e3ccd 100644 --- a/Source/Resource+Void.swift +++ b/Source/Resource+Void.swift @@ -14,7 +14,7 @@ public extension Resource where Model == Void { /// - Parameters: /// - request: The request to get the remote data payload /// - mapError: a closure which maps to Error - init(request: URLRequest, mapError: @escaping (_ networkError: NetworkError) -> E) { + init(request: URLRequest, mapError: @escaping @Sendable (_ networkError: NetworkError) -> E) { self.init(request: request, parse: { _ in }, mapError: mapError) } } diff --git a/Source/Resource.swift b/Source/Resource.swift index aa1015d..c0074cb 100644 --- a/Source/Resource.swift +++ b/Source/Resource.swift @@ -35,13 +35,13 @@ import Foundation }) ``` */ -public struct Resource { +public struct Resource: Sendable { /// The request to fetch the resource remote payload public let request: URLRequest /// Parses data into given model. - public let parse: (_ data: Data) throws -> Model - public let mapError: (_ networkError: NetworkError) -> E + public let parse: @Sendable (_ data: Data) throws -> Model + public let mapError: @Sendable (_ networkError: NetworkError) -> E /// Creates a type safe resource, which can be used to fetch it with NetworkService /// @@ -49,7 +49,7 @@ public struct Resource { /// - request: The request to get the remote data payload /// - parse: Parses data fetched with the request into given Model - public init(request: URLRequest, parse: @escaping (Data) throws -> Model, mapError: @escaping (_ networkError: NetworkError) -> E) { + public init(request: URLRequest, parse: @escaping @Sendable (Data) throws -> Model, mapError: @escaping @Sendable (_ networkError: NetworkError) -> E) { self.request = request self.parse = parse self.mapError = mapError diff --git a/Source/ResourceWithError+NetworkErrorConvertible.swift b/Source/ResourceWithError+NetworkErrorConvertible.swift index f230e84..920f53f 100644 --- a/Source/ResourceWithError+NetworkErrorConvertible.swift +++ b/Source/ResourceWithError+NetworkErrorConvertible.swift @@ -9,7 +9,7 @@ import Foundation public extension Resource where E: NetworkErrorConvertible { - init(request: URLRequest, parse: @escaping (Data) throws -> Model) { + init(request: URLRequest, parse: @escaping @Sendable (Data) throws -> Model) { self.request = request self.parse = parse self.mapError = { E(networkError: $0) } diff --git a/Source/ResourceWithError.swift b/Source/ResourceWithError.swift deleted file mode 100644 index 0fe9faa..0000000 --- a/Source/ResourceWithError.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright (C) 2021 DB Systel GmbH. -// DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -// - -import Foundation - -/** - `ResourceWithError` describes a remote resource of generic type and generic error. - The type can be fetched via HTTP(S) and parsed into the coresponding model object. - - **Example**: - ```swift - let request: URLRequest = // - let resource: ResourceWithError = Resource(request: request, parse: { data in - String(data: data, encoding: .utf8) - }, mapError: { networkError in - return CustomError(networkError) - }) - ``` - */ - From e7866bfb110483b79aea8190e4fb6cf479f478fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20H=C3=B6reth?= Date: Tue, 2 Apr 2024 15:22:38 +0200 Subject: [PATCH 13/15] Added: HTTPURLResponse to: NetworkError.serializationError(let error, let response, let data). --- Source/NetworkError.swift | 18 ++++---- .../NetworkServices/BasicNetworkService.swift | 4 +- .../NetworkServices/NetworkServiceMock.swift | 2 +- Source/Resource+Decodable.swift | 14 ++++++- Source/Resource+Inspect.swift | 8 ++-- Source/Resource+Map.swift | 4 +- Source/Resource+Void.swift | 4 +- Source/Resource.swift | 4 +- ...rceWithError+NetworkErrorConvertible.swift | 2 +- Tests/DecodableResoureTest.swift | 10 ++--- Tests/ModifyRequestNetworkService.swift | 6 +-- Tests/NetworkErrorTest.swift | 15 +++---- Tests/NetworkServiceMockTest.swift | 4 +- Tests/NetworkServiceTest.swift | 2 +- Tests/ResourceInspectTest.swift | 41 ++++++++++++++----- Tests/ResourceTest.swift | 8 ++-- Tests/ResourceWithErrorTest.swift | 12 +++--- Tests/RetryNetworkserviceTest.swift | 3 +- 18 files changed, 96 insertions(+), 65 deletions(-) diff --git a/Source/NetworkError.swift b/Source/NetworkError.swift index 4e341a4..11e5d91 100644 --- a/Source/NetworkError.swift +++ b/Source/NetworkError.swift @@ -36,7 +36,7 @@ public enum NetworkError: Error, Sendable { /// Error on the server (HTTP Error 500...511) case serverError(response: HTTPURLResponse?, data: Data?) /// Parsing the body into expected type failed. - case serializationError(error: Error, data: Data?) + case serializationError(error: Error, response: HTTPURLResponse, data: Data?) /// Complete request failed. case requestError(error: Error) @@ -75,21 +75,21 @@ extension NetworkError: CustomDebugStringConvertible { case .cancelled: return "Request cancelled" case .unauthorized(let response, let data): - return "Authorization error: \(response), response: ".appendingContentsOf(data: data) + return "Authorization error, response headers: \(response), response body: ".appendingContentsOf(data: data) case .clientError(let response, let data): if let response = response { - return "Client error: \((response)), response: ".appendingContentsOf(data: data) + return "Client error, response headers: \((response)), response body: ".appendingContentsOf(data: data) } - return "Client error, response: ".appendingContentsOf(data: data) - case .serializationError(let description, let data): - return "Serialization error: \(description), response: ".appendingContentsOf(data: data) + return "Client error, response headers: nil, response body: ".appendingContentsOf(data: data) + case .serializationError(let error, let response, let data): + return "Serialization error: \(error), response headers: \(response), response body: ".appendingContentsOf(data: data) case .requestError(let error): return "Request error: \(error)" case .serverError(let response, let data): - if let response = response { - return "Server error: \(String(describing: response)), response: ".appendingContentsOf(data: data) + if let response { + return "Server error, response headers: \(String(describing: response)), response body: ".appendingContentsOf(data: data) } else { - return "Server error: nil, response: ".appendingContentsOf(data: data) + return "Server error: nil, response body: ".appendingContentsOf(data: data) } } } diff --git a/Source/NetworkServices/BasicNetworkService.swift b/Source/NetworkServices/BasicNetworkService.swift index ec91b10..fba67b4 100644 --- a/Source/NetworkServices/BasicNetworkService.swift +++ b/Source/NetworkServices/BasicNetworkService.swift @@ -84,9 +84,9 @@ public final class BasicNetworkService: NetworkService { } do { - return .success((try resource.parse(data), response)) + return .success((try resource.parse(response, data), response)) } catch let error { - return .failure(.serializationError(error: error, data: data)) + return .failure(.serializationError(error: error, response: response, data: data)) } } catch let error { if case URLError.cancelled = error { diff --git a/Source/NetworkServices/NetworkServiceMock.swift b/Source/NetworkServices/NetworkServiceMock.swift index 1d8c1b3..1f533ce 100644 --- a/Source/NetworkServices/NetworkServiceMock.swift +++ b/Source/NetworkServices/NetworkServiceMock.swift @@ -182,7 +182,7 @@ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { switch scheduled { case .success((let data, let httpURLResponse)): do { - let result = try resource.parse(data) + let result = try resource.parse(httpURLResponse, data) return .success((result, httpURLResponse)) } catch { fatalError("Not able to parse data. Error: \(error)") diff --git a/Source/Resource+Decodable.swift b/Source/Resource+Decodable.swift index 949fb43..39a689d 100644 --- a/Source/Resource+Decodable.swift +++ b/Source/Resource+Decodable.swift @@ -33,7 +33,11 @@ extension Resource where Model: Decodable { /// - decoder: a decoder which can decode the payload into the model type /// - mapError: a closure which maps to Error public init(request: URLRequest, decoder: JSONDecoder, mapError: @escaping @Sendable (_ networkError: NetworkError) -> E) { - self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }, mapError: mapError) + self.init(request: request, parse: { + try decoder.decode(Model.self, from: $1) + }, + mapError: mapError + ) } } @@ -47,6 +51,12 @@ extension Resource where Model: Decodable, E: NetworkErrorConvertible { /// - decoder: a decoder which can decode the payload into the model type /// - mapError: a closure which maps to Error public init(request: URLRequest, decoder: JSONDecoder) { - self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }, mapError: { E(networkError: $0) }) + self.init(request: request, parse: { + try decoder.decode(Model.self, from: $1) + }, + mapError: { + E(networkError: $0) + } + ) } } diff --git a/Source/Resource+Inspect.swift b/Source/Resource+Inspect.swift index 8c9a56e..3b10b4d 100644 --- a/Source/Resource+Inspect.swift +++ b/Source/Resource+Inspect.swift @@ -36,10 +36,10 @@ extension Resource { - parameter inspector: closure which gets passed the data - returns: a new resource which gets instepcted before parsing */ - public func inspectData(_ inspector: @escaping @Sendable (Data) -> Void) -> Resource { - let parse: @Sendable (Data) throws -> Model = { data in - inspector(data) - return try self.parse(data) + public func inspectData(_ inspector: @escaping @Sendable (HTTPURLResponse, Data) -> Void) -> Resource { + let parse: @Sendable (HTTPURLResponse, Data) throws -> Model = { response, data in + inspector(response, data) + return try self.parse(response, data) } return Resource(request: request, parse: parse, mapError: mapError) } diff --git a/Source/Resource+Map.swift b/Source/Resource+Map.swift index f33df20..662ad53 100644 --- a/Source/Resource+Map.swift +++ b/Source/Resource+Map.swift @@ -30,7 +30,9 @@ extension Resource { public func map(transform: @escaping @Sendable (Model) throws -> T) -> Resource { return Resource( request: request, - parse: { return try transform(try self.parse($0)) }, + parse: { response, data in + return try transform(try self.parse(response, data)) + }, mapError: mapError ) } diff --git a/Source/Resource+Void.swift b/Source/Resource+Void.swift index 06e3ccd..1b6ec9c 100644 --- a/Source/Resource+Void.swift +++ b/Source/Resource+Void.swift @@ -15,7 +15,7 @@ public extension Resource where Model == Void { /// - request: The request to get the remote data payload /// - mapError: a closure which maps to Error init(request: URLRequest, mapError: @escaping @Sendable (_ networkError: NetworkError) -> E) { - self.init(request: request, parse: { _ in }, mapError: mapError) + self.init(request: request, parse: { _, _ in }, mapError: mapError) } } @@ -26,7 +26,7 @@ public extension Resource where Model == Void, E: NetworkErrorConvertible { /// - Parameters: /// - request: The request to get the remote data payload init(request: URLRequest) { - self.init(request: request, parse: { _ in }, mapError: { E(networkError: $0) }) + self.init(request: request, parse: { _, _ in }, mapError: { E(networkError: $0) }) } } diff --git a/Source/Resource.swift b/Source/Resource.swift index c0074cb..a3c75de 100644 --- a/Source/Resource.swift +++ b/Source/Resource.swift @@ -40,7 +40,7 @@ public struct Resource: Sendable { public let request: URLRequest /// Parses data into given model. - public let parse: @Sendable (_ data: Data) throws -> Model + public let parse: @Sendable (_ response: HTTPURLResponse, _ data: Data) throws -> Model public let mapError: @Sendable (_ networkError: NetworkError) -> E /// Creates a type safe resource, which can be used to fetch it with NetworkService @@ -49,7 +49,7 @@ public struct Resource: Sendable { /// - request: The request to get the remote data payload /// - parse: Parses data fetched with the request into given Model - public init(request: URLRequest, parse: @escaping @Sendable (Data) throws -> Model, mapError: @escaping @Sendable (_ networkError: NetworkError) -> E) { + public init(request: URLRequest, parse: @escaping @Sendable (HTTPURLResponse, Data) throws -> Model, mapError: @escaping @Sendable (_ networkError: NetworkError) -> E) { self.request = request self.parse = parse self.mapError = mapError diff --git a/Source/ResourceWithError+NetworkErrorConvertible.swift b/Source/ResourceWithError+NetworkErrorConvertible.swift index 920f53f..d3f500a 100644 --- a/Source/ResourceWithError+NetworkErrorConvertible.swift +++ b/Source/ResourceWithError+NetworkErrorConvertible.swift @@ -9,7 +9,7 @@ import Foundation public extension Resource where E: NetworkErrorConvertible { - init(request: URLRequest, parse: @escaping @Sendable (Data) throws -> Model) { + init(request: URLRequest, parse: @escaping @Sendable (HTTPURLResponse, Data) throws -> Model) { self.request = request self.parse = parse self.mapError = { E(networkError: $0) } diff --git a/Tests/DecodableResoureTest.swift b/Tests/DecodableResoureTest.swift index b70be21..ae0df7c 100644 --- a/Tests/DecodableResoureTest.swift +++ b/Tests/DecodableResoureTest.swift @@ -33,8 +33,8 @@ class DecodableResoureTest: XCTestCase { func testResource_withValidData() { //When - let fetchedTrain = try? resource.parse(Train.validJSONData) - + let fetchedTrain = try? resource.parse(HTTPURLResponse.defaultMock, Train.validJSONData) + //Then XCTAssertEqual(fetchedTrain?.name, "ICE") } @@ -42,8 +42,8 @@ class DecodableResoureTest: XCTestCase { func testResource_withMAppedResult() { //When let nameResource = resource.map { $0.name } - let fetchedTrainName = try? nameResource.parse(Train.validJSONData) - + let fetchedTrainName = try? nameResource.parse(HTTPURLResponse.defaultMock, Train.validJSONData) + //Then XCTAssertEqual(fetchedTrainName, "ICE") } @@ -51,7 +51,7 @@ class DecodableResoureTest: XCTestCase { func testResource_WithInvalidData() throws { //When do { - _ = try resource.parse(Train.invalidJSONData) + _ = try resource.parse(HTTPURLResponse.defaultMock, Train.invalidJSONData) XCTFail("Expected method to throws") } catch { } } diff --git a/Tests/ModifyRequestNetworkService.swift b/Tests/ModifyRequestNetworkService.swift index c25dde6..ab11081 100644 --- a/Tests/ModifyRequestNetworkService.swift +++ b/Tests/ModifyRequestNetworkService.swift @@ -35,13 +35,13 @@ class ModifyRequestNetworkServiceTest: XCTestCase { ] let networkService = ModifyRequestNetworkService(networkService: networkServiceMock, requestModifications: modification) let request = URLRequest(path: "/trains", baseURL: .defaultMock) - let resource = Resource(request: request, parse: { _ in return 1 }) - + let resource = Resource(request: request, parse: { _, _ in return 1 }) + //When await networkService.requestResult(for: resource) //Then - let lastRequest = await networkServiceMock.lastRequest + let lastRequest = networkServiceMock.lastRequest let lastRequestURL = try XCTUnwrap(lastRequest?.url) XCTAssert(lastRequestURL.absoluteString.contains("key=1")) } diff --git a/Tests/NetworkErrorTest.swift b/Tests/NetworkErrorTest.swift index 8f0af49..5dc8a75 100644 --- a/Tests/NetworkErrorTest.swift +++ b/Tests/NetworkErrorTest.swift @@ -135,8 +135,8 @@ class NetworkErrorTest: XCTestCase { let debugDescription = error.debugDescription //Then - XCTAssert(debugDescription.hasPrefix("Authorization error: (request: URLRequest.defaultMock, parse: { data in - capuredParsingData = data + let capturedParsingData = Container(nil) + let capturedInspectedData = Container(nil) + + let resource = Resource(request: URLRequest.defaultMock, parse: { response, data in + capturedParsingData.setValue(data) return 1 }) //When - let inspectedResource = resource.inspectData({ data in - capturedInspectedData = data + let inspectedResource = resource.inspectData({ response, data in + capturedInspectedData.setValue(data) }) - let result = try? inspectedResource.parse(data) - + let result = try? inspectedResource.parse(HTTPURLResponse.defaultMock, data) + //Then XCTAssertNotNil(result) - XCTAssertEqual(capuredParsingData, capturedInspectedData) - XCTAssertEqual(data, capturedInspectedData) - XCTAssertEqual(capuredParsingData, data) + XCTAssertEqual(capturedParsingData.getValue(), capturedInspectedData.getValue()) + XCTAssertEqual(data, capturedInspectedData.getValue()) + XCTAssertEqual(capturedParsingData.getValue(), data) + } +} + +private class Container { + private var value: T + + init(_ value: T) { + self.value = value + } + + /// caller should manage concurrent access to its own state + func setValue(_ newValue: T) { + value = newValue + } + + /// caller should manage concurrent access to its own state + func getValue() -> T { + value } } diff --git a/Tests/ResourceTest.swift b/Tests/ResourceTest.swift index ed9fc22..d1ae136 100644 --- a/Tests/ResourceTest.swift +++ b/Tests/ResourceTest.swift @@ -31,11 +31,11 @@ class ResourceTest: XCTestCase { //Given let validData: Data! = "ICE".data(using: .utf8) - let resource = Resource(request: URLRequest.defaultMock, parse: { String(data: $0, encoding: .utf8) }) + let resource = Resource(request: URLRequest.defaultMock, parse: { String(data: $1, encoding: .utf8) }) //When - let name = try resource.parse(validData) - + let name = try resource.parse(HTTPURLResponse.defaultMock, validData) + //Then XCTAssertEqual(name, "ICE") } @@ -45,7 +45,7 @@ class ResourceTest: XCTestCase { let resource = Resource(request: URLRequest.defaultMock) //When - try resource.parse(Data()) + try resource.parse(HTTPURLResponse.defaultMock, Data()) } } diff --git a/Tests/ResourceWithErrorTest.swift b/Tests/ResourceWithErrorTest.swift index ab81d4c..192ef06 100644 --- a/Tests/ResourceWithErrorTest.swift +++ b/Tests/ResourceWithErrorTest.swift @@ -33,13 +33,13 @@ class ResourceWithErrorTest: XCTestCase { let resource = Resource( request: URLRequest.defaultMock, - parse: { String(data: $0, encoding: .utf8) }, + parse: { String(data: $1, encoding: .utf8) }, mapError: { $0 } ) //When - let name = try resource.parse(validData) - + let name = try resource.parse(HTTPURLResponse.defaultMock, validData) + //Then XCTAssertEqual(name, "ICE") } @@ -50,12 +50,12 @@ class ResourceWithErrorTest: XCTestCase { let resource = Resource( request: URLRequest.defaultMock, - parse: { String(data: $0, encoding: .utf8) }, + parse: { String(data: $1, encoding: .utf8) }, mapError: { $0 } ) //When - let numberOfCharacters = try resource.map(transform: { $0?.count }).parse(validData) + let numberOfCharacters = try resource.map(transform: { $0?.count }).parse(HTTPURLResponse.defaultMock, validData) //Then XCTAssertEqual(numberOfCharacters, 3) @@ -86,7 +86,7 @@ class ResourceWithErrorTest: XCTestCase { ) //When - try resource.parse(Data()) + try resource.parse(HTTPURLResponse.defaultMock, Data()) } } diff --git a/Tests/RetryNetworkserviceTest.swift b/Tests/RetryNetworkserviceTest.swift index ad588f1..5e7c9cf 100644 --- a/Tests/RetryNetworkserviceTest.swift +++ b/Tests/RetryNetworkserviceTest.swift @@ -27,12 +27,11 @@ import XCTest class RetryNetworkserviceTest: XCTestCase { var resource: Resource { let request = URLRequest(path: "/train", baseURL: .defaultMock) - return Resource(request: request, parse: { _ in return 1}) + return Resource(request: request, parse: { _, _ in return 1}) } func testRetryRequest_shouldRetry() async throws { //Given - let errorCount = 2 let numberOfRetries = 2 let networkServiceMock = NetworkServiceMock( Result.failure(.unknownError), From a1775fb6587b9f87c4363c19bd2e558f1cc5b20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20H=C3=B6reth?= Date: Wed, 3 Jul 2024 12:31:20 +0200 Subject: [PATCH 14/15] reverted: HTTPURLResponse does not need to be part of Resource --- Source/NetworkServices/BasicNetworkService.swift | 2 +- Source/NetworkServices/NetworkServiceMock.swift | 2 +- Source/Resource+Decodable.swift | 4 ++-- Source/Resource+Inspect.swift | 8 ++++---- Source/Resource+Map.swift | 4 ++-- Source/Resource+Void.swift | 4 ++-- Source/Resource.swift | 4 ++-- Source/ResourceWithError+NetworkErrorConvertible.swift | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Source/NetworkServices/BasicNetworkService.swift b/Source/NetworkServices/BasicNetworkService.swift index fba67b4..c874653 100644 --- a/Source/NetworkServices/BasicNetworkService.swift +++ b/Source/NetworkServices/BasicNetworkService.swift @@ -84,7 +84,7 @@ public final class BasicNetworkService: NetworkService { } do { - return .success((try resource.parse(response, data), response)) + return .success((try resource.parse(data), response)) } catch let error { return .failure(.serializationError(error: error, response: response, data: data)) } diff --git a/Source/NetworkServices/NetworkServiceMock.swift b/Source/NetworkServices/NetworkServiceMock.swift index 1f533ce..1d8c1b3 100644 --- a/Source/NetworkServices/NetworkServiceMock.swift +++ b/Source/NetworkServices/NetworkServiceMock.swift @@ -182,7 +182,7 @@ public final class NetworkServiceMock: NetworkService, @unchecked Sendable { switch scheduled { case .success((let data, let httpURLResponse)): do { - let result = try resource.parse(httpURLResponse, data) + let result = try resource.parse(data) return .success((result, httpURLResponse)) } catch { fatalError("Not able to parse data. Error: \(error)") diff --git a/Source/Resource+Decodable.swift b/Source/Resource+Decodable.swift index 39a689d..731dfbb 100644 --- a/Source/Resource+Decodable.swift +++ b/Source/Resource+Decodable.swift @@ -34,7 +34,7 @@ extension Resource where Model: Decodable { /// - mapError: a closure which maps to Error public init(request: URLRequest, decoder: JSONDecoder, mapError: @escaping @Sendable (_ networkError: NetworkError) -> E) { self.init(request: request, parse: { - try decoder.decode(Model.self, from: $1) + try decoder.decode(Model.self, from: $0) }, mapError: mapError ) @@ -52,7 +52,7 @@ extension Resource where Model: Decodable, E: NetworkErrorConvertible { /// - mapError: a closure which maps to Error public init(request: URLRequest, decoder: JSONDecoder) { self.init(request: request, parse: { - try decoder.decode(Model.self, from: $1) + try decoder.decode(Model.self, from: $0) }, mapError: { E(networkError: $0) diff --git a/Source/Resource+Inspect.swift b/Source/Resource+Inspect.swift index 3b10b4d..5ad717c 100644 --- a/Source/Resource+Inspect.swift +++ b/Source/Resource+Inspect.swift @@ -36,10 +36,10 @@ extension Resource { - parameter inspector: closure which gets passed the data - returns: a new resource which gets instepcted before parsing */ - public func inspectData(_ inspector: @escaping @Sendable (HTTPURLResponse, Data) -> Void) -> Resource { - let parse: @Sendable (HTTPURLResponse, Data) throws -> Model = { response, data in - inspector(response, data) - return try self.parse(response, data) + public func inspectData(_ inspector: @escaping @Sendable (Data) -> Void) -> Resource { + let parse: @Sendable (Data) throws -> Model = { data in + inspector(data) + return try self.parse(data) } return Resource(request: request, parse: parse, mapError: mapError) } diff --git a/Source/Resource+Map.swift b/Source/Resource+Map.swift index 662ad53..aefb9a8 100644 --- a/Source/Resource+Map.swift +++ b/Source/Resource+Map.swift @@ -30,8 +30,8 @@ extension Resource { public func map(transform: @escaping @Sendable (Model) throws -> T) -> Resource { return Resource( request: request, - parse: { response, data in - return try transform(try self.parse(response, data)) + parse: { data in + return try transform(try self.parse(data)) }, mapError: mapError ) diff --git a/Source/Resource+Void.swift b/Source/Resource+Void.swift index 1b6ec9c..06e3ccd 100644 --- a/Source/Resource+Void.swift +++ b/Source/Resource+Void.swift @@ -15,7 +15,7 @@ public extension Resource where Model == Void { /// - request: The request to get the remote data payload /// - mapError: a closure which maps to Error init(request: URLRequest, mapError: @escaping @Sendable (_ networkError: NetworkError) -> E) { - self.init(request: request, parse: { _, _ in }, mapError: mapError) + self.init(request: request, parse: { _ in }, mapError: mapError) } } @@ -26,7 +26,7 @@ public extension Resource where Model == Void, E: NetworkErrorConvertible { /// - Parameters: /// - request: The request to get the remote data payload init(request: URLRequest) { - self.init(request: request, parse: { _, _ in }, mapError: { E(networkError: $0) }) + self.init(request: request, parse: { _ in }, mapError: { E(networkError: $0) }) } } diff --git a/Source/Resource.swift b/Source/Resource.swift index a3c75de..c0074cb 100644 --- a/Source/Resource.swift +++ b/Source/Resource.swift @@ -40,7 +40,7 @@ public struct Resource: Sendable { public let request: URLRequest /// Parses data into given model. - public let parse: @Sendable (_ response: HTTPURLResponse, _ data: Data) throws -> Model + public let parse: @Sendable (_ data: Data) throws -> Model public let mapError: @Sendable (_ networkError: NetworkError) -> E /// Creates a type safe resource, which can be used to fetch it with NetworkService @@ -49,7 +49,7 @@ public struct Resource: Sendable { /// - request: The request to get the remote data payload /// - parse: Parses data fetched with the request into given Model - public init(request: URLRequest, parse: @escaping @Sendable (HTTPURLResponse, Data) throws -> Model, mapError: @escaping @Sendable (_ networkError: NetworkError) -> E) { + public init(request: URLRequest, parse: @escaping @Sendable (Data) throws -> Model, mapError: @escaping @Sendable (_ networkError: NetworkError) -> E) { self.request = request self.parse = parse self.mapError = mapError diff --git a/Source/ResourceWithError+NetworkErrorConvertible.swift b/Source/ResourceWithError+NetworkErrorConvertible.swift index d3f500a..920f53f 100644 --- a/Source/ResourceWithError+NetworkErrorConvertible.swift +++ b/Source/ResourceWithError+NetworkErrorConvertible.swift @@ -9,7 +9,7 @@ import Foundation public extension Resource where E: NetworkErrorConvertible { - init(request: URLRequest, parse: @escaping @Sendable (HTTPURLResponse, Data) throws -> Model) { + init(request: URLRequest, parse: @escaping @Sendable (Data) throws -> Model) { self.request = request self.parse = parse self.mapError = { E(networkError: $0) } From 226c1baa236e614c537364023905954dcdc04939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20H=C3=B6reth?= Date: Wed, 3 Jul 2024 12:38:02 +0200 Subject: [PATCH 15/15] reverted: HTTPURLResponse does not need to be part of Resource --- Tests/DecodableResoureTest.swift | 6 +++--- Tests/ModifyRequestNetworkService.swift | 2 +- Tests/ResourceInspectTest.swift | 6 +++--- Tests/ResourceTest.swift | 6 +++--- Tests/ResourceWithErrorTest.swift | 10 +++++----- Tests/RetryNetworkserviceTest.swift | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Tests/DecodableResoureTest.swift b/Tests/DecodableResoureTest.swift index ae0df7c..2282fbb 100644 --- a/Tests/DecodableResoureTest.swift +++ b/Tests/DecodableResoureTest.swift @@ -33,7 +33,7 @@ class DecodableResoureTest: XCTestCase { func testResource_withValidData() { //When - let fetchedTrain = try? resource.parse(HTTPURLResponse.defaultMock, Train.validJSONData) + let fetchedTrain = try? resource.parse(Train.validJSONData) //Then XCTAssertEqual(fetchedTrain?.name, "ICE") @@ -42,7 +42,7 @@ class DecodableResoureTest: XCTestCase { func testResource_withMAppedResult() { //When let nameResource = resource.map { $0.name } - let fetchedTrainName = try? nameResource.parse(HTTPURLResponse.defaultMock, Train.validJSONData) + let fetchedTrainName = try? nameResource.parse(Train.validJSONData) //Then XCTAssertEqual(fetchedTrainName, "ICE") @@ -51,7 +51,7 @@ class DecodableResoureTest: XCTestCase { func testResource_WithInvalidData() throws { //When do { - _ = try resource.parse(HTTPURLResponse.defaultMock, Train.invalidJSONData) + _ = try resource.parse(Train.invalidJSONData) XCTFail("Expected method to throws") } catch { } } diff --git a/Tests/ModifyRequestNetworkService.swift b/Tests/ModifyRequestNetworkService.swift index ab11081..9133082 100644 --- a/Tests/ModifyRequestNetworkService.swift +++ b/Tests/ModifyRequestNetworkService.swift @@ -35,7 +35,7 @@ class ModifyRequestNetworkServiceTest: XCTestCase { ] let networkService = ModifyRequestNetworkService(networkService: networkServiceMock, requestModifications: modification) let request = URLRequest(path: "/trains", baseURL: .defaultMock) - let resource = Resource(request: request, parse: { _, _ in return 1 }) + let resource = Resource(request: request, parse: { _ in return 1 }) //When await networkService.requestResult(for: resource) diff --git a/Tests/ResourceInspectTest.swift b/Tests/ResourceInspectTest.swift index 812e4a1..b0d615e 100644 --- a/Tests/ResourceInspectTest.swift +++ b/Tests/ResourceInspectTest.swift @@ -30,16 +30,16 @@ final class ResourceInspectTest: XCTestCase { let capturedParsingData = Container(nil) let capturedInspectedData = Container(nil) - let resource = Resource(request: URLRequest.defaultMock, parse: { response, data in + let resource = Resource(request: URLRequest.defaultMock, parse: { data in capturedParsingData.setValue(data) return 1 }) //When - let inspectedResource = resource.inspectData({ response, data in + let inspectedResource = resource.inspectData({ data in capturedInspectedData.setValue(data) }) - let result = try? inspectedResource.parse(HTTPURLResponse.defaultMock, data) + let result = try? inspectedResource.parse(data) //Then XCTAssertNotNil(result) diff --git a/Tests/ResourceTest.swift b/Tests/ResourceTest.swift index d1ae136..c4ecd43 100644 --- a/Tests/ResourceTest.swift +++ b/Tests/ResourceTest.swift @@ -31,10 +31,10 @@ class ResourceTest: XCTestCase { //Given let validData: Data! = "ICE".data(using: .utf8) - let resource = Resource(request: URLRequest.defaultMock, parse: { String(data: $1, encoding: .utf8) }) + let resource = Resource(request: URLRequest.defaultMock, parse: { String(data: $0, encoding: .utf8) }) //When - let name = try resource.parse(HTTPURLResponse.defaultMock, validData) + let name = try resource.parse(validData) //Then XCTAssertEqual(name, "ICE") @@ -45,7 +45,7 @@ class ResourceTest: XCTestCase { let resource = Resource(request: URLRequest.defaultMock) //When - try resource.parse(HTTPURLResponse.defaultMock, Data()) + try resource.parse(Data()) } } diff --git a/Tests/ResourceWithErrorTest.swift b/Tests/ResourceWithErrorTest.swift index 192ef06..c073f9b 100644 --- a/Tests/ResourceWithErrorTest.swift +++ b/Tests/ResourceWithErrorTest.swift @@ -33,12 +33,12 @@ class ResourceWithErrorTest: XCTestCase { let resource = Resource( request: URLRequest.defaultMock, - parse: { String(data: $1, encoding: .utf8) }, + parse: { String(data: $0, encoding: .utf8) }, mapError: { $0 } ) //When - let name = try resource.parse(HTTPURLResponse.defaultMock, validData) + let name = try resource.parse(validData) //Then XCTAssertEqual(name, "ICE") @@ -50,12 +50,12 @@ class ResourceWithErrorTest: XCTestCase { let resource = Resource( request: URLRequest.defaultMock, - parse: { String(data: $1, encoding: .utf8) }, + parse: { String(data: $0, encoding: .utf8) }, mapError: { $0 } ) //When - let numberOfCharacters = try resource.map(transform: { $0?.count }).parse(HTTPURLResponse.defaultMock, validData) + let numberOfCharacters = try resource.map(transform: { $0?.count }).parse(validData) //Then XCTAssertEqual(numberOfCharacters, 3) @@ -86,7 +86,7 @@ class ResourceWithErrorTest: XCTestCase { ) //When - try resource.parse(HTTPURLResponse.defaultMock, Data()) + try resource.parse(Data()) } } diff --git a/Tests/RetryNetworkserviceTest.swift b/Tests/RetryNetworkserviceTest.swift index 5e7c9cf..0c3a70b 100644 --- a/Tests/RetryNetworkserviceTest.swift +++ b/Tests/RetryNetworkserviceTest.swift @@ -27,7 +27,7 @@ import XCTest class RetryNetworkserviceTest: XCTestCase { var resource: Resource { let request = URLRequest(path: "/train", baseURL: .defaultMock) - return Resource(request: request, parse: { _, _ in return 1}) + return Resource(request: request, parse: { _ in return 1}) } func testRetryRequest_shouldRetry() async throws {