diff --git a/Result/AnyError.swift b/Result/AnyError.swift index ee018d2..1b034e2 100644 --- a/Result/AnyError.swift +++ b/Result/AnyError.swift @@ -1,8 +1,16 @@ import Foundation +/// Protocols used to define a wrapper for arbitrary `Error`s. +public protocol ErrorInitializing: Swift.Error { + init(_ error: Swift.Error) +} +public protocol ErrorConvertible { + var error : Swift.Error { get } +} + /// A type-erased error which wraps an arbitrary error instance. This should be /// useful for generic contexts. -public struct AnyError: Swift.Error { +public struct AnyError: Swift.Error, ErrorInitializing, ErrorConvertible { /// The underlying error. public let error: Swift.Error @@ -10,17 +18,11 @@ public struct AnyError: Swift.Error { if let anyError = error as? AnyError { self = anyError } else { - self.error = error + self.error = (error as? ErrorConvertible)?.error ?? error } } } -extension AnyError: ErrorConvertible { - public static func error(from error: Error) -> AnyError { - return AnyError(error) - } -} - extension AnyError: CustomStringConvertible { public var description: String { return String(describing: error) @@ -44,3 +46,13 @@ extension AnyError: LocalizedError { return (error as? LocalizedError)?.recoverySuggestion } } + +public protocol NSErrorInitializing : ErrorInitializing {} + +extension NSErrorInitializing { + public init(_ error: Swift.Error) { + self = error as! Self + } +} + +extension NSError : NSErrorInitializing {} diff --git a/Result/Result.swift b/Result/Result.swift index de0330a..d86e65c 100644 --- a/Result/Result.swift +++ b/Result/Result.swift @@ -8,37 +8,26 @@ public enum Result: ResultProtocol, CustomStringConve // MARK: Constructors /// Constructs a success wrapping a `value`. - public init(value: Value) { + public init(value: Value, errorType: Error.Type) { self = .success(value) } + public init(value: Value) { + self.init(value: value, errorType: Error.self) + } /// Constructs a failure wrapping an `error`. - public init(error: Error) { + public init(error: Error, valueType: Value.Type) { self = .failure(error) } + public init(error: Error) { + self.init(error: error, valueType: Value.self) + } /// Constructs a result from an `Optional`, failing with `Error` if `nil`. public init(_ value: Value?, failWith: @autoclosure () -> Error) { self = value.map(Result.success) ?? .failure(failWith()) } - /// Constructs a result from a function that uses `throw`, failing with `Error` if throws. - public init(_ f: @autoclosure () throws -> Value) { - self.init(attempt: f) - } - - /// Constructs a result from a function that uses `throw`, failing with `Error` if throws. - public init(attempt f: () throws -> Value) { - do { - self = .success(try f()) - } catch var error { - if Error.self == AnyError.self { - error = AnyError(error) - } - self = .failure(error as! Error) - } - } - // MARK: Deconstruction /// Returns the value from `success` Results or `throw`s the error. @@ -47,6 +36,9 @@ public enum Result: ResultProtocol, CustomStringConve case let .success(value): return value case let .failure(error): + if let wrapper = error as? ErrorConvertible { + throw wrapper.error + } throw error } } @@ -115,6 +107,26 @@ public enum Result: ResultProtocol, CustomStringConve } } +extension Result where Error: ErrorInitializing { + /// Constructs a result from an expression that uses `throw`, failing with `Error` if throws. + public init(_ f: @autoclosure () throws -> Value) { + self.init(attempt: f) + } + + /// Constructs a result from a closure that uses `throw`, failing with `Error` if throws. + public init(attempt f: () throws -> Value) { + do { + self = .success(try f()) + } catch { + if let wrappedError = error as? Error { + self = .failure(wrappedError) + } else { + self = .failure(Error.init(error)) + } + } + } +} + extension Result where Error == AnyError { /// Constructs a result from an expression that uses `throw`, failing with `AnyError` if throws. public init(_ f: @autoclosure () throws -> Value) { @@ -131,6 +143,18 @@ extension Result where Error == AnyError { } } +extension Result where Error == NoError { + /// Constructs a success wrapping a `value`. + public init(value: Value) { + self = .success(value) + } + + /// Constructs a result from an expression that does not use `throw` and should never fail. + public init(_ f: @autoclosure () -> Value) { + self = .success(f()) + } +} + // MARK: - Derive result from failable closure @available(*, deprecated, renamed: "Result.init(attempt:)") @@ -143,18 +167,6 @@ public func materialize(_ f: @autoclosure () throws -> T) -> Result Self { - func cast(_ error: Swift.Error) -> T { - return error as! T - } - - return cast(error) - } -} - // MARK: - migration support @available(*, unavailable, message: "Use the overload which returns `Result` instead") diff --git a/Result/ResultProtocol.swift b/Result/ResultProtocol.swift index fdb71c5..2ce16c9 100644 --- a/Result/ResultProtocol.swift +++ b/Result/ResultProtocol.swift @@ -59,6 +59,11 @@ public extension Result { case let .failure(error): return transform(error) } } + + /// Returns the result of re-wrapping `Failure`’s errors in a different `Error` wrapper, or re-wrapping `Success`es’ values. + public func mapError(to errorType: Error2.Type = Error2.self) -> Result { + return mapError{ Error2(($0 as? ErrorConvertible)?.error ?? $0) } + } /// Returns a new Result by mapping `Success`es’ values using `success`, and by mapping `Failure`'s values using `failure`. public func bimap(success: (Value) -> U, failure: (Error) -> Error2) -> Result { @@ -87,12 +92,7 @@ public extension Result { } } -/// Protocol used to constrain `tryMap` to `Result`s with compatible `Error`s. -public protocol ErrorConvertible: Swift.Error { - static func error(from error: Swift.Error) -> Self -} - -public extension Result where Error: ErrorConvertible { +public extension Result where Error: ErrorInitializing { /// Returns the result of applying `transform` to `Success`es’ values, or wrapping thrown errors. public func tryMap(_ transform: (Value) throws -> U) -> Result { @@ -101,9 +101,10 @@ public extension Result where Error: ErrorConvertible { return .success(try transform(value)) } catch { - let convertedError = Error.error(from: error) - // Revisit this in a future version of Swift. https://twitter.com/jckarter/status/672931114944696321 - return .failure(convertedError) + guard let wrappedError = error as? Error else { + return .failure(Error.init(error)) + } + return .failure(wrappedError) } } } @@ -142,5 +143,6 @@ extension Result { // MARK: - migration support -@available(*, unavailable, renamed: "ErrorConvertible") -public protocol ErrorProtocolConvertible: ErrorConvertible {} +@available(*, unavailable, message: "This has been removed. Use `ErrorInitializing` instead.") +public protocol ErrorProtocolConvertible {} + diff --git a/Tests/ResultTests/AnyErrorTests.swift b/Tests/ResultTests/AnyErrorTests.swift index e0e2e20..42b2c04 100644 --- a/Tests/ResultTests/AnyErrorTests.swift +++ b/Tests/ResultTests/AnyErrorTests.swift @@ -4,7 +4,10 @@ import Result final class AnyErrorTests: XCTestCase { static var allTests: [(String, (AnyErrorTests) -> () throws -> Void)] { - return [ ("testAnyError", testAnyError) ] + return [ + ("testAnyError", testAnyError), + ("testWrapperError", testWrapperError), + ] } func testAnyError() { @@ -13,4 +16,10 @@ final class AnyErrorTests: XCTestCase { let anyErrorFromAnyError = AnyError(anyErrorFromError) XCTAssertTrue(anyErrorFromError == anyErrorFromAnyError) } + func testWrapperError() { + let error = Error.a + let wrapperErrorFromError = WrapperError(error) + let wrapperErrorFromWrapperError = WrapperError(wrapperErrorFromError) + XCTAssertTrue(wrapperErrorFromError == wrapperErrorFromWrapperError) + } } diff --git a/Tests/ResultTests/ResultTests.swift b/Tests/ResultTests/ResultTests.swift index 6e40ab4..5cc5117 100644 --- a/Tests/ResultTests/ResultTests.swift +++ b/Tests/ResultTests/ResultTests.swift @@ -117,6 +117,9 @@ final class ResultTests: XCTestCase { let result: Result = Result(attempt: function) XCTAssert(result.error == AnyError(nsError)) + + let wrapperResult: Result = Result(attempt: function) + XCTAssert(wrapperResult.error == WrapperError(nsError)) } func testMaterializeProducesSuccesses() { @@ -125,6 +128,7 @@ final class ResultTests: XCTestCase { let result2: Result = Result(attempt: { try tryIsSuccess("success") }) XCTAssert(result2 == success) + } func testMaterializeProducesFailures() { @@ -135,9 +139,69 @@ final class ResultTests: XCTestCase { XCTAssert(result2.error == error) } - func testMaterializeInferrence() { - let result = Result(attempt: { try tryIsSuccess(nil) }) - XCTAssert((type(of: result) as Any.Type) is Result.Type) + func testMaterializeProducesSuccessesForErrorInitializing() { + let result1: Result = Result(try tryIsSuccess("success")) + XCTAssert(result1 == wrapperSuccess) + + let result2: Result = Result(attempt: { try tryIsSuccess("success") }) + XCTAssert(result2 == wrapperSuccess) + } + + func testMaterializeProducesFailuresForErrorInitializing() { + let result1: Result = Result(try tryIsSuccess(nil)) + XCTAssert(result1.error == wrapperError) + + let result2: Result = Result(attempt: { try tryIsSuccess(nil) }) + XCTAssert(result2.error == wrapperError) + + } + + func testInference() { + + let resultAny = Result(attempt: { try tryIsSuccess(nil) }) + XCTAssert((type(of: resultAny) as Any.Type) is Result.Type) + + + let resultWrp: Result = Result(attempt: { try tryIsSuccess(nil) }) + XCTAssert((type(of: resultWrp) as Any.Type) is Result.Type) + + let resultWrp2 = resultAny.mapError{ WrapperError($0.error) } + XCTAssert((type(of: resultWrp2) as Any.Type) is Result.Type) + + let resultWrp3 = resultAny.mapError(to: WrapperError.self) + XCTAssert((type(of: resultWrp3) as Any.Type) is Result.Type) + + let resultWrp4: Result = resultAny.mapError() + XCTAssert((type(of: resultWrp4) as Any.Type) is Result.Type) + + + let resultNo = Result(successValue) + XCTAssert((type(of: resultNo) as Any.Type) is Result.Type) + + let resultNo2 = Result(value: successValue) + XCTAssert((type(of: resultNo2) as Any.Type) is Result.Type) + + } + + func testPartialInference() { + + let resultVal = Result(value: successValue, errorType: Error.self) + XCTAssert((type(of: resultVal) as Any.Type) is Result.Type) + + let resultErr = Result(error: WrapperError.c, valueType: String.self) + XCTAssert((type(of: resultErr) as Any.Type) is Result.Type) + + } + + func testParsingSuccess() { + let result = Result(attempt: { try tryParseJSON(jsonRaw) }) + XCTAssert((type(of: result) as Any.Type) is Result.Type) + XCTAssert(result.value != nil) + } + func testParsingFailure() { + let result = Result(attempt: { try tryParseJSON(jsonRawMalformed) }) + XCTAssert((type(of: result) as Any.Type) is Result.Type) + XCTAssert(result.error != nil) } // MARK: Recover @@ -213,13 +277,52 @@ enum Error: Swift.Error, LocalizedError { } } -let success = Result.success("success") +enum WrapperError: Swift.Error, LocalizedError, ErrorInitializing, ErrorConvertible { + case c, d, other(Swift.Error) + + init(_ error: Swift.Error) { + self = (error as? WrapperError) ?? .other((error as? ErrorConvertible)?.error ?? error) + } + + var error: Swift.Error { + switch self { + case .other(let error): return error + default: return self + } + } + + var errorDescription: String? { + return "localized description" + } + + var failureReason: String? { + return "failure reason" + } + + var helpAnchor: String? { + return "help anchor" + } + + var recoverySuggestion: String? { + return "recovery suggestion" + } +} + +let successValue = "success" +let success = Result.success(successValue) let error = AnyError(Error.a) let error2 = AnyError(Error.b) let error3 = AnyError(NSError(domain: "Result", code: 42, userInfo: [NSLocalizedDescriptionKey: "localized description"])) let failure = Result.failure(error) let failure2 = Result.failure(error2) +let wrapperSuccess = Result.success(successValue) +let wrapperError = WrapperError(Error.a) +let wrapperError2 = WrapperError(Error.b) +let wrapperError3 = WrapperError(NSError(domain: "Result", code: 42, userInfo: [NSLocalizedDescriptionKey: "localized description"])) +let wrapperFailure = Result.failure(wrapperError) +let wrapperFailure2 = Result.failure(wrapperError2) + // MARK: - Helpers extension AnyError: Equatable { @@ -229,14 +332,34 @@ extension AnyError: Equatable { } } +extension WrapperError: Equatable { + public static func ==(lhs: WrapperError, rhs: WrapperError) -> Bool { + switch (lhs, rhs) { + case (.c, .c), (.d, .d): return true + case (.other(let lError), .other(let rError)): + return lError._code == rError._code + && lError._domain == rError._domain + default: + return false + } + } +} + func tryIsSuccess(_ text: String?) throws -> String { guard let text = text, text == "success" else { - throw error + throw Error.a } return text } +let jsonObject: Any = ["foo": "bar"] +let jsonRaw = "{\"foo\": \"bar\"}" +let jsonRawMalformed = "{\"foo\": \"bar\"" +func tryParseJSON(_ jsonString: String) throws -> Any { + return try JSONSerialization.jsonObject(with: jsonString.data(using: .utf8) ?? Data(), options: []) +} + extension NSError { var function: String? { return userInfo[Result<(), NSError>.functionKey] as? String @@ -270,6 +393,12 @@ extension ResultTests { ("testTryCatchWithFunctionCatchProducesFailures", testTryCatchWithFunctionCatchProducesFailures), ("testMaterializeProducesSuccesses", testMaterializeProducesSuccesses), ("testMaterializeProducesFailures", testMaterializeProducesFailures), + ("testMaterializeProducesSuccessesForErrorInitializing", testMaterializeProducesSuccessesForErrorInitializing), + ("testMaterializeProducesFailuresForErrorInitializing", testMaterializeProducesFailuresForErrorInitializing), + ("testInference", testInference), + ("testPartialInference", testInference), + ("testParsingSuccess", testParsingSuccess), + ("testParsingFailure", testParsingFailure), ("testRecoverProducesLeftForLeftSuccess", testRecoverProducesLeftForLeftSuccess), ("testRecoverProducesRightForLeftFailure", testRecoverProducesRightForLeftFailure), ("testRecoverWithProducesLeftForLeftSuccess", testRecoverWithProducesLeftForLeftSuccess),