diff --git a/CHANGELOG.md b/CHANGELOG.md index 46eb97a..60a4a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning]. - RandomSkunk.Results: - Add `Maybe.ToFailIfNone` method. + - Add `Rescue` methods. ### Fixed - RandomSkunk.Results: diff --git a/RandomSkunk.Results.UnitTests/Rescue_methods.cs b/RandomSkunk.Results.UnitTests/Rescue_methods.cs new file mode 100644 index 0000000..7aca68c --- /dev/null +++ b/RandomSkunk.Results.UnitTests/Rescue_methods.cs @@ -0,0 +1,290 @@ +namespace RandomSkunk.Results.UnitTests; + +public class Rescue_methods +{ + public class For_Result_sync + { + [Fact] + public void Given_IsSuccess_Returns_source() + { + var sourceResult = Result.Success(); + var rescueResult = Result.Fail(); + + var actual = sourceResult.Rescue(error => rescueResult); + + actual.Should().Be(sourceResult); + } + + [Fact] + public void Given_IsFail_Returns_result_from_rescue_function() + { + var sourceResult = Result.Fail(); + var rescueResult = Result.Success(); + Error? capturedError = null; + + var actual = sourceResult.Rescue(error => + { + capturedError = error; + return rescueResult; + }); + + actual.Should().Be(rescueResult); + capturedError.Should().Be(sourceResult.Error); + } + + [Fact] + public void Given_IsFail_When_rescue_function_throws_Returns_result_from_caught_exception() + { + var sourceResult = Result.Fail(); + var thrownException = new Exception(); + + var actual = sourceResult.Rescue((Func)(error => throw thrownException)); + + actual.IsFail.Should().BeTrue(); + actual.Error.ErrorCode.Should().Be(ErrorCodes.CaughtException); + actual.Error.InnerError!.Title.Should().Be(nameof(Exception)); + } + } + + public class For_Result_async + { + [Fact] + public async Task Given_IsSuccess_Returns_source() + { + var sourceResult = Result.Success(); + var rescueResult = Result.Fail(); + + var actual = await sourceResult.Rescue(error => Task.FromResult(rescueResult)); + + actual.Should().Be(sourceResult); + } + + [Fact] + public async Task Given_IsFail_Returns_result_from_rescue_function() + { + var sourceResult = Result.Fail(); + var rescueResult = Result.Success(); + Error? capturedError = null; + + var actual = await sourceResult.Rescue(error => + { + capturedError = error; + return Task.FromResult(rescueResult); + }); + + actual.Should().Be(rescueResult); + capturedError.Should().Be(sourceResult.Error); + } + + [Fact] + public async Task Given_IsFail_When_rescue_function_throws_Returns_result_from_caught_exception() + { + var sourceResult = Result.Fail(); + var thrownException = new Exception(); + + var actual = await sourceResult.Rescue((Func>)(error => throw thrownException)); + + actual.IsFail.Should().BeTrue(); + actual.Error.ErrorCode.Should().Be(ErrorCodes.CaughtException); + actual.Error.InnerError!.Title.Should().Be(nameof(Exception)); + } + } + + public class For_Result_of_T_sync + { + [Fact] + public void Given_IsSuccess_Returns_source() + { + var sourceResult = Result.Success(123); + var rescueResult = Result.Fail(); + + var actual = sourceResult.Rescue(error => rescueResult); + + actual.Should().Be(sourceResult); + } + + [Fact] + public void Given_IsFail_Returns_result_from_rescue_function() + { + var sourceResult = Result.Fail(); + var rescueResult = Result.Success(123); + Error? capturedError = null; + + var actual = sourceResult.Rescue(error => + { + capturedError = error; + return rescueResult; + }); + + actual.Should().Be(rescueResult); + capturedError.Should().Be(sourceResult.Error); + } + + [Fact] + public void Given_IsFail_When_rescue_function_throws_Returns_result_from_caught_exception() + { + var sourceResult = Result.Fail(); + var thrownException = new Exception(); + + var actual = sourceResult.Rescue((Func>)(error => throw thrownException)); + + actual.IsFail.Should().BeTrue(); + actual.Error.ErrorCode.Should().Be(ErrorCodes.CaughtException); + actual.Error.InnerError!.Title.Should().Be(nameof(Exception)); + } + } + + public class For_Result_of_T_async + { + [Fact] + public async Task Given_IsSuccess_Returns_source() + { + var sourceResult = Result.Success(123); + var rescueResult = Result.Fail(); + + var actual = await sourceResult.Rescue(error => Task.FromResult(rescueResult)); + + actual.Should().Be(sourceResult); + } + + [Fact] + public async Task Given_IsFail_Returns_result_from_rescue_function() + { + var sourceResult = Result.Fail(); + var rescueResult = Result.Success(123); + Error? capturedError = null; + + var actual = await sourceResult.Rescue(error => + { + capturedError = error; + return Task.FromResult(rescueResult); + }); + + actual.Should().Be(rescueResult); + capturedError.Should().Be(sourceResult.Error); + } + + [Fact] + public async Task Given_IsFail_When_rescue_function_throws_Returns_result_from_caught_exception() + { + var sourceResult = Result.Fail(); + var thrownException = new Exception(); + + var actual = await sourceResult.Rescue((Func>>)(error => throw thrownException)); + + actual.IsFail.Should().BeTrue(); + actual.Error.ErrorCode.Should().Be(ErrorCodes.CaughtException); + actual.Error.InnerError!.Title.Should().Be(nameof(Exception)); + } + } + + public class For_Maybe_of_T_sync + { + [Fact] + public void Given_IsSuccess_Returns_source() + { + var sourceResult = Maybe.Success(123); + var rescueResult = Maybe.Fail(); + + var actual = sourceResult.Rescue(error => rescueResult); + + actual.Should().Be(sourceResult); + } + + [Fact] + public void Given_None_Returns_source() + { + var sourceResult = Maybe.None; + var rescueResult = Maybe.Fail(); + + var actual = sourceResult.Rescue(error => rescueResult); + + actual.Should().Be(sourceResult); + } + + [Fact] + public void Given_IsFail_Returns_result_from_rescue_function() + { + var sourceResult = Maybe.Fail(); + var rescueResult = Maybe.Success(123); + Error? capturedError = null; + + var actual = sourceResult.Rescue(error => + { + capturedError = error; + return rescueResult; + }); + + actual.Should().Be(rescueResult); + capturedError.Should().Be(sourceResult.Error); + } + + [Fact] + public void Given_IsFail_When_rescue_function_throws_Returns_result_from_caught_exception() + { + var sourceResult = Maybe.Fail(); + var thrownException = new Exception(); + + var actual = sourceResult.Rescue((Func>)(error => throw thrownException)); + + actual.IsFail.Should().BeTrue(); + actual.Error.ErrorCode.Should().Be(ErrorCodes.CaughtException); + actual.Error.InnerError!.Title.Should().Be(nameof(Exception)); + } + } + + public class For_Maybe_of_T_async + { + [Fact] + public async Task Given_IsSuccess_Returns_source() + { + var sourceResult = Maybe.Success(123); + var rescueResult = Maybe.Fail(); + + var actual = await sourceResult.Rescue(error => Task.FromResult(rescueResult)); + + actual.Should().Be(sourceResult); + } + + [Fact] + public async Task Given_IsNone_Returns_source() + { + var sourceResult = Maybe.None; + var rescueResult = Maybe.Fail(); + + var actual = await sourceResult.Rescue(error => Task.FromResult(rescueResult)); + + actual.Should().Be(sourceResult); + } + + [Fact] + public async Task Given_IsFail_Returns_result_from_rescue_function() + { + var sourceResult = Maybe.Fail(); + var rescueResult = Maybe.Success(123); + Error? capturedError = null; + + var actual = await sourceResult.Rescue(error => + { + capturedError = error; + return Task.FromResult(rescueResult); + }); + + actual.Should().Be(rescueResult); + capturedError.Should().Be(sourceResult.Error); + } + + [Fact] + public async Task Given_IsFail_When_rescue_function_throws_Returns_result_from_caught_exception() + { + var sourceResult = Maybe.Fail(); + var thrownException = new Exception(); + + var actual = await sourceResult.Rescue((Func>>)(error => throw thrownException)); + + actual.IsFail.Should().BeTrue(); + actual.Error.ErrorCode.Should().Be(ErrorCodes.CaughtException); + actual.Error.InnerError!.Title.Should().Be(nameof(Exception)); + } + } +} diff --git a/RandomSkunk.Results/Operations/Rescue.cs b/RandomSkunk.Results/Operations/Rescue.cs new file mode 100644 index 0000000..f482b23 --- /dev/null +++ b/RandomSkunk.Results/Operations/Rescue.cs @@ -0,0 +1,243 @@ +namespace RandomSkunk.Results; + +/// Defines the Rescue methods. +public partial struct Result +{ + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public Result Rescue(Func onFail) + { + if (onFail is null) throw new ArgumentNullException(nameof(onFail)); + + if (_outcome == Outcome.Fail) + { + try + { + return onFail(GetError()); + } + catch (Exception ex) + { + return Fail(ex, Error.GetMessageForExceptionThrownInCallback(nameof(onFail))); + } + } + + return this; + } + + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public async Task Rescue(Func> onFail) + { + if (onFail is null) throw new ArgumentNullException(nameof(onFail)); + + if (_outcome == Outcome.Fail) + { + try + { + return await onFail(GetError()); + } + catch (Exception ex) + { + return Fail(ex, Error.GetMessageForExceptionThrownInCallback(nameof(onFail))); + } + } + + return this; + } +} + +/// Defines the Rescue methods. +public partial struct Result +{ + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public Result Rescue(Func> onFail) + { + if (onFail is null) throw new ArgumentNullException(nameof(onFail)); + + if (_outcome == Outcome.Fail) + { + try + { + return onFail(GetError()); + } + catch (Exception ex) + { + return Fail(ex, Error.GetMessageForExceptionThrownInCallback(nameof(onFail))); + } + } + + return this; + } + + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public async Task> Rescue(Func>> onFail) + { + if (onFail is null) throw new ArgumentNullException(nameof(onFail)); + + if (_outcome == Outcome.Fail) + { + try + { + return await onFail(GetError()); + } + catch (Exception ex) + { + return Fail(ex, Error.GetMessageForExceptionThrownInCallback(nameof(onFail))); + } + } + + return this; + } +} + +/// Defines the Rescue methods. +public partial struct Maybe +{ + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public Maybe Rescue(Func> onFail) + { + if (onFail is null) throw new ArgumentNullException(nameof(onFail)); + + if (_outcome == Outcome.Fail) + { + try + { + return onFail(GetError()); + } + catch (Exception ex) + { + return Fail(ex, Error.GetMessageForExceptionThrownInCallback(nameof(onFail))); + } + } + + return this; + } + + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public async Task> Rescue(Func>> onFail) + { + if (onFail is null) throw new ArgumentNullException(nameof(onFail)); + + if (_outcome == Outcome.Fail) + { + try + { + return await onFail(GetError()); + } + catch (Exception ex) + { + return Fail(ex, Error.GetMessageForExceptionThrownInCallback(nameof(onFail))); + } + } + + return this; + } +} + +/// Defines the Rescue extension methods. +public static partial class ResultExtensions +{ + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The source result. + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public static async Task Rescue(this Task sourceResult, Func onFail) => + (await sourceResult.ConfigureAwait(ContinueOnCapturedContext)).Rescue(onFail); + + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The source result. + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public static async Task Rescue(this Task sourceResult, Func> onFail) => + await (await sourceResult.ConfigureAwait(ContinueOnCapturedContext)).Rescue(onFail).ConfigureAwait(ContinueOnCapturedContext); + + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The type of the source result value. + /// The source result. + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public static async Task> Rescue(this Task> sourceResult, Func> onFail) => + (await sourceResult.ConfigureAwait(ContinueOnCapturedContext)).Rescue(onFail); + + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The type of the source result value. + /// The source result. + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public static async Task> Rescue(this Task> sourceResult, Func>> onFail) => + await (await sourceResult.ConfigureAwait(ContinueOnCapturedContext)).Rescue(onFail).ConfigureAwait(ContinueOnCapturedContext); + + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The type of the source result value. + /// The source result. + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public static async Task> Rescue(this Task> sourceResult, Func> onFail) => + (await sourceResult.ConfigureAwait(ContinueOnCapturedContext)).Rescue(onFail); + + /// + /// Rescues a Fail result by returning the output of the function. If the current result is + /// Success, nothing happens and the current result is returned. + /// + /// The type of the source result value. + /// The source result. + /// The function that rescues a failed operation. + /// The output of the function if the current result is Fail, or the same result if + /// it is Success. + public static async Task> Rescue(this Task> sourceResult, Func>> onFail) => + await (await sourceResult.ConfigureAwait(ContinueOnCapturedContext)).Rescue(onFail).ConfigureAwait(ContinueOnCapturedContext); +}