From a0eb35497ab61bfe9768a8356711e9f47c0b277e Mon Sep 17 00:00:00 2001 From: nayanda Date: Tue, 27 Oct 2020 17:41:06 +0700 Subject: [PATCH 01/20] Added LockRunner --- Example/Pods/Pods.xcodeproj/project.pbxproj | 4 ++++ iONess/Classes/Common/LockRunner.swift | 24 +++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 iONess/Classes/Common/LockRunner.swift diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index bfa9773..e47f758 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ 739AB89F25402F3700EFB629 /* NetworkSessionManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739AB89E25402F3700EFB629 /* NetworkSessionManager+Extensions.swift */; }; 739AB8B22540525300EFB629 /* NetworkRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739AB8B12540525300EFB629 /* NetworkRequest.swift */; }; 739AB8B92540529600EFB629 /* DuplicatedHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739AB8B82540529600EFB629 /* DuplicatedHandler.swift */; }; + 73F38E1825482D05005D9644 /* LockRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F38E1725482D05005D9644 /* LockRunner.swift */; }; 7460C7CD0497AAE90DA55B895F47DEC4 /* DSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560E5610625EB24314B48FCA0575670A /* DSL.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; }; 76049DE81EB1E2B6C436086630F77CA0 /* NMBStringify.m in Sources */ = {isa = PBXBuildFile; fileRef = 5270361224437AC02141E27EB379DABF /* NMBStringify.m */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; }; 7A5B1CC9274B3E4E10EE1561F3AFD58E /* Callsite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15C89B2DC4D79FB8F6A690175A35A6DF /* Callsite.swift */; }; @@ -292,6 +293,7 @@ 739AB89E25402F3700EFB629 /* NetworkSessionManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkSessionManager+Extensions.swift"; sourceTree = ""; }; 739AB8B12540525300EFB629 /* NetworkRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequest.swift; sourceTree = ""; }; 739AB8B82540529600EFB629 /* DuplicatedHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuplicatedHandler.swift; sourceTree = ""; }; + 73F38E1725482D05005D9644 /* LockRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockRunner.swift; sourceTree = ""; }; 7C506A3350F263C8D84B9BBEAFE4773F /* PostNotification.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostNotification.swift; path = Sources/Nimble/Matchers/PostNotification.swift; sourceTree = ""; }; 7C70EB7B5190D124F405BEC5994EAE88 /* DSL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DSL.swift; path = Sources/Quick/DSL/DSL.swift; sourceTree = ""; }; 7F91C9989726FD623E3BC9D2301B93CB /* CwlDarwinDefinitions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CwlDarwinDefinitions.swift; path = Carthage/Checkouts/CwlPreconditionTesting/Sources/CwlPreconditionTesting/CwlDarwinDefinitions.swift; sourceTree = ""; }; @@ -511,6 +513,7 @@ 73562EF3253FD2050063EEC3 /* URLCompatible.swift */, 73562EFA253FD2310063EEC3 /* URLRequestBuilder.swift */, 73562F01253FD2580063EEC3 /* URLValidator.swift */, + 73F38E1725482D05005D9644 /* LockRunner.swift */, ); path = Common; sourceTree = ""; @@ -999,6 +1002,7 @@ 73562ED8253FD1530063EEC3 /* NetworkSessionManager.swift in Sources */, 73562F35253FD4910063EEC3 /* DropableAndResumable.swift in Sources */, 73562E9C253FD0070063EEC3 /* NetworkSessionError.swift in Sources */, + 73F38E1825482D05005D9644 /* LockRunner.swift in Sources */, 73562EF4253FD2050063EEC3 /* URLCompatible.swift in Sources */, 73562F02253FD2580063EEC3 /* URLValidator.swift in Sources */, 73562F51253FD4F90063EEC3 /* ResumableDownloadRequest.swift in Sources */, diff --git a/iONess/Classes/Common/LockRunner.swift b/iONess/Classes/Common/LockRunner.swift new file mode 100644 index 0000000..8e1d810 --- /dev/null +++ b/iONess/Classes/Common/LockRunner.swift @@ -0,0 +1,24 @@ +// +// LockRunner.swift +// iONess +// +// Created by Nayanda Haberty (ID) on 27/10/20. +// + +import Foundation + +protocol LockRunner { + var lock: NSLock { get } + func lockedRun(_ runner: () -> Result) -> Result +} + + +extension LockRunner { + func lockedRun(_ runner: () -> Result) -> Result { + lock.lock() + defer { + lock.unlock() + } + return runner() + } +} From b1475486a19590ac17b77a7950973a9c1c5e9407 Mon Sep 17 00:00:00 2001 From: nayanda Date: Tue, 27 Oct 2020 18:41:49 +0700 Subject: [PATCH 02/20] Implement LockRunner --- .../NetworkSessionManager+Extensions.swift | 10 +-- iONess/Classes/Common/RequestAggregator.swift | 73 +++++++++---------- iONess/Classes/Common/RetryControl.swift | 32 ++++---- 3 files changed, 51 insertions(+), 64 deletions(-) diff --git a/iONess/Classes/Common/NetworkSessionManager+Extensions.swift b/iONess/Classes/Common/NetworkSessionManager+Extensions.swift index e864bad..fbc379b 100644 --- a/iONess/Classes/Common/NetworkSessionManager+Extensions.swift +++ b/iONess/Classes/Common/NetworkSessionManager+Extensions.swift @@ -9,7 +9,7 @@ import Foundation public typealias URLCompletion = (Param?, URLResponse?, Error?) -> Void -extension NetworkSessionManager { +extension NetworkSessionManager: LockRunner { func task(for request: URLRequest) -> URLSessionTask? { lockedRun { @@ -69,14 +69,6 @@ extension NetworkSessionManager { } } - func lockedRun(_ runner: () -> Result) -> Result { - lock.lock() - defer { - lock.unlock() - } - return runner() - } - func downloadTask(with request: URLRequest, completionHandler: @escaping URLCompletion) -> URLSessionDownloadTask { let request = delegate?.ness(self, willRequest: request) ?? request defer { diff --git a/iONess/Classes/Common/RequestAggregator.swift b/iONess/Classes/Common/RequestAggregator.swift index b30d3b6..5ccdffc 100644 --- a/iONess/Classes/Common/RequestAggregator.swift +++ b/iONess/Classes/Common/RequestAggregator.swift @@ -71,22 +71,19 @@ public class RequestAggregator: Thenable extension RequestAggregator { - public class Results { - private let lock = NSLock() + public class Results: LockRunner { + let lock = NSLock() public var results: [AggregatedResult] = [] public var isFailed: Bool { - lock.lock() - defer { - lock.unlock() + lockedRun { + results.contains { $0.error != nil } } - return results.contains { $0.error != nil } } public var areCompleted: Bool { - lock.lock() - defer { - lock.unlock() + lockedRun { + results.count == targetCompletedCount + && !results.contains { $0.error != nil } } - return results.count == targetCompletedCount && !isFailed } var targetCompletedCount: Int @@ -95,9 +92,9 @@ extension RequestAggregator { } func add(result: AggregatedResult) { - lock.lock() - results.append(result) - lock.unlock() + lockedRun { + results.append(result) + } } } } @@ -145,31 +142,31 @@ extension DropableRequestAggregator { var responses: [AggregatedResponse] = [] } - class RunningRequests { - private let lock = NSLock() + class RunningRequests: LockRunner { + let lock = NSLock() var runningRequests: [DropableURLRequest] = [] var canceled: Bool = false var targetRunCount: Int var isAllRequestRun: Bool { - runningRequests.count == targetRunCount && !canceled + lockedRun { + runningRequests.count == targetRunCount && !canceled + } } var progress: Float { - lock.lock() - defer { - lock.unlock() - } - var total: Float = 0 - for dropable in runningRequests { - switch dropable.status { - case .running(let progress): - total += progress - case .completed(_): - total += 1 - default: - break + lockedRun { + var total: Float = 0 + for dropable in runningRequests { + switch dropable.status { + case .running(let progress): + total += progress + case .completed(_): + total += 1 + default: + break + } } + return total / Float(targetRunCount) } - return total / Float(targetRunCount) } init(targetRunCount: Int) { @@ -185,17 +182,17 @@ extension DropableRequestAggregator { dropable.drop() return } - lock.lock() - runningRequests.append(dropable) - lock.unlock() + lockedRun { + runningRequests.append(dropable) + } } func cancel() { - lock.lock() - canceled = true - runningRequests.forEach { $0.drop() } - runningRequests.removeAll() - lock.unlock() + lockedRun { + canceled = true + runningRequests.forEach { $0.drop() } + runningRequests.removeAll() + } } } } diff --git a/iONess/Classes/Common/RetryControl.swift b/iONess/Classes/Common/RetryControl.swift index bdf1e10..751e023 100644 --- a/iONess/Classes/Common/RetryControl.swift +++ b/iONess/Classes/Common/RetryControl.swift @@ -11,7 +11,7 @@ public protocol RetryControl { func shouldRetry(for request: URLRequest, response: URLResponse?, error: Error, didHaveDecision: (RetryControlDecision) -> Void) -> Void } -public class CounterRetryControl: RetryControl { +public class CounterRetryControl: RetryControl, LockRunner { var maxRetryCount: Int public var timeIntervalBeforeTryToRetry: TimeInterval? @@ -20,26 +20,24 @@ public class CounterRetryControl: RetryControl { self.timeIntervalBeforeTryToRetry = timeIntervalBeforeTryToRetry } - var lock: NSLock = .init() + let lock: NSLock = .init() var retriedRequests: [URLRequest: Int] = [:] public func shouldRetry(for request: URLRequest, response: URLResponse?, error: Error, didHaveDecision: (RetryControlDecision) -> Void) { - lock.lock() - defer { - lock.lock() + lockedRun { + let counter = retriedRequests[request] ?? 0 + guard counter < maxRetryCount else { + retriedRequests.removeValue(forKey: request) + didHaveDecision(.noRetry) + return + } + retriedRequests[request] = counter + 1 + guard let timeInterval = timeIntervalBeforeTryToRetry else { + didHaveDecision(.retry) + return + } + didHaveDecision(.retryAfter(timeInterval)) } - let counter = retriedRequests[request] ?? 0 - guard counter < maxRetryCount else { - retriedRequests.removeValue(forKey: request) - didHaveDecision(.noRetry) - return - } - retriedRequests[request] = counter + 1 - guard let timeInterval = timeIntervalBeforeTryToRetry else { - didHaveDecision(.retry) - return - } - didHaveDecision(.retryAfter(timeInterval)) } } From e33e150f9da556c48f5d934b619cd49b45027317 Mon Sep 17 00:00:00 2001 From: nayanda Date: Tue, 27 Oct 2020 18:42:58 +0700 Subject: [PATCH 03/20] Change Retry Control closure to escaping --- iONess/Classes/Common/RetryControl.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iONess/Classes/Common/RetryControl.swift b/iONess/Classes/Common/RetryControl.swift index 751e023..eb1cb0b 100644 --- a/iONess/Classes/Common/RetryControl.swift +++ b/iONess/Classes/Common/RetryControl.swift @@ -8,7 +8,7 @@ import Foundation public protocol RetryControl { - func shouldRetry(for request: URLRequest, response: URLResponse?, error: Error, didHaveDecision: (RetryControlDecision) -> Void) -> Void + func shouldRetry(for request: URLRequest, response: URLResponse?, error: Error, didHaveDecision: @escaping (RetryControlDecision) -> Void) -> Void } public class CounterRetryControl: RetryControl, LockRunner { @@ -23,7 +23,7 @@ public class CounterRetryControl: RetryControl, LockRunner { let lock: NSLock = .init() var retriedRequests: [URLRequest: Int] = [:] - public func shouldRetry(for request: URLRequest, response: URLResponse?, error: Error, didHaveDecision: (RetryControlDecision) -> Void) { + public func shouldRetry(for request: URLRequest, response: URLResponse?, error: Error, didHaveDecision: @escaping (RetryControlDecision) -> Void) { lockedRun { let counter = retriedRequests[request] ?? 0 guard counter < maxRetryCount else { From cc1652d2cff8db1803ad680ddbcdb69634c3b8a2 Mon Sep 17 00:00:00 2001 From: nayanda Date: Tue, 27 Oct 2020 18:47:40 +0700 Subject: [PATCH 04/20] Added Finally --- iONess/Classes/Thenable/Thenable.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/iONess/Classes/Thenable/Thenable.swift b/iONess/Classes/Thenable/Thenable.swift index 44621b2..1ad3848 100644 --- a/iONess/Classes/Thenable/Thenable.swift +++ b/iONess/Classes/Thenable/Thenable.swift @@ -16,5 +16,23 @@ public protocol Thenable { @discardableResult func then(run closure: @escaping (Result) -> Void) -> DropablePromise + + @discardableResult + func then(run closure: @escaping (Result) -> Void, whenFailed failClosure: @escaping (Result) -> Void, finally deferClosure: @escaping (Result) -> Void) -> DropablePromise } +public extension Thenable { + @discardableResult + func then(run closure: @escaping (Result) -> Void, whenFailed failClosure: @escaping (Result) -> Void, finally deferClosure: @escaping (Result) -> Void) -> DropablePromise { + then( + run: { + closure($0) + deferClosure($0) + }, + whenFailed: { + failClosure($0) + deferClosure($0) + } + ) + } +} From 4b7cedd5ce697407ac77ce4cd830ea98610111da Mon Sep 17 00:00:00 2001 From: nayanda Date: Tue, 27 Oct 2020 18:48:01 +0700 Subject: [PATCH 05/20] Added timeout --- iONess/Classes/Common/NetworkSessionManager.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/iONess/Classes/Common/NetworkSessionManager.swift b/iONess/Classes/Common/NetworkSessionManager.swift index 91a7396..c145818 100644 --- a/iONess/Classes/Common/NetworkSessionManager.swift +++ b/iONess/Classes/Common/NetworkSessionManager.swift @@ -15,12 +15,23 @@ open class NetworkSessionManager { public private(set) var session: URLSession public weak var delegate: NetworkSessionManagerDelegate? public var duplicatedHandler: DuplicatedHandler + public var timeout: TimeInterval = 30 { + didSet { + session.configuration.timeoutIntervalForRequest = timeout + session.configuration.timeoutIntervalForResource = timeout + } + } + let lock = NSLock() var completions: [NetworkRequest: URLCompletion] = [:] public init( with session: URLSession = .shared, onDuplicated handler: DefaultDuplicatedHandler = .keepAllCompletion) { + self.timeout = max( + session.configuration.timeoutIntervalForResource, + session.configuration.timeoutIntervalForResource + ) self.session = session self.duplicatedHandler = handler } From 89e3ff5f70693608576f659846da72bb5e4b96d5 Mon Sep 17 00:00:00 2001 From: nayanda Date: Tue, 27 Oct 2020 18:48:37 +0700 Subject: [PATCH 06/20] Fix missing first aggregate --- iONess/Classes/Thenable/URLThenableRequest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iONess/Classes/Thenable/URLThenableRequest.swift b/iONess/Classes/Thenable/URLThenableRequest.swift index 07ef4f9..491f146 100644 --- a/iONess/Classes/Thenable/URLThenableRequest.swift +++ b/iONess/Classes/Thenable/URLThenableRequest.swift @@ -65,6 +65,6 @@ public extension URLThenableRequest { public extension URLThenableRequest { func aggregate(with request: Self) -> RequestAggregator { - return RequestAggregator(requests: []).aggregate(request) + RequestAggregator(requests: [self, request]) } } From 4256626cece817cdcf8b9e94a88241ab4a121a23 Mon Sep 17 00:00:00 2001 From: nayanda Date: Tue, 27 Oct 2020 18:49:37 +0700 Subject: [PATCH 07/20] Remove unnecessary weak which will always nil --- .../DropableAndResumable/DropableDataRequest.swift | 6 +++--- .../DropableAndResumable/ResumableDownloadRequest.swift | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/iONess/Classes/DropableAndResumable/DropableDataRequest.swift b/iONess/Classes/DropableAndResumable/DropableDataRequest.swift index 63944ce..402e71c 100644 --- a/iONess/Classes/DropableAndResumable/DropableDataRequest.swift +++ b/iONess/Classes/DropableAndResumable/DropableDataRequest.swift @@ -55,13 +55,13 @@ public class DropableDataRequest: BaseDropableURLRequest< } static func request( - for dropable: DropableDataRequest?, + for dropable: DropableDataRequest, in networkSessionManager: NetworkSessionManager, with request: URLRequest, _ retryControl: RetryControl?, _ validator: URLValidator?, _ completion: @escaping (URLResult) -> Void) -> URLSessionDataTask { - networkSessionManager.dataTask(with: request) { [weak dropable] data, response, error in + networkSessionManager.dataTask(with: request) { data, response, error in guard let requestError = error ?? validate(response: response, with: validator) else { completion( .init( @@ -77,7 +77,7 @@ public class DropableDataRequest: BaseDropableURLRequest< error: requestError, request: request, response, { - dropable?.task = Self.request( + dropable.task = Self.request( for: dropable, in: networkSessionManager, with: request, diff --git a/iONess/Classes/DropableAndResumable/ResumableDownloadRequest.swift b/iONess/Classes/DropableAndResumable/ResumableDownloadRequest.swift index b96ec90..9174c03 100644 --- a/iONess/Classes/DropableAndResumable/ResumableDownloadRequest.swift +++ b/iONess/Classes/DropableAndResumable/ResumableDownloadRequest.swift @@ -103,7 +103,7 @@ public class ResumableDownloadRequest: BaseDropableURLRequest Void) -> URLSessionDownloadTask { - let downloadCompletion: (URL?, URLResponse?, Error?) -> Void = { [weak dropable] url, response, error in + let downloadCompletion: (URL?, URLResponse?, Error?) -> Void = { url, response, error in guard let errorAfterValidate = moveResult(into: targetUrl, from: url, currentError: error, response: response) ?? validate(response: response, with: validator) else { completion( .init( @@ -127,8 +127,8 @@ public class ResumableDownloadRequest: BaseDropableURLRequest Date: Tue, 27 Oct 2020 18:49:54 +0700 Subject: [PATCH 08/20] Refactor --- .../NetworkSessionManager+Extensions.swift | 64 ++++++++++--------- .../DropableUploadRequest.swift | 6 +- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/iONess/Classes/Common/NetworkSessionManager+Extensions.swift b/iONess/Classes/Common/NetworkSessionManager+Extensions.swift index fbc379b..2b610fa 100644 --- a/iONess/Classes/Common/NetworkSessionManager+Extensions.swift +++ b/iONess/Classes/Common/NetworkSessionManager+Extensions.swift @@ -26,7 +26,9 @@ extension NetworkSessionManager: LockRunner { biConsumer.value( nil, nil, - NetworkSessionError(description: "iONess Error: Cancelled by NetworkSessionManager") + NetworkSessionError( + description: "iONess Error: Cancelled by NetworkSessionManager" + ) ) } } @@ -70,94 +72,94 @@ extension NetworkSessionManager: LockRunner { } func downloadTask(with request: URLRequest, completionHandler: @escaping URLCompletion) -> URLSessionDownloadTask { - let request = delegate?.ness(self, willRequest: request) ?? request + let updatedRequest = delegate?.ness(self, willRequest: request) ?? request defer { - delegate?.ness(self, didRequest: request) + delegate?.ness(self, didRequest: updatedRequest) } var completion: URLCompletion = completionHandler - if let prevCompletion: URLCompletion = currentCompletion(for: request) { + if let prevCompletion: URLCompletion = currentCompletion(for: updatedRequest) { let decision = duplicatedHandler.duplicatedDownload( - request: request, + request: updatedRequest, withPreviousCompletion: prevCompletion, currentCompletion: completionHandler ) - completion = decide(from: decision, request, completionHandler, prevCompletion) - if let task = task(for: request) as? URLSessionDownloadTask { - assign(for: request, completion: completion) + completion = decide(from: decision, updatedRequest, completionHandler, prevCompletion) + if let task = task(for: updatedRequest) as? URLSessionDownloadTask { + assign(for: updatedRequest, completion: completion) return task } } - let task = session.downloadTask(with: request) { [weak self] url, response, error in + let task = session.downloadTask(with: updatedRequest) { [weak self] url, response, error in guard let self = self else { completion(url, response, error) return } - let currentCompletion: URLCompletion? = self.removeAndGetCompletion(for: request) + let currentCompletion: URLCompletion? = self.removeAndGetCompletion(for: updatedRequest) currentCompletion?(url, response, error) } - assign(for: request, task: task, completion: completion) + assign(for: updatedRequest, task: task, completion: completion) task.resume() return task } func uploadTask(with request: URLRequest, fromFile fileURL: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionUploadTask { - let request = delegate?.ness(self, willRequest: request) ?? request + let updatedRequest = delegate?.ness(self, willRequest: request) ?? request defer { - delegate?.ness(self, didRequest: request) + delegate?.ness(self, didRequest: updatedRequest) } var completion: URLCompletion = completionHandler - if let prevCompletion: URLCompletion = currentCompletion(for: request) { + if let prevCompletion: URLCompletion = currentCompletion(for: updatedRequest) { let decision = duplicatedHandler.duplicatedUpload( - request: request, + request: updatedRequest, withPreviousCompletion: prevCompletion, currentCompletion: completionHandler ) - completion = decide(from: decision, request, completionHandler, prevCompletion) - if let task = task(for: request) as? URLSessionUploadTask { - assign(for: request, completion: completion) + completion = decide(from: decision, updatedRequest, completionHandler, prevCompletion) + if let task = task(for: updatedRequest) as? URLSessionUploadTask { + assign(for: updatedRequest, completion: completion) return task } } - let task = session.uploadTask(with: request, fromFile: fileURL) { [weak self] data, response, error in + let task = session.uploadTask(with: updatedRequest, fromFile: fileURL) { [weak self] data, response, error in guard let self = self else { completion(data, response, error) return } - let currentCompletion: URLCompletion? = self.removeAndGetCompletion(for: request) + let currentCompletion: URLCompletion? = self.removeAndGetCompletion(for: updatedRequest) currentCompletion?(data, response, error) } - assign(for: request, task: task, completion: completion) + assign(for: updatedRequest, task: task, completion: completion) task.resume() return task } func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { - let request = delegate?.ness(self, willRequest: request) ?? request + let updatedRequest = delegate?.ness(self, willRequest: request) ?? request defer { - delegate?.ness(self, didRequest: request) + delegate?.ness(self, didRequest: updatedRequest) } var completion: URLCompletion = completionHandler - if let prevCompletion: URLCompletion = currentCompletion(for: request) { + if let prevCompletion: URLCompletion = currentCompletion(for: updatedRequest) { let decision = duplicatedHandler.duplicatedData( - request: request, + request: updatedRequest, withPreviousCompletion: prevCompletion, currentCompletion: completionHandler ) - completion = decide(from: decision, request, completionHandler, prevCompletion) - if let task = task(for: request) as? URLSessionDataTask { - assign(for: request, completion: completion) + completion = decide(from: decision, updatedRequest, completionHandler, prevCompletion) + if let task = task(for: updatedRequest) as? URLSessionDataTask { + assign(for: updatedRequest, completion: completion) return task } } - let task = session.dataTask(with: request) { [weak self] data, response, error in + let task = session.dataTask(with: updatedRequest) { [weak self] data, response, error in guard let self = self else { completion(data, response, error) return } - let currentCompletion: URLCompletion? = self.removeAndGetCompletion(for: request) + let currentCompletion: URLCompletion? = self.removeAndGetCompletion(for: updatedRequest) currentCompletion?(data, response, error) } - assign(for: request, task: task, completion: completion) + assign(for: updatedRequest, task: task, completion: completion) task.resume() return task } diff --git a/iONess/Classes/DropableAndResumable/DropableUploadRequest.swift b/iONess/Classes/DropableAndResumable/DropableUploadRequest.swift index ccee237..046a236 100644 --- a/iONess/Classes/DropableAndResumable/DropableUploadRequest.swift +++ b/iONess/Classes/DropableAndResumable/DropableUploadRequest.swift @@ -59,7 +59,7 @@ public class DropableUploadRequest: BaseDropableURLReques } static func upload( - for promise: DropableUploadRequest, + for dropable: DropableUploadRequest, in networkSessionManager: NetworkSessionManager, with request: URLRequest, fromFile url: URL, @@ -82,8 +82,8 @@ public class DropableUploadRequest: BaseDropableURLReques error: requestError, request: request, response, { - promise.task = Self.upload( - for: promise, + dropable.task = Self.upload( + for: dropable, in: networkSessionManager, with: request, fromFile: url, From 477e067da6614560ba1bce8a046405ef0685f629 Mon Sep 17 00:00:00 2001 From: nayanda Date: Tue, 27 Oct 2020 18:50:10 +0700 Subject: [PATCH 09/20] Added Test for aggregate --- Example/Tests/IntegratedTest.swift | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Example/Tests/IntegratedTest.swift b/Example/Tests/IntegratedTest.swift index a610f09..1784a4b 100644 --- a/Example/Tests/IntegratedTest.swift +++ b/Example/Tests/IntegratedTest.swift @@ -233,6 +233,34 @@ class IntegratedTestSpec: QuickSpec { expect(obj.body).to(equal("bar")) expect(obj.completed).to(beNil()) } + it("should aggregate request") { + let firstRealUrl: URL = try! "https://jsonplaceholder.typicode.com/todos/\(Int.random(in: 0..<10))".asUrl() + let secondRealUrl: URL = try! "https://jsonplaceholder.typicode.com/todos/\(Int.random(in: 10..<20))".asUrl() + let firstRequest = Ness.default + .httpRequest(.get, withUrl: firstRealUrl) + .prepareDataRequest() + let secondRequest = Ness.default + .httpRequest(.get, withUrl: secondRealUrl) + .prepareDataRequest() + var firstResult: URLResult? + var secondResult: URLResult? + waitUntil(timeout: 15) { done in + firstRequest.aggregate(with: secondRequest).then { result in + expect(result.areCompleted).to(beTrue()) + expect(result.results.count).to(equal(2)) + firstResult = result.results[0] + secondResult = result.results[1] + done() + } + } + let firstUrl = try! firstResult?.httpMessage?.url.asUrl() + let secondUrl = try! secondResult?.httpMessage?.url.asUrl() + expect(firstUrl).toNot(beNil()) + expect(secondUrl).toNot(beNil()) + expect(firstUrl).toNot(equal(secondUrl)) + expect(firstUrl == firstRealUrl || firstUrl == secondRealUrl).to(beTrue()) + expect(secondUrl == firstRealUrl || secondUrl == secondRealUrl).to(beTrue()) + } } } From 9aa665e13b947812f43cb8315d448133759c7882 Mon Sep 17 00:00:00 2001 From: nayanda Date: Tue, 27 Oct 2020 19:03:46 +0700 Subject: [PATCH 10/20] Update README and updating podspec --- README.md | 90 ++++++++++++++++++++++++++++++++++++++++++-------- iONess.podspec | 2 +- 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a5902a5..55be204 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,54 @@ Ness.default .httpRequest(.get, withUrl: "https://myurl.com") .prepareDataRequest() .then { result in - // do something with result + // do something with result ignoring its state (succeed or fail) } ``` +with failure handler: + +```swift +Ness.default + .httpRequest(.get, withUrl: "https://myurl.com") + .prepareDataRequest() + .then( + run: { result in + // do something with result + }, + whenFailed: { result in + // do something with error result + } + ) +``` + +with finally: + +```swift +Ness.default + .httpRequest(.get, withUrl: "https://myurl.com") + .prepareDataRequest() + .then( + run: { result in + // do something with result + }, + whenFailed: { result in + // do something with error result + }, + finally: { result in + // do something after succeed or fail + } + ) +``` + +or with no completion at all: + +```swift +Ness.default + .httpRequest(.get, withUrl: "https://myurl.com") + .prepareDataRequest() + .executeAndForget() +``` + ### Create Request To create request you can do something like this: @@ -180,12 +224,35 @@ Ness.default .. .. .prepareDataRequest() - .then(run: { result in - // do something when get response - }, whenFailed: { result in - // do something when failed - } -) + .then( + run: { result in + // do something when get response + }, + whenFailed: { result in + // do something when failed + } + ) +``` + +Or with finally completion: + +```swift +Ness.default + .httpRequest(.get, withUrl: "https://myurl.com") + .. + .. + .prepareDataRequest() + .then( + run: { result in + // do something when get response + }, + whenFailed: { result in + // do something when failed + }, + finally: { result in + // do something when after request finished + } + ) ``` With custom dispatcher which will be the thread where completion run: @@ -197,12 +264,9 @@ Ness.default .. .prepareDataRequest() .completionDispatch(on: .global(qos: .background)) - .then(run: { result in - // do something when get response - }, whenFailed: { result in - // do something when failed - } -) + .then { result in + // this block will run on DispatchQueue.global(qos: .background) +} ``` The default dispatcher is `DispatchQueue.main` diff --git a/iONess.podspec b/iONess.podspec index 69a284f..c148f2d 100644 --- a/iONess.podspec +++ b/iONess.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'iONess' - s.version = '1.2.0' + s.version = '1.2.1' s.summary = 'iOS Network Session' # This description is used to generate tags and improve search results. From 6bfb78f09e825cab4bc5c86bbcc55850d33e8f79 Mon Sep 17 00:00:00 2001 From: nayanda Date: Wed, 28 Oct 2020 11:03:29 +0700 Subject: [PATCH 11/20] Added Response Decoder Unit Test --- Example/Tests/IntegratedTest.swift | 8 - Example/Tests/Mock.swift | 57 +++++++ Example/Tests/ResponseDecoderSpec.swift | 158 ++++++++++++++++++++ Example/iONess.xcodeproj/project.pbxproj | 34 ++++- iONess/Classes/Common/ResponseDecoder.swift | 6 - 5 files changed, 248 insertions(+), 15 deletions(-) create mode 100644 Example/Tests/Mock.swift create mode 100644 Example/Tests/ResponseDecoderSpec.swift diff --git a/Example/Tests/IntegratedTest.swift b/Example/Tests/IntegratedTest.swift index 1784a4b..c80a167 100644 --- a/Example/Tests/IntegratedTest.swift +++ b/Example/Tests/IntegratedTest.swift @@ -263,11 +263,3 @@ class IntegratedTestSpec: QuickSpec { } } } - -struct JSONPlaceholder: Codable { - var id: Int = 0 - var title: String - var body: String? - var userId: Int? - var completed: Bool? = nil -} diff --git a/Example/Tests/Mock.swift b/Example/Tests/Mock.swift new file mode 100644 index 0000000..287109e --- /dev/null +++ b/Example/Tests/Mock.swift @@ -0,0 +1,57 @@ +// +// Mock.swift +// iONess_Tests +// +// Created by Nayanda Haberty (ID) on 27/10/20. +// Copyright © 2020 CocoaPods. All rights reserved. +// + +import Foundation + +struct JSONPlaceholder: Codable, Equatable { + var id: Int = 0 + var title: String + var body: String? + var userId: Int? + var completed: Bool? = nil + + static func mock() -> JSONPlaceholder { + .init( + id: .random(in: 0..<100), + title: .random(length: .random(in: 10..<100)), + body: .random(length: .random(in: 10..<100)), + userId: .random(in: 0..<100), + completed: .random() + ) + } + + static var `default`: JSONPlaceholder { + .init( + id: -1, + title: "", + body: nil, + userId: nil, + completed: nil + ) + } +} + +struct RandomJSON: Codable { + var randomInt: Int = .random(in: 0..<100) + var randomString: String = .random(length: .random(in: 10..<100)) + var randomBool: Bool = .random() +} + +extension String { + public static func random(length: Int = 9) -> String { + let letters : NSString = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + let len = UInt32(letters.length) + var randomString = "" + for _ in 0 ..< length { + let rand = arc4random_uniform(len) + var nextChar = letters.character(at: Int(rand)) + randomString += NSString(characters: &nextChar, length: 1) as String + } + return randomString + } +} diff --git a/Example/Tests/ResponseDecoderSpec.swift b/Example/Tests/ResponseDecoderSpec.swift new file mode 100644 index 0000000..95981f4 --- /dev/null +++ b/Example/Tests/ResponseDecoderSpec.swift @@ -0,0 +1,158 @@ +// +// ResponseDecoderSpec.swift +// iONess_Tests +// +// Created by Nayanda Haberty (ID) on 27/10/20. +// Copyright © 2020 CocoaPods. All rights reserved. +// + +import Foundation +import Quick +import Nimble +@testable import iONess + +class ResponseDecoderSpec: QuickSpec { + override func spec() { + describe("positive case") { + it("should decode json from data") { + let mock = JSONPlaceholder.mock() + let data = try! JSONEncoder().encode(mock) + let decoded = try! JSONDecodableDecoder().decode(from: data) + expect(decoded).to(equal(mock)) + } + it("should decode json array from data") { + let mocks: [JSONPlaceholder] = [.mock(), .mock(), .mock(), .mock(), .mock()] + let data = try! JSONEncoder().encode(mocks) + let decodeds = try! ArrayJSONDecodableDecoder().decode(from: data) + expect(decodeds).to(equal(mocks)) + } + it("should decode json from data") { + let mock = JSONPlaceholder.mock() + let data = try! JSONEncoder().encode(mock) + let decoded = try! JSONResponseDecoder().decode(from: data) + expect(decoded["id"] as? Int).to(equal(mock.id)) + expect(decoded["userId"] as? Int).to(equal(mock.userId)) + expect(decoded["body"] as? String).to(equal(mock.body)) + expect(decoded["completed"] as? Bool).to(equal(mock.completed)) + expect(decoded["title"] as? String).to(equal(mock.title)) + } + it("should decode json array from data") { + let mocks: [JSONPlaceholder] = [.mock(), .mock(), .mock(), .mock(), .mock()] + let data = try! JSONEncoder().encode(mocks) + let decodeds = try! ArrayJSONResponseDecoder().decode(from: data) + expect(decodeds.count).to(equal(mocks.count)) + for (index, decoded) in decodeds.enumerated() { + guard let dict: [String: Any] = decoded as? [String : Any] else { + fail() + return + } + let mock = mocks[index] + expect(dict["id"] as? Int).to(equal(mock.id)) + expect(dict["userId"] as? Int).to(equal(mock.userId)) + expect(dict["body"] as? String).to(equal(mock.body)) + expect(dict["completed"] as? Bool).to(equal(mock.completed)) + expect(dict["title"] as? String).to(equal(mock.title)) + } + } + it("should decode json from data") { + let mock = JSONPlaceholder.mock() + let data = try! JSONEncoder().encode(mock) + let decoded = try! PlaceholderJSONDecoder().decode(from: data) + expect(decoded).to(equal(mock)) + } + it("should decode json from string") { + let mock = JSONPlaceholder.mock() + let data = try! JSONEncoder().encode(mock) + let decoded = try! PlaceholderStringDecoder().decode(from: data) + expect(decoded).to(equal(mock)) + } + it("should decode array json from data") { + let mocks: [JSONPlaceholder] = [.mock(), .mock(), .mock(), .mock(), .mock()] + let data = try! JSONEncoder().encode(mocks) + let decoder = ArrayedJSONDecoder(singleDecoder: PlaceholderJSONDecoder()) + let decodeds = try! decoder.decode(from: data) + expect(decodeds).to(equal(mocks)) + } + } + describe("negative test") { + it("should fail decode json from data") { + let mock = RandomJSON() + let data = try! JSONEncoder().encode(mock) + expect { + let decoded = try JSONDecodableDecoder().decode(from: data) + return decoded + }.to(throwError()) + } + it("should fail decode json array from data") { + let mocks: [RandomJSON] = [.init(), .init(), .init(), .init(), .init()] + let data = try! JSONEncoder().encode(mocks) + expect { + let decoded = try ArrayJSONDecodableDecoder().decode(from: data) + return decoded + }.to(throwError()) + } + it("should fail decode json from data") { + let data = String.random().data(using: .utf8)! + expect { + let decoded = try JSONResponseDecoder().decode(from: data) + return decoded + }.to(throwError()) + } + it("should fail decode array json from data") { + let data = String.random().data(using: .utf8)! + expect { + let decoded = try ArrayJSONResponseDecoder().decode(from: data) + return decoded + }.to(throwError()) + } + it("should fail decode json from data") { + let data = String.random().data(using: .utf8)! + expect { + let decoded = try PlaceholderJSONDecoder().decode(from: data) + return decoded + }.to(throwError()) + } + it("should fail decode json from string") { + let data = String.random().data(using: .utf8)! + expect { + let decoded = try PlaceholderStringDecoder().decode(from: data) + return decoded + }.to(throwError()) + } + it("should fail decode array json from data") { + let data = String.random().data(using: .utf8)! + let decoder = ArrayedJSONDecoder(singleDecoder: PlaceholderJSONDecoder()) + expect { + let decodeds = try decoder.decode(from: data) + return decodeds + }.to(throwError()) + } + } + } +} + +class PlaceholderJSONDecoder: BaseJSONDecoder { + override func decode(from json: [String : Any]) throws -> JSONPlaceholder { + .init( + id: json["id"] as? Int ?? -1, + title: json["title"] as? String ?? "", + body: json["body"] as? String, + userId: json["userId"] as? Int, + completed: json["completed"] as? Bool + ) + } +} + +class PlaceholderStringDecoder: BaseStringDecoder { + override func decode(from json: String) throws -> JSONPlaceholder { + let data = json.data(using: .utf8)! + let dict = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] + return .init( + id: dict["id"] as? Int ?? -1, + title: dict["title"] as? String ?? "", + body: dict["body"] as? String, + userId: dict["userId"] as? Int, + completed: dict["completed"] as? Bool + ) + } +} diff --git a/Example/iONess.xcodeproj/project.pbxproj b/Example/iONess.xcodeproj/project.pbxproj index e1e157e..617747d 100644 --- a/Example/iONess.xcodeproj/project.pbxproj +++ b/Example/iONess.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 73562F61253FDD560063EEC3 /* IntegratedTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73562F60253FDD560063EEC3 /* IntegratedTest.swift */; }; + 73A0612A254853CE000A7A49 /* ResponseDecoderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73A06124254853AE000A7A49 /* ResponseDecoderSpec.swift */; }; + 73A0612F2548545D000A7A49 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73A0612E2548545D000A7A49 /* Mock.swift */; }; DF53A4C88324D6B58FAC52D1 /* Pods_iONess_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 82DB49C06D5FF9CD1FB3650A /* Pods_iONess_Tests.framework */; }; /* End PBXBuildFile section */ @@ -43,6 +45,8 @@ 61E6CD8C4565024E1C7CB3B6 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 692206617FDF7BB0DC2ED947 /* Pods-iONess_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iONess_Example.release.xcconfig"; path = "Target Support Files/Pods-iONess_Example/Pods-iONess_Example.release.xcconfig"; sourceTree = ""; }; 73562F60253FDD560063EEC3 /* IntegratedTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegratedTest.swift; sourceTree = ""; }; + 73A06124254853AE000A7A49 /* ResponseDecoderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseDecoderSpec.swift; sourceTree = ""; }; + 73A0612E2548545D000A7A49 /* Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = ""; }; 82DB49C06D5FF9CD1FB3650A /* Pods_iONess_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iONess_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 96F91EE4A5D9A716C943568A /* Pods-iONess_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iONess_Tests.debug.xcconfig"; path = "Target Support Files/Pods-iONess_Tests/Pods-iONess_Tests.debug.xcconfig"; sourceTree = ""; }; C676819A125F6B968302329A /* iONess.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = iONess.podspec; path = ../iONess.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; @@ -115,7 +119,9 @@ 607FACE81AFB9204008FA782 /* Tests */ = { isa = PBXGroup; children = ( - 73562F60253FDD560063EEC3 /* IntegratedTest.swift */, + 73A0612D2548544C000A7A49 /* Model */, + 73A061232548538A000A7A49 /* UnitTest */, + 73A061222548537D000A7A49 /* IntegratedTest */, 607FACE91AFB9204008FA782 /* Supporting Files */, ); path = Tests; @@ -139,6 +145,30 @@ name = "Podspec Metadata"; sourceTree = ""; }; + 73A061222548537D000A7A49 /* IntegratedTest */ = { + isa = PBXGroup; + children = ( + 73562F60253FDD560063EEC3 /* IntegratedTest.swift */, + ); + name = IntegratedTest; + sourceTree = ""; + }; + 73A061232548538A000A7A49 /* UnitTest */ = { + isa = PBXGroup; + children = ( + 73A06124254853AE000A7A49 /* ResponseDecoderSpec.swift */, + ); + name = UnitTest; + sourceTree = ""; + }; + 73A0612D2548544C000A7A49 /* Model */ = { + isa = PBXGroup; + children = ( + 73A0612E2548545D000A7A49 /* Mock.swift */, + ); + name = Model; + sourceTree = ""; + }; 950D1A2AD70729E02C738F7C /* Frameworks */ = { isa = PBXGroup; children = ( @@ -361,7 +391,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 73A0612A254853CE000A7A49 /* ResponseDecoderSpec.swift in Sources */, 73562F61253FDD560063EEC3 /* IntegratedTest.swift in Sources */, + 73A0612F2548545D000A7A49 /* Mock.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iONess/Classes/Common/ResponseDecoder.swift b/iONess/Classes/Common/ResponseDecoder.swift index ed2e237..6f0a4b7 100644 --- a/iONess/Classes/Common/ResponseDecoder.swift +++ b/iONess/Classes/Common/ResponseDecoder.swift @@ -86,12 +86,6 @@ open class BaseStringDecoder: ResponseDecoder { } } -public extension BaseJSONDecoder { - static var forArray: ArrayedJSONDecoder { - .init(singleDecoder: Self.init()) - } -} - public struct ArrayedJSONDecoder: ResponseDecoder { public typealias Decoded = Array From 740fcebd0f7a7496fdcaeafe0ce52fe62063fff3 Mon Sep 17 00:00:00 2001 From: nayanda Date: Wed, 28 Oct 2020 11:03:39 +0700 Subject: [PATCH 12/20] Update README --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 55be204..9e954e5 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,19 @@ Ness.default .executeAndForget() ``` +you can do something very readable like this by separating all closure using function: + +```swift +Ness.default + .httpRequest(.get, withUrl: "https://myurl.com") + .prepareDataRequest() + .then( + run: updateTheViewWithData, + whenFailed: showFailureAlert, + finally: removeLoading + ) +``` + ### Create Request To create request you can do something like this: From 5bf0a6e971ccd6cf554db3ded1701540a0db2796 Mon Sep 17 00:00:00 2001 From: nayanda Date: Wed, 28 Oct 2020 12:08:00 +0700 Subject: [PATCH 13/20] Added Validate and RetryControl to integrate test --- Example/Tests/IntegratedTest.swift | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/Example/Tests/IntegratedTest.swift b/Example/Tests/IntegratedTest.swift index c80a167..82ffd50 100644 --- a/Example/Tests/IntegratedTest.swift +++ b/Example/Tests/IntegratedTest.swift @@ -19,7 +19,8 @@ class IntegratedTestSpec: QuickSpec { var secondResult: URLResult? let request = Ness(onDuplicated: .keepAllCompletion) .httpRequest(.get, withUrl: "https://jsonplaceholder.typicode.com/todos/\(randomNumber)") - .prepareDataRequest() + .prepareDataRequest(with: CounterRetryControl(maxRetryCount: 3)) + .validate(statusCodes: 200..<300) request.then { result in firstResult = result } @@ -47,7 +48,8 @@ class IntegratedTestSpec: QuickSpec { var secondResult: URLResult? let request = Ness(onDuplicated: .dropPreviousRequest) .httpRequest(.get, withUrl: "https://jsonplaceholder.typicode.com/todos/\(randomNumber)") - .prepareDataRequest() + .prepareDataRequest(with: CounterRetryControl(maxRetryCount: 3)) + .validate(statusCodes: 200..<300) request.then { result in firstResult = result } @@ -70,7 +72,8 @@ class IntegratedTestSpec: QuickSpec { var firstResult: URLResult? let request = Ness(onDuplicated: .keepFirstCompletion) .httpRequest(.get, withUrl: "https://jsonplaceholder.typicode.com/todos/\(randomNumber)") - .prepareDataRequest() + .prepareDataRequest(with: CounterRetryControl(maxRetryCount: 3)) + .validate(statusCodes: 200..<300) waitUntil(timeout: 15) { done in request.then { result in firstResult = result @@ -92,7 +95,8 @@ class IntegratedTestSpec: QuickSpec { var secondResult: URLResult? let request = Ness(onDuplicated: .keepLatestCompletion) .httpRequest(.get, withUrl: "https://jsonplaceholder.typicode.com/todos/\(randomNumber)") - .prepareDataRequest() + .prepareDataRequest(with: CounterRetryControl(maxRetryCount: 3)) + .validate(statusCodes: 200..<300) waitUntil(timeout: 15) { done in request.then { _ in fail("This completion should not be executed") @@ -115,7 +119,8 @@ class IntegratedTestSpec: QuickSpec { waitUntil(timeout: 15) { done in Ness.default .httpRequest(.get, withUrl: "https://jsonplaceholder.typicode.com/todos/\(randomNumber)") - .prepareDataRequest() + .prepareDataRequest(with: CounterRetryControl(maxRetryCount: 3)) + .validate(statusCodes: 200..<300) .then { result in requestResult = result done() @@ -154,7 +159,8 @@ class IntegratedTestSpec: QuickSpec { waitUntil(timeout: 15) { done in Ness.default .httpRequest(.get, withUrl: "https://jsonplaceholder.typicode.com/posts") - .prepareDataRequest() + .prepareDataRequest(with: CounterRetryControl(maxRetryCount: 3)) + .validate(statusCodes: 200..<300) .then { result in requestResult = result done() @@ -199,7 +205,8 @@ class IntegratedTestSpec: QuickSpec { Ness.default .httpRequest(.post, withUrl: "https://jsonplaceholder.typicode.com/posts") .set(jsonEncodable: JSONPlaceholder(title: "foo", body: "bar", userId: randomNumber)) - .prepareDataRequest() + .prepareDataRequest(with: CounterRetryControl(maxRetryCount: 3)) + .validate(statusCodes: 200..<300) .then { result in requestResult = result done() @@ -238,10 +245,12 @@ class IntegratedTestSpec: QuickSpec { let secondRealUrl: URL = try! "https://jsonplaceholder.typicode.com/todos/\(Int.random(in: 10..<20))".asUrl() let firstRequest = Ness.default .httpRequest(.get, withUrl: firstRealUrl) - .prepareDataRequest() + .prepareDataRequest(with: CounterRetryControl(maxRetryCount: 3)) + .validate(statusCodes: 200..<300) let secondRequest = Ness.default .httpRequest(.get, withUrl: secondRealUrl) - .prepareDataRequest() + .prepareDataRequest(with: CounterRetryControl(maxRetryCount: 3)) + .validate(statusCodes: 200..<300) var firstResult: URLResult? var secondResult: URLResult? waitUntil(timeout: 15) { done in From 6cdfccd1800c798f059e57405408c43feb95cdbb Mon Sep 17 00:00:00 2001 From: nayanda Date: Wed, 28 Oct 2020 12:08:13 +0700 Subject: [PATCH 14/20] Make lockrunner discardable --- iONess/Classes/Common/LockRunner.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iONess/Classes/Common/LockRunner.swift b/iONess/Classes/Common/LockRunner.swift index 8e1d810..8d83b09 100644 --- a/iONess/Classes/Common/LockRunner.swift +++ b/iONess/Classes/Common/LockRunner.swift @@ -9,11 +9,13 @@ import Foundation protocol LockRunner { var lock: NSLock { get } + @discardableResult func lockedRun(_ runner: () -> Result) -> Result } extension LockRunner { + @discardableResult func lockedRun(_ runner: () -> Result) -> Result { lock.lock() defer { From a513f568a259846c18929100c26618173d0ef2c3 Mon Sep 17 00:00:00 2001 From: nayanda Date: Wed, 28 Oct 2020 12:09:15 +0700 Subject: [PATCH 15/20] Make Locked closure as simple as possible to prevent race condition --- .../NetworkSessionManager+Extensions.swift | 37 ++++++++++++------- iONess/Classes/Common/RequestAggregator.swift | 9 +++-- iONess/Classes/Common/RetryControl.swift | 24 +++++++----- 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/iONess/Classes/Common/NetworkSessionManager+Extensions.swift b/iONess/Classes/Common/NetworkSessionManager+Extensions.swift index 2b610fa..43c4d86 100644 --- a/iONess/Classes/Common/NetworkSessionManager+Extensions.swift +++ b/iONess/Classes/Common/NetworkSessionManager+Extensions.swift @@ -18,29 +18,38 @@ extension NetworkSessionManager: LockRunner { } func removeAndCancelCompletion(for request: URLRequest) { + let result = lockedRun { + completions.first(where: { $0.key =~ request }) + } + guard let biConsumer = result else { + return + } lockedRun { - guard let biConsumer = completions.first(where: { $0.key =~ request }) else { - return - } completions.removeValue(forKey: biConsumer.key) - biConsumer.value( - nil, - nil, - NetworkSessionError( - description: "iONess Error: Cancelled by NetworkSessionManager" - ) - ) } + biConsumer.value( + nil, + nil, + NetworkSessionError( + statusCode: NSURLErrorCancelled, + description: "iONess Error: Cancelled by NetworkSessionManager" + ) + ) + } func removeAndGetCompletion(for request: URLRequest) -> URLCompletion? { + let result = lockedRun { + completions.first(where: { $0.key =~ request }) + } + guard let biConsumer = result else { + return nil + } lockedRun { - guard let biConsumer = completions.first(where: { $0.key =~ request }) else { - return nil - } completions.removeValue(forKey: biConsumer.key) - return { biConsumer.value($0, $1, $2) } } + return { biConsumer.value($0, $1, $2) } + } func currentCompletion(for request: URLRequest) -> URLCompletion? { diff --git a/iONess/Classes/Common/RequestAggregator.swift b/iONess/Classes/Common/RequestAggregator.swift index 5ccdffc..5527b7b 100644 --- a/iONess/Classes/Common/RequestAggregator.swift +++ b/iONess/Classes/Common/RequestAggregator.swift @@ -188,11 +188,14 @@ extension DropableRequestAggregator { } func cancel() { - lockedRun { + let requests: [DropableURLRequest] = lockedRun { canceled = true - runningRequests.forEach { $0.drop() } - runningRequests.removeAll() + return self.runningRequests + } + lockedRun { + self.runningRequests.removeAll() } + requests.forEach { $0.drop() } } } } diff --git a/iONess/Classes/Common/RetryControl.swift b/iONess/Classes/Common/RetryControl.swift index eb1cb0b..3ad2eab 100644 --- a/iONess/Classes/Common/RetryControl.swift +++ b/iONess/Classes/Common/RetryControl.swift @@ -24,20 +24,24 @@ public class CounterRetryControl: RetryControl, LockRunner { var retriedRequests: [URLRequest: Int] = [:] public func shouldRetry(for request: URLRequest, response: URLResponse?, error: Error, didHaveDecision: @escaping (RetryControlDecision) -> Void) { - lockedRun { - let counter = retriedRequests[request] ?? 0 - guard counter < maxRetryCount else { + let counter = lockedRun { + retriedRequests[request] ?? 0 + } + guard counter < maxRetryCount else { + lockedRun { retriedRequests.removeValue(forKey: request) - didHaveDecision(.noRetry) - return } + didHaveDecision(.noRetry) + return + } + lockedRun { retriedRequests[request] = counter + 1 - guard let timeInterval = timeIntervalBeforeTryToRetry else { - didHaveDecision(.retry) - return - } - didHaveDecision(.retryAfter(timeInterval)) } + guard let timeInterval = timeIntervalBeforeTryToRetry else { + didHaveDecision(.retry) + return + } + didHaveDecision(.retryAfter(timeInterval)) } } From 713fa577c4abf2a8dbdea8686244952333f175f3 Mon Sep 17 00:00:00 2001 From: nayanda Date: Wed, 28 Oct 2020 12:10:01 +0700 Subject: [PATCH 16/20] Make some Thenable method to be not mutating to achieve more functional code --- .../Classes/Thenable/URLThenableRequest.swift | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/iONess/Classes/Thenable/URLThenableRequest.swift b/iONess/Classes/Thenable/URLThenableRequest.swift index 491f146..ddd307d 100644 --- a/iONess/Classes/Thenable/URLThenableRequest.swift +++ b/iONess/Classes/Thenable/URLThenableRequest.swift @@ -36,29 +36,36 @@ public extension URLThenableRequest { public extension URLThenableRequest { - mutating func completionDispatch(on dispatcher: DispatchQueue) -> Self { - self.dispatcher = dispatcher - return self + @discardableResult + func completionDispatch(on dispatcher: DispatchQueue) -> Self { + var requestWithDispatch = self + requestWithDispatch.dispatcher = dispatcher + return requestWithDispatch } - mutating func validate(statusCode: Int) -> Self { + @discardableResult + func validate(statusCode: Int) -> Self { return validate(statusCodes: statusCode..) -> Self { + @discardableResult + func validate(statusCodes: Range) -> Self { validate(using: StatusCodeValidator(statusCodes)) } - mutating func validate(shouldHaveHeaders headers: [String:String]) -> Self { + @discardableResult + func validate(shouldHaveHeaders headers: [String:String]) -> Self { validate(using: HeaderValidator(headers)) } - mutating func validate(using validator: URLValidator) -> Self { + @discardableResult + func validate(using validator: URLValidator) -> Self { + var requestWithValidation = self guard let current = urlValidator else { - self.urlValidator = validator + requestWithValidation.urlValidator = validator return self } - self.urlValidator = current.combine(with: validator) + requestWithValidation.urlValidator = current.combine(with: validator) return self } } From ad898cadc2476bc72419f6e78e0bd9d66669e960 Mon Sep 17 00:00:00 2001 From: nayanda Date: Wed, 28 Oct 2020 12:10:38 +0700 Subject: [PATCH 17/20] Add cause by cancel to Error, so it will not call retry control if cancelled --- .../DropableAndResumable/DropableURLRequest.swift | 3 ++- iONess/Classes/Model/NetworkSessionError.swift | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/iONess/Classes/DropableAndResumable/DropableURLRequest.swift b/iONess/Classes/DropableAndResumable/DropableURLRequest.swift index 6edeafa..391d9c1 100644 --- a/iONess/Classes/DropableAndResumable/DropableURLRequest.swift +++ b/iONess/Classes/DropableAndResumable/DropableURLRequest.swift @@ -64,7 +64,8 @@ extension BaseDropableURLRequest { _ response: URLResponse?, _ onRetry: @escaping () -> Void, onNoRetry: @escaping () -> Void) -> Void { - guard let retryControl = retryControl else { + guard !error.causeByCancel, + let retryControl = retryControl else { onNoRetry() return } diff --git a/iONess/Classes/Model/NetworkSessionError.swift b/iONess/Classes/Model/NetworkSessionError.swift index 122bba6..c75dbb8 100644 --- a/iONess/Classes/Model/NetworkSessionError.swift +++ b/iONess/Classes/Model/NetworkSessionError.swift @@ -75,13 +75,18 @@ public class NetworkSessionError: NSError, NetworkSessionErrorProtocol { public var errorDescription: String? { localizedDescription } - init(originalError: Error? = nil, statusCode: Int = -1, description: String? = nil) { + init(originalError: Error? = nil, statusCode: Int? = nil, description: String? = nil) { self.originalError = originalError - let desc: String = description ?? (NetworkSessionError.statusCodeMesage[statusCode] ?? "Unknown Error") - super.init(domain: "homecredit.co.id.ioness", code: statusCode, userInfo: [NSLocalizedDescriptionKey: desc]) + let code = (statusCode ?? (originalError as NSError?)?.code) ?? -1 + let desc: String = description ?? (NetworkSessionError.statusCodeMesage[code] ?? "Unknown Error") + super.init(domain: "homecredit.co.id.ioness", code: code, userInfo: [NSLocalizedDescriptionKey: desc]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } + +public extension Error { + var causeByCancel: Bool { (self as NSError).code == NSURLErrorCancelled } +} From f29e008d752ce2b99b36c856943d4839318484e8 Mon Sep 17 00:00:00 2001 From: nayanda Date: Wed, 28 Oct 2020 12:27:30 +0700 Subject: [PATCH 18/20] Added Error code for Unknown and Cancelled --- iONess/Classes/Model/NetworkSessionError.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/iONess/Classes/Model/NetworkSessionError.swift b/iONess/Classes/Model/NetworkSessionError.swift index c75dbb8..29f5690 100644 --- a/iONess/Classes/Model/NetworkSessionError.swift +++ b/iONess/Classes/Model/NetworkSessionError.swift @@ -15,6 +15,8 @@ public protocol NetworkSessionErrorProtocol: LocalizedError { public class NetworkSessionError: NSError, NetworkSessionErrorProtocol { public static let statusCodeMesage: [Int: String] = [ + NSURLErrorCancelled: "Request Canceled", + NSURLErrorUnknown: "Unknown Error", 203: "Non-Authoritative Information (since HTTP/1.1)", 204: "No Content", 205: "Reset Content", @@ -77,8 +79,8 @@ public class NetworkSessionError: NSError, NetworkSessionErrorProtocol { init(originalError: Error? = nil, statusCode: Int? = nil, description: String? = nil) { self.originalError = originalError - let code = (statusCode ?? (originalError as NSError?)?.code) ?? -1 - let desc: String = description ?? (NetworkSessionError.statusCodeMesage[code] ?? "Unknown Error") + let code = (statusCode ?? (originalError as NSError?)?.code) ?? NSURLErrorUnknown + let desc: String = description ?? (NetworkSessionError.statusCodeMesage[code] ?? "Unknown Custom Error") super.init(domain: "homecredit.co.id.ioness", code: code, userInfo: [NSLocalizedDescriptionKey: desc]) } From 5d72ba4fbaa7662794fe0b85de5893a70217237b Mon Sep 17 00:00:00 2001 From: nayanda Date: Wed, 28 Oct 2020 12:27:41 +0700 Subject: [PATCH 19/20] Fix indentation --- iONess/Classes/Common/URLCompatible.swift | 4 ++-- .../DropableAndResumable/ResumableDownloadRequest.swift | 2 +- iONess/Classes/Model/HTTPResultMessage.swift | 2 +- iONess/Classes/Thenable/DataRequestPromise.swift | 6 +++--- iONess/Classes/Thenable/DownloadRequestPromise.swift | 6 +++--- iONess/Classes/Thenable/UploadRequestPromise.swift | 6 +++--- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/iONess/Classes/Common/URLCompatible.swift b/iONess/Classes/Common/URLCompatible.swift index 5ca328d..9934c79 100644 --- a/iONess/Classes/Common/URLCompatible.swift +++ b/iONess/Classes/Common/URLCompatible.swift @@ -17,8 +17,8 @@ extension String: URLCompatible { var constructedURL: String = "" for parameter in parameters { guard let key: String = parameter.key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), - let value = parameter.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { - continue + let value = parameter.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + continue } constructedURL = "\(constructedURL)\(key)=\(value)&" } diff --git a/iONess/Classes/DropableAndResumable/ResumableDownloadRequest.swift b/iONess/Classes/DropableAndResumable/ResumableDownloadRequest.swift index 9174c03..c5cf695 100644 --- a/iONess/Classes/DropableAndResumable/ResumableDownloadRequest.swift +++ b/iONess/Classes/DropableAndResumable/ResumableDownloadRequest.swift @@ -98,7 +98,7 @@ public class ResumableDownloadRequest: BaseDropableURLRequest: HTTPRequestPromise { fileUrl: fileURL, retryControl: retryControl, urlValidator: urlValidator) { result in - dispatcher.async { - closure(result) - } + dispatcher.async { + closure(result) + } } } } From 42adebc9c39fbaf03e3502ba00386a53bf389745 Mon Sep 17 00:00:00 2001 From: nayanda Date: Wed, 28 Oct 2020 13:15:05 +0700 Subject: [PATCH 20/20] Added Comment for Thenable Object --- .../Classes/Thenable/DataRequestPromise.swift | 14 +++++++++ .../Thenable/DownloadRequestPromise.swift | 11 +++++++ iONess/Classes/Thenable/Thenable.swift | 19 ++++++++++++ .../Classes/Thenable/URLRequestPromise.swift | 24 ++++++++++++++- .../Classes/Thenable/URLThenableRequest.swift | 29 +++++++++++++++++++ .../Thenable/UploadRequestPromise.swift | 17 +++++++++-- 6 files changed, 110 insertions(+), 4 deletions(-) diff --git a/iONess/Classes/Thenable/DataRequestPromise.swift b/iONess/Classes/Thenable/DataRequestPromise.swift index 6249fc7..2198f87 100644 --- a/iONess/Classes/Thenable/DataRequestPromise.swift +++ b/iONess/Classes/Thenable/DataRequestPromise.swift @@ -7,8 +7,22 @@ import Foundation +/// Thenable HTTPRequest for get response data open class DataRequestPromise: HTTPRequestPromise { + /// Default Init + /// - Parameters: + /// - request: HTTPRequestMessage object which describe the request + /// - networkSessionManager: NetworkSessionManager + /// - retryControl: RetryControl object + /// - Throws: Error when generating URLRequest from HTTPRequestMessage + public override init(request: HTTPRequestMessage, with networkSessionManager: NetworkSessionManager, retryControl: RetryControl?) throws { + try super.init(request: request, with: networkSessionManager, retryControl: retryControl) + } + + /// Method to run closure after the request is finished. + /// - Parameter closure: closure which will be run when the request is finished + /// - Returns: DropableURLRequest object @discardableResult open override func then(run closure: @escaping (URLResult) -> Void) -> DropableURLRequest { let dispatcher = self.dispatcher diff --git a/iONess/Classes/Thenable/DownloadRequestPromise.swift b/iONess/Classes/Thenable/DownloadRequestPromise.swift index d3f15a9..507d6fb 100644 --- a/iONess/Classes/Thenable/DownloadRequestPromise.swift +++ b/iONess/Classes/Thenable/DownloadRequestPromise.swift @@ -7,14 +7,25 @@ import Foundation +/// Thenable HTTPRequest for downloading data open class DownloadRequestPromise: HTTPRequestPromise { var targetUrl: URL + /// Default Init + /// - Parameters: + /// - request: HTTPRequestMessage object which describe the request + /// - networkSessionManager: NetworkSessionManager object + /// - retryControl: RetryControl object + /// - targetUrl: local URL file which data will be downloaded + /// - Throws: Error when generating URLRequest from HTTPRequestMessage public init(request: HTTPRequestMessage, with networkSessionManager: NetworkSessionManager, retryControl: RetryControl?, targetUrl: URL) throws { self.targetUrl = targetUrl try super.init(request: request, with: networkSessionManager, retryControl: retryControl) } + /// Method to run closure after the request is finished. + /// - Parameter closure: closure which will be run when the request is finished + /// - Returns: DropableURLRequest object @discardableResult open override func then(run closure: @escaping (DownloadResult) -> Void) -> DropableURLRequest { let dispatcher = self.dispatcher diff --git a/iONess/Classes/Thenable/Thenable.swift b/iONess/Classes/Thenable/Thenable.swift index 1ad3848..fd67804 100644 --- a/iONess/Classes/Thenable/Thenable.swift +++ b/iONess/Classes/Thenable/Thenable.swift @@ -7,21 +7,40 @@ import Foundation +/// Thenable protocol +/// This protocol are designed to run the thenable closure after doing some task which produced Result and Dropable object public protocol Thenable { associatedtype Result associatedtype DropablePromise: Dropable + /// Method to run task and then running closures passed + /// - Parameters: + /// - closure: closure which will be run when task succeed + /// - failClosure: closure which will be run when task fail @discardableResult func then(run closure: @escaping (Result) -> Void, whenFailed failClosure: @escaping (Result) -> Void) -> DropablePromise + /// Method to run task and then running closure passed + /// - Parameter closure: closure which will be run when task finished @discardableResult func then(run closure: @escaping (Result) -> Void) -> DropablePromise + /// Method to run task and then running closures passed + /// - Parameters: + /// - closure: closure which will be run when task succeed + /// - failClosure: closure which will be run when task fail + /// - deferClosure: closure which will be run after closure or failClosure @discardableResult func then(run closure: @escaping (Result) -> Void, whenFailed failClosure: @escaping (Result) -> Void, finally deferClosure: @escaping (Result) -> Void) -> DropablePromise } public extension Thenable { + /// Method to run task and then running closures passed + /// - Parameters: + /// - closure: closure which will be run when task succeed + /// - failClosure: closure which will be run when task fail + /// - deferClosure: closure which will be run after closure or failClosure + /// - Returns: DropablePromise object @discardableResult func then(run closure: @escaping (Result) -> Void, whenFailed failClosure: @escaping (Result) -> Void, finally deferClosure: @escaping (Result) -> Void) -> DropablePromise { then( diff --git a/iONess/Classes/Thenable/URLRequestPromise.swift b/iONess/Classes/Thenable/URLRequestPromise.swift index 706fe9e..b1e22c4 100644 --- a/iONess/Classes/Thenable/URLRequestPromise.swift +++ b/iONess/Classes/Thenable/URLRequestPromise.swift @@ -7,23 +7,38 @@ import Foundation +/// Base class for Thenable URLRequest open class URLRequestPromise: URLThenableRequest { + /// URLValidator which will validate result from URL Request public var urlValidator: URLValidator? + /// DispatchQueue which will run the completion closure public var dispatcher: DispatchQueue = .main + /// Default init + public init() { } + + /// Method to run closure after the request is finished. It should be overriden, otherwise it will throw fatalError + /// - Parameter closure: closure which will be run when the request is finished + /// - Returns: DropableURLRequest object @discardableResult open func then(run closure: @escaping (Result) -> Void) -> DropableURLRequest { fatalError("iONess Error: method did not overridden 'then(run closure: @escaping (URLResult) -> Void) -> DropableURLRequest'") } } +/// Thenable URLRequest which should be used as Thenable for URLRequest which already failed when prepared open class ErrorRequestPromise: URLRequestPromise { var error: Error - init(error: Error) { + /// Default Init + /// - Parameter error: Error on prepared + public init(error: Error) { self.error = error } + /// Method to run closure after the request is finished. It should automatically fail + /// - Parameter closure: Closure which will fail + /// - Returns: FailedURLRequest object @discardableResult open override func then(run closure: @escaping (Result) -> Void) -> DropableURLRequest { let error = self.error @@ -35,11 +50,18 @@ open class ErrorRequestPromise: UR } +/// Thenable HTTPRequest open class HTTPRequestPromise: URLRequestPromise { var urlRequest: URLRequest var networkSessionManager: NetworkSessionManager var retryControl: RetryControl? + /// Default Init + /// - Parameters: + /// - request: HTTPRequestMessage object which describe the request + /// - networkSessionManager: NetworkSessionManager + /// - retryControl: RetryControl object + /// - Throws: Error when generating URLRequest from HTTPRequestMessage public init(request: HTTPRequestMessage, with networkSessionManager: NetworkSessionManager, retryControl: RetryControl?) throws { let mutableRequest = try request.getFullUrl().asMutableRequest() for header in request.headers { diff --git a/iONess/Classes/Thenable/URLThenableRequest.swift b/iONess/Classes/Thenable/URLThenableRequest.swift index ddd307d..bef97b5 100644 --- a/iONess/Classes/Thenable/URLThenableRequest.swift +++ b/iONess/Classes/Thenable/URLThenableRequest.swift @@ -7,16 +7,25 @@ import Foundation +/// Thenable URL Request protocol public protocol URLThenableRequest: Thenable where DropablePromise: DropableURLRequest, Result: NetworkResult { associatedtype Response: URLResponse + /// URLValidator which will validate result from URL Request var urlValidator: URLValidator? { get set } + /// DispatchQueue which will run the completion closure var dispatcher: DispatchQueue { get set } + /// Method to execute and ignore any result @discardableResult func executeAndForget() -> DropableURLRequest } public extension URLThenableRequest { + /// Method to execute request and then run any closure passed + /// - Parameters: + /// - closure: Closure which will run when request succeed + /// - failClosure: Closure which will run when request fail + /// - Returns: DroppableURLRequest object @discardableResult func then(run closure: @escaping (Result) -> Void, whenFailed failClosure: @escaping (Result) -> Void) -> DropableURLRequest { return then { result in @@ -28,6 +37,8 @@ public extension URLThenableRequest { } } + /// Method to execute and ignore any result + /// - Returns: DroppableURLRequest object @discardableResult func executeAndForget() -> DropableURLRequest { then { _ in } @@ -36,6 +47,9 @@ public extension URLThenableRequest { public extension URLThenableRequest { + /// Method to set DispatchQueue where the completion will run + /// - Parameter dispatcher: DispatchQueue object + /// - Returns: URLThenableRequest which have custom DispatchQueue @discardableResult func completionDispatch(on dispatcher: DispatchQueue) -> Self { var requestWithDispatch = self @@ -43,21 +57,33 @@ public extension URLThenableRequest { return requestWithDispatch } + /// Method to add URLValidator which validate using status code + /// - Parameter statusCode: valid status code + /// - Returns: URLThenableRequest which have status code URLValidator combined with previous validator if have any @discardableResult func validate(statusCode: Int) -> Self { return validate(statusCodes: statusCode..) -> Self { validate(using: StatusCodeValidator(statusCodes)) } + /// Method to add URLValidator which validate result headers + /// - Parameter headers: valid result headers + /// - Returns: URLThenableRequest which have headers URLValidator combined with previous validator if have any @discardableResult func validate(shouldHaveHeaders headers: [String:String]) -> Self { validate(using: HeaderValidator(headers)) } + /// Method to add custom URLValidator + /// - Parameter validator: custom URLValidator + /// - Returns: URLThenableRequest which have custom URLValidator combined with previous validator if have any @discardableResult func validate(using validator: URLValidator) -> Self { var requestWithValidation = self @@ -71,6 +97,9 @@ public extension URLThenableRequest { } public extension URLThenableRequest { + /// Method to aggregate request with another request + /// - Parameter request: other request to aggregate + /// - Returns: RequestAggregator object func aggregate(with request: Self) -> RequestAggregator { RequestAggregator(requests: [self, request]) } diff --git a/iONess/Classes/Thenable/UploadRequestPromise.swift b/iONess/Classes/Thenable/UploadRequestPromise.swift index 8d9218c..16a4f26 100644 --- a/iONess/Classes/Thenable/UploadRequestPromise.swift +++ b/iONess/Classes/Thenable/UploadRequestPromise.swift @@ -7,14 +7,25 @@ import Foundation +/// Thenable HTTPRequest for uploading data open class UploadRequestPromise: HTTPRequestPromise { var fileURL: URL + /// Default init + /// - Parameters: + /// - request: HTTPRequestMessage object which describe the request + /// - networkSessionManager: NetworkSessionManager object + /// - retryControl: RetryControl object + /// - fileURL: local URL file which will be uploaded + /// - Throws: Error when generating URLRequest from HTTPRequestMessage public init(request: HTTPRequestMessage, with networkSessionManager: NetworkSessionManager, retryControl: RetryControl?, fileURL: URL) throws { self.fileURL = fileURL try super.init(request: request, with: networkSessionManager, retryControl: retryControl) } + /// Method to run closure after the request is finished. + /// - Parameter closure: closure which will be run when the request is finished + /// - Returns: DropableURLRequest object @discardableResult open override func then(run closure: @escaping (URLResult) -> Void) -> DropableURLRequest { let dispatcher = self.dispatcher @@ -24,9 +35,9 @@ open class UploadRequestPromise: HTTPRequestPromise { fileUrl: fileURL, retryControl: retryControl, urlValidator: urlValidator) { result in - dispatcher.async { - closure(result) - } + dispatcher.async { + closure(result) + } } } }