From 1cbafd68f6cafcd78df0d4b9e03edebc6686ad72 Mon Sep 17 00:00:00 2001 From: Vadym Markov Date: Sat, 27 May 2017 14:33:57 +0200 Subject: [PATCH 1/4] Add tests --- WhenTests/Shared/PromiseSpec.swift | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/WhenTests/Shared/PromiseSpec.swift b/WhenTests/Shared/PromiseSpec.swift index a3573b2..346b17f 100644 --- a/WhenTests/Shared/PromiseSpec.swift +++ b/WhenTests/Shared/PromiseSpec.swift @@ -221,6 +221,53 @@ class PromiseSpec: QuickSpec { } } + describe("#fail:policy") { + let string = "Success!" + + beforeEach { + promise = Promise() + } + + context("all errors") { + it("invokes the handler") { + let failExpectation = self.expectation(description: "Fail expectation") + + promise + .then({ value in + throw PromiseError.cancelled + }) + .fail(policy: .allErrors, { error in + expect((error as? PromiseError) == .cancelled).to(beTrue()) + failExpectation.fulfill() + }) + + promise.resolve(string) + self.waitForExpectations(timeout: 2.0, handler:nil) + } + } + + context("all errors") { + it("does not invoke the handler") { + let failExpectation = self.expectation(description: "Fail expectation") + + promise + .then({ value in + throw PromiseError.cancelled + }) + .fail(policy: .notCancelled, { error in + fail("Handler should not be called") + }) + .always({ result in + expect((result.error as? PromiseError) == .cancelled).to(beTrue()) + failExpectation.fulfill() + }) + + promise.resolve(string) + self.waitForExpectations(timeout: 2.0, handler:nil) + } + } + } + describe("#always") { beforeEach { promise = Promise() From 85d6c6ec166fbb1e95a2b3492a06bb7c891c1273 Mon Sep 17 00:00:00 2001 From: Vadym Markov Date: Sat, 27 May 2017 14:51:36 +0200 Subject: [PATCH 2/4] Implement failure policy --- Sources/When/Error.swift | 8 +++++++ Sources/When/Promise.swift | 39 ++++++++++++++++++++-------------- When.xcodeproj/project.pbxproj | 8 +++++++ 3 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 Sources/When/Error.swift diff --git a/Sources/When/Error.swift b/Sources/When/Error.swift new file mode 100644 index 0000000..a881696 --- /dev/null +++ b/Sources/When/Error.swift @@ -0,0 +1,8 @@ +public enum PromiseError: Error { + case cancelled +} + +public enum FailurePolicy { + case allErrors + case notCancelled +} diff --git a/Sources/When/Promise.swift b/Sources/When/Promise.swift index e6ba4cf..55698be 100644 --- a/Sources/When/Promise.swift +++ b/Sources/When/Promise.swift @@ -1,14 +1,12 @@ import Foundation open class Promise { - public typealias DoneHandler = (T) -> Void public typealias FailureHandler = (Error) -> Void public typealias CompletionHandler = (Result) -> Void - open let key = UUID().uuidString - - var queue: DispatchQueue + public let key = UUID().uuidString + fileprivate(set) var queue: DispatchQueue fileprivate(set) public var state: State fileprivate(set) var observer: Observer? @@ -62,7 +60,7 @@ open class Promise { // MARK: - States - open func reject(_ error: Error) { + public func reject(_ error: Error) { guard self.state.isPending else { return } @@ -71,7 +69,7 @@ open class Promise { update(state: state) } - open func resolve(_ value: T) { + public func resolve(_ value: T) { guard self.state.isPending else { return } @@ -80,26 +78,36 @@ open class Promise { update(state: state) } + public func cancel() { + reject(PromiseError.cancelled) + } + // MARK: - Callbacks - @discardableResult open func done(_ handler: @escaping DoneHandler) -> Self { + @discardableResult public func done(_ handler: @escaping DoneHandler) -> Self { doneHandler = handler return self } - @discardableResult open func fail(_ handler: @escaping FailureHandler) -> Self { - failureHandler = handler + @discardableResult public func fail(policy: FailurePolicy = .notCancelled, + _ handler: @escaping FailureHandler) -> Self { + failureHandler = { error in + if case PromiseError.cancelled = error, policy == .notCancelled { + return + } + handler(error) + } return self } - @discardableResult open func always(_ handler: @escaping CompletionHandler) -> Self { + @discardableResult public func always(_ handler: @escaping CompletionHandler) -> Self { completionHandler = handler return self } // MARK: - Helpers - fileprivate func update(state: State?) { + private func update(state: State?) { dispatch(queue) { guard let state = state, let result = state.result else { return @@ -110,7 +118,7 @@ open class Promise { } } - fileprivate func notify(_ result: Result) { + private func notify(_ result: Result) { switch result { case let .success(value): doneHandler?(value) @@ -151,7 +159,7 @@ open class Promise { update(state: state) } - fileprivate func dispatch(_ queue: DispatchQueue, closure: @escaping () -> Void) { + private func dispatch(_ queue: DispatchQueue, closure: @escaping () -> Void) { if queue === instantQueue { closure() } else { @@ -163,16 +171,15 @@ open class Promise { // MARK: - Then extension Promise { - public func then(on queue: DispatchQueue = mainQueue, _ body: @escaping (T) throws -> U) -> Promise { - let promise = Promise() + let promise = Promise(queue: queue) addObserver(on: queue, promise: promise, body) return promise } public func then(on queue: DispatchQueue = mainQueue, _ body: @escaping (T) throws -> Promise) -> Promise { - let promise = Promise() + let promise = Promise(queue: queue) addObserver(on: queue, promise: promise) { value -> U? in let nextPromise = try body(value) diff --git a/When.xcodeproj/project.pbxproj b/When.xcodeproj/project.pbxproj index 6e4b222..2b6c0f7 100644 --- a/When.xcodeproj/project.pbxproj +++ b/When.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ D509E46B1C6ABDD8009DEB57 /* PromiseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1900B1C69D9E300ECCB66 /* PromiseSpec.swift */; }; D509E46E1C6B2A16009DEB57 /* SpecError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D509E46D1C6B2A16009DEB57 /* SpecError.swift */; }; D509E46F1C6B2A16009DEB57 /* SpecError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D509E46D1C6B2A16009DEB57 /* SpecError.swift */; }; + D582E5831ED9AD9400D0E21B /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = D582E5821ED9AD9400D0E21B /* Error.swift */; }; + D582E5841ED9AD9400D0E21B /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = D582E5821ED9AD9400D0E21B /* Error.swift */; }; + D582E5851ED9AD9400D0E21B /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = D582E5821ED9AD9400D0E21B /* Error.swift */; }; D58B2C281E412E7D0099F6D7 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58B2C231E412E7D0099F6D7 /* Functions.swift */; }; D58B2C291E412E7D0099F6D7 /* Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58B2C241E412E7D0099F6D7 /* Observer.swift */; }; D58B2C2A1E412E7D0099F6D7 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58B2C251E412E7D0099F6D7 /* Promise.swift */; }; @@ -88,6 +91,7 @@ /* Begin PBXFileReference section */ D500FD111C3AABED00782D78 /* Playground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Playground.playground; sourceTree = ""; }; D509E46D1C6B2A16009DEB57 /* SpecError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpecError.swift; sourceTree = ""; }; + D582E5821ED9AD9400D0E21B /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; D58B2C231E412E7D0099F6D7 /* Functions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; D58B2C241E412E7D0099F6D7 /* Observer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.swift; sourceTree = ""; }; D58B2C251E412E7D0099F6D7 /* Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = ""; }; @@ -212,6 +216,7 @@ D58B2C251E412E7D0099F6D7 /* Promise.swift */, D58B2C261E412E7D0099F6D7 /* Result.swift */, D58B2C271E412E7D0099F6D7 /* State.swift */, + D582E5821ED9AD9400D0E21B /* Error.swift */, ); path = When; sourceTree = ""; @@ -738,6 +743,7 @@ D58B2C291E412E7D0099F6D7 /* Observer.swift in Sources */, D58B2C2B1E412E7D0099F6D7 /* Result.swift in Sources */, D58B2C2C1E412E7D0099F6D7 /* State.swift in Sources */, + D582E5831ED9AD9400D0E21B /* Error.swift in Sources */, D58B2C2A1E412E7D0099F6D7 /* Promise.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -762,6 +768,7 @@ D58B2C2E1E412E820099F6D7 /* Observer.swift in Sources */, D58B2C301E412E820099F6D7 /* Result.swift in Sources */, D58B2C311E412E820099F6D7 /* State.swift in Sources */, + D582E5841ED9AD9400D0E21B /* Error.swift in Sources */, D58B2C2F1E412E820099F6D7 /* Promise.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -786,6 +793,7 @@ D58B2C331E412E820099F6D7 /* Observer.swift in Sources */, D58B2C351E412E820099F6D7 /* Result.swift in Sources */, D58B2C361E412E820099F6D7 /* State.swift in Sources */, + D582E5851ED9AD9400D0E21B /* Error.swift in Sources */, D58B2C341E412E820099F6D7 /* Promise.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From d03148dc93490903e7a6fe725b7ad17cc7f12a29 Mon Sep 17 00:00:00 2001 From: Vadym Markov Date: Sat, 27 May 2017 14:58:15 +0200 Subject: [PATCH 3/4] Add docs --- Sources/When/Promise.swift | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/Sources/When/Promise.swift b/Sources/When/Promise.swift index 55698be..f27a92a 100644 --- a/Sources/When/Promise.swift +++ b/Sources/When/Promise.swift @@ -8,7 +8,6 @@ open class Promise { public let key = UUID().uuidString fileprivate(set) var queue: DispatchQueue fileprivate(set) public var state: State - fileprivate(set) var observer: Observer? fileprivate(set) var doneHandler: DoneHandler? fileprivate(set) var failureHandler: FailureHandler? @@ -16,7 +15,7 @@ open class Promise { // MARK: - Initialization - /// Create a promise that resolves using a synchronous closure + /// Create a promise that resolves using a synchronous closure. public init(queue: DispatchQueue = mainQueue, _ body: @escaping (Void) throws -> T) { state = .pending self.queue = queue @@ -31,7 +30,7 @@ open class Promise { } } - /// Create a promise that resolves using an asynchronous closure that can either resolve or reject + /// Create a promise that resolves using an asynchronous closure that can either resolve or reject. public init(queue: DispatchQueue = mainQueue, _ body: @escaping (_ resolve: (T) -> Void, _ reject: (Error) -> Void) -> Void) { state = .pending @@ -42,7 +41,7 @@ open class Promise { } } - /// Create a promise that resolves using an asynchronous closure that can only resolve + /// Create a promise that resolves using an asynchronous closure that can only resolve. public init(queue: DispatchQueue = mainQueue, _ body: @escaping (@escaping (T) -> Void) -> Void) { state = .pending self.queue = queue @@ -52,7 +51,7 @@ open class Promise { } } - /// Create a promise with a given state + /// Create a promise with a given state. public init(queue: DispatchQueue = mainQueue, state: State = .pending) { self.queue = queue self.state = state @@ -60,6 +59,9 @@ open class Promise { // MARK: - States + /** + Rejects a promise with a given error. + */ public func reject(_ error: Error) { guard self.state.isPending else { return @@ -69,6 +71,9 @@ open class Promise { update(state: state) } + /** + Resolves a promise with a given value. + */ public func resolve(_ value: T) { guard self.state.isPending else { return @@ -78,17 +83,24 @@ open class Promise { update(state: state) } + /// Rejects a promise with the cancelled error. public func cancel() { reject(PromiseError.cancelled) } // MARK: - Callbacks + /** + Adds a handler to be called when the promise object is resolved with a value. + */ @discardableResult public func done(_ handler: @escaping DoneHandler) -> Self { doneHandler = handler return self } + /** + Adds a handler to be called when the promise object is rejected with an error. + */ @discardableResult public func fail(policy: FailurePolicy = .notCancelled, _ handler: @escaping FailureHandler) -> Self { failureHandler = { error in @@ -100,6 +112,10 @@ open class Promise { return self } + /** + Adds a handler to be called when the promise object is either resolved or rejected. + This callback will be called after done or fail handlers + **/ @discardableResult public func always(_ handler: @escaping CompletionHandler) -> Self { completionHandler = handler return self From 21f8e884b1f253bbb66506a10abc8cb5ed032378 Mon Sep 17 00:00:00 2001 From: Vadym Markov Date: Sat, 27 May 2017 15:09:11 +0200 Subject: [PATCH 4/4] Update readme --- README.md | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7b24322..e257266 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,8 @@ state and then could be resolved with a value or rejected with an error. ```swift // Creates a new promise that could be resolved with a String value let promise = Promise() - // Resolves the promise promise.resolve("String") - // Or rejects the promise promise.reject(Error.notFound) ``` @@ -80,7 +78,7 @@ let promise = Promise({ ``` ```swift -// Creates a new promise that is rejected with an ErrorType +// Creates a new promise that is rejected with an Error let promise = Promise({ //... throw Error.notFound @@ -96,48 +94,62 @@ let promise = Promise(queue: dispatch_get_main_queue()) ``` ### Done -Add a handler to be called when the ***promise*** object is resolved with a value: +Adds a handler to be called when the ***promise*** object is resolved with a value: ```swift // Create a new promise in a pending state let promise = Promise() - // Add done callback promise.done({ value in print(value) }) - // Resolve the promise promise.resolve("String") ``` ### Fail -Add a handler to be called when the ***promise*** object is rejected with -an `ErrorType`: +Adds a handler to be called when the ***promise*** object is rejected with +an `Error`: ```swift // Create a new promise in a pending state let promise = Promise() - -// Add done callback +// Add fail callback promise.fail({ error in print(error) }) - // Reject the promise promise.reject(Error.notFound) ``` +It's also possible to cancel a promise, which means it will be rejected with +`PromiseError.cancelled` error. `FailurePolicy` can be used if you want to +ignore this error in your `fail` handler: + +```swift +// Create a new promise in a pending state +let promise = Promise() +// This callback will not be called when a promise is cancelled +promise.fail({ error in + print(error) +}) +// This callback will be called when a promise is cancelled +promise.fail(policy: .allErrors, { error in + print(error) +}) +// Cancel the promise +promise.cancel() +``` + ### Always -Add a handler to be called when the ***promise*** object is either resolved or +Adds a handler to be called when the ***promise*** object is either resolved or rejected. This callback will be called after [done](#done) or [fail](#fail) handlers. ```swift // Create a new promise in a pending state let promise = Promise() - -// Add done callback +// Add always callback promise.always({ result in switch result { case let .success(value): @@ -146,7 +158,6 @@ promise.always({ result in print(error) } }) - // Resolve or reject the promise promise.resolve("String") // promise.reject(Error.notFound) ```