From 3fc8e849b58565f8038d1f499b9baf02c24063b8 Mon Sep 17 00:00:00 2001 From: Matt Thornton Date: Mon, 19 Aug 2019 10:14:03 +0100 Subject: [PATCH] Add methods to Results to make error handling simpler. (#11) --- README.md | 2 +- ...mainModelling.Abstractions.sln.DotSettings | 1 + .../AsyncResultExtensions.cs | 270 ++++++++++++++++-- .../Failure.cs | 54 +++- .../Result.cs | 164 +++++++++-- .../Success.cs | 61 +++- .../FailureTests.cs | 142 +++++++++ .../SuccessTests.cs | 149 +++++++++- 8 files changed, 786 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 1ab4f67..973e9ea 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ public Person GetAdult(int id) ``` This implementation has two major drawbacks: -1) From a client's perspective, the API is not experessive enough. The method signature gives no indication that it might throw, so the client would need to peek inside to find that out. +1) From a client's perspective, the API is not expressive enough. The method signature gives no indication that it might throw, so the client would need to peek inside to find that out. 2) From an implementer's perspective, the error checking, whilst simple enough in this example, can often grow quite complex. This makes the implementation of the method hard to follow due to the number of conditional branches. We may try factoring out the condition checking blocks into separate methods to solve this problem. This would also allow us to share some of this logic with other parts of the code base. These factored-out methods would then have a signature like `void CheckPersonExists(Person person)`. Again, this signature tells us nothing about the fact that the method might throw an exception. Currently, the compiler is also not able to do the flow analysis necessary to determine that the `person` is not `null` after calling such a method and so we may be left with warnings in the original call site about possible null references, even though we know we've checked for that condition. These can both be resolved by using a `Result` type and re-writing the method like this: diff --git a/Winton.DomainModelling.Abstractions.sln.DotSettings b/Winton.DomainModelling.Abstractions.sln.DotSettings index 7e72239..40242f8 100644 --- a/Winton.DomainModelling.Abstractions.sln.DotSettings +++ b/Winton.DomainModelling.Abstractions.sln.DotSettings @@ -461,6 +461,7 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> True True + True True True True diff --git a/src/Winton.DomainModelling.Abstractions/AsyncResultExtensions.cs b/src/Winton.DomainModelling.Abstractions/AsyncResultExtensions.cs index 3e8b662..8341480 100644 --- a/src/Winton.DomainModelling.Abstractions/AsyncResultExtensions.cs +++ b/src/Winton.DomainModelling.Abstractions/AsyncResultExtensions.cs @@ -12,6 +12,64 @@ namespace Winton.DomainModelling /// public static class AsyncResultExtensions { + /// + /// Invokes another result generating function which takes as input the error of this result + /// if it is a failure after it has been awaited. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for handling errors. + /// + /// + /// The type of data encapsulated by the result. + /// + /// + /// The asynchronous result that this extension method is invoked on. + /// + /// + /// The function that is invoked if this result is a failure. + /// + /// + /// If this result is a failure, then the result of onFailure function; + /// otherwise the original error. + /// + public static async Task> Catch( + this Task> resultTask, + Func> onFailure) + { + Result result = await resultTask; + return result.Catch(onFailure); + } + + /// + /// Invokes another result generating function which takes as input the error of this result + /// if it is a failure after it has been awaited. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for handling errors. + /// + /// + /// The type of data encapsulated by the result. + /// + /// + /// The asynchronous result that this extension method is invoked on. + /// + /// + /// The asynchronous function that is invoked if this result is a failure. + /// + /// + /// If this result is a failure, then the result of onFailure function; + /// otherwise the original error. + /// + public static async Task> Catch( + this Task> resultTask, + Func>> onFailure) + { + Result result = await resultTask; + return await result.Catch(onFailure); + } + /// /// Combines this result with another. /// If both are successful then combineData is invoked; @@ -122,11 +180,117 @@ public static async Task Match( } /// - /// Invokes the specified action if the result was successful and returns the original result. + /// Invokes the specified action if the result is a failure and returns the original result. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for publishing domain model notifications when an operation fails. + /// + /// + /// The type of data encapsulated by the result. + /// + /// + /// The asynchronous result that this extension method is invoked on. + /// + /// + /// The action that will be invoked if this result is a failure. + /// + /// + /// The original result. + /// + public static async Task> OnFailure( + this Task> resultTask, + Action onFailure) + { + return await resultTask.OnFailure(_ => onFailure()); + } + + /// + /// Invokes the specified action if the result is a failure and returns the original result. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for publishing domain model notifications when an operation fails. + /// + /// + /// The type of data encapsulated by the result. + /// + /// + /// The asynchronous result that this extension method is invoked on. + /// + /// + /// The action that will be invoked if this result is a failure. + /// + /// + /// The original result. + /// + public static async Task> OnFailure( + this Task> resultTask, + Action onFailure) + { + Result result = await resultTask; + return result.OnFailure(onFailure); + } + + /// + /// Invokes the specified action if the result is a failure and returns the original result. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for publishing domain model notifications when an operation fails. + /// + /// + /// The type of data encapsulated by the result. + /// + /// + /// The asynchronous result that this extension method is invoked on. + /// + /// + /// The asynchronous action that will be invoked if this result is a failure. + /// + /// + /// The original result. + /// + public static async Task> OnFailure( + this Task> resultTask, + Func onFailure) + { + return await resultTask.OnFailure(_ => onFailure()); + } + + /// + /// Invokes the specified action if the result is a failure and returns the original result. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for publishing domain model notifications when an operation fails. + /// + /// + /// The type of data encapsulated by the result. + /// + /// + /// The asynchronous result that this extension method is invoked on. + /// + /// + /// The asynchronous action that will be invoked if this result is a failure. + /// + /// + /// The original result. + /// + public static async Task> OnFailure( + this Task> resultTask, + Func onFailure) + { + Result result = await resultTask; + return await result.OnFailure(onFailure); + } + + /// + /// Invokes the specified action if the result is a success and returns the original result. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. - /// This is useful for publishing domain model notifications when an operation has been successful. + /// This is useful for publishing domain model notifications when an operation succeeds. /// /// /// The type of data encapsulated by the result. @@ -135,7 +299,7 @@ public static async Task Match( /// The asynchronous result that this extension method is invoked on. /// /// - /// The action that will be invoked if this result represents a success. + /// The action that will be invoked if this result is a success. /// /// /// The original result. @@ -148,11 +312,11 @@ public static async Task> OnSuccess( } /// - /// Invokes the specified action if the result was successful and returns the original result. + /// Invokes the specified action if the result is a success and returns the original result. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. - /// This is useful for publishing domain model notifications when an operation has been successful. + /// This is useful for publishing domain model notifications when an operation succeeds. /// /// /// The type of data encapsulated by the result. @@ -161,7 +325,7 @@ public static async Task> OnSuccess( /// The asynchronous result that this extension method is invoked on. /// /// - /// The action that will be invoked if this result represents a success. + /// The action that will be invoked if this result is a success. /// /// /// The original result. @@ -175,11 +339,11 @@ public static async Task> OnSuccess( } /// - /// Invokes the specified action if the result was successful and returns the original result. + /// Invokes the specified action if the result is a success and returns the original result. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. - /// This is useful for publishing domain model notifications when an operation has been successful. + /// This is useful for publishing domain model notifications when an operation succeeds. /// /// /// The type of data encapsulated by the result. @@ -188,7 +352,7 @@ public static async Task> OnSuccess( /// The asynchronous result that this extension method is invoked on. /// /// - /// The asynchronous action that will be invoked if this result represents a success. + /// The asynchronous action that will be invoked if this result is a success. /// /// /// The original result. @@ -201,11 +365,11 @@ public static async Task> OnSuccess( } /// - /// Invokes the specified action if the result was successful and returns the original result. + /// Invokes the specified action if the result is a success and returns the original result. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. - /// This is useful for publishing domain model notifications when an operation has been successful. + /// This is useful for publishing domain model notifications when an operation succeeds. /// /// /// The type of data encapsulated by the result. @@ -214,7 +378,7 @@ public static async Task> OnSuccess( /// The asynchronous result that this extension method is invoked on. /// /// - /// The asynchronous action that will be invoked if this result represents a success. + /// The asynchronous action that will be invoked if this result is a success. /// /// /// The original result. @@ -242,19 +406,19 @@ public static async Task> OnSuccess( /// /// The asynchronous result that this extension method is invoked on. /// - /// + /// /// The function that is invoked to select the data. /// /// - /// A new result containing either; the output of the selectData function - /// if this result is a success, otherwise the original error. + /// A new result containing either; the output of the selector function + /// if this result is a success, otherwise the original failure. /// public static async Task> Select( this Task> resultTask, - Func selectData) + Func selector) { Result result = await resultTask; - return result.Select(selectData); + return result.Select(selector); } /// @@ -272,24 +436,78 @@ public static async Task> Select( /// /// The asynchronous result that this extension method is invoked on. /// - /// + /// /// The asynchronous function that is invoked to select the data. /// /// - /// A new result containing either; the output of the selectData function - /// if this result is a success, otherwise the original error. + /// A new result containing either; the output of the selector function + /// if this result is a success, otherwise the original failure. /// public static async Task> Select( this Task> resultTask, - Func> selectData) + Func> selector) + { + Result result = await resultTask; + return await result.Select(selector); + } + + /// + /// Projects a failed result's data from one type to another. + /// + /// + /// If this result is a success then this is a no-op. + /// + /// + /// The type of data encapsulated by the result. + /// + /// + /// The asynchronous result that this extension method is invoked on. + /// + /// + /// The function that is invoked to select the error. + /// + /// + /// A new result containing either; the output of the selector function + /// if this result is a failure, otherwise the original success. + /// + public static async Task> SelectError( + this Task> resultTask, + Func selector) { Result result = await resultTask; - return await result.Select(selectData); + return result.SelectError(selector); + } + + /// + /// Projects a failed result's error from one type to another. + /// + /// + /// If this result is a success then this is a no-op. + /// + /// + /// The type of data encapsulated by the result. + /// + /// + /// The asynchronous result that this extension method is invoked on. + /// + /// + /// The asynchronous function that is invoked to select the error. + /// + /// + /// A new result containing either; the output of the selector function + /// if this result is a failure, otherwise the original success. + /// + public static async Task> SelectError( + this Task> resultTask, + Func> selector) + { + Result result = await resultTask; + return await result.SelectError(selector); } /// /// Invokes another result generating function which takes as input the data of this result - /// if it is successful after it has been awaited. + /// if it is a success after it has been awaited. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. @@ -305,7 +523,7 @@ public static async Task> Select( /// The asynchronous result that this extension method is invoked on. /// /// - /// The function that is invoked if this result represents a success. + /// The function that is invoked if this result is a success. /// /// /// If this result is a success, then the result of onSuccess function; @@ -321,7 +539,7 @@ public static async Task> Then( /// /// Invokes another result generating function which takes as input the data of this result - /// if it is successful after it has been awaited. + /// if it is a success after it has been awaited. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. @@ -337,7 +555,7 @@ public static async Task> Then( /// The asynchronous result that this extension method is invoked on. /// /// - /// The asynchronous function that is invoked if this result represents a success. + /// The asynchronous function that is invoked if this result is a success. /// /// /// If this result is a success, then the result of onSuccess function; diff --git a/src/Winton.DomainModelling.Abstractions/Failure.cs b/src/Winton.DomainModelling.Abstractions/Failure.cs index 70a532c..cc1f8fa 100644 --- a/src/Winton.DomainModelling.Abstractions/Failure.cs +++ b/src/Winton.DomainModelling.Abstractions/Failure.cs @@ -27,6 +27,18 @@ public Failure(Error error) /// public Error Error { get; } + /// + public override Result Catch(Func> onFailure) + { + return onFailure(Error); + } + + /// + public override Task> Catch(Func>> onFailure) + { + return onFailure(Error); + } + /// public override Result Combine( Result other, @@ -44,6 +56,32 @@ public override T Match(Func onSuccess, Func onFailure) return onFailure(Error); } + /// + public override Result OnFailure(Action onFailure) + { + return OnFailure(_ => onFailure()); + } + + /// + public override Result OnFailure(Action onFailure) + { + onFailure(Error); + return this; + } + + /// + public override Task> OnFailure(Func onFailure) + { + return OnFailure(_ => onFailure()); + } + + /// + public override async Task> OnFailure(Func onFailure) + { + await onFailure(Error); + return this; + } + /// public override Result OnSuccess(Action onSuccess) { @@ -69,17 +107,29 @@ public override Task> OnSuccess(Func onSuccess) } /// - public override Result Select(Func selectData) + public override Result Select(Func selector) { return new Failure(Error); } /// - public override Task> Select(Func> selectData) + public override Task> Select(Func> selector) { return Task.FromResult>(new Failure(Error)); } + /// + public override Result SelectError(Func selector) + { + return new Failure(selector(Error)); + } + + /// + public override async Task> SelectError(Func> selector) + { + return new Failure(await selector(Error)); + } + /// public override Result Then(Func> onSuccess) { diff --git a/src/Winton.DomainModelling.Abstractions/Result.cs b/src/Winton.DomainModelling.Abstractions/Result.cs index 430887e..8745385 100644 --- a/src/Winton.DomainModelling.Abstractions/Result.cs +++ b/src/Winton.DomainModelling.Abstractions/Result.cs @@ -17,6 +17,40 @@ namespace Winton.DomainModelling /// public abstract class Result { + /// + /// Invokes another result generating function which takes as input the error of this result + /// if it is a failure. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for handling errors. + /// + /// + /// The function that is invoked if this result is a failure. + /// + /// + /// If this result is a failure, then the result of the onFailure function; + /// otherwise the original error. + /// + public abstract Result Catch(Func> onFailure); + + /// + /// Invokes another result generating function which takes as input the error of this result + /// if it is a failure. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for handling errors. + /// + /// + /// The function that is invoked if this result is a failure. + /// + /// + /// If this result is a failure, then the result of the onFailure function; + /// otherwise the original error. + /// + public abstract Task> Catch(Func>> onFailure); + /// /// Combines this result with another. /// If both are successful then combineData is invoked; @@ -63,11 +97,71 @@ public abstract Result Combine( public abstract T Match(Func onSuccess, Func onFailure); /// - /// Invokes the specified action if the result was successful and returns the original result. + /// Invokes the specified action if the result is a failure and returns the original result. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for publishing domain model notifications when an operation fails. + /// + /// + /// The action that will be invoked if this result is a failure. + /// + /// + /// The original result. + /// + public abstract Result OnFailure(Action onFailure); + + /// + /// Invokes the specified action if the result is a failure and returns the original result. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for publishing domain model notifications when an operation fails. + /// + /// + /// The action that will be invoked if this result is a failure. + /// + /// + /// The original result. + /// + public abstract Result OnFailure(Action onFailure); + + /// + /// Invokes the specified action if the result is a failure and returns the original result. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for publishing domain model notifications when an operation fails. + /// + /// + /// The asynchronous action that will be invoked if this result is a failure. + /// + /// + /// The original result. + /// + public abstract Task> OnFailure(Func onFailure); + + /// + /// Invokes the specified action if the result is a failure and returns the original result. + /// + /// + /// If this result is a success then this is a no-op and the original success is retained. + /// This is useful for publishing domain model notifications when an operation fails. + /// + /// + /// The asynchronous action that will be invoked if this result is a failure. + /// + /// + /// The original result. + /// + public abstract Task> OnFailure(Func onFailure); + + /// + /// Invokes the specified action if the result is a success and returns the original result. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. - /// This is useful for publishing domain model notifications when an operation has been successful. + /// This is useful for publishing domain model notifications when an operation succeeds. /// /// /// The action that will be invoked if this result represents a success. @@ -78,11 +172,11 @@ public abstract Result Combine( public abstract Result OnSuccess(Action onSuccess); /// - /// Invokes the specified action if the result was successful and returns the original result. + /// Invokes the specified action if the result is a success and returns the original result. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. - /// This is useful for publishing domain model notifications when an operation has been successful. + /// This is useful for publishing domain model notifications when an operation succeeds. /// /// /// The action that will be invoked if this result represents a success. @@ -93,11 +187,11 @@ public abstract Result Combine( public abstract Result OnSuccess(Action onSuccess); /// - /// Invokes the specified action if the result was successful and returns the original result. + /// Invokes the specified action if the result is a success and returns the original result. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. - /// This is useful for publishing domain model notifications when an operation has been successful. + /// This is useful for publishing domain model notifications when an operation succeeds. /// /// /// The asynchronous action that will be invoked if this result represents a success. @@ -108,11 +202,11 @@ public abstract Result Combine( public abstract Task> OnSuccess(Func onSuccess); /// - /// Invokes the specified action if the result was successful and returns the original result. + /// Invokes the specified action if the result is a success and returns the original result. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. - /// This is useful for publishing domain model notifications when an operation has been successful. + /// This is useful for publishing domain model notifications when an operation succeeds. /// /// /// The asynchronous action that will be invoked if this result represents a success. @@ -131,14 +225,14 @@ public abstract Result Combine( /// /// The type of data in the new result. /// - /// + /// /// The function that is invoked to select the data. /// /// - /// A new result containing either; the output of the selectData function - /// if this result is a success, otherwise the original error. + /// A new result containing either; the output of the selector function + /// if this result is a success, otherwise the original failure. /// - public abstract Result Select(Func selectData); + public abstract Result Select(Func selector); /// /// Projects a successful result's data from one type to another. @@ -149,18 +243,48 @@ public abstract Result Combine( /// /// The type of data in the new result. /// - /// + /// /// The asynchronous function that is invoked to select the data. /// /// - /// A new result containing either; the output of the selectData function - /// if this result is a success, otherwise the original error. + /// A new result containing either; the output of the selector function + /// if this result is a success, otherwise the original failure. + /// + public abstract Task> Select(Func> selector); + + /// + /// Projects a failed result's error from one type to another. + /// + /// + /// If this result is a success then this is a no-op. + /// + /// + /// The function that is invoked to select the error. + /// + /// + /// A new result containing either; the output of the selector function + /// if this result is a failure, otherwise the original success. /// - public abstract Task> Select(Func> selectData); + public abstract Result SelectError(Func selector); + + /// + /// Projects a failed result's error from one type to another. + /// + /// + /// If this result is a success then this is a no-op. + /// + /// + /// The asynchronous function that is invoked to select the error. + /// + /// + /// A new result containing either; the output of the selector function + /// if this result is a failure, otherwise the original success. + /// + public abstract Task> SelectError(Func> selector); /// /// Invokes another result generating function which takes as input the data of this result - /// if it was successful. + /// if it is a success. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. @@ -170,7 +294,7 @@ public abstract Result Combine( /// The type of data in the new result. /// /// - /// The function that is invoked if this result represents a success. + /// The function that is invoked if this result is a success. /// /// /// If this result is a success, then the result of onSuccess function; @@ -180,7 +304,7 @@ public abstract Result Combine( /// /// Invokes another result generating function which takes as input the data of this result - /// if it was successful. + /// if it is a success. /// /// /// If this result is a failure then this is a no-op and the original failure is retained. @@ -190,7 +314,7 @@ public abstract Result Combine( /// The type of data in the new result. /// /// - /// The asynchronous function that is invoked if this result represents a success. + /// The asynchronous function that is invoked if this result is a success. /// /// /// If this result is a success, then the result of onSuccess function; diff --git a/src/Winton.DomainModelling.Abstractions/Success.cs b/src/Winton.DomainModelling.Abstractions/Success.cs index a3ff794..35e56ca 100644 --- a/src/Winton.DomainModelling.Abstractions/Success.cs +++ b/src/Winton.DomainModelling.Abstractions/Success.cs @@ -54,6 +54,18 @@ public Success(TData data) /// public TData Data { get; } + /// + public override Result Catch(Func> onFailure) + { + return new Success(Data); + } + + /// + public override Task> Catch(Func>> onFailure) + { + return Task.FromResult>(new Success(Data)); + } + /// public override Result Combine( Result other, @@ -71,10 +83,34 @@ public override T Match(Func onSuccess, Func onFailure) return onSuccess(Data); } + /// + public override Result OnFailure(Action onFailure) + { + return this; + } + + /// + public override Result OnFailure(Action onFailure) + { + return this; + } + + /// + public override Task> OnFailure(Func onFailure) + { + return Task.FromResult>(this); + } + + /// + public override Task> OnFailure(Func onFailure) + { + return Task.FromResult>(this); + } + /// public override Result OnSuccess(Action onSuccess) { - return OnSuccess(data => onSuccess()); + return OnSuccess(_ => onSuccess()); } /// @@ -87,7 +123,7 @@ public override Result OnSuccess(Action onSuccess) /// public override async Task> OnSuccess(Func onSuccess) { - return await OnSuccess(data => onSuccess()); + return await OnSuccess(_ => onSuccess()); } /// @@ -98,16 +134,27 @@ public override async Task> OnSuccess(Func onSuccess) } /// - public override Result Select(Func selectData) + public override Result Select(Func selector) + { + return new Success(selector(Data)); + } + + /// + public override async Task> Select(Func> selector) + { + return new Success(await selector(Data)); + } + + /// + public override Result SelectError(Func selector) { - return new Success(selectData(Data)); + return new Success(Data); } /// - public override async Task> Select(Func> selectData) + public override Task> SelectError(Func> selector) { - TNewData data = await selectData(Data); - return new Success(data); + return Task.FromResult>(new Success(Data)); } /// diff --git a/test/Winton.DomainModelling.Abstractions.Tests/FailureTests.cs b/test/Winton.DomainModelling.Abstractions.Tests/FailureTests.cs index 52c0b18..7427f2b 100644 --- a/test/Winton.DomainModelling.Abstractions.Tests/FailureTests.cs +++ b/test/Winton.DomainModelling.Abstractions.Tests/FailureTests.cs @@ -9,6 +9,40 @@ namespace Winton.DomainModelling { public class FailureTests { + public sealed class Catch : FailureTests + { + [Fact] + private void ShouldInvokeOnFailureFunc() + { + Result OnFailure(Error e) + { + return new Failure(new Error(e.Title, $"Ka-{e.Detail}")); + } + + var failure = new Failure(new Error("Error", "Boom!")); + + Result result = failure.Catch(e => OnFailure(e)); + + result.Should().BeEquivalentTo(new Failure(new Error("Error", "Ka-Boom!"))); + } + + [Fact] + private async Task ShouldInvokeOnFailureFuncAsynchronously() + { + async Task> OnFailure(Error e) + { + await Task.Yield(); + return new Failure(new Error(e.Title, $"Ka-{e.Detail}")); + } + + var failure = new Failure(new Error("Error", "Boom!")); + + Result result = await failure.Catch(async e => await OnFailure(e)); + + result.Should().BeEquivalentTo(new Failure(new Error("Error", "Ka-Boom!"))); + } + } + public sealed class Combine : FailureTests { [Fact] @@ -51,6 +85,90 @@ private void ShouldInvokeOnFailureFunc() } } + public sealed class OnFailure : FailureTests + { + [Fact] + private void ShouldInvokeAction() + { + var invoked = false; + var failure = new Failure(new Error("Error", "Boom!")); + + failure.OnFailure(() => invoked = true); + + invoked.Should().BeTrue(); + } + + [Fact] + private void ShouldNotInvokeActionWithParameters() + { + var invoked = false; + var failure = new Failure(new Error("Error", "Boom!")); + + failure.OnFailure(i => invoked = true); + + invoked.Should().BeTrue(); + } + + [Fact] + private async Task ShouldNotInvokeAsyncAction() + { + var invoked = false; + async Task OnFailure() + { + await Task.Yield(); + invoked = true; + } + + var failure = new Failure(new Error("Error", "Boom!")); + + await failure.OnFailure(OnFailure); + + invoked.Should().BeTrue(); + } + + [Fact] + private async Task ShouldNotInvokeAsyncActionWithParameters() + { + var invoked = false; + async Task OnFailure(Error e) + { + await Task.Yield(); + invoked = true; + } + + var failure = new Failure(new Error("Error", "Boom!")); + + await failure.OnFailure(OnFailure); + + invoked.Should().BeTrue(); + } + + [Fact] + private void ShouldReturnOriginalResult() + { + var failure = new Failure(new Error("Error", "Boom!")); + + Result result = failure.OnFailure(() => { }); + + result.Should().BeSameAs(failure); + } + + [Fact] + private async Task ShouldReturnOriginalResultWhenAsyncAction() + { + async Task OnFailure() + { + await Task.Yield(); + } + + var failure = new Failure(new Error("Error", "Boom!")); + + Result result = await failure.OnFailure(OnFailure); + + result.Should().BeSameAs(failure); + } + } + public sealed class OnSuccess : FailureTests { [Fact] @@ -158,6 +276,30 @@ private async Task ShouldReturnOriginalFailureAsynchronously() } } + public sealed class SelectError : SuccessTests + { + [Fact] + private void ShouldProjectError() + { + var failure = new Failure(new Error("Error", "Boom!")); + + Result result = failure.SelectError(e => new NotFoundError(e.Detail)); + + result.Should().BeEquivalentTo(new Failure(new NotFoundError("Boom!"))); + } + + [Fact] + private async Task ShouldProjectErrorAsynchronously() + { + var failure = new Failure(new Error("Error", "Boom!")); + + Result result = await failure.SelectError( + e => Task.FromResult(new NotFoundError(e.Detail))); + + result.Should().BeEquivalentTo(new Failure(new NotFoundError("Boom!"))); + } + } + public sealed class Then : FailureTests { [Fact] diff --git a/test/Winton.DomainModelling.Abstractions.Tests/SuccessTests.cs b/test/Winton.DomainModelling.Abstractions.Tests/SuccessTests.cs index acb1345..6751796 100644 --- a/test/Winton.DomainModelling.Abstractions.Tests/SuccessTests.cs +++ b/test/Winton.DomainModelling.Abstractions.Tests/SuccessTests.cs @@ -9,6 +9,40 @@ namespace Winton.DomainModelling { public class SuccessTests { + public sealed class Catch : FailureTests + { + [Fact] + private void ShouldReturnOriginalSuccess() + { + Result OnFailure(Error e) + { + return new Failure(new Error(e.Title, $"Ka-{e.Detail}")); + } + + var success = new Success(1); + + Result result = success.Catch(e => OnFailure(e)); + + result.Should().BeEquivalentTo(success); + } + + [Fact] + private async Task ShouldReturnOriginalSuccessAsynchronously() + { + async Task> OnFailure(Error e) + { + await Task.Yield(); + return new Failure(new Error(e.Title, $"Ka-{e.Detail}")); + } + + var success = new Success(1); + + Result result = await success.Catch(e => OnFailure(e)); + + result.Should().BeEquivalentTo(success); + } + } + public sealed class Combine : SuccessTests { [Fact] @@ -52,6 +86,90 @@ private void ShouldInvokeOnSuccessFunc() } } + public sealed class OnFailure : FailureTests + { + [Fact] + private void ShouldNotInvokeAction() + { + var invoked = false; + var success = new Success(1); + + success.OnFailure(() => invoked = true); + + invoked.Should().BeFalse(); + } + + [Fact] + private void ShouldNotInvokeActionWithParameters() + { + var invoked = false; + var success = new Success(1); + + success.OnFailure(i => invoked = true); + + invoked.Should().BeFalse(); + } + + [Fact] + private async Task ShouldNotInvokeAsyncAction() + { + var invoked = false; + async Task OnSuccess() + { + await Task.Yield(); + invoked = true; + } + + var success = new Success(1); + + await success.OnFailure(OnSuccess); + + invoked.Should().BeFalse(); + } + + [Fact] + private async Task ShouldNotInvokeAsyncActionWithParameters() + { + var invoked = false; + async Task OnSuccess(Error e) + { + await Task.Yield(); + invoked = true; + } + + var success = new Success(1); + + await success.OnFailure(OnSuccess); + + invoked.Should().BeFalse(); + } + + [Fact] + private void ShouldReturnOriginalResult() + { + var success = new Success(1); + + Result result = success.OnFailure(() => { }); + + result.Should().BeSameAs(success); + } + + [Fact] + private async Task ShouldReturnOriginalResultWhenAsyncAction() + { + async Task OnSuccess() + { + await Task.Yield(); + } + + var failure = new Failure(new Error("Error", "Boom!")); + + Result result = await failure.OnFailure(OnSuccess); + + result.Should().BeSameAs(failure); + } + } + public sealed class OnSuccess : FailureTests { [Fact] @@ -159,14 +277,43 @@ private async Task ShouldProjectDataToNewTypeAsynchronously() } } + public sealed class SelectError : FailureTests + { + [Fact] + private void ShouldReturnOriginalSuccess() + { + var success = new Success(1); + + Result result = success.SelectError(e => new NotFoundError(e.Detail)); + + result.Should().BeEquivalentTo(success); + } + + [Fact] + private async Task ShouldReturnOriginalSuccessAsynchronously() + { + var success = new Success(1); + + Result result = await success.SelectError( + e => Task.FromResult(new NotFoundError(e.Detail))); + + result.Should().BeEquivalentTo(success); + } + } + public sealed class Then : SuccessTests { [Fact] private void ShouldInvokeOnSuccessFunc() { + Result OnSuccess(int i) + { + return new Success(i + 1); + } + var success = new Success(1); - Result result = success.Then(i => new Success(i + 1)); + Result result = success.Then(OnSuccess); result.Should().BeEquivalentTo(new Success(2)); }