From 38ff5fa8abf44e6b629580bc43ff3f61b429db9e Mon Sep 17 00:00:00 2001 From: Chris Sami Date: Sun, 20 Aug 2023 14:16:32 +0200 Subject: [PATCH] Add Option.MapOrElse (#41) --- ...syncOptionExtensions.MapOrElseAsyncTest.cs | 345 ++++++++++++++++++ .../Option/OptionTests.cs | 35 ++ .../(Async)/AsyncOptionExtensions.Map.cs | 76 ++++ .../(Option)/(Features)/Option.Map.cs | 30 ++ .../(Option)/AsyncOptionExtensions.cs | 2 +- 5 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 Galaxus.Functional.Tests/Option/Async/AsyncOptionExtensions.MapOrElseAsyncTest.cs create mode 100644 Galaxus.Functional/(Option)/(Async)/AsyncOptionExtensions.Map.cs diff --git a/Galaxus.Functional.Tests/Option/Async/AsyncOptionExtensions.MapOrElseAsyncTest.cs b/Galaxus.Functional.Tests/Option/Async/AsyncOptionExtensions.MapOrElseAsyncTest.cs new file mode 100644 index 0000000..dc59956 --- /dev/null +++ b/Galaxus.Functional.Tests/Option/Async/AsyncOptionExtensions.MapOrElseAsyncTest.cs @@ -0,0 +1,345 @@ +using System.Threading.Tasks; +using NUnit.Framework; +using static Galaxus.Functional.Tests.Option.Async.OptionFactory; + +namespace Galaxus.Functional.Tests.Option.Async; + +[TestFixture] +internal class MapOrElseAsyncTest +{ + public sealed class AsyncOption_BothSyncArguments : MapOrElseAsyncTest + { + [Test] + public async Task CallsMap_WhenSelfIsSome() + { + var mapInvoked = false; + var fallbackInvoked = false; + + var value = await CreateSomeTask("value").MapOrElseAsync( + s => + { + mapInvoked = true; + return s.Length.ToOption(); + }, + () => + { + fallbackInvoked = true; + return 2.ToOption(); + }); + + Assert.AreEqual(5.ToOption(), value); + Assert.IsTrue(mapInvoked); + Assert.IsFalse(fallbackInvoked); + } + + [Test] + public async Task CallsFallback_WhenSelfIsNone() + { + var mapInvoked = false; + var fallbackInvoked = false; + + Assert.AreEqual(42.ToOption(), await CreateNoneTask().MapOrElseAsync( + s => + { + mapInvoked = true; + return s.Length.ToOption(); + }, + () => + { + fallbackInvoked = true; + return 42.ToOption(); + })); + + Assert.IsFalse(mapInvoked); + Assert.IsTrue(fallbackInvoked); + } + } + + public sealed class AsyncOption_AsyncMapArguments : MapOrElseAsyncTest + { + [Test] + public async Task CallsMap_WhenSelfIsSome() + { + var mapInvoked = false; + var fallbackInvoked = false; + + var value = await CreateSomeTask("value").MapOrElseAsync( + s => + { + mapInvoked = true; + return Task.FromResult(s.Length.ToOption()); + }, + () => + { + fallbackInvoked = true; + return 2.ToOption(); + }); + + Assert.AreEqual(5.ToOption(), value); + Assert.IsTrue(mapInvoked); + Assert.IsFalse(fallbackInvoked); + } + + [Test] + public async Task CallsFallback_WhenSelfIsNone() + { + var mapInvoked = false; + var fallbackInvoked = false; + + Assert.AreEqual(42.ToOption(), await CreateNoneTask().MapOrElseAsync( + s => + { + mapInvoked = true; + return Task.FromResult(s.Length.ToOption()); + }, + () => + { + fallbackInvoked = true; + return 42.ToOption(); + })); + + Assert.IsFalse(mapInvoked); + Assert.IsTrue(fallbackInvoked); + } + } + + public sealed class AsyncOption_AsyncFallbackArguments : MapOrElseAsyncTest + { + [Test] + public async Task CallsMap_WhenSelfIsSome() + { + var mapInvoked = false; + var fallbackInvoked = false; + + var value = await CreateSomeTask("value").MapOrElseAsync( + s => + { + mapInvoked = true; + return s.Length.ToOption(); + }, + () => + { + fallbackInvoked = true; + return Task.FromResult(2.ToOption()); + }); + + Assert.AreEqual(5.ToOption(), value); + Assert.IsTrue(mapInvoked); + Assert.IsFalse(fallbackInvoked); + } + + [Test] + public async Task CallsFallback_WhenSelfIsNone() + { + var mapInvoked = false; + var fallbackInvoked = false; + + Assert.AreEqual(42.ToOption(), await CreateNoneTask().MapOrElseAsync( + s => + { + mapInvoked = true; + return s.Length.ToOption(); + }, + () => + { + fallbackInvoked = true; + return Task.FromResult(42.ToOption()); + })); + + Assert.IsFalse(mapInvoked); + Assert.IsTrue(fallbackInvoked); + } + } + + public sealed class AsyncOption_BothAsyncArguments : MapOrElseAsyncTest + { + [Test] + public async Task CallsMap_WhenSelfIsSome() + { + var mapInvoked = false; + var fallbackInvoked = false; + + var value = await CreateSomeTask("value").MapOrElseAsync( + s => + { + mapInvoked = true; + return Task.FromResult(s.Length.ToOption()); + }, + () => + { + fallbackInvoked = true; + return Task.FromResult(2.ToOption()); + }); + + Assert.AreEqual(5.ToOption(), value); + Assert.IsTrue(mapInvoked); + Assert.IsFalse(fallbackInvoked); + } + + [Test] + public async Task CallsFallback_WhenSelfIsNone() + { + var mapInvoked = false; + var fallbackInvoked = false; + + Assert.AreEqual(42.ToOption(), await CreateNoneTask().MapOrElseAsync( + s => + { + mapInvoked = true; + return Task.FromResult(s.Length.ToOption()); + }, + () => + { + fallbackInvoked = true; + return Task.FromResult(42.ToOption()); + })); + + Assert.IsFalse(mapInvoked); + Assert.IsTrue(fallbackInvoked); + } + } + + public sealed class SyncOption_AsyncMapArguments : MapOrElseAsyncTest + { + [Test] + public async Task CallsMap_WhenSelfIsSome() + { + var mapInvoked = false; + var fallbackInvoked = false; + + var value = await CreateSome("value").MapOrElseAsync( + s => + { + mapInvoked = true; + return Task.FromResult(s.Length.ToOption()); + }, + () => + { + fallbackInvoked = true; + return 2.ToOption(); + }); + + Assert.AreEqual(5.ToOption(), value); + Assert.IsTrue(mapInvoked); + Assert.IsFalse(fallbackInvoked); + } + + [Test] + public async Task CallsFallback_WhenSelfIsNone() + { + var mapInvoked = false; + var fallbackInvoked = false; + + Assert.AreEqual(42.ToOption(), await Option.None.MapOrElseAsync( + s => + { + mapInvoked = true; + return Task.FromResult(s.Length.ToOption()); + }, + () => + { + fallbackInvoked = true; + return 42.ToOption(); + })); + + Assert.IsFalse(mapInvoked); + Assert.IsTrue(fallbackInvoked); + } + } + + public sealed class SyncOption_AsyncFallbackArguments : MapOrElseAsyncTest + { + [Test] + public async Task CallsMap_WhenSelfIsSome() + { + var mapInvoked = false; + var fallbackInvoked = false; + + var value = await CreateSome("value").MapOrElseAsync( + s => + { + mapInvoked = true; + return s.Length.ToOption(); + }, + () => + { + fallbackInvoked = true; + return Task.FromResult(2.ToOption()); + }); + + Assert.AreEqual(5.ToOption(), value); + Assert.IsTrue(mapInvoked); + Assert.IsFalse(fallbackInvoked); + } + + [Test] + public async Task CallsFallback_WhenSelfIsNone() + { + var mapInvoked = false; + var fallbackInvoked = false; + + Assert.AreEqual(42.ToOption(), await Option.None.MapOrElseAsync( + s => + { + mapInvoked = true; + return s.Length.ToOption(); + }, + () => + { + fallbackInvoked = true; + return Task.FromResult(42.ToOption()); + })); + + Assert.IsFalse(mapInvoked); + Assert.IsTrue(fallbackInvoked); + } + } + + public sealed class SyncOption_BothAsyncArguments : MapOrElseAsyncTest + { + [Test] + public async Task CallsMap_WhenSelfIsSome() + { + var mapInvoked = false; + var fallbackInvoked = false; + + var value = await CreateSome("value").MapOrElseAsync( + s => + { + mapInvoked = true; + return Task.FromResult(s.Length.ToOption()); + }, + () => + { + fallbackInvoked = true; + return Task.FromResult(2.ToOption()); + }); + + Assert.AreEqual(5.ToOption(), value); + Assert.IsTrue(mapInvoked); + Assert.IsFalse(fallbackInvoked); + } + + [Test] + public async Task CallsFallback_WhenSelfIsNone() + { + var mapInvoked = false; + var fallbackInvoked = false; + + Assert.AreEqual(42.ToOption(), await Option.None.MapOrElseAsync( + s => + { + mapInvoked = true; + return Task.FromResult(s.Length.ToOption()); + }, + () => + { + fallbackInvoked = true; + return Task.FromResult(42.ToOption()); + })); + + Assert.IsFalse(mapInvoked); + Assert.IsTrue(fallbackInvoked); + } + } +} diff --git a/Galaxus.Functional.Tests/Option/OptionTests.cs b/Galaxus.Functional.Tests/Option/OptionTests.cs index 992cfce..0d2327e 100644 --- a/Galaxus.Functional.Tests/Option/OptionTests.cs +++ b/Galaxus.Functional.Tests/Option/OptionTests.cs @@ -366,6 +366,41 @@ public void Option_MapOr() Assert.AreEqual(42.ToOption(), none.MapOr(s => s.Length.ToOption(), 42.ToOption())); } + [Test] + public void Option_MapOrElse() + { + var some = "hello".ToOption(); + var none = Option.None; + + { + var invoked = false; + + Assert.AreEqual(5.ToOption(), some.MapOrElse( + s => s.Length.ToOption(), + () => + { + invoked = true; + return 2.ToOption(); + })); + + Assert.IsFalse(invoked); + } + + { + var invoked = false; + + Assert.AreEqual(42.ToOption(), none.MapOrElse( + s => s.Length.ToOption(), + () => + { + invoked = true; + return 42.ToOption(); + })); + + Assert.IsTrue(invoked); + } + } + [Test] public void Option_ToObject() { diff --git a/Galaxus.Functional/(Option)/(Async)/AsyncOptionExtensions.Map.cs b/Galaxus.Functional/(Option)/(Async)/AsyncOptionExtensions.Map.cs new file mode 100644 index 0000000..e993862 --- /dev/null +++ b/Galaxus.Functional/(Option)/(Async)/AsyncOptionExtensions.Map.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading.Tasks; + +namespace Galaxus.Functional; + +/// +/// Extensions to map operations for using async methods or s. +/// +public static partial class AsyncOptionExtensions +{ + /// + public static async Task MapOrElseAsync(this Task> self, Func map, Func fallback) + { + var option = await self; + return option.MapOrElse(map, fallback); + } + + /// + public static async Task MapOrElseAsync(this Task> self, Func> map, Func fallback) + { + var option = await self; + return await option.MapOrElseAsync(map, fallback); + } + + /// + public static async Task MapOrElseAsync(this Task> self, Func map, Func> fallback) + { + var option = await self; + return await option.MapOrElseAsync(map, fallback); + } + + /// + public static async Task MapOrElseAsync(this Task> self, Func> map, Func> fallback) + { + var option = await self; + return await option.MapOrElseAsync(map, fallback); + } + + /// + public static async Task MapOrElseAsync(this Option self, Func> map, Func fallback) + { + return await self.MapOrElseAsync( + map, + () => + { + if (fallback == null) + { + throw new ArgumentException(nameof(fallback)); + } + + return Task.FromResult(fallback()); + }); + } + + /// + public static async Task MapOrElseAsync(this Option self, Func map, Func> fallback) + { + return await self.MapOrElseAsync( + t => + { + if (map == null) + { + throw new ArgumentNullException(nameof(map)); + } + + return Task.FromResult(map(t)); + }, + fallback); + } + + /// + public static async Task MapOrElseAsync(this Option self, Func> map, Func> fallback) + { + return await self.MapOrElse(map, fallback); + } +} diff --git a/Galaxus.Functional/(Option)/(Features)/Option.Map.cs b/Galaxus.Functional/(Option)/(Features)/Option.Map.cs index a2d4369..84b06e3 100644 --- a/Galaxus.Functional/(Option)/(Features)/Option.Map.cs +++ b/Galaxus.Functional/(Option)/(Features)/Option.Map.cs @@ -47,5 +47,35 @@ public TTo MapOr(Func map, TTo fallback) () => fallback ); } + + /// + /// Applies a function to the contained Some value (if any), otherwise calls and returns the result. + /// + /// The type to map to. + /// The function to call if self contains None. + /// The mapping function. + public TTo MapOrElse(Func map, Func fallback) + { + return Match( + some => + { + if (map == null) + { + throw new ArgumentNullException(nameof(map)); + } + + return map(some); + }, + () => + { + if (fallback == null) + { + throw new ArgumentNullException(nameof(fallback)); + } + + return fallback(); + } + ); + } } } diff --git a/Galaxus.Functional/(Option)/AsyncOptionExtensions.cs b/Galaxus.Functional/(Option)/AsyncOptionExtensions.cs index 13c0884..1c8d3c2 100644 --- a/Galaxus.Functional/(Option)/AsyncOptionExtensions.cs +++ b/Galaxus.Functional/(Option)/AsyncOptionExtensions.cs @@ -6,7 +6,7 @@ namespace Galaxus.Functional; /// /// Extensions to common operations for using async methods or s. /// -public static class AsyncOptionExtensions +public static partial class AsyncOptionExtensions { /// public static async Task UnwrapAsync(this Task> self)