diff --git a/README.md b/README.md index 199440c..0f633a5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,32 @@ using LanguageExt.Common; using Jds.LanguageExt.Extras; ``` -## `Result` Extensions +### `Either` Extensions + +* `Either.Filter(Func filter, Func onFalse)` + * Filters right values by executing `filter`. If it returns `true`, the existing value is returned. If it returns `false`, then it executes `onFalse` to create a `TLeft`. +* `Either.FilterAsync(Func> filter, Func onFalse)` + * Filters right values by executing `filter`. If it returns `true`, the existing value is returned. If it returns `false`, then it executes `onFalse` to create a `TLeft`. +* `Either.FilterAsync(Func> filter, Func> onFalse)` + * Filters right values by executing `filter`. If it returns `true`, the existing value is returned. If it returns `false`, then it executes `onFalse` to create a `TLeft`. +* `Either.Tap(Action onRight, Action onLeft)` + * Executes a side effect, e.g., logging, based upon the state of the either. The current value is returned unchanged. +* `Either.TapAsync(Func onSuccess, Func onFailure)` + * Executes an asynchronous side effect, e.g., dispatching a status notification via `HttpClient`, based upon the state of the either. The current value is returned unchanged. +* `Either.TapLeft(Action onFailure)` + * Executes a side effect, e.g., logging, when the either is a left. The current value is returned unchanged. +* `Either.TapLeftAsync(Func onFailure)` + * Executes an asynchronous side effect, e.g., dispatching a status notification via `HttpClient`, when the either is a left. The current value is returned unchanged. +* `Either.TapRight(Action onSuccess)` + * Executes a side effect, e.g., logging, when the either is a right. The current value is returned unchanged. +* `Either.TapRightAsync(Func onSuccess)` + * Executes an asynchronous side effect, e.g., dispatching a status notification via `HttpClient`, when the either is a right. The current value is returned unchanged. +* `Either.ToResult(Func ifLeft)` + * Converts the `Either` into a `Result`. +* `Either.ToResult() where TLeft : Exception` + * Converts the `Either` into a `Result`. + +### `Result` Extensions * `Result.Bind(Func> func)` * Executes `func`, which returns `Result`, when the result is a success. diff --git a/src/EitherExtensions.cs b/src/EitherExtensions.cs new file mode 100644 index 0000000..0a2b581 --- /dev/null +++ b/src/EitherExtensions.cs @@ -0,0 +1,298 @@ +using LanguageExt; +using LanguageExt.Common; + +namespace Jds.LanguageExt.Extras; + +/// +/// Extension methods for . +/// +public static class EitherExtensions +{ + /// + /// Filters right values by executing . If true, returns the current + /// value. If false, executes to convert the filtered + /// to a . + /// + /// + /// + /// This overload of avoids returning . + /// + /// + /// A left type. + /// A right type. + /// An . + /// + /// A filtering function. If true, value continues as-is. If false, value is converted to a + /// . + /// + /// + /// A function to convert to a . + /// + /// + /// If returns true, current is returned. If it returns + /// false, the converted is returned. + /// + public static Either Filter(this Either either, + Func filter, + Func onFalse + ) + { + return either.Bind(right => + filter(right) + ? Prelude.Right(right) + : Prelude.Left(value: onFalse(right))); + } + + /// + /// Filters right values by executing . If true, returns the current + /// value. If false, executes to convert the filtered + /// to a . + /// + /// + /// + /// This overload of avoids returning . + /// + /// + /// A left type. + /// A right type. + /// An . + /// + /// A filtering function. If true, value continues as-is. If false, value is converted to a + /// . + /// + /// + /// A function to convert to a . + /// + /// + /// If returns true, current is returned. If it returns + /// false, the converted is returned. + /// + public static Task> FilterAsync(this Either either, + Func> filter, + Func onFalse + ) + { + return either.BindAsync(async right => + await filter(right) + ? Prelude.Right(right) + : Prelude.Left(value: onFalse(right))); + } + + /// + /// Execute . If true, returns the current value. If false, + /// executes to convert the filtered to a + /// . + /// + /// + /// + /// This overload of avoids returning . + /// + /// + /// A left type. + /// A right type. + /// An . + /// + /// A filtering function. If true, value continues as-is. If false, value is converted to a + /// . + /// + /// + /// A function to convert to a . + /// + /// + /// If returns true, current is returned. If it returns + /// false, the converted is returned. + /// + public static Task> FilterAsync(this Either either, + Func> filter, + Func> onFalse + ) + { + return either.BindAsync(async right => + await filter(right) + ? Prelude.Right(right) + : Prelude.Left(value: await onFalse(right))); + } + + /// + /// Execute a side effect and returns unchanged. + /// + /// + /// Often used to perform logging. + /// + /// A left type. + /// A right type. + /// An . + /// A side-effect for right cases. + /// A side-effect for left cases. + /// + /// + /// + public static Either Tap(this Either either, Action onRight, + Action onLeft) + { + return either.Match(right => + { + onRight(right); + return either; + }, left => + { + onLeft(left); + return either; + }); + } + + /// + /// Execute an asynchronous side effect and returns unchanged. + /// + /// + /// Often used to dispatch an asynchronous status message, e.g., a workflow heartbeat. + /// + /// A left type. + /// A right type. + /// An . + /// A side-effect for Right cases. + /// A side-effect for Left cases. + /// + /// + /// + public static Task> TapAsync(this Either either, + Func onSuccess, + Func onFailure + ) + { + return either.Match(async right => + { + await onSuccess(right); + return either; + }, async left => + { + await onFailure(left); + return either; + }); + } + + /// + /// Execute a side effect when is a Left and returns + /// unchanged. + /// + /// + /// Often used to perform logging. + /// + /// A left type. + /// A right type. + /// An . + /// A side-effect for Left cases. + /// + /// + /// + public static Either TapLeft(this Either either, + Action onFailure) + { + return either.Match(right => either, left => + { + onFailure(left); + return either; + }); + } + + /// + /// Execute an asynchronous side effect when is a Left and returns + /// unchanged. + /// + /// + /// Often used to dispatch an asynchronous status message, e.g., a workflow heartbeat. + /// + /// A left type. + /// A right type. + /// An . + /// A side-effect for Left cases. + /// + /// + /// + public static Task> TapLeftAsync(this Either either, + Func onFailure) + { + return either.Match(right => either.AsTask(), async left => + { + await onFailure(left); + return either; + }); + } + + /// + /// Execute a side effect when is a Right and returns unchanged. + /// + /// + /// Often used to perform logging. + /// + /// A left type. + /// A right type. + /// An . + /// A side-effect for Right cases. + /// + /// + /// + public static Either TapRight(this Either either, + Action onSuccess) + { + return either.Match(right => + { + onSuccess(right); + return either; + }, _ => either); + } + + /// + /// Execute an asynchronous side effect when is a Right and returns + /// unchanged. + /// + /// + /// Often used to dispatch an asynchronous status message, e.g., a workflow heartbeat. + /// + /// A left type. + /// A right type. + /// An . + /// A side-effect for Right cases. + /// + /// + /// + public static Task> TapRightAsync(this Either either, + Func onSuccess + ) + { + return either.Match(async right => + { + await onSuccess(right); + return either; + }, _ => either.AsTask()); + } + + /// + /// Convert to a . + /// + /// A left type. + /// A right type. + /// An . + /// + /// A function which will convert values into + /// values. + /// + /// A . + public static Result ToResult(this Either either, Func ifLeft) + { + return either.Match(right => new Result(right), left => new Result(e: ifLeft(left))); + } + + /// + /// Convert to a . + /// + /// A left type. + /// A right type. + /// An . + /// A . + public static Result ToResult(this Either either) + where TLeft : Exception + { + return either.Match(right => new Result(right), left => new Result(left)); + } +} diff --git a/tests/unit/EitherExtensionsTests/FilterAsyncTests.cs b/tests/unit/EitherExtensionsTests/FilterAsyncTests.cs new file mode 100644 index 0000000..a4ec227 --- /dev/null +++ b/tests/unit/EitherExtensionsTests/FilterAsyncTests.cs @@ -0,0 +1,58 @@ +using LanguageExt; + +using Xunit; + +namespace Jds.LanguageExt.Extras.Tests.Unit.EitherExtensionsTests; + +public class FilterAsyncTests +{ + private static async Task FilterFunc(int value) + { + await Task.Delay(1); + return value % 2 == 0; + } + + private static string RightToLeftConverter(int value) + { + return value.ToString(); + } + + private static async Task RightToLeftConverterAsync(int value) + { + await Task.Delay(1); + return value.ToString(); + } + + public static IEnumerable CreateTestCases() + { + var goodValue = 42; + var badValue = 41; + return new[] + { + new object[] { Prelude.Right(goodValue), Prelude.Right(goodValue) }, + new object[] { Prelude.Right(badValue), Prelude.Left(value: badValue.ToString()) }, + new object[] + { + Prelude.Left(value: "existing value"), Prelude.Left(value: "existing value") + } + }; + } + + [Theory] + [MemberData(memberName: nameof(CreateTestCases))] + public async Task GivenSyncConverter_ReturnsExpectedResults(Either either, Either expected) + { + var actual = await either.FilterAsync(FilterFunc, RightToLeftConverter); + + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(memberName: nameof(CreateTestCases))] + public async Task GivenAsyncConverter_ReturnsExpectedResults(Either either, Either expected) + { + var actual = await either.FilterAsync(FilterFunc, RightToLeftConverterAsync); + + Assert.Equal(expected, actual); + } +} diff --git a/tests/unit/EitherExtensionsTests/FilterTests.cs b/tests/unit/EitherExtensionsTests/FilterTests.cs new file mode 100644 index 0000000..7f0f327 --- /dev/null +++ b/tests/unit/EitherExtensionsTests/FilterTests.cs @@ -0,0 +1,42 @@ +using LanguageExt; + +using Xunit; + +namespace Jds.LanguageExt.Extras.Tests.Unit.EitherExtensionsTests; + +public class FilterTests +{ + private static bool FilterFunc(int value) + { + return value % 2 == 0; + } + + private static string RightToLeftConverter(int value) + { + return value.ToString(); + } + + public static IEnumerable CreateTestCases() + { + var goodValue = 42; + var badValue = 41; + return new[] + { + new object[] { Prelude.Right(goodValue), Prelude.Right(goodValue) }, + new object[] { Prelude.Right(badValue), Prelude.Left(value: badValue.ToString()) }, + new object[] + { + Prelude.Left(value: "existing value"), Prelude.Left(value: "existing value") + } + }; + } + + [Theory] + [MemberData(memberName: nameof(CreateTestCases))] + public void ReturnsExpectedResults(Either either, Either expected) + { + var actual = either.Filter(FilterFunc, RightToLeftConverter); + + Assert.Equal(expected, actual); + } +} diff --git a/tests/unit/EitherExtensionsTests/TapAsyncTests.cs b/tests/unit/EitherExtensionsTests/TapAsyncTests.cs new file mode 100644 index 0000000..0090d2e --- /dev/null +++ b/tests/unit/EitherExtensionsTests/TapAsyncTests.cs @@ -0,0 +1,53 @@ +using LanguageExt; + +using Xunit; + +namespace Jds.LanguageExt.Extras.Tests.Unit.EitherExtensionsTests; + +public class TapAsyncTests +{ + private const int RightValue = 42; + private Either RightResult { get; } = Prelude.Right(RightValue); + private static Exception LeftValue { get; } = new ArgumentOutOfRangeException(); + private Either LeftResult { get; } = Prelude.Left(LeftValue); + + [Fact] + public async Task GivenSuccess_ExecutesSuccessSideEffect() + { + int? onSuccessValue = null; + Exception? onLeftValue = null; + + await RightResult.TapAsync(async right => + { + await Task.Delay(1); + onSuccessValue = right; + }, async left => + { + await Task.Delay(1); + onLeftValue = left; + }); + + Assert.Equal(RightValue, onSuccessValue); + Assert.Null(onLeftValue); + } + + [Fact] + public async Task GivenLeft_ExecutesLeftSideEffect() + { + int? onSuccessValue = null; + Exception? onLeftValue = null; + + await LeftResult.TapAsync(async right => + { + await Task.Delay(1); + onSuccessValue = right; + }, async left => + { + await Task.Delay(1); + onLeftValue = left; + }); + + Assert.Null(onSuccessValue); + Assert.Equal(LeftValue, onLeftValue); + } +} diff --git a/tests/unit/EitherExtensionsTests/TapFailureAsyncTests.cs b/tests/unit/EitherExtensionsTests/TapFailureAsyncTests.cs new file mode 100644 index 0000000..c8c8004 --- /dev/null +++ b/tests/unit/EitherExtensionsTests/TapFailureAsyncTests.cs @@ -0,0 +1,41 @@ +using LanguageExt; + +using Xunit; + +namespace Jds.LanguageExt.Extras.Tests.Unit.EitherExtensionsTests; + +public class TapLeftAsyncTests +{ + private const int RightValue = 42; + private Either RightResult { get; } = Prelude.Right(RightValue); + private static Exception LeftValue { get; } = new ArgumentOutOfRangeException(); + private Either LeftResult { get; } = Prelude.Left(LeftValue); + + [Fact] + public async Task GivenSuccess_DoesNotExecuteSideEffect() + { + Exception? onLeftValue = null; + + await RightResult.TapLeftAsync(async left => + { + await Task.Delay(1); + onLeftValue = left; + }); + + Assert.Null(onLeftValue); + } + + [Fact] + public async Task GivenFailure_ExecutesSideEffect() + { + Exception? onLeftValue = null; + + await LeftResult.TapLeftAsync(async left => + { + await Task.Delay(1); + onLeftValue = left; + }); + + Assert.Equal(LeftValue, onLeftValue); + } +} diff --git a/tests/unit/EitherExtensionsTests/TapFailureTests.cs b/tests/unit/EitherExtensionsTests/TapFailureTests.cs new file mode 100644 index 0000000..84e4238 --- /dev/null +++ b/tests/unit/EitherExtensionsTests/TapFailureTests.cs @@ -0,0 +1,33 @@ +using LanguageExt; + +using Xunit; + +namespace Jds.LanguageExt.Extras.Tests.Unit.EitherExtensionsTests; + +public class TapLeftTests +{ + private const int RightValue = 42; + private Either RightResult { get; } = Prelude.Right(RightValue); + private static Exception LeftValue { get; } = new ArgumentOutOfRangeException(); + private Either LeftResult { get; } = Prelude.Left(LeftValue); + + [Fact] + public void GivenSuccess_DoesNotExecuteSideEffect() + { + Exception? onLeftValue = null; + + RightResult.TapLeft(left => onLeftValue = left); + + Assert.Null(onLeftValue); + } + + [Fact] + public void GivenLeft_ExecutesSideEffect() + { + Exception? onLeftValue = null; + + LeftResult.TapLeft(left => onLeftValue = left); + + Assert.Equal(LeftValue, onLeftValue); + } +} diff --git a/tests/unit/EitherExtensionsTests/TapSuccessAsyncTests.cs b/tests/unit/EitherExtensionsTests/TapSuccessAsyncTests.cs new file mode 100644 index 0000000..74987ea --- /dev/null +++ b/tests/unit/EitherExtensionsTests/TapSuccessAsyncTests.cs @@ -0,0 +1,41 @@ +using LanguageExt; + +using Xunit; + +namespace Jds.LanguageExt.Extras.Tests.Unit.EitherExtensionsTests; + +public class TapRightAsyncTests +{ + private const int RightValue = 42; + private Either RightResult { get; } = Prelude.Right(RightValue); + private static Exception LeftValue { get; } = new ArgumentOutOfRangeException(); + private Either LeftResult { get; } = Prelude.Left(LeftValue); + + [Fact] + public async Task GivenSuccess_ExecutesSideEffect() + { + int? onRightValue = null; + + await RightResult.TapRightAsync(async right => + { + await Task.Delay(1); + onRightValue = right; + }); + + Assert.Equal(RightValue, onRightValue); + } + + [Fact] + public async Task GivenLeft_DoesNotExecuteSideEffect() + { + int? onRightValue = null; + + await LeftResult.TapRightAsync(async right => + { + await Task.Delay(1); + onRightValue = right; + }); + + Assert.Null(onRightValue); + } +} diff --git a/tests/unit/EitherExtensionsTests/TapSuccessTests.cs b/tests/unit/EitherExtensionsTests/TapSuccessTests.cs new file mode 100644 index 0000000..2ba0ba0 --- /dev/null +++ b/tests/unit/EitherExtensionsTests/TapSuccessTests.cs @@ -0,0 +1,33 @@ +using LanguageExt; + +using Xunit; + +namespace Jds.LanguageExt.Extras.Tests.Unit.EitherExtensionsTests; + +public class TapRightTests +{ + private const int RightValue = 42; + private Either RightResult { get; } = Prelude.Right(RightValue); + private static Exception LeftValue { get; } = new ArgumentOutOfRangeException(); + private Either LeftResult { get; } = Prelude.Left(LeftValue); + + [Fact] + public void GivenSuccess_ExecutesSideEffect() + { + int? onRightValue = null; + + RightResult.TapRight(right => onRightValue = right); + + Assert.Equal(RightValue, onRightValue); + } + + [Fact] + public void GivenLeft_DoesNotExecuteSideEffect() + { + int? onRightValue = null; + + LeftResult.TapRight(right => onRightValue = right); + + Assert.Null(onRightValue); + } +} diff --git a/tests/unit/EitherExtensionsTests/TapTests.cs b/tests/unit/EitherExtensionsTests/TapTests.cs new file mode 100644 index 0000000..e2e907b --- /dev/null +++ b/tests/unit/EitherExtensionsTests/TapTests.cs @@ -0,0 +1,37 @@ +using LanguageExt; + +using Xunit; + +namespace Jds.LanguageExt.Extras.Tests.Unit.EitherExtensionsTests; + +public class TapTests +{ + private const int RightValue = 42; + private Either RightResult { get; } = Prelude.Right(RightValue); + private static Exception LeftValue { get; } = new ArgumentOutOfRangeException(); + private Either LeftResult { get; } = Prelude.Left(LeftValue); + + [Fact] + public void GivenSuccess_ExecutesSuccessSideEffect() + { + int? onRightValue = null; + Exception? onLeftValue = null; + + RightResult.Tap(right => onRightValue = right, left => onLeftValue = left); + + Assert.Equal(RightValue, onRightValue); + Assert.Null(onLeftValue); + } + + [Fact] + public void GivenLeft_ExecutesLeftSideEffect() + { + int? onRightValue = null; + Exception? onLeftValue = null; + + LeftResult.Tap(right => onRightValue = right, left => onLeftValue = left); + + Assert.Null(onRightValue); + Assert.Equal(LeftValue, onLeftValue); + } +} diff --git a/tests/unit/EitherExtensionsTests/ToResultTests.cs b/tests/unit/EitherExtensionsTests/ToResultTests.cs new file mode 100644 index 0000000..cd75525 --- /dev/null +++ b/tests/unit/EitherExtensionsTests/ToResultTests.cs @@ -0,0 +1,61 @@ +using LanguageExt; +using LanguageExt.Common; + +using Xunit; + +namespace Jds.LanguageExt.Extras.Tests.Unit.EitherExtensionsTests; + +public class ToResultTests +{ + private const int RightValue = 42; + private const string LeftStringValue = "someMissingPropertyName"; + private static Result RightResult { get; } = new(RightValue); + private static ArgumentNullException LeftExceptionValue { get; } = new(); + + private static ArgumentException ToArgumentException(string propertyName) + { + return new ArgumentException(propertyName); + } + + public static IEnumerable CreateExceptionTestCases() + { + return new[] + { + new object[] { Prelude.Right(RightValue), RightResult }, + new object[] + { + Prelude.Left(LeftExceptionValue), new Result(LeftExceptionValue) + } + }; + } + + public static IEnumerable CreateNonExceptionTestCases() + { + return new[] + { + new object[] { Prelude.Right(RightValue), RightResult }, + new object[] + { + Prelude.Left(LeftStringValue), new Result(e: ToArgumentException(LeftStringValue)) + } + }; + } + + [Theory] + [MemberData(memberName: nameof(CreateExceptionTestCases))] + public void GivenLeftException_ReturnsExpectedValues(Either either, Result expected) + { + var actual = either.ToResult(); + + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(memberName: nameof(CreateNonExceptionTestCases))] + public void GivenLeftNonException_ReturnsExpectedValues(Either either, Result expected) + { + var actual = either.ToResult(ToArgumentException); + + Assert.Equal(expected, actual); + } +}