diff --git a/Inspiring.Primitives.Tests/Result/ResultTests.cs b/Inspiring.Primitives.Tests/Result/ResultTests.cs index 2033dbf..b03aa31 100644 --- a/Inspiring.Primitives.Tests/Result/ResultTests.cs +++ b/Inspiring.Primitives.Tests/Result/ResultTests.cs @@ -98,7 +98,7 @@ internal void Value(Result t, string value, InvalidOperationException ex [Scenario] - internal void SetToValue(Result t, Result v, Result actual) { + internal void SetToValue(Result t, Result v, Result actual, Result actualStruct) { WHEN["setting a value on a void result"] |= () => { v = AnItem; actual = v.SetTo("test"); @@ -113,6 +113,38 @@ internal void SetToValue(Result t, Result v, Result actual) { }; THEN["the result has the value"] |= () => actual.Should().HaveValue("test"); AND["and contains the original items"] |= () => actual.Should().HaveItem(AnItem); + + WHEN["setting null value on result"] |= () => { + t = AnItem; + t = t.SetTo(5); + actual = t.SetTo((string)null); + }; + THEN["the result has no value"] |= () => actual.Should().NotHaveAValue(); + AND["and contains the original items"] |= () => actual.Should().HaveItem(AnItem); + + WHEN["setting null value explicitly"] |= () => { + t = AnItem; + t = t.SetTo(5); + actual = t.SetToExplicit((string)null); + }; + THEN["the result has a value"] |= () => actual.Should().HaveValue(); + AND["and contains the original items"] |= () => actual.Should().HaveItem(AnItem); + + WHEN["setting null value on struct result"] |= () => { + t = AnItem; + t = t.SetTo(5); + actualStruct = t.SetTo((int?)null); + }; + THEN["the result has no value"] |= () => actualStruct.Should().NotHaveAValue(); + AND["and contains the original items"] |= () => actualStruct.Should().HaveItem(AnItem); + + WHEN["setting null value explicitly on struct result"] |= () => { + t = AnItem; + t = t.SetTo(5); + actualStruct = t.SetToExplicit((int?)null); + }; + THEN["the result has a value"] |= () => actualStruct.Should().HaveValue(); + AND["and contains the original items"] |= () => actualStruct.Should().HaveItem(AnItem); } @@ -179,8 +211,11 @@ Nothing n WHEN["comparing a Result without a value to null"] |= () => s1 = Result.Of(); THEN["it is not equal"] |= () => s1.Equals((object)null).Should().BeFalse(); - WHEN["comparing a Result with a null value to null"] |= () => s1 = Result.From(null); + WHEN["comparing a explicit Result with a null value to null"] |= () => s1 = Result.FromExplicit(null); THEN["it is equal"] |= () => s1.Equals((object)null).Should().BeTrue(); + + WHEN["comparing a implicit Result with a null value to null"] |= () => s1 = Result.From(null); + THEN["it is equal"] |= () => s1.Should().Be(Result.Of()); WHEN["comparing a VoidResult to null"] |= () => v1 = Result.Empty; THEN["it is not equal"] |= () => v1.Equals(null).Should().BeFalse(); @@ -204,7 +239,7 @@ Nothing n WHEN["comparing a Result to an Result without values"] |= () => (i1, l1) = (Result.Of(), Result.Of()); THEN["they are never equal"] |= () => assertInequality(i1, l1, allowHashCodesToBeEqual: true); - WHEN["comparing two Result with null values"] |= () => (s1, d1) = (Result.From(null), Result.From(null)); + WHEN["comparing two explicit Result with null values"] |= () => (s1, d1) = (Result.FromExplicit(null), Result.FromExplicit(null)); THEN["they are equal"] |= () => assertEquality(s1, d1); WHEN["comparing a Result to a Nothing"] |= () => (v1, n) = (Result.Empty, Result.Nothing); @@ -265,7 +300,7 @@ bool equals(IResult r1, IResult r2) } [Scenario(DisplayName = "Transformation")] - internal void Transformation(Result s, TestItem item1, TestItem item2) { + internal void Transformation(Result s, Result structResult, TestItem item1, TestItem item2) { GIVEN["a few items"] |= () => (item1, item2) = (new TestItem(), new TestItem()); WHEN["the result does not have a value"] |= () => { Result i = item1; @@ -290,6 +325,38 @@ internal void Transformation(Result s, TestItem item1, TestItem item2) { THEN["the result of the transformation is merged with the original result"] |= () => s.Should() .HaveValue("27").And .HaveItemsInOrder(item1, item2); + + WHEN["the result has a value and is converted to null"] |= () => { + Result i = Result.From(27) + item1; + s = i.Transform(val => (string)null); + }; + THEN["the transformation is executed"] |= () => s.Should() + .NotHaveAValue().And + .HaveItemsInOrder(item1); + + WHEN["the result has a value and is converted to null struct"] |= () => { + Result i = Result.From(27) + item1; + structResult = i.Transform(val => (int?)null); + }; + THEN["the transformation is executed"] |= () => structResult.Should() + .NotHaveAValue().And + .HaveItemsInOrder(item1); + + WHEN["the result has no value and is converted to a struct"] |= () => { + Result i = Result.From(null) + item1; + s = i.Transform(val => "27"); + }; + THEN["the transformation is not executed"] |= () => s.Should() + .NotHaveAValue().And + .HaveItemsInOrder(item1); + + WHEN["the explicit result has no value and is converted to a struct"] |= () => { + Result i = Result.FromExplicit(null) + item1; + s = i.Transform(val => "27"); + }; + THEN["the transformation is executed"] |= () => s.Should() + .HaveValue("27").And + .HaveItemsInOrder(item1); } [Scenario(DisplayName = "LINQ syntax")] @@ -453,6 +520,68 @@ internal void ImplictTaskSupport(Task vt, Result v, Task> WHEN["assining a result item to a task"] |= () => vt = AnItem; THEN["a completed task is implicitly created"] |= () => vt.Result.Should().Be(Result.Empty + AnItem); } + + [Scenario(DisplayName = "OnCondition Support")] + internal void OnConditionExecuteSupport(Result s, bool executed) { + WHEN["there is a result with a value"] |= () => { + s = "42"; + executed = false; + }; + THEN["OnValue should be executed"] |= () => { + s.OnValue(() => executed = true); + executed.Should().BeTrue(); + }; + + WHEN["there is a result without a value"] |= () => { + s = (string)null; + executed = false; + }; + THEN["OnValue should not be executed"] |= () => { + s.OnValue(() => executed = true); + executed.Should().BeFalse(); + }; + + WHEN["there is a result with a value"] |= () => { + s = "42"; + executed = false; + }; + THEN["OnNoValue should not be executed"] |= () => { + s.OnNoValue(() => executed = true); + executed.Should().BeFalse(); + }; + + WHEN["there is a result without a value"] |= () => { + s = (string)null; + executed = false; + }; + THEN["OnNoValue should be executed"] |= () => { + s.OnNoValue(() => executed = true); + executed.Should().BeTrue(); + }; + } + + [Scenario(DisplayName = "OrUse Support")] + internal void OrUseSupport(Result s, Result u, TestItem i1) { + GIVEN["two test items"] |= () => i1 = new TestItem(); + + WHEN["there is a result with no value and an conditional item"] |= () => { + s = (string)null; + s = s.OrUse(() => i1); + }; + THEN["the item should be present"] |= () => { + s.Should().NotHaveAValue(); + s.Should().HaveItem(i1); + }; + + WHEN["there is a result with a value and an conditional item"] |= () => { + s = "42"; + s = s.OrUse(() => i1); + }; + THEN["the item should not be present"] |= () => { + s.Should().HaveValue(); + s.Should().NotHaveItems(); + }; + } internal class TestItem : ResultItem, IResultItemInfo { public string Message { get; set; } diff --git a/Inspiring.Primitives/Result/Result.cs b/Inspiring.Primitives/Result/Result.cs index 06a1b23..a3fdc70 100644 --- a/Inspiring.Primitives/Result/Result.cs +++ b/Inspiring.Primitives/Result/Result.cs @@ -44,10 +44,24 @@ public IEnumerable Get() where TItem : IResultItem public Result To() => new Result(_items); + + public Result SetTo(T? value) + => SetTo(false, value)!; + + public Result SetToExplicit(T value) + => SetTo(true, value); + + private Result SetTo(bool treatNullAsValue, T value) + => new Result(treatNullAsValue || value != null, value, _items); - public Result SetTo(T value) - => new Result(value, _items); + public Result SetTo(T? value) where T : struct + => SetTo(false, value); + public Result SetToExplicit(T? value) where T : struct + => SetTo(true, value); + + private Result SetTo(bool treatNullAsValue, T? value) where T : struct + => new Result(treatNullAsValue || value.HasValue, value.GetValueOrDefault(), _items); /****************************** EQUALITY *****************************/ @@ -76,8 +90,27 @@ public override string ToString() { public static Result Of() => default; - public static Result From(T value) => value; + public static Result From(T? value) where T : class => + From(value, false)!; + + public static Result FromExplicit(T value) => + From(value, true); + internal static Result From(T value, bool treatNullAsValue) => + new Result(treatNullAsValue || value != null, value); + + public static Result From(T? value) where T : struct => + From(value, false); + + public static Result FromExplicit(T? value) where T : struct => + From(value, true); + + // second parameter needed to prevent compile issue with duplicate method signature + public static Result From(T value, T _ = default) where T : struct => + From(value, false); + + internal static Result From(T? value, bool treatNullAsValue) where T : struct => + new Result(value.HasValue, value.GetValueOrDefault()); /************************** CAST OPERATORS ***************************/ diff --git a/Inspiring.Primitives/Result/ResultExtensions.cs b/Inspiring.Primitives/Result/ResultExtensions.cs index 3387da1..345b624 100644 --- a/Inspiring.Primitives/Result/ResultExtensions.cs +++ b/Inspiring.Primitives/Result/ResultExtensions.cs @@ -90,5 +90,63 @@ Func func (acc + x).SetTo(func(acc, x)) : (acc + x)); } + + private static Result On(this Result source, Predicate> condition, Func, Result> mapper) { + return condition.Invoke(source) ? mapper.Invoke(source) : source; + } + + private static Result OnHasItem(this Result source, Func, Result> mapper, Func? itemPredicate = null) where TItem : IResultItem { + return On(source, s => s.Has(itemPredicate), mapper); + } + + private static Result OnValue(this Result source, Func, Result> mapper) { + return On(source, s => s.HasValue, mapper); + } + + public static Result OnValue(this Result source, Action action) { + return OnValue(source, _ => { + action.Invoke(); + return _; + }); + } + + public static Result OnValue(this Result source, Action> action) { + return OnValue(source, s => { + action.Invoke(s); + return s; + }); + } + + private static Result OnNoValue(this Result source, Func, Result> mapper) { + return On(source, s => !s.HasValue, mapper); + } + + public static Result OnNoValue(this Result source, Action action) { + return OnNoValue(source, _ => { + action.Invoke(); + return _; + }); + } + + public static Result OnNoValue(this Result source, Action> action) { + return OnNoValue(source, s => { + action.Invoke(s); + return s; + }); + } + + public static Result OnHasItem(this Result source, Action action, Func? itemPredicate = null) where TItem : IResultItem { + return OnHasItem(source, _ => { + action.Invoke(); + return _; + }, itemPredicate); + } + + public static Result OnHasItem(this Result source, Action> action, Func? itemPredicate = null) where TItem : IResultItem { + return OnHasItem(source, s => { + action.Invoke(s); + return s; + }, itemPredicate); + } } } diff --git a/Inspiring.Primitives/Result/Result`1.cs b/Inspiring.Primitives/Result/Result`1.cs index b233dde..74325eb 100644 --- a/Inspiring.Primitives/Result/Result`1.cs +++ b/Inspiring.Primitives/Result/Result`1.cs @@ -38,7 +38,7 @@ internal Result(ImmutableList? items) internal Result(T value, ImmutableList? items) : this(true, value, items) { } - private Result(bool hasValue = false, T value = default!, ImmutableList? items = null) { + internal Result(bool hasValue = false, T value = default!, ImmutableList? items = null) { _items = items; _value = value!; _hasValue = hasValue; @@ -65,12 +65,32 @@ public Result ToVoid() public Result To() => new Result(_items); - public Result SetTo(U value) - => new Result(value, _items); + public Result SetTo(U? value) + => SetTo(false, value)!; + + public Result SetToExplicit(U value) + => SetTo(true, value); + + private Result SetTo(bool treatNullAsValue, U value) + => new Result(treatNullAsValue || value != null, value, _items); + + public Result SetTo(U? value) where U : struct + => SetTo(false, value); + + public Result SetToExplicit(U? value) where U : struct + => SetTo(true, value); + + private Result SetTo(bool treatNullAsValue, U? value) where U : struct + => new Result(treatNullAsValue || value.HasValue, value.GetValueOrDefault(), _items); public Result Or(T defaultValue) => HasValue ? this : SetTo(defaultValue); + public Result OrUse(Func> transformation) => + HasValue ? this : this + transformation(); + + public Result OrUse(Func transformation) => + HasValue ? this : this + transformation(); /*********************** TRANSFORMATION METHODS **********************/ @@ -79,10 +99,26 @@ public Result Transform(Func> transformation) => ToVoid() + transformation(Value) : To(); - public Result Transform(Func transformation) => - HasValue ? - ToVoid() + Result.From(transformation(Value)) : - To(); + public Result Transform(Func transformation) where U : struct { + if (!HasValue) + return To(); + + var transformResult = transformation(Value); + return transformResult.HasValue ? ToVoid() + Result.From(transformResult.Value) : To(); + } + + public Result Transform(Func transformation) where U : class => + Transform(false, transformation)!; + + public Result TransformExplicit(Func transformation) => + Transform(true, transformation); + + private Result Transform(bool treatNullAsValue, Func transformation) { + if (!HasValue) + return To(); + + return ToVoid() + Result.From(transformation(Value), treatNullAsValue); + } [EditorBrowsable(EditorBrowsableState.Never)] public Result SelectMany(Func> transformation, Func resultSelector) { @@ -95,6 +131,21 @@ public Result SelectMany(Func> transformation, Func(); } + /****************************** OnCallbacks *****************************/ + + // can't put these into extension methods because of inferring generic parameters + public Result OnItems(Action> action, Func? itemPredicate = null) where TItem : IResultItem { + return this.OnHasItem(s => { + action.Invoke(s.Get()); + }, itemPredicate); + } + + public Result OnItem(Action action, Func? itemPredicate = null) where TItem : IResultItem { + return this.OnHasItem(s => { + action.Invoke(s.Get().First()); + }, itemPredicate); + } + /****************************** EQUALITY *****************************/ public bool Equals(Result other) => @@ -168,9 +219,9 @@ private Result CreateCopy(ImmutableList? items) /************************** CAST OPERATORS ***************************/ - - public static implicit operator Result(T value) - => new Result(true, value); + + public static implicit operator Result(T? value) + => Result.From(value, treatNullAsValue: false)!; public static implicit operator T(Result result) => result.Value;