Skip to content

Commit

Permalink
Merge pull request #17 from vadymmarkov/feature/failure-policy
Browse files Browse the repository at this point in the history
Feature: failure policy
  • Loading branch information
vadymmarkov authored May 27, 2017
2 parents c78aced + 21f8e88 commit 37dfb81
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 36 deletions.
41 changes: 26 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>()

// Resolves the promise
promise.resolve("String")

// Or rejects the promise
promise.reject(Error.notFound)
```
Expand All @@ -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
Expand All @@ -96,48 +94,62 @@ let promise = Promise<String>(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<String>()

// 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<String>()

// 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<String>()
// 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<String>()

// Add done callback
// Add always callback
promise.always({ result in
switch result {
case let .success(value):
Expand All @@ -146,7 +158,6 @@ promise.always({ result in
print(error)
}
})

// Resolve or reject the promise
promise.resolve("String") // promise.reject(Error.notFound)
```
Expand Down
8 changes: 8 additions & 0 deletions Sources/When/Error.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
public enum PromiseError: Error {
case cancelled
}

public enum FailurePolicy {
case allErrors
case notCancelled
}
65 changes: 44 additions & 21 deletions Sources/When/Promise.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import Foundation

open class Promise<T> {

public typealias DoneHandler = (T) -> Void
public typealias FailureHandler = (Error) -> Void
public typealias CompletionHandler = (Result<T>) -> 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<T>

fileprivate(set) var observer: Observer<T>?
fileprivate(set) var doneHandler: DoneHandler?
fileprivate(set) var failureHandler: FailureHandler?
fileprivate(set) var completionHandler: CompletionHandler?

// 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
Expand All @@ -33,7 +30,7 @@ open class Promise<T> {
}
}

/// 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
Expand All @@ -44,7 +41,7 @@ open class Promise<T> {
}
}

/// 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
Expand All @@ -54,15 +51,18 @@ open class Promise<T> {
}
}

/// Create a promise with a given state
/// Create a promise with a given state.
public init(queue: DispatchQueue = mainQueue, state: State<T> = .pending) {
self.queue = queue
self.state = state
}

// MARK: - States

open func reject(_ error: Error) {
/**
Rejects a promise with a given error.
*/
public func reject(_ error: Error) {
guard self.state.isPending else {
return
}
Expand All @@ -71,7 +71,10 @@ open class Promise<T> {
update(state: state)
}

open func resolve(_ value: T) {
/**
Resolves a promise with a given value.
*/
public func resolve(_ value: T) {
guard self.state.isPending else {
return
}
Expand All @@ -80,26 +83,47 @@ open class Promise<T> {
update(state: state)
}

/// Rejects a promise with the cancelled error.
public func cancel() {
reject(PromiseError.cancelled)
}

// MARK: - Callbacks

@discardableResult open func done(_ handler: @escaping DoneHandler) -> Self {
/**
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
}

@discardableResult open func fail(_ handler: @escaping FailureHandler) -> Self {
failureHandler = handler
/**
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
if case PromiseError.cancelled = error, policy == .notCancelled {
return
}
handler(error)
}
return self
}

@discardableResult open func always(_ handler: @escaping CompletionHandler) -> 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
}

// MARK: - Helpers

fileprivate func update(state: State<T>?) {
private func update(state: State<T>?) {
dispatch(queue) {
guard let state = state, let result = state.result else {
return
Expand All @@ -110,7 +134,7 @@ open class Promise<T> {
}
}

fileprivate func notify(_ result: Result<T>) {
private func notify(_ result: Result<T>) {
switch result {
case let .success(value):
doneHandler?(value)
Expand Down Expand Up @@ -151,7 +175,7 @@ open class Promise<T> {
update(state: state)
}

fileprivate func dispatch(_ queue: DispatchQueue, closure: @escaping () -> Void) {
private func dispatch(_ queue: DispatchQueue, closure: @escaping () -> Void) {
if queue === instantQueue {
closure()
} else {
Expand All @@ -163,16 +187,15 @@ open class Promise<T> {
// MARK: - Then

extension Promise {

public func then<U>(on queue: DispatchQueue = mainQueue, _ body: @escaping (T) throws -> U) -> Promise<U> {
let promise = Promise<U>()
let promise = Promise<U>(queue: queue)
addObserver(on: queue, promise: promise, body)

return promise
}

public func then<U>(on queue: DispatchQueue = mainQueue, _ body: @escaping (T) throws -> Promise<U>) -> Promise<U> {
let promise = Promise<U>()
let promise = Promise<U>(queue: queue)

addObserver(on: queue, promise: promise) { value -> U? in
let nextPromise = try body(value)
Expand Down
8 changes: 8 additions & 0 deletions When.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -88,6 +91,7 @@
/* Begin PBXFileReference section */
D500FD111C3AABED00782D78 /* Playground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Playground.playground; sourceTree = "<group>"; };
D509E46D1C6B2A16009DEB57 /* SpecError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpecError.swift; sourceTree = "<group>"; };
D582E5821ED9AD9400D0E21B /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = "<group>"; };
D58B2C231E412E7D0099F6D7 /* Functions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = "<group>"; };
D58B2C241E412E7D0099F6D7 /* Observer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.swift; sourceTree = "<group>"; };
D58B2C251E412E7D0099F6D7 /* Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -212,6 +216,7 @@
D58B2C251E412E7D0099F6D7 /* Promise.swift */,
D58B2C261E412E7D0099F6D7 /* Result.swift */,
D58B2C271E412E7D0099F6D7 /* State.swift */,
D582E5821ED9AD9400D0E21B /* Error.swift */,
);
path = When;
sourceTree = "<group>";
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
47 changes: 47 additions & 0 deletions WhenTests/Shared/PromiseSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,53 @@ class PromiseSpec: QuickSpec {
}
}

describe("#fail:policy") {
let string = "Success!"

beforeEach {
promise = Promise<String>()
}

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<String>()
Expand Down

0 comments on commit 37dfb81

Please sign in to comment.