Skip to content

Commit

Permalink
Merge pull request #83 from glessard/devel
Browse files Browse the repository at this point in the history
improve `recover`
  • Loading branch information
glessard authored Feb 19, 2020
2 parents 5fea189 + 6d67bd9 commit 9d58420
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 111 deletions.
134 changes: 93 additions & 41 deletions Source/deferred/deferred-extras.swift
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ extension Deferred

extension Deferred
{
/// Flatten a `Deferred<Deferred<Success>, Failure>` to a `Deferred<Success, Failure>`
/// Flatten a `Deferred<Deferred<Success, Failure>, Failure>` to a `Deferred<Success, Failure>`
///
/// In the right conditions, acts like a fast path for a flatMap with no transform.
///
Expand Down Expand Up @@ -462,7 +462,7 @@ extension Deferred
}
}

/// Flatten a `Deferred<Deferred<Success>, Never>` to a `Deferred<Success, Failure>`
/// Flatten a `Deferred<Deferred<Success, Failure>, Never>` to a `Deferred<Success, Failure>`
///
/// In the right conditions, acts like a fast path for a flatMap with no transform.
///
Expand Down Expand Up @@ -516,7 +516,7 @@ extension Deferred

}

extension Deferred where Failure == Error
extension Deferred
{
/// Enqueue a transform to be computed asynchronously if and when `self` becomes resolved with an error.
///
Expand All @@ -526,7 +526,7 @@ extension Deferred where Failure == Error
/// - parameter error: the Error to be transformed for the new `Deferred`

public func recover(queue: DispatchQueue? = nil,
transform: @escaping (_ error: Error) throws -> Deferred) -> Deferred
transform: @escaping (_ error: Failure) -> Deferred) -> Deferred
{
return Deferred(queue: queue ?? self.queue) {
resolver in
Expand All @@ -535,24 +535,19 @@ extension Deferred where Failure == Error
guard resolver.needsResolution else { return }
switch result
{
case let .success(value):
resolver.resolve(value: value)
case .success:
resolver.resolve(result)

case let .failure(error):
do {
let transformed = try transform(error)
if let transformed = transformed.peek()
{
resolver.resolve(transformed)
}
else
{
transformed.notify(queue: queue, handler: resolver.resolve)
resolver.retainSource(transformed)
}
let transformed = transform(error)
if let transformed = transformed.peek()
{
resolver.resolve(transformed)
}
catch {
resolver.resolve(error: error)
else
{
transformed.notify(queue: queue, handler: resolver.resolve)
resolver.retainSource(transformed)
}
}
}
Expand All @@ -568,7 +563,7 @@ extension Deferred where Failure == Error
/// - parameter error: the Error to be transformed for the new `Deferred`

public func recover(qos: DispatchQoS,
transform: @escaping (_ error: Error) throws -> Deferred) -> Deferred
transform: @escaping (_ error: Failure) -> Deferred) -> Deferred
{
let queue = DispatchQueue(label: "deferred-recover", qos: qos)
return recover(queue: queue, transform: transform)
Expand All @@ -582,25 +577,86 @@ extension Deferred where Failure == Error
/// - parameter qos: the QoS at which the computation (and notifications) should be performed; defaults to the current QoS class.
/// - parameter task: the computation to be performed

public static func RetryTask(_ attempts: Int, qos: DispatchQoS = .current,
task: @escaping () throws -> Success) -> Deferred
public static func Retrying(_ attempts: Int, qos: DispatchQoS = .current,
task: @escaping () -> Deferred) -> Deferred
{
let queue = DispatchQueue(label: "deferred", qos: qos)
return Deferred.RetryTask(attempts, queue: queue, task: task)
let queue = DispatchQueue(label: "retrying", qos: qos)
return Deferred.Retrying(attempts, queue: queue, task: task)
}

/// Initialize a `Deferred` with a computation task to be performed in the background
///
/// If at first it does not succeed, it will try `attempts` times in total before being resolved with an `Error`.
///
/// - parameter attempts: a maximum number of times to attempt `task`
/// - parameter attempts: a maximum number of times to attempt `task` (must be greater than zero)
/// - parameter queue: the `DispatchQueue` on which the computation (and notifications) will be executed
/// - parameter task: the computation to be performed

public static func RetryTask(_ attempts: Int, queue: DispatchQueue,
task: @escaping () throws -> Success) -> Deferred
public static func Retrying(_ attempts: Int, queue: DispatchQueue,
task: @escaping () -> Deferred) -> Deferred
{
return Deferred.Retrying(attempts, queue: queue, task: { Deferred(queue: queue, task: task) })
if attempts < 1
{
let message = "number of attempts must be greater than 0 in \(#function)"
guard let error = Invalidation.invalid(message) as? Failure else { fatalError(message) }
return Deferred(error: error)
}

return (1..<attempts).reduce(task()) {
(deferred, _) in
deferred.recover(transform: { _ in task() })
}
}
}

extension Deferred where Failure == Error
{
/// Enqueue a transform to be computed asynchronously if and when `self` becomes resolved with an error.
///
/// - parameter queue: the `DispatchQueue` to attach to the new `Deferred`; defaults to `self`'s queue.
/// - parameter transform: the transform to be performed
/// - returns: a `Deferred` reference representing the return value of the transform
/// - parameter error: the Error to be transformed for the new `Deferred`

public func recover(queue: DispatchQueue? = nil,
transform: @escaping (_ error: Error) throws -> Success) -> Deferred
{
return Deferred(queue: queue ?? self.queue) {
resolver in
self.notify(queue: queue) {
result in
guard resolver.needsResolution else { return }
switch result
{
case .success:
resolver.resolve(result)

case let .failure(error):
do {
let value = try transform(error)
resolver.resolve(value: value)
}
catch {
resolver.resolve(error: error)
}
}
}
resolver.retainSource(self)
}
}

/// Enqueue a transform to be computed asynchronously if and when `self` becomes resolved with an error.
///
/// - parameter qos: the QoS at which to execute the transform and the new `Deferred`'s notifications
/// - parameter transform: the transform to be performed
/// - returns: a `Deferred` reference representing the return value of the transform
/// - parameter error: the Error to be transformed for the new `Deferred`

public func recover(qos: DispatchQoS,
transform: @escaping (_ error: Error) throws -> Success) -> Deferred
{
let queue = DispatchQueue(label: "deferred-recover", qos: qos)
return recover(queue: queue, transform: transform)
}

/// Initialize a `Deferred` with a computation task to be performed in the background
Expand All @@ -612,9 +668,9 @@ extension Deferred where Failure == Error
/// - parameter task: the computation to be performed

public static func Retrying(_ attempts: Int, qos: DispatchQoS = .current,
task: @escaping () throws -> Deferred) -> Deferred
task: @escaping () throws -> Success) -> Deferred
{
let queue = DispatchQueue(label: "retrying", qos: qos)
let queue = DispatchQueue(label: "deferred", qos: qos)
return Deferred.Retrying(attempts, queue: queue, task: task)
}

Expand All @@ -627,19 +683,15 @@ extension Deferred where Failure == Error
/// - parameter task: the computation to be performed

public static func Retrying(_ attempts: Int, queue: DispatchQueue,
task: @escaping () throws -> Deferred) -> Deferred
task: @escaping () throws -> Success) -> Deferred
{
let error = Invalidation.invalid("task was not allowed a single attempt in \(#function)")
let deferred = Deferred(queue: queue, error: error)

if attempts < 1 { return deferred }

return Deferred.Retrying(attempts, deferred, task: task)
}
if attempts < 1
{
let message = "number of attempts must be greater than 0 in \(#function)"
return Deferred(queue: queue, error: Invalidation.invalid(message))
}

private static func Retrying(_ attempts: Int, _ deferred: Deferred, task: @escaping () throws -> Deferred) -> Deferred
{
return (0..<attempts).reduce(deferred) {
return (1..<attempts).reduce(Deferred(queue: queue, task: task)) {
(deferred, _) in
deferred.recover(transform: { _ in try task() })
}
Expand Down
118 changes: 50 additions & 68 deletions Tests/deferredTests/DeferredExtrasTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,111 +110,93 @@ class DeferredExtrasTests: XCTestCase
XCTAssertEqual(d4.result, value)
}

func testRecover()
func testRecover1()
{
let value = nzRandom()
let error = nzRandom()
let goodOperand = Deferred<Int, Error>(value: value)
let badOperand = Deferred<Double, Error>(error: TestError(error))
let badOperand = Deferred<Double, TestError>(error: TestError(error))

// good operand, transform short-circuited
let d1 = goodOperand.recover(qos: .default) { e in XCTFail(); return Deferred(error: TestError(error)) }
let d1 = goodOperand.recover(qos: .default) { _ -> Deferred<Int, Error> in fatalError(#function) }
XCTAssertEqual(d1.value, value)
XCTAssertEqual(d1.error, nil)

// bad operand, transform throws (type 1)
let d2 = badOperand.recover { error in Deferred { throw TestError(value) } }
// bad operand, transform errors
let d2 = badOperand.recover { error in Deferred(error: TestError(error.error)) }
XCTAssertEqual(d2.value, nil)
XCTAssertEqual(d2.error, TestError(value))

// bad operand, transform throws (type 2)
let d5 = badOperand.recover { _ in throw TestError(value) }
XCTAssertEqual(d5.value, nil)
XCTAssertEqual(d5.error, TestError(value))
XCTAssertEqual(d2.error, TestError(error))

// bad operand, transform executes
let d3 = badOperand.recover { error in Deferred(value: Double(value)) }
XCTAssertEqual(d3.value, Double(value))
// bad operand, transform executes later
let d3 = badOperand.recover { error in Deferred(value: Double(error.error)).delay(.milliseconds(10)) }
XCTAssertEqual(d3.value, Double(error))
XCTAssertEqual(d3.error, nil)

// test early return from notification block
let reason = "reason"
let d4 = goodOperand.delay(.milliseconds(50))
let r4 = d4.recover { e in Deferred(value: value) }
r4.cancel(reason)
XCTAssertEqual(r4.value, nil)
XCTAssertEqual(r4.error as? Cancellation, .canceled(reason))
}

func testRetrying1()
{
let retries = 5
let queue = DispatchQueue(label: "test")

let r1 = Deferred<Void, Error>.Retrying(0, queue: queue, task: { Deferred<Void, Error>(task: {XCTFail()}) })
XCTAssertNotNil(r1.error as? Invalidation)

var counter = 0
let r2 = Deferred<Int, Error>.Retrying(retries, queue: queue) {
() -> Deferred<Int, Error> in
let retried = Deferred<Int, TestError>.Retrying(retries, qos: .utility) {
() -> Deferred<Int, TestError> in
counter += 1
if counter < retries { return Deferred(error: TestError(counter)) }
return Deferred(value: counter)
}
XCTAssertEqual(r2.value, retries)
XCTAssertEqual(retried.value, retries)

let errored = Deferred<Int, Error>.Retrying(0, task: { () -> Deferred<Int, Error> in fatalError() })
XCTAssertEqual(errored.value, nil)
XCTAssertNotNil(errored.error as? Invalidation)
}

func testRetrying2()
func testRecover2()
{
let retries = 5
let value = nzRandom()
let error = nzRandom()
let goodOperand = Deferred<Int, Error>(value: value)
let badOperand = Deferred<Double, Error>(error: TestError(error))

let r1 = Deferred<Void, Error>.Retrying(0, task: { Deferred<Void, Error>(task: {XCTFail()}) })
XCTAssertNotNil(r1.error as? Invalidation)
// good operand, transform short-circuited
let d1 = goodOperand.recover(qos: .default) { _ throws -> Int in fatalError(#function) }
XCTAssertEqual(d1.value, value)
XCTAssertEqual(d1.error, nil)

var counter = 0
let r2 = Deferred<Int, Error>.Retrying(retries) {
() -> Deferred<Int, Error> in
counter += 1
if counter < retries { return Deferred(error: TestError(counter)) }
return Deferred(value: counter)
}
XCTAssertEqual(r2.value, retries)
// bad operand, transform errors
let d2 = badOperand.recover { try ($0 as? TestError).map { throw TestError($0.error) } ?? 0.0 }
XCTAssertEqual(d2.value, nil)
XCTAssertEqual(d2.error, TestError(error))

let r3 = Deferred<Int, Error>.Retrying(retries, qos: .background) {
() -> Deferred<Int, Error> in
counter += 1
return Deferred(error: TestError(counter))
}
XCTAssertEqual(r3.error, TestError(2*retries))
// bad operand, transform executes
let d3 = badOperand.recover { ($0 as? TestError).map { Double($0.error) } ?? 0.0 }
XCTAssertEqual(d3.value, Double(error))
XCTAssertEqual(d3.error, nil)
}

func testRetryTask()
func testRetrying2()
{
let retries = 5
let queue = DispatchQueue(label: "test", qos: .background)

var counter = 0
let r1 = Deferred.RetryTask(retries, queue: queue) {
() throws -> Int in
counter += 1
throw TestError(counter)
var counter = retries+retries-1
func transform() throws -> Int
{
counter -= 1
guard counter <= 0 else { throw TestError(counter) }
return counter
}

let r1 = Deferred.Retrying(retries, queue: queue, task: transform)
XCTAssertEqual(r1.value, nil)
XCTAssertEqual(r1.error, TestError(retries))
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
XCTAssertEqual(r1.qos, .background)
#endif
XCTAssertEqual(r1.error, TestError(retries-1))

let r2 = Deferred.RetryTask(retries, qos: .utility) {
() throws -> Int in
counter += 1
throw TestError(counter)
}
XCTAssertEqual(r2.value, nil)
XCTAssertEqual(r2.error, TestError(2*retries))
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
XCTAssertEqual(r2.qos, .utility)
#endif
let r2 = Deferred.Retrying(retries, qos: .utility, task: transform)
XCTAssertEqual(r2.value, 0)
XCTAssertEqual(r2.error, nil)

let r3 = Deferred.Retrying(0, task: { Double.nan })
XCTAssertEqual(r3.value, nil)
XCTAssertNotNil(r3.error as? Invalidation)
}

func testFlatMap()
Expand Down
4 changes: 2 additions & 2 deletions Tests/deferredTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ extension DeferredExtrasTests {
("testOnValueAndOnError", testOnValueAndOnError),
("testOptional", testOptional),
("testQoS", testQoS),
("testRecover", testRecover),
("testRecover1", testRecover1),
("testRecover2", testRecover2),
("testRetrying1", testRetrying1),
("testRetrying2", testRetrying2),
("testRetryTask", testRetryTask),
("testSplit", testSplit),
("testTryFlatMap", testTryFlatMap),
("testTryMap", testTryMap),
Expand Down

0 comments on commit 9d58420

Please sign in to comment.