diff --git a/Maybe.UnitTest/ElseErrorForwardingTests.cs b/Maybe.UnitTest/ElseErrorForwardingTests.cs new file mode 100644 index 0000000..04f0a6c --- /dev/null +++ b/Maybe.UnitTest/ElseErrorForwardingTests.cs @@ -0,0 +1,210 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Maybe; +using Xunit; + +namespace Maybe.UnitTest; + +public class ElseErrorForwardingTests +{ + // Dummy error type used in tests. Must satisfy the "new()" constraint. + private sealed class DummyError : Error + { + public string? Info { get; } + public DummyError() { } + public DummyError(string? info) => Info = info; + } + + [Fact] + public void Else_WithForwardedError_ReturnsSuccess_WhenSuccess() + { + var success = 42.MightBe(); + + var result = success.Else(new DummyError("fwd")); + + result.IsSuccess.Should().BeTrue(); + result.ValueOrThrow().Should().Be(42); + } + + [Fact] + public void Else_WithForwardedError_ReplacesError_WhenError() + { + var original = new DummyError("orig").MightBe(); + + var fwd = new DummyError("fwd"); + var result = original.Else(fwd); + + result.IsError.Should().BeTrue(); + result.ErrorOrThrow().Should().BeSameAs(fwd); + } + + [Fact] + public void Else_WithValueFallback_ReturnsFallback_WhenError() + { + var original = new DummyError("orig").MightBe(); + + var result = original.Else("fallback"); + + result.IsSuccess.Should().BeTrue(); + result.ValueOrThrow().Should().Be("fallback"); + } + + [Fact] + public void Else_WithValueFunc_UsesFunc_WhenError_And_NotInvoked_WhenSuccess() + { + var invoked = false; + Func func = e => + { + invoked = true; + return $"wrapped:" + e.Info; + }; + + var success = "ok".MightBe(); + success.Else(func).IsSuccess.Should().BeTrue(); + invoked.Should().BeFalse(); + + invoked = false; + var error = new DummyError("orig").MightBe(); + var result = error.Else(func); + invoked.Should().BeTrue(); + result.IsSuccess.Should().BeTrue(); + result.ValueOrThrow().Should().Be("wrapped:orig"); + } + + [Fact] + public void Else_WithErrorFunc_TransformsError_WhenError_And_NotInvoked_WhenSuccess() + { + var invoked = false; + Func func = e => + { + invoked = true; + return new DummyError($"x:" + e.Info); + }; + + var success = 1.MightBe(); + var s = success.Else(func); + s.IsSuccess.Should().BeTrue(); + invoked.Should().BeFalse(); + + invoked = false; + var err = new DummyError("orig").MightBe(); + var r = err.Else(func); + invoked.Should().BeTrue(); + r.IsError.Should().BeTrue(); + r.ErrorOrThrow().Info.Should().Be("x:orig"); + } + + [Fact] + public void Else_ValueFunc_Null_Throws() + { + var err = new DummyError("e").MightBe(); + Action act = () => err.Else((Func)null!); + act.Should().Throw().WithParameterName("fallbackFunc"); + } + + [Fact] + public void Else_ErrorFunc_Null_Throws() + { + var err = new DummyError("e").MightBe(); + Action act = () => err.Else((Func)null!); + act.Should().Throw().WithParameterName("fallbackErrorFunc"); + } + + [Fact] + public async Task Else_Task_WithForwardedError_ReplacesError_WhenError() + { + Task> task = Task.FromResult(new DummyError("orig").MightBe()); + + var forwarded = new DummyError("fwd"); + var result = await task.Else(forwarded); + + result.IsError.Should().BeTrue(); + result.ErrorOrThrow().Should().BeSameAs(forwarded); + } + + [Fact] + public async Task Else_Task_WithValueFunc_Works_WhenError_And_NotInvoked_WhenSuccess() + { + var invoked = false; + Func func = e => + { + invoked = true; + return "val"; + }; + + var sTask = Task.FromResult("ok".MightBe()); + (await sTask.Else(func)).IsSuccess.Should().BeTrue(); + invoked.Should().BeFalse(); + + invoked = false; + var eTask = Task.FromResult(new DummyError("e").MightBe()); + var r = await eTask.Else(func); + invoked.Should().BeTrue(); + r.IsSuccess.Should().BeTrue(); + r.ValueOrThrow().Should().Be("val"); + } + + [Fact] + public async Task ElseAsync_WithAsyncValueFunc_UsesTransformedValue_WhenError() + { + var maybe = new DummyError("orig").MightBe(); + + var result = await maybe.ElseAsync(async e => + { + await Task.Delay(1); + return 99; + }); + + result.IsSuccess.Should().BeTrue(); + result.ValueOrThrow().Should().Be(99); + } + + [Fact] + public async Task ElseAsync_WithAsyncErrorFunc_UsesTransformedError_WhenError() + { + var maybe = new DummyError("orig").MightBe(); + + var result = await maybe.ElseAsync(async e => + { + await Task.Delay(1); + return new DummyError($"async:" + e.Info); + }); + + result.IsError.Should().BeTrue(); + result.ErrorOrThrow().Info.Should().Be("async:orig"); + } + + [Fact] + public async Task ElseAsync_Task_WithAsyncErrorFunc_UsesTransformedError_WhenError() + { + var task = Task.FromResult(new DummyError("orig").MightBe()); + + var result = await task.ElseAsync(async e => + { + await Task.Delay(1); + return new DummyError($"async:" + e.Info); + }); + + result.IsError.Should().BeTrue(); + result.ErrorOrThrow().Info.Should().Be("async:orig"); + } + + [Fact] + public async Task ElseAsync_ValueFunc_Null_Throws() + { + var err = new DummyError("e").MightBe(); + Func> f = null!; + var act = async () => await err.ElseAsync(f); + await act.Should().ThrowAsync().WithParameterName("fallbackAsync"); + } + + [Fact] + public async Task ElseAsync_ErrorFunc_Null_Throws() + { + var err = new DummyError("e").MightBe(); + Func> f = null!; + var act = async () => await err.ElseAsync(f); + await act.Should().ThrowAsync().WithParameterName("forwardedErrorAsync"); + } +} \ No newline at end of file diff --git a/Maybe/MaybeExtensions.Else.cs b/Maybe/MaybeExtensions.Else.cs index 6920e29..ded7bd9 100644 --- a/Maybe/MaybeExtensions.Else.cs +++ b/Maybe/MaybeExtensions.Else.cs @@ -1,4 +1,4 @@ -namespace Maybe; +namespace Maybe; /// /// Provides extension methods for exiting the Maybe context by providing a fallback value or function. @@ -9,37 +9,107 @@ public static partial class MaybeExtensions #region Synchronous Else (with Sync Fallback) /// - /// Returns the success value or, if the Maybe is an error, the provided fallback value. + /// Returns the original success as-is; otherwise returns the provided . /// + /// + /// Note: passing null as may be ambiguous when both TValue and TError + /// are reference types and other overloads exist. Prefer explicit casts or the delegate-based overloads. + /// + /// The Maybe to evaluate. + /// The value to return if is an error. + /// The success value type. + /// The error type. + /// + /// A successful Maybe with the original value, or a successful Maybe with . + /// public static Maybe Else( this in Maybe maybe, TValue fallbackValue) where TError : BaseError, new() { return maybe.IsSuccess - ? maybe.ValueOrThrow() + ? maybe : fallbackValue; } /// - /// Returns the success value or, if the Maybe is an error, the result of the provided fallback function. + /// Returns the original success as-is; otherwise replaces the original error with the provided . /// + /// + /// Useful to adapt low-level errors to higher-level domain errors. The original error is discarded. + /// + /// The Maybe to evaluate. + /// The error to return if is an error. + /// The success value type. + /// The error type. + /// + /// A successful Maybe with the original value, or an error Maybe with . + /// + public static Maybe Else( + this in Maybe maybe, + TError forwardedError) + where TError : BaseError, new() + { + return maybe.IsSuccess + ? maybe + : forwardedError; + } + + /// + /// Returns the original success as-is; otherwise returns the result of invoked with the original error. + /// + /// + /// The function is not invoked if the Maybe is successful. + /// + /// The Maybe to evaluate. + /// The function that maps the original error to a fallback success value. + /// The success value type. + /// The error type. + /// Thrown if is null. public static Maybe Else( this in Maybe maybe, Func fallbackFunc) where TError : BaseError, new() { + if (fallbackFunc is null) throw new ArgumentNullException(nameof(fallbackFunc)); + return maybe.IsSuccess - ? maybe.ValueOrThrow() + ? maybe : fallbackFunc(maybe.ErrorOrThrow()); } + /// + /// Returns the original success as-is; otherwise transforms the original error via . + /// + /// + /// The function is not invoked if the Maybe is successful. + /// + /// The Maybe to evaluate. + /// The function that maps the original error to a new error to return. + /// The success value type. + /// The error type. + /// + /// A successful Maybe with the original value, or an error Maybe with the transformed error. + /// + /// Thrown if is null. + public static Maybe Else( + this in Maybe maybe, + Func fallbackErrorFunc) + where TError : BaseError, new() + { + if (fallbackErrorFunc is null) throw new ArgumentNullException(nameof(fallbackErrorFunc)); + + return maybe.IsSuccess + ? maybe + : fallbackErrorFunc(maybe.ErrorOrThrow()); + } + #endregion #region Asynchronous Else on Task (with Sync Fallback) /// - /// Asynchronously returns the success value or, if the Maybe is an error, the provided fallback value. + /// Asynchronously returns the original success as-is; otherwise returns the provided . /// public static async Task> Else( this Task> maybeTask, @@ -51,45 +121,112 @@ public static async Task> Else( } /// - /// Asynchronously returns the success value or, if the Maybe is an error, the result of the provided fallback function. + /// Asynchronously returns the original success as-is; otherwise replaces the original error with . /// + public static async Task> Else( + this Task> maybeTask, + TError forwardedError) + where TError : BaseError, new() + { + var maybe = await maybeTask.ConfigureAwait(false); + return maybe.Else(forwardedError); + } + + /// + /// Asynchronously returns the original success as-is; otherwise returns the result of invoked with the original error. + /// + /// Thrown if is null. public static async Task> Else( this Task> maybeTask, Func fallbackFunc) where TError : BaseError, new() { + if (fallbackFunc is null) throw new ArgumentNullException(nameof(fallbackFunc)); + var maybe = await maybeTask.ConfigureAwait(false); return maybe.Else(fallbackFunc); } + /// + /// Asynchronously returns the original success as-is; otherwise transforms the original error via . + /// + /// Thrown if is null. + public static async Task> Else( + this Task> maybeTask, + Func fallbackErrorFunc) + where TError : BaseError, new() + { + if (fallbackErrorFunc is null) throw new ArgumentNullException(nameof(fallbackErrorFunc)); + + var maybe = await maybeTask.ConfigureAwait(false); + return maybe.Else(fallbackErrorFunc); + } + #endregion #region Asynchronous ElseAsync (with Async Fallback) /// - /// Returns the success value or, if the Maybe is an error, asynchronously executes the fallback function and returns its result. + /// Returns the original success as-is; otherwise asynchronously executes and returns its result. /// + /// Thrown if is null. public static async Task> ElseAsync( this Maybe maybe, Func> fallbackAsync) where TError : BaseError, new() { + if (fallbackAsync is null) throw new ArgumentNullException(nameof(fallbackAsync)); + return maybe.IsSuccess ? maybe : await fallbackAsync(maybe.ErrorOrThrow()); } /// - /// Asynchronously returns the success value or, if the Maybe is an error, asynchronously executes the fallback function and returns its result. + /// Returns the original success as-is; otherwise asynchronously transforms the original error via . /// + /// Thrown if is null. + public static async Task> ElseAsync( + this Maybe maybe, + Func> forwardedErrorAsync) + where TError : BaseError, new() + { + if (forwardedErrorAsync is null) throw new ArgumentNullException(nameof(forwardedErrorAsync)); + + return maybe.IsSuccess + ? maybe + : await forwardedErrorAsync(maybe.ErrorOrThrow()); + } + + /// + /// Asynchronously returns the original success as-is; otherwise asynchronously executes and returns its result. + /// + /// Thrown if is null. public static async Task> ElseAsync( this Task> maybeTask, Func> fallbackAsync) where TError : BaseError, new() { + if (fallbackAsync is null) throw new ArgumentNullException(nameof(fallbackAsync)); + var maybe = await maybeTask.ConfigureAwait(false); return await maybe.ElseAsync(fallbackAsync).ConfigureAwait(false); } + /// + /// Asynchronously returns the original success as-is; otherwise asynchronously transforms the original error via . + /// + /// Thrown if is null. + public static async Task> ElseAsync( + this Task> maybeTask, + Func> fallbackErrorAsync) + where TError : BaseError, new() + { + if (fallbackErrorAsync is null) throw new ArgumentNullException(nameof(fallbackErrorAsync)); + + var maybe = await maybeTask.ConfigureAwait(false); + return await maybe.ElseAsync(fallbackErrorAsync).ConfigureAwait(false); + } + #endregion -} +} \ No newline at end of file