2
0
Эх сурвалжийг харах

Add mapError, flatMapError, withValue, withError, ifSuccess, and ifFailure to Result.

Jon Shier 8 жил өмнө
parent
commit
088d593136

+ 98 - 1
Source/Result.swift

@@ -162,7 +162,7 @@ extension Result {
     ///     try print(noInt.unwrap())
     ///     // Throws error
     ///
-    /// - parameter transform: A closure that takes the success value of the result instance.
+    /// - parameter transform: A closure that takes the success value of the `Result` instance.
     ///
     /// - returns: A `Result` containing the result of the given closure. If this instance is a failure, returns the
     ///            same failure.
@@ -200,4 +200,101 @@ extension Result {
             return .failure(error)
         }
     }
+    
+    /// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a parameter.
+    ///
+    /// Use the `mapError` function with a closure that does not throw. For example:
+    ///
+    ///     let possibleData: Result<Data> = .failure(someError)
+    ///     let withMyError: Result<Data> = possibleData.mapError { MyError.error($0) }
+    ///
+    /// - Parameter transform: A closure that takes the error of the instance.
+    /// - Returns: A `Result` instance containing the result of the transform. If this instance is a success, returns
+    ///            the same instance.
+    public func mapError<T: Error>(_ transform: (Error) -> T) -> Result {
+        switch self {
+        case .failure(let error):
+            return .failure(transform(error))
+        case .success:
+            return self
+        }
+    }
+    
+    /// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a parameter.
+    ///
+    /// Use the `flatMapError` function with a closure that may throw an error. For example:
+    ///
+    ///     let possibleData: Result<Data> = .success(Data(...))
+    ///     let possibleObject = possibleData.flatMapError {
+    ///         try someFailableFunction(taking: $0)
+    ///     }
+    ///
+    /// - Parameter transform: A throwing closure that takes the error of the instance.
+    ///
+    /// - Returns: A `Result` instance containing the result of the transform. If this instance is a success, returns
+    ///            the same instance.
+    public func flatMapError<T: Error>(_ transform: (Error) throws -> T) -> Result {
+        switch self {
+        case .failure(let error):
+            do {
+                return try .failure(transform(error))
+            } catch {
+                return .failure(error)
+            }
+        case .success:
+            return self
+        }
+    }
+    
+    /// Evaluates the specified closure when the `Result` is a success, passing the unwrapped value as a paramter.
+    ///
+    /// Use the `withValue` function to evaluate the passed closure without modifying the `Result` instance.
+    ///
+    /// - Parameter closure: A closure that takes the success value of this instance.
+    /// - Returns: This `Result` instance, unmodified.
+    @discardableResult
+    public func withValue(_ closure: (Value) -> Void) -> Result {
+        if case let .success(value) = self { closure(value) }
+        
+        return self
+    }
+    
+    /// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a paramter.
+    ///
+    /// Use the `withError` function to evaluate the passed closure without modifying the `Result` instance.
+    ///
+    /// - Parameter closure: A closure that takes the success value of this instance.
+    /// - Returns: This `Result` instance, unmodified.
+    @discardableResult
+    public func withError(_ closure: (Error) -> Void) -> Result {
+        if case let .failure(error) = self { closure(error) }
+        
+        return self
+    }
+    
+    /// Evaluates the specified closure when the `Result` is a success.
+    ///
+    /// Use the `ifSuccess` function to evaluate the passed closure without modifying the `Result` instance.
+    ///
+    /// - Parameter closure: A `Void` closure.
+    /// - Returns: This `Result` instance, unmodified.
+    @discardableResult
+    public func ifSuccess(_ closure: () -> Void) -> Result {
+        if isSuccess { closure() }
+        
+        return self
+    }
+    
+    /// Evaluates the specified closure when the `Result` is a failure.
+    ///
+    /// Use the `ifFailure` function to evaluate the passed closure without modifying the `Result` instance.
+    ///
+    /// - Parameter closure: A `Void` closure.
+    /// - Returns: This `Result` instance, unmodified.
+    @discardableResult
+    public func ifFailure(_ closure: () -> Void) -> Result {
+        if isFailure { closure() }
+        
+        return self
+    }
 }

+ 201 - 8
Tests/ResultTests.swift

