From 3d34a3021dc74f282864048c48a906ed34892fe6 Mon Sep 17 00:00:00 2001 From: JustFixMe Date: Mon, 18 Dec 2023 18:24:15 +0400 Subject: [PATCH] added TryRecover extensions --- README.md | 23 +++-- .../EnsureExtensionsExecutor.cs | 30 ++++++- .../ExtensionsMethodGenerator.cs | 1 + .../ResultTryRecoverExecutor.cs | 86 +++++++++++++++++++ Railway/Ensure.cs | 12 --- Railway/Result.cs | 20 ++++- Raliway.Tests/Results/GeneralUsage.cs | 29 +++++++ 7 files changed, 176 insertions(+), 25 deletions(-) create mode 100644 Railway.SourceGenerator/ResultTryRecoverExecutor.cs diff --git a/README.md b/README.md index e669e4d..a35cc4e 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,9 @@ Result Bar() ```csharp Result result = GetResult(); -var value = result - .Append("new") - .Map((i, s) => $"{s} result {i}") +string value = result + .Append("new") // -> Result<(int, string)> + .Map((i, s) => $"{s} result {i}") // -> Result .Match( onSuccess: x => x, onFailure: err => err.ToString() @@ -81,6 +81,17 @@ var value = result Result GetResult() => Result.Success(1); ``` +#### Recover from failure + +```csharp +Result failed = new NotImplementedException(); + +Result result = failed.TryRecover(err => err.Type == "System.NotImplementedException" + ? "recovered" + : err); +// result with value: "recovered" +``` + ### Try ```csharp @@ -99,9 +110,9 @@ int SomeFunction() => 1; ### Ensure ```csharp -var value = GetValue(); -Result result = Ensure.That(value) - .NotNull() +int? value = GetValue(); +Result result = Ensure.That(value) // -> Ensure + .NotNull() // -> Ensure .Satisfies(i => i < 100) .Result(); diff --git a/Railway.SourceGenerator/EnsureExtensionsExecutor.cs b/Railway.SourceGenerator/EnsureExtensionsExecutor.cs index 292a633..4fbe65a 100644 --- a/Railway.SourceGenerator/EnsureExtensionsExecutor.cs +++ b/Railway.SourceGenerator/EnsureExtensionsExecutor.cs @@ -41,22 +41,46 @@ public sealed class EnsureExtensionsExecutor : IGeneratorExecutor var sb = new StringBuilder(); - sb.AppendLine($"#region Satisfies"); + sb.AppendLine("#region Satisfies"); errorGenerationDefinitions.ForEach(def => GenerateSatisfiesExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr)); sb.AppendLine("#endregion"); - sb.AppendLine($"#region NotNull"); + sb.AppendLine("#region NotNull"); errorGenerationDefinitions.ForEach(def => GenerateNotNullExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr)); sb.AppendLine("#endregion"); - sb.AppendLine($"#region NotEmpty"); + sb.AppendLine("#region NotEmpty"); errorGenerationDefinitions.ForEach(def => GenerateNotEmptyExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr)); sb.AppendLine("#endregion"); + sb.AppendLine("#region NotWhitespace"); + errorGenerationDefinitions.ForEach(def => GenerateNotWhitespaceExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr)); + sb.AppendLine("#endregion"); return sb.ToString(); } + private void GenerateNotWhitespaceExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr) + { + string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is empty or consists exclusively of white-space characters.\")"; + + sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")] + public static Ensure NotWhitespace(this in Ensure ensure, {{errorParameterDecl}}) + { + return ensure.State switch + { + ResultState.Success => string.IsNullOrWhiteSpace(ensure.Value) + ? new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression) + : new(ensure.Value!, ensure.ValueExpression), + ResultState.Error => new(ensure.Error!, ensure.ValueExpression), + _ => throw new EnsureNotInitializedException(nameof(ensure)) + }; + } + """); + } + private void GenerateNotEmptyExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr) { string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is empty.\")"; diff --git a/Railway.SourceGenerator/ExtensionsMethodGenerator.cs b/Railway.SourceGenerator/ExtensionsMethodGenerator.cs index b775928..f626b06 100644 --- a/Railway.SourceGenerator/ExtensionsMethodGenerator.cs +++ b/Railway.SourceGenerator/ExtensionsMethodGenerator.cs @@ -17,6 +17,7 @@ public class ExtensionsMethodGenerator : IIncrementalGenerator new ResultMapExecutor(), new ResultBindExecutor(), new ResultTapExecutor(), + new ResultTryRecoverExecutor(), new ResultAppendExecutor(), new TryExtensionsExecutor(), new EnsureExtensionsExecutor(), diff --git a/Railway.SourceGenerator/ResultTryRecoverExecutor.cs b/Railway.SourceGenerator/ResultTryRecoverExecutor.cs new file mode 100644 index 0000000..2c68676 --- /dev/null +++ b/Railway.SourceGenerator/ResultTryRecoverExecutor.cs @@ -0,0 +1,86 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Just.Railway.SourceGen; + +internal sealed class ResultTryRecoverExecutor : ResultExtensionsExecutor +{ + protected override string ExtensionType => "TryRecover"; + protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) + { + if (argCount > 1) return; + + var templateArgNames = Enumerable.Repeat("T", argCount) + .ToImmutableArray(); + + string methodTemplateDecl = GenerateTemplateDecl(templateArgNames); + string resultTypeDef = GenerateResultTypeDef(templateArgNames); + + sb.AppendLine($"#region {resultTypeDef}"); + + sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(ResultTryRecoverExecutor)}}", "1.0.0.0")] + public static {{resultTypeDef}} TryRecover{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func recover) + { + return result.State switch + { + ResultState.Success => ({{resultTypeDef}})result.Value, + ResultState.Error => recover(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + """); + + GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, methodTemplateDecl); + GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, methodTemplateDecl); + + sb.AppendLine("#endregion"); + } + private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string methodTemplateDecl) + { + sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(ResultTryRecoverExecutor)}}", "1.0.0.0")] + public static async {{taskType}}<{{resultTypeDef}}> TryRecover{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func recover) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => ({{resultTypeDef}})result.Value, + ResultState.Error => recover(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + """); + sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(ResultTryRecoverExecutor)}}", "1.0.0.0")] + public static async {{taskType}}<{{resultTypeDef}}> TryRecover{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func> recover) + { + return result.State switch + { + ResultState.Success => ({{resultTypeDef}})result.Value, + ResultState.Error => await recover(result.Error!).ConfigureAwait(false), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + """); + sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(ResultTryRecoverExecutor)}}", "1.0.0.0")] + public static async {{taskType}}<{{resultTypeDef}}> TryRecover{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func> recover) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => ({{resultTypeDef}})result.Value, + ResultState.Error => await recover(result.Error!).ConfigureAwait(false), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + """); + } +} diff --git a/Railway/Ensure.cs b/Railway/Ensure.cs index 2d7c171..5c725b8 100644 --- a/Railway/Ensure.cs +++ b/Railway/Ensure.cs @@ -34,18 +34,6 @@ public static partial class Ensure _ => throw new EnsureNotInitializedException(nameof(ensureTask)) }; } - - [Pure] public static Ensure NotWhitespace(this in Ensure ensure, Error error = default!) - { - return ensure.State switch - { - ResultState.Success => string.IsNullOrWhiteSpace(ensure.Value) - ? new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty or consists exclusively of white-space characters."), ensure.ValueExpression) - : new(ensure.Value, ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } } public readonly struct Ensure diff --git a/Railway/Result.cs b/Railway/Result.cs index aa2ae89..7dbcb4e 100644 --- a/Railway/Result.cs +++ b/Railway/Result.cs @@ -8,6 +8,7 @@ internal enum ResultState : byte public readonly partial struct Result : IEquatable { + internal SuccessUnit Value => new(); internal readonly Error? Error; internal readonly ResultState State; @@ -51,13 +52,18 @@ public readonly partial struct Result : IEquatable [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Result(Error error) => new(error ?? throw new ArgumentNullException(nameof(error))); [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Result(Exception exception) => new( + new ExceptionalError(exception ?? throw new ArgumentNullException(nameof(exception)))); + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Result(Result result) => result.State switch { ResultState.Success => new(new SuccessUnit()), ResultState.Error => new(result.Error!), _ => throw new ResultNotInitializedException(nameof(result)) }; - + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Result(SuccessUnit _) => new(null); + [Pure] public bool IsSuccess => Error is null; [Pure] public bool IsFailure => Error is not null; @@ -139,14 +145,20 @@ public readonly struct Result : IEquatable> Error = default; } - [Pure] public static explicit operator Result(Result result) => result.State switch + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Result(Result result) => result.State switch { ResultState.Success => new(null), ResultState.Error => new(result.Error!), _ => throw new ResultNotInitializedException(nameof(result)) }; - [Pure] public static implicit operator Result(Error error) => new(error); - [Pure] public static implicit operator Result(T value) => new(value); + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Result(Error error) => new(error); + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Result(Exception exception) => new( + new ExceptionalError(exception ?? throw new ArgumentNullException(nameof(exception)))); + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Result(T value) => new(value); [Pure] public bool IsSuccess => State == ResultState.Success; [Pure] public bool IsFailure => State == ResultState.Error; diff --git a/Raliway.Tests/Results/GeneralUsage.cs b/Raliway.Tests/Results/GeneralUsage.cs index 789da36..6784ce0 100644 --- a/Raliway.Tests/Results/GeneralUsage.cs +++ b/Raliway.Tests/Results/GeneralUsage.cs @@ -128,4 +128,33 @@ public class GeneralUsage // Then Assert.Equal("satisfied", result); } + + [Fact] + public void RecoverResultFromFailureState() + { + // Given + Result failed = new NotImplementedException(); + // When + var result = failed.TryRecover(err => + { + Assert.IsType(err.ToException()); + return "recovered"; + }); + // Then + Assert.True(result.IsSuccess); + Assert.Equal("recovered", result.Value); + } + + [Fact] + public void WhenCanNotRecoverResultFromFailureState() + { + // Given + var error = Error.New("test"); + Result failed = new NotImplementedException(); + // When + var result = failed.TryRecover(err => error); + // Then + Assert.True(result.IsFailure); + Assert.Equal(error, result.Error); + } }