From b8ea74ec5b5dab2da755edd8ed242ea8db280f82 Mon Sep 17 00:00:00 2001 From: JustFixMe Date: Tue, 12 Dec 2023 19:02:11 +0400 Subject: [PATCH] added missing Append extensions --- README.md | 106 ++++++++++++- ...xecutor.cs => EnsureExtensionsExecutor.cs} | 16 +- .../ExtensionsMethodGenerator.cs | 2 +- .../ResultAppendExecutor.cs | 42 +++++ Raliway.Tests/Results/Combine.cs | 78 ++++++++++ Raliway.Tests/Results/GeneralUsage.cs | 143 ++++++++---------- 6 files changed, 299 insertions(+), 88 deletions(-) rename Railway.SourceGenerator/{EnsureExtensionExecutor.cs => EnsureExtensionsExecutor.cs} (93%) create mode 100644 Raliway.Tests/Results/Combine.cs diff --git a/README.md b/README.md index 43f2278..0f3ed4c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,108 @@ This library uses features of C# to achieve railway-oriented programming. The desire is to make somewhat user-friendly experience while using result-object pattern. -## Contents +## Features -_Coming soon..._ +- Immutable ```Error``` class +- ```Result``` object +- A bunch of extensions to use result-object pattern with +- ```Try``` extensions to wrap function calls with result-object +- ```Ensure``` extensions to utilize result-object in validation scenarios + +## Getting Started + +### Install from local Gitea package repository + +```sh +# Setup NuGet registry from the command line +dotnet nuget add source --name gitea_jstdev https://gitea.jstdev.ru/api/packages/just/nuget/index.json +# then install the package using NuGet +dotnet add package --source gitea_jstdev Just.Railway +``` + +## Examples + +### Error + +```csharp +using Just.Railway; +Error expectedError = Error.New(type: "Some Error", message: "Some error detail"); +Error exceptionalError = Error.New(new Exception("Some Exception")); +Error manyErrors = Error.Many(expectedError, exceptionalError); +// the same result while using .Append(..) or + +manyErrors = expectedError.Append(exceptionalError); +manyErrors = expectedError + exceptionalError; +``` + +> **Note** +> You can easily serialize/deserialize Error to and from JSON + +### Result + +#### As return value: + +```csharp +Result Foo() +{ + // ... + if (SomeCondition()) + return Result.Failure(Error.New("Some Error")); + // or just: return Error.New("Some Error"); + // ... + return Result.Success(); +} + +Result Bar() +{ + T value; + // ... + if (SomeCondition()) + return Error.New("Some Error"); + // ... + return value; +} +``` + +#### Consume Result object + +```csharp +Result result = GetResult(); + +var value = result + .Append("new") + .Map((i, s) => $"{s} result {i}") + .Match( + onSuccess: x => x, + onFailure: err => err.ToString() + ); +// value: "new result 1" + +Result GetResult() => Result.Success(1); +``` + +### Try + +```csharp +Result result = Try.Run(SomeAction); +// you can pass up to 5 arguments like this +result = Try.Run(SomeActionWithArguments, 1, 2.0, "3"); + +// you also can call functions +Result resultWithValue = Try.Run(SomeFunction); + +void SomeAction() {} +void SomeActionWithArguments(int a1, double a2, string? a3) {} +int SomeFunction() => 1; +``` + +### Ensure + +```csharp +var value = GetValue(); +Result result = Ensure.That(value) + .NotNull() + .Satisfies(i => i < 100) + .Result(); + +int? GetValue() => 1; +``` diff --git a/Railway.SourceGenerator/EnsureExtensionExecutor.cs b/Railway.SourceGenerator/EnsureExtensionsExecutor.cs similarity index 93% rename from Railway.SourceGenerator/EnsureExtensionExecutor.cs rename to Railway.SourceGenerator/EnsureExtensionsExecutor.cs index ce4c9c4..292a633 100644 --- a/Railway.SourceGenerator/EnsureExtensionExecutor.cs +++ b/Railway.SourceGenerator/EnsureExtensionsExecutor.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis; namespace Just.Railway.SourceGen; -public sealed class EnsureExtensionExecutor : IGeneratorExecutor +public sealed class EnsureExtensionsExecutor : IGeneratorExecutor { public void Execute(SourceProductionContext context, Compilation source) { @@ -80,7 +80,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor typeOverloads.ForEach(def => sb.AppendLine($$""" [PureAttribute] - [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")] public static Ensure<{{def.CollectionType}}> NotEmpty{{def.TemplateDef}}(this in Ensure<{{def.CollectionType}}> ensure, {{errorParameterDecl}}) { return ensure.State switch @@ -101,7 +101,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor sb.AppendLine($$""" [PureAttribute] - [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")] public static Ensure NotNull(this in Ensure ensure, {{errorParameterDecl}}) where T : struct { @@ -117,7 +117,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor """); sb.AppendLine($$""" [PureAttribute] - [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")] public static Ensure NotNull(this in Ensure ensure, {{errorParameterDecl}}) where T : notnull { @@ -138,7 +138,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} does not satisfy the requirement.\")"; sb.AppendLine($$""" [PureAttribute] - [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")] public static Ensure Satisfies(this in Ensure ensure, Func requirement, {{errorParameterDecl}}) { return ensure.State switch @@ -160,7 +160,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor { sb.AppendLine($$""" [PureAttribute] - [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")] public static async {{taskType}}> Satisfies(this {{taskType}}> ensureTask, Func requirement, {{errorParameterDecl}}) { var ensure = await ensureTask.ConfigureAwait(false); @@ -176,7 +176,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor """); sb.AppendLine($$""" [PureAttribute] - [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")] public static async {{taskType}}> Satisfies(this Ensure ensure, Func> requirement, {{errorParameterDecl}}) { return ensure.State switch @@ -191,7 +191,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor """); sb.AppendLine($$""" [PureAttribute] - [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")] public static async {{taskType}}> Satisfies(this {{taskType}}> ensureTask, Func> requirement, {{errorParameterDecl}}) { var ensure = await ensureTask.ConfigureAwait(false); diff --git a/Railway.SourceGenerator/ExtensionsMethodGenerator.cs b/Railway.SourceGenerator/ExtensionsMethodGenerator.cs index 32bb4e1..b775928 100644 --- a/Railway.SourceGenerator/ExtensionsMethodGenerator.cs +++ b/Railway.SourceGenerator/ExtensionsMethodGenerator.cs @@ -19,7 +19,7 @@ public class ExtensionsMethodGenerator : IIncrementalGenerator new ResultTapExecutor(), new ResultAppendExecutor(), new TryExtensionsExecutor(), - new EnsureExtensionExecutor(), + new EnsureExtensionsExecutor(), }; public void Initialize(IncrementalGeneratorInitializationContext context) diff --git a/Railway.SourceGenerator/ResultAppendExecutor.cs b/Railway.SourceGenerator/ResultAppendExecutor.cs index 69aa3c9..73a294b 100644 --- a/Railway.SourceGenerator/ResultAppendExecutor.cs +++ b/Railway.SourceGenerator/ResultAppendExecutor.cs @@ -200,6 +200,48 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames); string methodExpandedTemplateDecl = GenerateTemplateDecl(expandedTemplateArgNames); + sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] + public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, TNext next) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => Result.Success({{JoinArguments(resultValueExpansion, "next")}}), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + """); + + sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] + public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Result next) + { + var result = await resultTask.ConfigureAwait(false); + if ((result.State & next.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State))); + } + + Error? error = null; + if (result.IsFailure) + { + error += result.Error; + } + if (next.IsFailure) + { + error += next.Error; + } + + return error is null + ? Result.Success({{JoinArguments(resultValueExpansion, "next.Value")}}) + : error; + } + """); + sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] diff --git a/Raliway.Tests/Results/Combine.cs b/Raliway.Tests/Results/Combine.cs new file mode 100644 index 0000000..4c25450 --- /dev/null +++ b/Raliway.Tests/Results/Combine.cs @@ -0,0 +1,78 @@ +namespace Raliway.Tests.Results; + +public class Combine +{ + [Fact] + public void TwoResultCombination_WhenThereIsAnError() + { + // Given + var result1 = Result.Success(1); + var result2 = Result.Failure(Error.New("some error")); + // When + var result = Result.Combine(result1, result2); + // Then + Assert.True(result.IsFailure); + Assert.Equal(result2.Error, result.Error); + } + [Fact] + public void TwoResultCombination_WhenThereAreTwoErrors() + { + // Given + var result1 = Result.Failure(Error.New("1")); + var result2 = Result.Failure(Error.New("2")); + // When + var result = Result.Combine(result1, result2); + // Then + Assert.True(result.IsFailure); + Assert.Equal(result1.Error + result2.Error, result.Error); + } + [Fact] + public void TwoResultCombination_WhenThereIsNoError() + { + // Given + var result1 = Result.Success(1); + var result2 = Result.Success(3.14); + // When + var result = Result.Combine(result1, result2); + // Then + Assert.True(result.IsSuccess); + } + [Fact] + public void ThreeResultCombination_WhenThereIsAnError() + { + // Given + var result1 = Result.Success(1); + var result2 = Result.Success(3.14); + var result3 = Result.Failure(Error.New("some error")); + // When + Result<(int, double)> result = Result.Combine(result1, result2, result3); + // Then + Assert.True(result.IsFailure); + Assert.Equal(result3.Error, result.Error); + } + [Fact] + public void ThreeResultCombination_WhenThereAreTwoErrors() + { + // Given + var result1 = Result.Failure(Error.New("1")); + var result2 = Result.Success(3.14); + var result3 = Result.Failure(Error.New("3")); + // When + Result<(int?, double)> result = Result.Combine(result1, result2, result3); + // Then + Assert.True(result.IsFailure); + Assert.Equal(result1.Error + result3.Error, result.Error); + } + [Fact] + public void ThreeResultCombination_WhenThereIsNoError() + { + // Given + var result1 = Result.Success(1); + var result2 = Result.Success(3.14); + var result3 = Result.Success(); + // When + var result = Result.Combine(result1, result2, result3); + // Then + Assert.True(result.IsSuccess); + } +} diff --git a/Raliway.Tests/Results/GeneralUsage.cs b/Raliway.Tests/Results/GeneralUsage.cs index ecbab17..789da36 100644 --- a/Raliway.Tests/Results/GeneralUsage.cs +++ b/Raliway.Tests/Results/GeneralUsage.cs @@ -2,80 +2,6 @@ namespace Raliway.Tests.Results; public class GeneralUsage { - [Fact] - public void TwoResultCombination_WhenThereIsAnError() - { - // Given - var result1 = Result.Success(1); - var result2 = Result.Failure(Error.New("some error")); - // When - var result = Result.Combine(result1, result2); - // Then - Assert.True(result.IsFailure); - Assert.Equal(result2.Error, result.Error); - } - [Fact] - public void TwoResultCombination_WhenThereAreTwoErrors() - { - // Given - var result1 = Result.Failure(Error.New("1")); - var result2 = Result.Failure(Error.New("2")); - // When - var result = Result.Combine(result1, result2); - // Then - Assert.True(result.IsFailure); - Assert.Equal(result1.Error + result2.Error, result.Error); - } - [Fact] - public void TwoResultCombination_WhenThereIsNoError() - { - // Given - var result1 = Result.Success(1); - var result2 = Result.Success(3.14); - // When - var result = Result.Combine(result1, result2); - // Then - Assert.True(result.IsSuccess); - } - [Fact] - public void ThreeResultCombination_WhenThereIsAnError() - { - // Given - var result1 = Result.Success(1); - var result2 = Result.Success(3.14); - var result3 = Result.Failure(Error.New("some error")); - // When - Result<(int, double)> result = Result.Combine(result1, result2, result3); - // Then - Assert.True(result.IsFailure); - Assert.Equal(result3.Error, result.Error); - } - [Fact] - public void ThreeResultCombination_WhenThereAreTwoErrors() - { - // Given - var result1 = Result.Failure(Error.New("1")); - var result2 = Result.Success(3.14); - var result3 = Result.Failure(Error.New("3")); - // When - Result<(int?, double)> result = Result.Combine(result1, result2, result3); - // Then - Assert.True(result.IsFailure); - Assert.Equal(result1.Error + result3.Error, result.Error); - } - [Fact] - public void ThreeResultCombination_WhenThereIsNoError() - { - // Given - var result1 = Result.Success(1); - var result2 = Result.Success(3.14); - var result3 = Result.Success(); - // When - var result = Result.Combine(result1, result2, result3); - // Then - Assert.True(result.IsSuccess); - } - [Fact] public void ChainedResultExtensions_WhenThereIsNoError() { @@ -104,10 +30,9 @@ public class GeneralUsage public void ChainedResultExtensions_WhenThereIsAnError() { // Given + var error = Error.New("test"); // When - var error = Error.New("test"); - var result = Result.Success() .Append(() => Result.Failure(error)) @@ -115,7 +40,7 @@ public class GeneralUsage .Map((i, s) => { Assert.Fail(); - return Result.Success(""); + return ""; }) .Append("some") .Bind((s1, s2) => @@ -139,4 +64,68 @@ public class GeneralUsage // Then Assert.Equal("satisfied", result); } + + [Fact] + public async Task ChainedResultAsyncExtensions_WhenThereIsNoError() + { + // Given + + // When + var result = await Result.Success() + .Append(() => ValueTask.FromResult(Result.Success(1))) + .Append("test") + .Map((i, s) => $"{s}_{i}") + .Append("some") + .Bind(async (s1, s2) => await ValueTask.FromResult(Result.Success(string.Join(';', s1, s2)))) + .Match( + onSuccess: s => s.ToUpper(), + onFailure: _ => + { + Assert.Fail(); + return ""; + } + ); + + Assert.Equal("TEST_1;SOME", result); + } + + [Fact] + public async Task ChainedResultAsyncExtensions_WhenThereIsAnError() + { + // Given + var error = Error.New("test"); + + // When + + var result = await Result.Success() + .Append(() => Task.FromResult(Result.Failure(error))) + .Append("test") + .Map((i, s) => + { + Assert.Fail(); + return ""; + }) + .Append("some") + .Bind(async (s1, s2) => + { + Assert.Fail(); + await Task.CompletedTask; + return Result.Success(""); + }) + .Match( + onSuccess: _ => + { + Assert.Fail(); + return ""; + }, + onFailure: err => + { + Assert.Equal(error, err); + return "satisfied"; + } + ); + + // Then + Assert.Equal("satisfied", result); + } }