@@ -152,8 +152,8 @@ class ResultTestCase: BaseTestCase {
         let value = "success value"
 
         // When
-        let result1 = Result(value: { value })  // syntax 1
-        let result2 = Result { value }          // syntax 2
+        let result1 = Result(value: { value })
+        let result2 = Result { value }
 
         // Then
         for result in [result1, result2] {
@@ -167,8 +167,8 @@ class ResultTestCase: BaseTestCase {
         struct ResultError: Error {}
 
         // When
-        let result1 = Result(value: { throw ResultError() })    // syntax 1
-        let result2 = Result { throw ResultError() }            // syntax 2
+        let result1 = Result(value: { throw ResultError() })
+        let result2 = Result { throw ResultError() }
 
         // Then
         for result in [result1, result2] {
@@ -180,11 +180,14 @@ class ResultTestCase: BaseTestCase {
     // MARK: - Unwrap Tests
 
     func testThatUnwrapReturnsSuccessValue() {
-        // Given, When
+        // Given
         let result = Result<String>.success("success value")
 
+        // When
+        let unwrappedValue = try? result.unwrap()
+        
         // Then
-        XCTAssertEqual(try result.unwrap(), "success value")
+        XCTAssertEqual(unwrappedValue, "success value")
     }
 
     func testThatUnwrapThrowsFailureError() {
@@ -246,7 +249,7 @@ class ResultTestCase: BaseTestCase {
     }
 
     func testThatFlatMapCatchesTransformationError() {
-        // Given, When
+        // Given
         struct TransformError: Error {}
         let result = Result<String>.success("success value")
 
@@ -262,7 +265,7 @@ class ResultTestCase: BaseTestCase {
     }
 
     func testThatFlatMapPreservesFailureError() {
-        // Given, When
+        // Given
         struct ResultError: Error {}
         struct TransformError: Error {}
         let result = Result<String>.failure(ResultError())
@@ -277,4 +280,194 @@ class ResultTestCase: BaseTestCase {
             XCTFail("flatMap should preserve the failure error")
         }
     }
+    
+    // MARK: - map/flatMapError Tests
+    
+    func testMapErrorTransformsErrorValue() {
+        // Given
+        struct ResultError: Error {}
+        struct OtherError: Error { let error: Error }
+        let result: Result<String> = .failure(ResultError())
+        
+        // When
+        let mappedResult = result.mapError { OtherError(error: $0) }
+        
+        // Then
+        if let error = mappedResult.error {
+            XCTAssertTrue(error is OtherError)
+        } else {
+            XCTFail("mapError should transform error value")
+        }
+    }
+    
+    func testMapErrorPreservesSuccessError() {
+        // Given
+        struct ResultError: Error {}
+        struct OtherError: Error { let error: Error }
+        let result: Result<String> = .success("success")
+        
+        // When
+        let mappedResult = result.mapError { OtherError(error: $0) }
+        
+        // Then
+        XCTAssertEqual(mappedResult.value, "success")
+    }
+    
+    func testFlatMapErrorTransformsErrorValue() {
+        // Given
+        struct ResultError: Error {}
+        struct OtherError: Error { let error: Error }
+        let result: Result<String> = .failure(ResultError())
+        
+        // When
+        let mappedResult = result.flatMapError { OtherError(error: $0) }
+        
+        // Then
+        if let error = mappedResult.error {
+            XCTAssertTrue(error is OtherError)
+        } else {
+            XCTFail("mapError should transform error value")
+        }
+    }
+    
+    func testFlatMapErrorCapturesThrownError() {
+        // Given
+        struct ResultError: Error {}
+        struct OtherError: Error {
+            let error: Error
+            init(error: Error) throws { throw ThrownError() }
+        }
+        struct ThrownError: Error {}
+        let result: Result<String> = .failure(ResultError())
+        
+        // When
+        let mappedResult = result.flatMapError { try OtherError(error: $0) }
+        // Then
+        if let error = mappedResult.error {
+            XCTAssertTrue(error is ThrownError)
+        } else {
+            XCTFail("mapError should capture thrown error value")
+        }
+    }
+    
+    // MARK: with/if Tests
+    
+    func testWithValueExecutesWhenSuccess() {
+        // Given
+        let result: Result<String> = .success("success")
+        var string = "failure"
+        
+        // When
+        result.withValue { string = $0 }
+        
+        // Then
+        XCTAssertEqual(string, "success")
+    }
+    
+    func testWithValueDoesNotExecutesWhenFailure() {
+        // Given
+        struct ResultError: Error {}
+        let result: Result<String> = .failure(ResultError())
+        var string = "failure"
+        
+        // When
+        result.withValue { string = $0 }
+        
+        // Then
+        XCTAssertEqual(string, "failure")
+    }
+    
+    func testWithErrorExecutesWhenFailure() {
+        // Given
+        struct ResultError: Error {}
+        let result: Result<String> = .failure(ResultError())
+        var string = "success"
+        
+        // When
+        result.withError { string = "\(type(of: $0))" }
+        
+        // Then
+        XCTAssertEqual(string, "(ResultError #1)")
+    }
+    
+    func testWithErrorDoesNotExecuteWhenSuccess() {
+        // Given
+        let result: Result<String> = .success("success")
+        var string = "success"
+        
+        // When
+        result.withError { string = "\(type(of: $0))" }
+        
+        // Then
+        XCTAssertEqual(string, "success")
+    }
+    
+    func testIfSuccessExecutesWhenSuccess() {
+        // Given
+        let result: Result<String> = .success("success")
+        var string = "failure"
+        
+        // When
+        result.ifSuccess { string = "success" }
+        
+        // Then
+        XCTAssertEqual(string, "success")
+    }
+    
+    func testIfSuccessDoesNotExecutesWhenFailure() {
+        // Given
+        struct ResultError: Error {}
+        let result: Result<String> = .failure(ResultError())
+        var string = "failure"
+        
+        // When
+        result.ifSuccess { string = "success" }
+        
+        // Then
+        XCTAssertEqual(string, "failure")
+    }
+    
+    func testIfFailureExecutesWhenFailure() {
+        // Given
+        struct ResultError: Error {}
+        let result: Result<String> = .failure(ResultError())
+        var string = "success"
+        
+        // When
+        result.ifFailure { string = "failure" }
+        
+        // Then
+        XCTAssertEqual(string, "failure")
+    }
+    
+    func testIfFailureDoesNotExecuteWhenSuccess() {
+        // Given
+        let result: Result<String> = .success("success")
+        var string = "success"
+        
+        // When
+        result.ifFailure { string = "failure" }
+        
+        // Then
+        XCTAssertEqual(string, "success")
+    }
+    
+    func testFunctionalMethodsCanBeChained() {
+        // Given
+        struct ResultError: Error {}
+        let result: Result<String> = .success("first")
+        var string = "first"
+        var success = false
+        
+        // When
+        let endResult = result.map { _ in "second" }
+                              .flatMap { _ in "third" }
+                              .withValue { if $0 == "third" { string = "fourth" } }
+                              .ifSuccess { success = true }
+        
+        // Then
+        XCTAssertEqual(endResult.value, "third")
+        XCTAssertEqual(string, "fourth")
+        XCTAssertTrue(success)
+    }
 }