diff --git a/src/DynamicData.Tests/Kernal/OptionObservableFixture.cs b/src/DynamicData.Tests/Kernal/OptionObservableFixture.cs new file mode 100644 index 000000000..06095bc20 --- /dev/null +++ b/src/DynamicData.Tests/Kernal/OptionObservableFixture.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using DynamicData.Kernel; + +using FluentAssertions; + +using Xunit; + +namespace DynamicData.Tests.Kernal; + +public class OptionObservableFixture +{ + private const int NoneCount = 5; + private const int SomeCount = 10; + + private static Optional NotConvertableToInt { get; } = Optional.Some("NOT AN INT"); + private static IEnumerable IntEnum { get; } = Enumerable.Range(0, SomeCount); + private static IEnumerable StringEnum { get; } = IntEnum.Select(n => n.ToString()); + private static IEnumerable> OptIntEnum { get; } = IntEnum.Select(i => Optional.Some(i)); + private static IEnumerable> OptNoneIntEnum { get; } = Enumerable.Repeat(Optional.None(), NoneCount); + private static IEnumerable> OptNoneStringEnum { get; } = Enumerable.Repeat(Optional.None(), NoneCount); + private static IEnumerable> OptStringEnum { get; } = StringEnum.Select(str => Optional.Some(str)); + private static IEnumerable> OptStringWithNoneEnum { get; } = OptNoneStringEnum.Concat(OptStringEnum); + private static IEnumerable> OptStringWithBadEnum { get; } = OptStringEnum.Prepend(NotConvertableToInt); + private static IEnumerable> OptStringWithBadAndNoneEnum { get; } = OptStringWithNoneEnum.Prepend(NotConvertableToInt); + + [Fact] + public void NullChecks() + { + // having + var neverObservable = Observable.Never>(); + var nullObservable = (IObservable>)null!; + var nullConverter = (Func)null!; + var nullOptionalConverter = (Func>)null!; + var converter = (Func)(i => i); + var nullFallback = (Func)null!; + var nullConvertFallback = (Func)null!; + var nullOptionalFallback = (Func>)null!; + var action = (Action)null!; + var actionVal = (Action)null!; + var nullExceptionGenerator = (Func)null!; + + // when + var convert1 = () => nullObservable.Convert(nullConverter); + var convert2 = () => neverObservable.Convert(nullConverter); + var convertOpt1 = () => nullObservable.Convert(nullOptionalConverter); + var convertOpt2 = () => neverObservable.Convert(nullOptionalConverter); + var convertOr1 = () => nullObservable.ConvertOr(nullConverter, nullConvertFallback); + var convertOr2 = () => neverObservable.ConvertOr(nullConverter, nullConvertFallback); + var convertOr3 = () => neverObservable.ConvertOr(converter, nullConvertFallback); + var orElse1 = () => nullObservable.OrElse(nullOptionalFallback); + var orElse2 = () => neverObservable.OrElse(nullOptionalFallback); + var onHasValue = () => nullObservable.OnHasValue(actionVal); + var onHasValue2 = () => neverObservable.OnHasValue(actionVal); + var onHasNoValue = () => nullObservable.OnHasNoValue(action); + var onHasNoValue2 = () => neverObservable.OnHasNoValue(action); + var selectValues = () => nullObservable.SelectValues(); + var valueOr = () => nullObservable.ValueOr(nullFallback); + var valueOrDefault = () => nullObservable.ValueOrDefault(); + var valueOrThrow1 = () => nullObservable.ValueOrThrow(nullExceptionGenerator); + var valueOrThrow2 = () => neverObservable.ValueOrThrow(nullExceptionGenerator); + + // then + convert1.Should().Throw(); + convert2.Should().Throw(); + convertOpt1.Should().Throw(); + convertOpt2.Should().Throw(); + convertOr1.Should().Throw(); + convertOr2.Should().Throw(); + convertOr3.Should().Throw(); + orElse1.Should().Throw(); + orElse2.Should().Throw(); + onHasValue.Should().Throw(); + onHasValue2.Should().Throw(); + onHasNoValue.Should().Throw(); + onHasNoValue2.Should().Throw(); + selectValues.Should().Throw(); + valueOr.Should().Throw(); + valueOrDefault.Should().Throw(); + valueOrThrow1.Should().Throw(); + valueOrThrow2.Should().Throw(); + } + + [Fact] + public void ConvertWillConvertValues() + { + // having + var observable = OptStringEnum.ToObservable(); + + // when + var results = observable.Convert(ParseInt).ToEnumerable().ToList(); + var intList = OptIntEnum.ToList(); + + // then + results.Should().BeSubsetOf(intList); + intList.Should().BeSubsetOf(results); + } + + [Fact] + public void ConvertPreservesNone() + { + // having + var enumerable = OptStringWithNoneEnum; + var observable = enumerable.ToObservable(); + + // when + var results = observable.Convert(ParseInt).Where(opt => !opt.HasValue).ToEnumerable().Count(); + var expected = enumerable.Where(opt => !opt.HasValue).Count(); + + // then + results.Should().Be(expected); + results.Should().Be(NoneCount); + } + + [Fact] + public void ConvertOptionalWillConvertValues() + { + // having + var observable = OptStringWithBadEnum.ToObservable(); + + // when + var results = observable.Convert(ParseIntOpt).ToEnumerable().ToList(); + var intList = OptIntEnum.ToList(); + + // then + intList.Should().BeSubsetOf(results); + results.Should().Contain(Optional.None()); + } + + [Fact] + public void ConvertOptionalPreservesNone() + { + // having + var enumerable = OptStringWithBadAndNoneEnum; + var observable = enumerable.ToObservable(); + + // when + var results = observable.Convert(ParseIntOpt).Where(opt => !opt.HasValue).ToEnumerable().Count(); + var expected = OptStringWithNoneEnum.Where(opt => !opt.HasValue).Count() + 1; + + // then + results.Should().Be(expected); + results.Should().BeGreaterThan(1); + } + + [Fact] + public void ConvertOrConvertsOrFallsback() + { + // having + var observable = OptStringWithNoneEnum.ToObservable(); + + // when + var results = observable.ConvertOr(ParseInt, () => -1).ToEnumerable(); + var intList = IntEnum.Prepend(-1); + + // then + results.Should().BeSubsetOf(intList); + intList.Should().BeSubsetOf(results); + } + + [Fact] + public void OrElseFallsback() + { + // having + var observable = OptIntEnum.ToObservable().StartWith(Optional.None()); + + // when + var results = observable.OrElse(() => -1).ToEnumerable(); + var intList = OptIntEnum.Prepend(-1); + + // then + results.Should().BeSubsetOf(intList); + intList.Should().BeSubsetOf(results); + } + + [Fact] + public void OnHasValueInvokesCorrectAction() + { + // having + int value = 0; + int noValue = 0; + Action onVal = _ => value++; + Action onNoVal = () => noValue++; + var observable = OptIntEnum.Concat(OptNoneIntEnum).ToObservable().OnHasValue(onVal, onNoVal); + + // when + var results = observable.ToEnumerable().ToList(); + + // then + value.Should().Be(SomeCount); + noValue.Should().Be(NoneCount); + } + + [Fact] + public void OnHasNoValueInvokesCorrectAction() + { + // having + int value = 0; + int noValue = 0; + Action onVal = _ => value++; + Action onNoVal = () => noValue++; + var observable = OptIntEnum.Concat(OptNoneIntEnum).ToObservable().OnHasNoValue(onNoVal, onVal); + + // when + var results = observable.ToEnumerable().ToList(); + + // then + value.Should().Be(SomeCount); + noValue.Should().Be(NoneCount); + } + + [Fact] + public void SelectValuesReturnsTheValues() + { + // having + var enumerable = OptIntEnum.Concat(OptNoneIntEnum); + var observable = enumerable.ToObservable().SelectValues(); + + // when + var expected = enumerable.Where(opt => opt.HasValue).Count(); + var results = observable.ToEnumerable().Count(); + + // then + expected.Should().Be(results); + results.Should().Be(SomeCount); + } + + [Fact] + public void ValueOrInvokesSelector() + { + // having + int invokeCount = 0; + Func selector = () => { invokeCount++; return -1; }; + var enumerable = OptIntEnum.Concat(OptNoneIntEnum); + var observable = enumerable.ToObservable().ValueOr(selector); + + // when + var expected = enumerable.Where(opt => !opt.HasValue).Count(); + var results = observable.ToEnumerable().Where(i => i.Equals(-1)).Count(); + + // then + expected.Should().Be(results); + results.Should().Be(NoneCount); + invokeCount.Should().Be(NoneCount); + } + + [Fact] + public void ValueOrDefaultReturnsDefaultValues() + { + // having + var enumerable = OptStringWithNoneEnum; + var observable = enumerable.ToObservable().ValueOrDefault(); + + // when + var expected = enumerable.Where(opt => !opt.HasValue).Count(); + var results = observable.ToEnumerable().Where(str => str == default).Count(); + + // then + expected.Should().Be(results); + results.Should().Be(NoneCount); + } + + [Fact] + public void ValueOrThrowFailsWithGeneratedError() + { + // having + var expectedError = new Exception("Nope"); + var exceptionGenerator = () => expectedError; + var enumerable = OptStringWithNoneEnum; + var observable = enumerable.ToObservable().ValueOrThrow(exceptionGenerator); + var receivedError = default(Exception); + + // when + using var cleanup = observable.Subscribe(_ => { }, err => receivedError = err); + + // then + receivedError.Should().Be(expectedError); + } + + private static Optional ParseIntOpt(string input) => + int.TryParse(input, out var result) ? Optional.Some(result) : Optional.None(); + + private static int ParseInt(string input) => int.Parse(input); +} diff --git a/src/DynamicData/Kernel/OptionObservableExtensions.cs b/src/DynamicData/Kernel/OptionObservableExtensions.cs new file mode 100644 index 000000000..c57ba7383 --- /dev/null +++ b/src/DynamicData/Kernel/OptionObservableExtensions.cs @@ -0,0 +1,271 @@ +// Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. +// Roland Pheasant licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; + +namespace DynamicData.Kernel; + +/// +/// Extensions for optional. +/// +public static class OptionObservableExtensions +{ + /// + /// Converts an Observable Optional of into an Observable Optional of by applying + /// the conversion function to those Optionals that have a value. + /// + /// The type of the source. + /// The type of the destination. + /// The source. + /// The converter. + /// Observable Optional of . + /// Source or Converter was null. + /// Observable version of . + public static IObservable> Convert(this IObservable> source, Func converter) + where TSource : notnull + where TDestination : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (converter is null) + { + throw new ArgumentNullException(nameof(converter)); + } + + return source.Select(optional => optional.HasValue ? converter(optional.Value) : Optional.None()); + } + + /// + /// Overload of that allows the conversion + /// operation to also return an Optional. + /// + /// The type of the source. + /// The type of the destination. + /// The source. + /// The converter that returns an optional value. + /// Observable Optional of . + /// Source or Converter was null. + /// Observable version of . + public static IObservable> Convert(this IObservable> source, Func> converter) + where TSource : notnull + where TDestination : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (converter is null) + { + throw new ArgumentNullException(nameof(converter)); + } + + return source.Select(optional => optional.HasValue ? converter(optional.Value) : Optional.None()); + } + + /// + /// Converts an observable of optional into an observable of by applying to convert Optionals with a value + /// and using to generate a value for those that don't have a value. + /// + /// The type of the source. + /// The type of the destination. + /// The source. + /// The converter. + /// The fallback converter. + /// Observable of . + /// + /// source + /// or + /// converter + /// or + /// fallbackConverter. + /// + public static IObservable ConvertOr(this IObservable> source, Func converter, Func fallbackConverter) + where TSource : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (converter is null) + { + throw new ArgumentNullException(nameof(converter)); + } + + if (fallbackConverter is null) + { + throw new ArgumentNullException(nameof(fallbackConverter)); + } + + return source.Select(optional => optional.HasValue ? converter(optional.Value) : fallbackConverter()); + } + + /// + /// Observable Optional operator that provides a way to (possibly) create a value for those Optionals that don't already have one. + /// + /// The type of the source. + /// The source. + /// The fallback operation. + /// An Observable Optional that contains the Optionals with Values or the results of the fallback operation. + /// + /// source + /// or + /// fallbackOperation. + /// + /// Observable version of . + public static IObservable> OrElse(this IObservable> source, Func> fallbackOperation) + where T : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (fallbackOperation is null) + { + throw new ArgumentNullException(nameof(fallbackOperation)); + } + + return source.Select(optional => optional.HasValue ? optional : fallbackOperation()); + } + + /// + /// Pass-Thru operator that invokes the specified action for Optionals with a value (or, if provided, the else Action for those without a value). + /// + /// The type of the item. + /// The source. + /// The action. + /// Optional alternative action for the Else case. + /// The same Observable Optional. + /// Observable version of . + public static IObservable> OnHasValue(this IObservable> source, Action action, Action? elseAction = null) + where T : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + return source.Do(optional => optional.IfHasValue(action).Else(() => elseAction?.Invoke())); + } + + /// + /// Pass-Thru operator that invokes the specified action for Optionals without a value (or, if provided, the else Action for those with a value). + /// + /// The type of the item. + /// The source. + /// The action. + /// Optional alternative action for the Else case. + /// The same Observable Optional. + public static IObservable> OnHasNoValue(this IObservable> source, Action action, Action? elseAction = null) + where T : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + return source.Do(optional => optional.IfHasValue(val => elseAction?.Invoke(val)).Else(action)); + } + + /// + /// Converts an Observable of into an IObservable of by extracting + /// the values from Optionals that have one. + /// + /// The type of item. + /// The source. + /// An Observable with the Values. + /// Observable version of . + public static IObservable SelectValues(this IObservable> source) + where T : notnull + { + return source.Where(t => t.HasValue && t.Value is not null).Select(t => t.Value!); + } + + /// + /// Converts an Observable of into an IObservable of by extracting the + /// values from the ones that contain a value and then using to generate a value for the others. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// If the value or a provided default. + /// valueSelector. + /// Observable version of . + public static IObservable ValueOr(this IObservable> source, Func valueSelector) + where T : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + return source.Select(optional => optional.HasValue ? optional.Value : valueSelector()); + } + + /// + /// Returns the value if the optional has a value, otherwise returns the default value of T. + /// + /// The type of the item. + /// The source. + /// The value or default. + /// Observable version of . + public static IObservable ValueOrDefault(this IObservable> source) + where T : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + return source.Select(optional => optional.ValueOrDefault()); + } + + /// + /// Converts an Observable of into an IObservable of by extracting the values. + /// If it has no value, is used to generate an exception that is injected into the stream as error. + /// + /// The type of the item. + /// The source. + /// The exception generator. + /// The value. + /// exceptionGenerator. + /// Observable version of . + public static IObservable ValueOrThrow(this IObservable> source, Func exceptionGenerator) + where T : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (exceptionGenerator is null) + { + throw new ArgumentNullException(nameof(exceptionGenerator)); + } + + return Observable.Create(observer => + source.Subscribe( + optional => optional.IfHasValue(val => observer.OnNext(val)).Else(() => observer.OnError(exceptionGenerator())), + observer.OnError, + observer.OnCompleted)); + } +}