diff --git a/SessionArtist/Info.plist b/SessionArtist/Info.plist index 8c30608..3f11dbf 100644 --- a/SessionArtist/Info.plist +++ b/SessionArtist/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.6.4 + 0.6.5 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/SessionArtist/Result.swift b/SessionArtist/Result.swift index 461d4ed..e71e355 100644 --- a/SessionArtist/Result.swift +++ b/SessionArtist/Result.swift @@ -2,16 +2,21 @@ import Foundation +/** + An `Either` type wrapping either abitrary value of type `T` or an `Error`. + */ public enum Result { case success(T) case failure(Error) + @available(*, deprecated, message: "Considered harmful. `Result(MyError)` will unexpectedly return a `Result.success`." ) public init(_ value: T) { self = .success(value) } + @available(*, deprecated, message: "Considered harmful. `Result(MyError)` will unexpectedly return a `Result.success`." ) public init(_ error: Error) { self = .failure(error) } @@ -20,6 +25,26 @@ public enum Result { public extension Result { + var isSuccess: Bool { + switch self { + case .success: + return true + case .failure: + return false + } + } + + + var isFailure: Bool { + return !isSuccess + } + + + /** + If `self` is a `success`, unwraps the value and maps it to a new value via `transform` which is, itself, wrapped in a new `success`. If `self` is a `failure`, reutrns that failure directly without evaluating `transform`. + * note: Implicit here is the idea that `transform` will always succeed. If it can fail, use `flatMap(transform:)` instead so that an error can be thrown or a `failure` returned. + * seeAlso: `flatMap(transform:)` + */ func map(transform: (T)->U) -> Result { switch self { case .success(let t): @@ -30,6 +55,10 @@ public extension Result { } + /** + If `self` is a `success`, unwraps the value and maps it to a new `success` *or* `failure` via `transform`. As a convenience, any errors thrown during `transform` will be mapped to a `failure`. If `self` is already a `failure`, reutrns that failure directly without evaluating `transform`. + * seeAlso: `map(transform:)`, `asyncFlatMap(asyncTransform:completion:)` + */ func flatMap(transform: (T) throws -> Result) -> Result { switch self { case .success(let t): @@ -44,6 +73,24 @@ public extension Result { } + /** + If `self` is a `success`, unwraps the value and maps it to a new `success` *or* `failure` via `transform`, calling `completion` with the result. If `self` is already a `failure`, passes that failure directly `completion` without evaluating `transform`. + * note: the transform function is, itself, asynchronous in the CPS-style. + * seeAlso: `flatMap(transform:)` + */ + func asyncFlatMap(asyncTransform: (T, @escaping (Result)->Void)->Void, completion: @escaping (Result)->Void) { + switch self { + case .success(let t): + asyncTransform(t) { u in + completion(u) + } + case .failure(let e): + completion(.failure(e)) + } + } + + + /// If `self` is a `success`, unwrap the value and return it. Otherwise, throw the `failure`'s error. func resolve() throws -> T { switch self { case .success(let t): @@ -52,4 +99,23 @@ public extension Result { throw e } } + + + /// Compose two `Result` continuations. Unlike the generic `route(continuation:adaptor)`, this version `flatMap`s over results with `adaptor` so we don't have to deal with `failure` branches. + static func flatRoute(continuation: @escaping (Result)->Void, adaptor: @escaping (T)->Result) -> (Result)->Void { + return { t in + continuation(t.flatMap(transform: adaptor)) + } + } + + + /** + Compose two `Result` continuations via an adaptor that is, itself, asynchronous in the CPS-style. Necessary for adaptors that need to call async routines. + * seeAlso: `flatRoute(continuation:adaptor:) + */ + static func asyncFlatRoute(continuation: @escaping (Result)->Void, asyncAdaptor: @escaping (T, @escaping (Result)->Void)->Void) -> (Result)->Void { + return { t in + t.asyncFlatMap(asyncTransform: asyncAdaptor, completion: continuation) + } + } } diff --git a/SessionArtistTests/ResultTests.swift b/SessionArtistTests/ResultTests.swift index e08385c..a54bf83 100644 --- a/SessionArtistTests/ResultTests.swift +++ b/SessionArtistTests/ResultTests.swift @@ -15,6 +15,14 @@ class ResultTests: XCTestCase { } + func testPredicates() { + XCTAssert(success.isSuccess) + XCTAssert(failure.isFailure) + XCTAssertFalse(success.isFailure) + XCTAssertFalse(failure.isSuccess) + } + + func testInitializer() { switch Result("foo") { case .success("foo"): @@ -83,6 +91,66 @@ class ResultTests: XCTestCase { } + func testAsyncFlatMapToSuccess() { + let expectedSuccessFromSuccess = expectation(description: "Waiting for successful result") + let expectedFailureFromFailure = expectation(description: "Waiting for failure result") + + let transform: (Int, (Result)->Void)->Void = { i, completion in + completion(.success(String(i))) + } + + success.asyncFlatMap(asyncTransform: transform) { res in + switch res { + case .success("42"): + expectedSuccessFromSuccess.fulfill() + default: + XCTFail() + } + } + + failure.asyncFlatMap(asyncTransform: transform) { res in + switch res { + case .failure(E.original): + expectedFailureFromFailure.fulfill() + default: + XCTFail() + } + } + + wait(for: [expectedSuccessFromSuccess, expectedFailureFromFailure], timeout: 0) + } + + + func testAsyncFlatMapToFailure() { + let expectedFailureFromSuccess = expectation(description: "Waiting for new failure") + let expectedFailureFromFailure = expectation(description: "Waiting for originnal failure") + + let transform: (Int, (Result)->Void)->Void = { i, completion in + completion(.failure(E.new)) + } + + success.asyncFlatMap(asyncTransform: transform) { res in + switch res { + case .failure(E.new): + expectedFailureFromSuccess.fulfill() + default: + XCTFail() + } + } + + failure.asyncFlatMap(asyncTransform: transform) { res in + switch res { + case .failure(E.original): + expectedFailureFromFailure.fulfill() + default: + XCTFail() + } + } + + wait(for: [expectedFailureFromSuccess, expectedFailureFromFailure], timeout: 0) + } + + func testMap() { switch success.map(transform: { String($0) }) { case .success("42"): @@ -134,4 +202,136 @@ class ResultTests: XCTestCase { XCTFail() } } + + + func testFlatRouteToSuccess() { + let expectedSuccessFromSuccess = expectation(description: "Waiting for success.") + let expectedFailureFromFailure = expectation(description: "Waiting for failure.") + + let successContinuation: (Result)->Void = { res in + switch res { + case .success("42"): + expectedSuccessFromSuccess.fulfill() + default: + XCTFail() + } + } + + let failureContinuation: (Result)->Void = { res in + switch res { + case .failure(E.original): + expectedFailureFromFailure.fulfill() + default: + XCTFail() + } + } + + let adaptor: (Int)->Result = { i in + return .success(String(i)) + } + + Result.flatRoute(continuation: successContinuation, adaptor: adaptor)(success) + Result.flatRoute(continuation: failureContinuation, adaptor: adaptor)(failure) + + wait(for: [expectedSuccessFromSuccess, expectedFailureFromFailure], timeout: 0) + } + + + func testFlatRouteToFailure() { + let expectedFailureFromSuccess = expectation(description: "Waiting for failure from success.") + let expectedFailureFromFailure = expectation(description: "Waiting for failure.") + + let successContinuation: (Result)->Void = { res in + switch res { + case .failure(E.new): + expectedFailureFromSuccess.fulfill() + default: + XCTFail() + } + } + + let failureContinuation: (Result)->Void = { res in + switch res { + case .failure(E.original): + expectedFailureFromFailure.fulfill() + default: + XCTFail() + } + } + + let adaptor: (Int)->Result = { i in + return .failure(E.new) + } + + Result.flatRoute(continuation: successContinuation, adaptor: adaptor)(success) + Result.flatRoute(continuation: failureContinuation, adaptor: adaptor)(failure) + + wait(for: [expectedFailureFromSuccess, expectedFailureFromFailure], timeout: 0) + } + + + func testAsyncFlatRouteToSuccess() { + let expectedSuccessFromSuccess = expectation(description: "Waiting for success.") + let expectedFailureFromFailure = expectation(description: "Waiting for failure.") + + let successContinuation: (Result)->Void = { res in + switch res { + case .success("42"): + expectedSuccessFromSuccess.fulfill() + default: + XCTFail() + } + } + + let failureContinuation: (Result)->Void = { res in + switch res { + case .failure(E.original): + expectedFailureFromFailure.fulfill() + default: + XCTFail() + } + } + + let adaptor: (Int, (Result)->Void)->Void = { i, completion in + completion(.success(String(i))) + } + + Result.asyncFlatRoute(continuation: successContinuation, asyncAdaptor: adaptor)(success) + Result.asyncFlatRoute(continuation: failureContinuation, asyncAdaptor: adaptor)(failure) + + wait(for: [expectedSuccessFromSuccess, expectedFailureFromFailure], timeout: 0) + } + + + func testAsyncFlatRouteToFailure() { + let expectedFailureFromSuccess = expectation(description: "Waiting for failure from success.") + let expectedFailureFromFailure = expectation(description: "Waiting for failure.") + + let successContinuation: (Result)->Void = { res in + switch res { + case .failure(E.new): + expectedFailureFromSuccess.fulfill() + default: + XCTFail() + } + } + + let failureContinuation: (Result)->Void = { res in + switch res { + case .failure(E.original): + expectedFailureFromFailure.fulfill() + default: + XCTFail() + } + } + + let adaptor: (Int, (Result)->Void)->Void = { i, completion in + completion(.failure(E.new)) + } + + Result.asyncFlatRoute(continuation: successContinuation, asyncAdaptor: adaptor)(success) + Result.asyncFlatRoute(continuation: failureContinuation, asyncAdaptor: adaptor)(failure) + + wait(for: [expectedFailureFromSuccess, expectedFailureFromFailure], timeout: 0) + } }