From 0e7986996fb16aa405175adef5abdda2c0db6480 Mon Sep 17 00:00:00 2001 From: just Date: Mon, 13 Jul 2026 22:20:47 +0400 Subject: [PATCH] extensions rework pt1 --- .../EnsureExtensionsExecutor.cs | 4 +- .../MainExtensions}/ResultAppendExecutor.cs | 12 +- .../MainExtensions}/ResultBindExecutor.cs | 9 +- .../MainExtensions}/ResultCombineExecutor.cs | 4 +- .../MainExtensions}/ResultExtendExecutor.cs | 9 +- .../MainExtensions}/ResultMapExecutor.cs | 11 +- .../MainExtensions}/ResultMatchExecutor.cs | 9 +- .../MainExtensions}/ResultTapExecutor.cs | 13 +- .../ResultTryRecoverExecutor.cs | 13 +- .../ResultExtensionsExecutor.cs | 12 +- .../ResultValueTaskAppendExecutor.cs | 47 ++ .../ResultValueTaskBindExecutor.cs | 29 + .../ResultValueTaskExtendExecutor.cs | 35 ++ .../ResultValueTaskMapExecutor.cs | 28 + .../ResultValueTaskMatchExecutor.cs | 29 + .../ResultValueTaskTapExecutor.cs | 29 + .../ResultValueTaskTryRecoverExecutor.cs | 30 + .../{ => Executors}/TryExtensionsExecutor.cs | 18 +- .../ExtensionsMethodGenerator.cs | 21 +- Railway.SourceGenerator/IGeneratorExecutor.cs | 2 +- Railway/Error.cs | 6 +- Railway/ReflectionHelper.cs | 26 - Railway/Result.cs | 8 +- Railway/ResultExtensions.cs | 211 ++++--- Railway/Try.cs | 4 + .../ResultValueTaskExtensions.cs | 98 +++ .../ReflectionHelper/ReflectionHelperTests.cs | 173 ++++++ Raliway.Tests/Results/Bugs.cs | 185 ++++++ Raliway.Tests/Results/GeneralUsage.cs | 3 +- Raliway.Tests/TryExtensions/Run.cs | 558 ++++++++++++++++++ 30 files changed, 1476 insertions(+), 160 deletions(-) rename Railway.SourceGenerator/{ => Executors}/EnsureExtensionsExecutor.cs (99%) rename Railway.SourceGenerator/{ => Executors/ResultExtensions/MainExtensions}/ResultAppendExecutor.cs (95%) rename Railway.SourceGenerator/{ => Executors/ResultExtensions/MainExtensions}/ResultBindExecutor.cs (89%) rename Railway.SourceGenerator/{ => Executors/ResultExtensions/MainExtensions}/ResultCombineExecutor.cs (97%) rename Railway.SourceGenerator/{ => Executors/ResultExtensions/MainExtensions}/ResultExtendExecutor.cs (92%) rename Railway.SourceGenerator/{ => Executors/ResultExtensions/MainExtensions}/ResultMapExecutor.cs (88%) rename Railway.SourceGenerator/{ => Executors/ResultExtensions/MainExtensions}/ResultMatchExecutor.cs (89%) rename Railway.SourceGenerator/{ => Executors/ResultExtensions/MainExtensions}/ResultTapExecutor.cs (87%) rename Railway.SourceGenerator/{ => Executors/ResultExtensions/MainExtensions}/ResultTryRecoverExecutor.cs (88%) rename Railway.SourceGenerator/{ => Executors/ResultExtensions}/ResultExtensionsExecutor.cs (85%) create mode 100644 Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskAppendExecutor.cs create mode 100644 Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskBindExecutor.cs create mode 100644 Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskExtendExecutor.cs create mode 100644 Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskMapExecutor.cs create mode 100644 Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskMatchExecutor.cs create mode 100644 Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskTapExecutor.cs create mode 100644 Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskTryRecoverExecutor.cs rename Railway.SourceGenerator/{ => Executors}/TryExtensionsExecutor.cs (87%) create mode 100644 Railway/ValueTaskExtensions/ResultValueTaskExtensions.cs create mode 100644 Raliway.Tests/ReflectionHelper/ReflectionHelperTests.cs create mode 100644 Raliway.Tests/Results/Bugs.cs create mode 100644 Raliway.Tests/TryExtensions/Run.cs diff --git a/Railway.SourceGenerator/EnsureExtensionsExecutor.cs b/Railway.SourceGenerator/Executors/EnsureExtensionsExecutor.cs similarity index 99% rename from Railway.SourceGenerator/EnsureExtensionsExecutor.cs rename to Railway.SourceGenerator/Executors/EnsureExtensionsExecutor.cs index b49a299..845a5ff 100644 --- a/Railway.SourceGenerator/EnsureExtensionsExecutor.cs +++ b/Railway.SourceGenerator/Executors/EnsureExtensionsExecutor.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using System.Text; using Microsoft.CodeAnalysis; -namespace Just.Railway.SourceGen; +namespace Just.Railway.SourceGen.Executors; public sealed class EnsureExtensionsExecutor : IGeneratorExecutor { - public void Execute(SourceProductionContext context, Compilation source) + public void Execute(IncrementalGeneratorPostInitializationContext context) { var methods = GenerateMethods(); var code = $$""" diff --git a/Railway.SourceGenerator/ResultAppendExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultAppendExecutor.cs similarity index 95% rename from Railway.SourceGenerator/ResultAppendExecutor.cs rename to Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultAppendExecutor.cs index 792d151..0a50d76 100644 --- a/Railway.SourceGenerator/ResultAppendExecutor.cs +++ b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultAppendExecutor.cs @@ -3,11 +3,13 @@ using System.Linq; using System.Text; using Microsoft.CodeAnalysis; -namespace Just.Railway.SourceGen; +namespace Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; -internal sealed class ResultAppendExecutor : ResultExtensionsExecutor +internal class ResultAppendExecutor : ResultExtensionsExecutor { protected override string ExtensionType => "Append"; + protected override string NamespaceName => "Just.Railway"; + protected override string PartialClassName => "ResultExtensions"; protected override void GenerateHelperMethods(StringBuilder sb) { @@ -89,19 +91,17 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor """); GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); if (argCount < Constants.MaxResultTupleSize) { GenerateExpandedMethods(sb, templateArgNames, resultTypeDef, resultValueExpansion); GenerateExpandedAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateExpandedAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); } sb.AppendLine("#endregion"); } - private void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { string methodTemplateDecl = GenerateTemplateDecl(templateArgNames); @@ -192,7 +192,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor """); } - private void GenerateExpandedAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + protected static void GenerateExpandedAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { var expandedTemplateArgNames = templateArgNames.Add("TNext"); string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames); diff --git a/Railway.SourceGenerator/ResultBindExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultBindExecutor.cs similarity index 89% rename from Railway.SourceGenerator/ResultBindExecutor.cs rename to Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultBindExecutor.cs index c38d8cc..47c0d99 100644 --- a/Railway.SourceGenerator/ResultBindExecutor.cs +++ b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultBindExecutor.cs @@ -3,11 +3,13 @@ using System.Linq; using System.Text; using Microsoft.CodeAnalysis; -namespace Just.Railway.SourceGen; +namespace Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; -internal sealed class ResultBindExecutor : ResultExtensionsExecutor +internal class ResultBindExecutor : ResultExtensionsExecutor { protected override string ExtensionType => "Bind"; + protected override string NamespaceName => "Just.Railway"; + protected override string PartialClassName => "ResultExtensions"; protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) { @@ -37,12 +39,11 @@ internal sealed class ResultBindExecutor : ResultExtensionsExecutor """); GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { string methodTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("R")); string bindTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result")); diff --git a/Railway.SourceGenerator/ResultCombineExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultCombineExecutor.cs similarity index 97% rename from Railway.SourceGenerator/ResultCombineExecutor.cs rename to Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultCombineExecutor.cs index 2b53d20..1a0fcfe 100644 --- a/Railway.SourceGenerator/ResultCombineExecutor.cs +++ b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultCombineExecutor.cs @@ -5,11 +5,11 @@ using System.Linq; using System.Text; using Microsoft.CodeAnalysis; -namespace Just.Railway.SourceGen; +namespace Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; internal sealed class ResultCombineExecutor : IGeneratorExecutor { - public void Execute(SourceProductionContext context, Compilation source) + public void Execute(IncrementalGeneratorPostInitializationContext context) { var methods = GenerateCombineMethods(); var code = $$""" diff --git a/Railway.SourceGenerator/ResultExtendExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultExtendExecutor.cs similarity index 92% rename from Railway.SourceGenerator/ResultExtendExecutor.cs rename to Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultExtendExecutor.cs index bee095e..901fde2 100644 --- a/Railway.SourceGenerator/ResultExtendExecutor.cs +++ b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultExtendExecutor.cs @@ -3,11 +3,13 @@ using System.Linq; using System.Text; using Microsoft.CodeAnalysis; -namespace Just.Railway.SourceGen; +namespace Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; -internal sealed class ResultExtendExecutor : ResultExtensionsExecutor +internal class ResultExtendExecutor : ResultExtensionsExecutor { protected override string ExtensionType => "Extend"; + protected override string NamespaceName => "Just.Railway"; + protected override string PartialClassName => "ResultExtensions"; protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) { @@ -58,12 +60,11 @@ internal sealed class ResultExtendExecutor : ResultExtensionsExecutor """); GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { var expandedTemplateArgNames = templateArgNames.Add("R"); string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames); diff --git a/Railway.SourceGenerator/ResultMapExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultMapExecutor.cs similarity index 88% rename from Railway.SourceGenerator/ResultMapExecutor.cs rename to Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultMapExecutor.cs index abd50be..6056cdc 100644 --- a/Railway.SourceGenerator/ResultMapExecutor.cs +++ b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultMapExecutor.cs @@ -1,13 +1,17 @@ using System.Collections.Immutable; using System.Linq; using System.Text; +using Just.Railway.SourceGen.Executors.ResultExtensions; using Microsoft.CodeAnalysis; -namespace Just.Railway.SourceGen; +namespace Just.Railway.SourceGen.Executors; -internal sealed class ResultMapExecutor : ResultExtensionsExecutor +internal class ResultMapExecutor : ResultExtensionsExecutor { protected override string ExtensionType => "Map"; + protected override string NamespaceName => "Just.Railway"; + protected override string PartialClassName => "ResultExtensions"; + protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) { var templateArgNames = Enumerable.Range(1, argCount) @@ -35,12 +39,11 @@ internal sealed class ResultMapExecutor : ResultExtensionsExecutor """); GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { var methodTemplateArgNames = templateArgNames.Add("R"); string methodTemplateDecl = GenerateTemplateDecl(methodTemplateArgNames); diff --git a/Railway.SourceGenerator/ResultMatchExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultMatchExecutor.cs similarity index 89% rename from Railway.SourceGenerator/ResultMatchExecutor.cs rename to Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultMatchExecutor.cs index bbe26fd..90152ba 100644 --- a/Railway.SourceGenerator/ResultMatchExecutor.cs +++ b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultMatchExecutor.cs @@ -3,11 +3,13 @@ using System.Linq; using System.Text; using Microsoft.CodeAnalysis; -namespace Just.Railway.SourceGen; +namespace Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; -internal sealed class ResultMatchExecutor : ResultExtensionsExecutor +internal class ResultMatchExecutor : ResultExtensionsExecutor { protected override string ExtensionType => "Match"; + protected override string NamespaceName => "Just.Railway"; + protected override string PartialClassName => "ResultExtensions"; protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) { @@ -36,12 +38,11 @@ internal sealed class ResultMatchExecutor : ResultExtensionsExecutor """); GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { var methodTemplateArgNames = templateArgNames.Add("R"); string methodTemplateDecl = GenerateTemplateDecl(methodTemplateArgNames); diff --git a/Railway.SourceGenerator/ResultTapExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultTapExecutor.cs similarity index 87% rename from Railway.SourceGenerator/ResultTapExecutor.cs rename to Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultTapExecutor.cs index cdb5139..9e0f4a5 100644 --- a/Railway.SourceGenerator/ResultTapExecutor.cs +++ b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultTapExecutor.cs @@ -3,11 +3,13 @@ using System.Linq; using System.Text; using Microsoft.CodeAnalysis; -namespace Just.Railway.SourceGen; +namespace Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; -internal sealed class ResultTapExecutor : ResultExtensionsExecutor +internal class ResultTapExecutor : ResultExtensionsExecutor { protected override string ExtensionType => "Tap"; + protected override string NamespaceName => "Just.Railway"; + protected override string PartialClassName => "ResultExtensions"; protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) { @@ -24,7 +26,7 @@ internal sealed class ResultTapExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultTapExecutor)}}", "1.0.0.0")] - public static ref readonly {{resultTypeDef}} Tap{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Action{{methodTemplateDecl}}? onSuccess = null, Action? onFailure = null) + public static {{resultTypeDef}} Tap{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Action{{methodTemplateDecl}}? onSuccess = null, Action? onFailure = null) { switch (result.State) { @@ -37,17 +39,16 @@ internal sealed class ResultTapExecutor : ResultExtensionsExecutor default: throw new ResultNotInitializedException(nameof(result)); } - return ref result; + return result; } """); GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { string methodTemplateDecl = GenerateTemplateDecl(templateArgNames); string asyncActionTemplateDecl = GenerateTemplateDecl(templateArgNames.Add(taskType)); diff --git a/Railway.SourceGenerator/ResultTryRecoverExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultTryRecoverExecutor.cs similarity index 88% rename from Railway.SourceGenerator/ResultTryRecoverExecutor.cs rename to Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultTryRecoverExecutor.cs index 6db3ae2..eff623e 100644 --- a/Railway.SourceGenerator/ResultTryRecoverExecutor.cs +++ b/Railway.SourceGenerator/Executors/ResultExtensions/MainExtensions/ResultTryRecoverExecutor.cs @@ -2,15 +2,18 @@ using System.Collections.Immutable; using System.Linq; using System.Text; -namespace Just.Railway.SourceGen; +namespace Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; -internal sealed class ResultTryRecoverExecutor : ResultExtensionsExecutor +internal class ResultTryRecoverExecutor : ResultExtensionsExecutor { protected override string ExtensionType => "TryRecover"; + protected override string NamespaceName => "Just.Railway"; + protected override string PartialClassName => "ResultExtensions"; + protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) { if (argCount > 1) return; - + var templateArgNames = Enumerable.Repeat("T", argCount) .ToImmutableArray(); @@ -34,11 +37,11 @@ internal sealed class ResultTryRecoverExecutor : ResultExtensionsExecutor """); GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, methodTemplateDecl); - GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, methodTemplateDecl); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string methodTemplateDecl) + + protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string methodTemplateDecl) { sb.AppendLine($$""" [PureAttribute] diff --git a/Railway.SourceGenerator/ResultExtensionsExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/ResultExtensionsExecutor.cs similarity index 85% rename from Railway.SourceGenerator/ResultExtensionsExecutor.cs rename to Railway.SourceGenerator/Executors/ResultExtensions/ResultExtensionsExecutor.cs index c96656d..8cac68c 100644 --- a/Railway.SourceGenerator/ResultExtensionsExecutor.cs +++ b/Railway.SourceGenerator/Executors/ResultExtensions/ResultExtensionsExecutor.cs @@ -2,11 +2,11 @@ using System.Collections.Immutable; using System.Text; using Microsoft.CodeAnalysis; -namespace Just.Railway.SourceGen; +namespace Just.Railway.SourceGen.Executors.ResultExtensions; internal abstract class ResultExtensionsExecutor : IGeneratorExecutor { - public void Execute(SourceProductionContext context, Compilation source) + public void Execute(IncrementalGeneratorPostInitializationContext context) { var methods = GenerateMethods(); var code = $$""" @@ -16,15 +16,15 @@ internal abstract class ResultExtensionsExecutor : IGeneratorExecutor using System.Diagnostics.Contracts; using System.CodeDom.Compiler; - namespace Just.Railway; + namespace {{NamespaceName}}; - public static partial class ResultExtensions + public static partial class {{PartialClassName}} { {{methods}} } """; - context.AddSource($"ResultExtensions.{ExtensionType}.g.cs", code); + context.AddSource($"{PartialClassName}.{ExtensionType}.g.cs", code); } private string GenerateMethods() @@ -87,6 +87,8 @@ internal abstract class ResultExtensionsExecutor : IGeneratorExecutor } protected abstract string ExtensionType { get; } + protected abstract string NamespaceName { get; } + protected abstract string PartialClassName { get; } protected abstract void GenerateMethodsForArgCount(StringBuilder sb, int argCount); protected virtual void GenerateHelperMethods(StringBuilder sb) {} } diff --git a/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskAppendExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskAppendExecutor.cs new file mode 100644 index 0000000..b5e7308 --- /dev/null +++ b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskAppendExecutor.cs @@ -0,0 +1,47 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; +using Microsoft.CodeAnalysis; + +namespace Just.Railway.SourceGen.Executors.ResultExtensions.ValueTaskExtensions; + +internal class ResultValueTaskAppendExecutor : ResultAppendExecutor +{ + protected override string NamespaceName => "Just.Railway.ValueTaskExtensions"; + protected override string PartialClassName => "ResultValueTaskExtensions"; + + protected override void GenerateHelperMethods(StringBuilder sb) + { + sb.AppendLine(""" + private static IEnumerable GetBottom(ResultState r1, ResultState r2, string firstArg = "result", string secondArg = "next") + { + if (r1 == ResultState.Bottom) + yield return firstArg; + if (r2 == ResultState.Bottom) + yield return secondArg; + } + """); + } + + protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) + { + var templateArgNames = Enumerable.Range(1, argCount) + .Select(i => $"T{i}") + .ToImmutableArray(); + + string resultTypeDef = GenerateResultTypeDef(templateArgNames); + string resultValueExpansion = GenerateResultValueExpansion(templateArgNames); + + sb.AppendLine($"#region {resultTypeDef}"); + + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + + if (argCount < Constants.MaxResultTupleSize) + { + GenerateExpandedAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + } + + sb.AppendLine("#endregion"); + } +} diff --git a/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskBindExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskBindExecutor.cs new file mode 100644 index 0000000..7b7efd6 --- /dev/null +++ b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskBindExecutor.cs @@ -0,0 +1,29 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; +using Microsoft.CodeAnalysis; + +namespace Just.Railway.SourceGen.Executors.ResultExtensions.ValueTaskExtensions; + +internal class ResultValueTaskBindExecutor : ResultBindExecutor +{ + protected override string NamespaceName => "Just.Railway.ValueTaskExtensions"; + protected override string PartialClassName => "ResultValueTaskExtensions"; + + protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) + { + var templateArgNames = Enumerable.Range(1, argCount) + .Select(i => $"T{i}") + .ToImmutableArray(); + + string resultTypeDef = GenerateResultTypeDef(templateArgNames); + string resultValueExpansion = GenerateResultValueExpansion(templateArgNames); + + sb.AppendLine($"#region {resultTypeDef}"); + + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + + sb.AppendLine("#endregion"); + } +} diff --git a/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskExtendExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskExtendExecutor.cs new file mode 100644 index 0000000..0bed651 --- /dev/null +++ b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskExtendExecutor.cs @@ -0,0 +1,35 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; +using Microsoft.CodeAnalysis; + +namespace Just.Railway.SourceGen.Executors.ResultExtensions.ValueTaskExtensions; + +internal sealed class ResultValueTaskExtendExecutor : ResultExtendExecutor +{ + protected override string NamespaceName => "Just.Railway.ValueTaskExtensions"; + protected override string PartialClassName => "ResultValueTaskExtensions"; + + protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) + { + if (argCount == 0 || argCount == Constants.MaxResultTupleSize) + { + return; + } + + var templateArgNames = Enumerable.Range(1, argCount) + .Select(i => $"T{i}") + .ToImmutableArray(); + + var expandedTemplateArgNames = templateArgNames.Add("R"); + string resultTypeDef = GenerateResultTypeDef(templateArgNames); + string resultValueExpansion = GenerateResultValueExpansion(templateArgNames); + + sb.AppendLine($"#region {resultTypeDef}"); + + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + + sb.AppendLine("#endregion"); + } +} diff --git a/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskMapExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskMapExecutor.cs new file mode 100644 index 0000000..c3c8398 --- /dev/null +++ b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskMapExecutor.cs @@ -0,0 +1,28 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Just.Railway.SourceGen.Executors.ResultExtensions.ValueTaskExtensions; + +internal sealed class ResultValueTaskMapExecutor : ResultMapExecutor +{ + protected override string NamespaceName => "Just.Railway.ValueTaskExtensions"; + protected override string PartialClassName => "ResultValueTaskExtensions"; + + protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) + { + var templateArgNames = Enumerable.Range(1, argCount) + .Select(i => $"T{i}") + .ToImmutableArray(); + + string resultTypeDef = GenerateResultTypeDef(templateArgNames); + string resultValueExpansion = GenerateResultValueExpansion(templateArgNames); + + sb.AppendLine($"#region {resultTypeDef}"); + + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + + sb.AppendLine("#endregion"); + } +} diff --git a/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskMatchExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskMatchExecutor.cs new file mode 100644 index 0000000..f2d0dc5 --- /dev/null +++ b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskMatchExecutor.cs @@ -0,0 +1,29 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; +using Microsoft.CodeAnalysis; + +namespace Just.Railway.SourceGen.Executors.ResultExtensions.ValueTaskExtensions; + +internal class ResultValueTaskMatchExecutor : ResultMatchExecutor +{ + protected override string NamespaceName => "Just.Railway.ValueTaskExtensions"; + protected override string PartialClassName => "ResultValueTaskExtensions"; + + protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) + { + var templateArgNames = Enumerable.Range(1, argCount) + .Select(i => $"T{i}") + .ToImmutableArray(); + + string resultTypeDef = GenerateResultTypeDef(templateArgNames); + string resultValueExpansion = GenerateResultValueExpansion(templateArgNames); + + sb.AppendLine($"#region {resultTypeDef}"); + + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + + sb.AppendLine("#endregion"); + } +} diff --git a/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskTapExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskTapExecutor.cs new file mode 100644 index 0000000..9e4876c --- /dev/null +++ b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskTapExecutor.cs @@ -0,0 +1,29 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; +using Microsoft.CodeAnalysis; + +namespace Just.Railway.SourceGen.Executors.ResultExtensions.ValueTaskExtensions; + +internal class ResultValueTaskTapExecutor : ResultTapExecutor +{ + protected override string NamespaceName => "Just.Railway.ValueTaskExtensions"; + protected override string PartialClassName => "ResultValueTaskExtensions"; + + protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) + { + var templateArgNames = Enumerable.Range(1, argCount) + .Select(i => $"T{i}") + .ToImmutableArray(); + + string resultTypeDef = GenerateResultTypeDef(templateArgNames); + string resultValueExpansion = GenerateResultValueExpansion(templateArgNames); + + sb.AppendLine($"#region {resultTypeDef}"); + + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + + sb.AppendLine("#endregion"); + } +} diff --git a/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskTryRecoverExecutor.cs b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskTryRecoverExecutor.cs new file mode 100644 index 0000000..e9e9d2a --- /dev/null +++ b/Railway.SourceGenerator/Executors/ResultExtensions/ValueTaskExtensions/ResultValueTaskTryRecoverExecutor.cs @@ -0,0 +1,30 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; + +namespace Just.Railway.SourceGen.Executors.ResultExtensions.ValueTaskExtensions; + +internal class ResultValueTaskTryRecoverExecutor : ResultTryRecoverExecutor +{ + protected override string NamespaceName => "Just.Railway.ValueTaskExtensions"; + protected override string PartialClassName => "ResultValueTaskExtensions"; + + + 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}"); + + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, methodTemplateDecl); + + sb.AppendLine("#endregion"); + } +} diff --git a/Railway.SourceGenerator/TryExtensionsExecutor.cs b/Railway.SourceGenerator/Executors/TryExtensionsExecutor.cs similarity index 87% rename from Railway.SourceGenerator/TryExtensionsExecutor.cs rename to Railway.SourceGenerator/Executors/TryExtensionsExecutor.cs index 0d74b2e..72f62b9 100644 --- a/Railway.SourceGenerator/TryExtensionsExecutor.cs +++ b/Railway.SourceGenerator/Executors/TryExtensionsExecutor.cs @@ -3,11 +3,11 @@ using System.Linq; using System.Text; using Microsoft.CodeAnalysis; -namespace Just.Railway.SourceGen; +namespace Just.Railway.SourceGen.Executors; public sealed class TryExtensionsExecutor : IGeneratorExecutor { - public void Execute(SourceProductionContext context, Compilation source) + public void Execute(IncrementalGeneratorPostInitializationContext context) { var methods = GenerateMethods(); var code = $$""" @@ -123,13 +123,13 @@ public sealed class TryExtensionsExecutor : IGeneratorExecutor } """); - GenerateAsyncMethods(sb, templateArgNames, actionTemplateDecl, funcTemplateDecl, argumentsDeclExpansion, argumentsExpansion, "Task"); - GenerateAsyncMethods(sb, templateArgNames, actionTemplateDecl, funcTemplateDecl, argumentsDeclExpansion, argumentsExpansion, "ValueTask"); + GenerateAsyncMethods(sb, templateArgNames, actionTemplateDecl, funcTemplateDecl, argumentsDeclExpansion, argumentsExpansion, "Task", string.Empty); + GenerateAsyncMethods(sb, templateArgNames, actionTemplateDecl, funcTemplateDecl, argumentsDeclExpansion, argumentsExpansion, "ValueTask", "ValueTask"); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(StringBuilder sb, ImmutableArray templateArgNames, string actionTemplateDecl, string funcTemplateDecl, string argumentsDeclExpansion, string argumentsExpansion, string taskType) + private static void GenerateAsyncMethods(StringBuilder sb, ImmutableArray templateArgNames, string actionTemplateDecl, string funcTemplateDecl, string argumentsDeclExpansion, string argumentsExpansion, string taskType, string methodSuffix) { string actionTaskTemplateDecl = GenerateTemplateDecl(templateArgNames.Add(taskType)); string resultActionTaskTemplateDecl = GenerateTemplateDecl(templateArgNames.Add($"{taskType}")); @@ -138,7 +138,7 @@ public sealed class TryExtensionsExecutor : IGeneratorExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")] - public static async {{taskType}} Run{{actionTemplateDecl}}(Func{{actionTaskTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}}) + public static async {{taskType}} Run{{methodSuffix}}{{actionTemplateDecl}}(Func{{actionTaskTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}}) { try { @@ -155,7 +155,7 @@ public sealed class TryExtensionsExecutor : IGeneratorExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")] - public static async {{taskType}} Run{{actionTemplateDecl}}(Func{{resultActionTaskTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}}) + public static async {{taskType}} Run{{methodSuffix}}{{actionTemplateDecl}}(Func{{resultActionTaskTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}}) { try { @@ -171,7 +171,7 @@ public sealed class TryExtensionsExecutor : IGeneratorExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")] - public static async {{taskType}}> Run{{funcTemplateDecl}}(Func{{funcTaskTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}}) + public static async {{taskType}}> Run{{methodSuffix}}{{funcTemplateDecl}}(Func{{funcTaskTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}}) { try { @@ -187,7 +187,7 @@ public sealed class TryExtensionsExecutor : IGeneratorExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")] - public static async {{taskType}}> Run{{funcTemplateDecl}}(Func{{resultFuncTaskTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}}) + public static async {{taskType}}> Run{{methodSuffix}}{{funcTemplateDecl}}(Func{{resultFuncTaskTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}}) { try { diff --git a/Railway.SourceGenerator/ExtensionsMethodGenerator.cs b/Railway.SourceGenerator/ExtensionsMethodGenerator.cs index 28e4970..e140050 100644 --- a/Railway.SourceGenerator/ExtensionsMethodGenerator.cs +++ b/Railway.SourceGenerator/ExtensionsMethodGenerator.cs @@ -1,4 +1,7 @@ using System.Collections.Generic; +using Just.Railway.SourceGen.Executors; +using Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions; +using Just.Railway.SourceGen.Executors.ResultExtensions.ValueTaskExtensions; using Microsoft.CodeAnalysis; namespace Just.Railway.SourceGen; @@ -6,9 +9,10 @@ namespace Just.Railway.SourceGen; [Generator] public class ExtensionsMethodGenerator : IIncrementalGenerator { - private readonly IEnumerable _executors = new IGeneratorExecutor[] - { + private readonly IEnumerable _executors = + [ new ResultCombineExecutor(), + new ResultMatchExecutor(), new ResultMapExecutor(), new ResultBindExecutor(), @@ -16,15 +20,24 @@ public class ExtensionsMethodGenerator : IIncrementalGenerator new ResultExtendExecutor(), new ResultTryRecoverExecutor(), new ResultAppendExecutor(), + + new ResultValueTaskMatchExecutor(), + new ResultValueTaskMapExecutor(), + new ResultValueTaskBindExecutor(), + new ResultValueTaskTapExecutor(), + new ResultValueTaskExtendExecutor(), + new ResultValueTaskTryRecoverExecutor(), + new ResultValueTaskAppendExecutor(), + new TryExtensionsExecutor(), new EnsureExtensionsExecutor(), - }; + ]; public void Initialize(IncrementalGeneratorInitializationContext context) { foreach (var executor in _executors) { - context.RegisterSourceOutput(context.CompilationProvider, executor.Execute); + context.RegisterPostInitializationOutput(executor.Execute); } } } diff --git a/Railway.SourceGenerator/IGeneratorExecutor.cs b/Railway.SourceGenerator/IGeneratorExecutor.cs index e42d884..07353cf 100644 --- a/Railway.SourceGenerator/IGeneratorExecutor.cs +++ b/Railway.SourceGenerator/IGeneratorExecutor.cs @@ -4,5 +4,5 @@ namespace Just.Railway.SourceGen; internal interface IGeneratorExecutor { - public abstract void Execute(SourceProductionContext context, Compilation source); + public abstract void Execute(IncrementalGeneratorPostInitializationContext context); } diff --git a/Railway/Error.cs b/Railway/Error.cs index 713359c..58e1df7 100644 --- a/Railway/Error.cs +++ b/Railway/Error.cs @@ -206,6 +206,7 @@ public sealed class ExpectedError : Error { public ExpectedError(string type, string message) { + ArgumentNullException.ThrowIfNull(type); Type = type; Message = message; } @@ -252,6 +253,7 @@ public sealed class ExceptionalError : Error public ExceptionalError(string type, string message) { + ArgumentNullException.ThrowIfNull(type); Type = type; Message = message; } @@ -343,7 +345,7 @@ public sealed class ManyErrors : Error, IEnumerable, IReadOnlyList [Pure] public override string Type => "many_errors"; private string? _lazyMessage = null; - [Pure] public override string Message => _lazyMessage ??= ToFullArrayString(_errors); + [Pure] public override string Message => _lazyMessage ??= ToFullArrayString(_errors); // not thread safe. wontfix [Pure] private static string ToFullArrayString(in ImmutableArray errors) { @@ -428,7 +430,7 @@ public sealed class ManyErrors : Error, IEnumerable, IReadOnlyList } private int? _lazyHashCode = null; - [Pure] public override int GetHashCode() => _lazyHashCode ??= CalcHashCode(_errors); + [Pure] public override int GetHashCode() => _lazyHashCode ??= CalcHashCode(_errors); // not thread safe. wontfix private static int CalcHashCode(in ImmutableArray errors) { if (errors.IsEmpty) diff --git a/Railway/ReflectionHelper.cs b/Railway/ReflectionHelper.cs index 1db6ef0..3996a10 100644 --- a/Railway/ReflectionHelper.cs +++ b/Railway/ReflectionHelper.cs @@ -7,13 +7,9 @@ internal static class ReflectionHelper [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsEqual(T? left, T? right) => TypeReflectionCache.IsEqualFunc(left, right); - [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int Compare(T? left, T? right) => TypeReflectionCache.CompareFunc(left, right); - private static class TypeReflectionCache { public static readonly Func IsEqualFunc; - public static readonly Func CompareFunc; static TypeReflectionCache() { @@ -34,33 +30,11 @@ internal static class ReflectionHelper { IsEqualFunc = static (left, right) => left is null ? right is null : left.Equals(right); } - - var comparableType = typeof(IComparable<>).MakeGenericType(underlyingType); - if (comparableType.IsAssignableFrom(underlyingType)) - { - var compareFunc = thisType.GetMethod(isNullableStruct ? nameof(CompareNullable) : nameof(Compare), BindingFlags.Static | BindingFlags.Public) - !.MakeGenericMethod(underlyingType); - - CompareFunc = (Func)Delegate.CreateDelegate(typeof(Func), compareFunc); - } - else - { - CompareFunc = static (left, right) => left is null - ? right is null ? 0 : -1 - : right is null ? 1 : left.GetHashCode().CompareTo(right.GetHashCode()); - } } #pragma warning disable CS8604 // Possible null reference argument. [Pure] public static bool IsEqual(R? left, R? right) where R : notnull, IEquatable, T => left is null ? right is null : left.Equals(right); [Pure] public static bool IsEqualNullable(R? left, R? right) where R : struct, IEquatable => left is null ? right is null : right is not null && left.Value.Equals(right.Value); - - [Pure] public static int Compare(R? left, R? right) where R : notnull, IComparable, T => left is null - ? right is null ? 0 : -1 - : right is null ? 1 : left.CompareTo(right); - [Pure] public static int CompareNullable(R? left, R? right) where R : struct, IComparable => left is null - ? right is null ? 0 : -1 - : right is null ? 1 : left.Value.CompareTo(right.Value); #pragma warning restore CS8604 // Possible null reference argument. } } diff --git a/Railway/Result.cs b/Railway/Result.cs index 6db74e5..5e260ca 100644 --- a/Railway/Result.cs +++ b/Railway/Result.cs @@ -127,9 +127,9 @@ public readonly partial struct Result : IEquatable [Pure] public override bool Equals(object? obj) => obj is Result other && Equals(other); [Pure] public bool Equals(Result other) { - if (State == ResultState.Bottom) + if (State == ResultState.Bottom || other.State == ResultState.Bottom) throw new ResultNotInitializedException(); - + return Error == other.Error; } [Pure] public static bool operator ==(Result left, Result right) => left.Equals(right); @@ -260,9 +260,9 @@ public readonly struct Result : IEquatable> [Pure] public override bool Equals(object? obj) => obj is Result other && Equals(other); [Pure] public bool Equals(Result other) { - if (State == ResultState.Bottom) + if (State == ResultState.Bottom || other.State == ResultState.Bottom) throw new ResultNotInitializedException(); - + if (IsSuccess != other.IsSuccess) return false; diff --git a/Railway/ResultExtensions.cs b/Railway/ResultExtensions.cs index cbe0c99..5315045 100644 --- a/Railway/ResultExtensions.cs +++ b/Railway/ResultExtensions.cs @@ -4,9 +4,82 @@ namespace Just.Railway; public static partial class ResultExtensions { - #region Match (with fallback) + #region Finally - public static T Match(this in Result result, Func fallback) + public static Result Finally(this in Result result, Action action) + { + switch (result.State) + { + case ResultState.Success: + case ResultState.Error: + action.Invoke(); + break; + + default: throw new ResultNotInitializedException(nameof(result)); + } + + return result; + } + + public static async Task Finally(this Task resultTask, Action action) + { + var result = await resultTask; + switch (result.State) + { + case ResultState.Success: + case ResultState.Error: + action.Invoke(); + break; + + default: throw new ResultNotInitializedException(nameof(resultTask)); + } + + return result; + } + public static async Task Finally(this Result result, Func action) + { + switch (result.State) + { + case ResultState.Success: + case ResultState.Error: + await action.Invoke(); + break; + + default: throw new ResultNotInitializedException(nameof(result)); + } + + return result; + } + public static async Task Finally(this Task resultTask, Func action) + { + var result = await resultTask; + switch (result.State) + { + case ResultState.Success: + case ResultState.Error: + await action.Invoke(); + break; + + default: throw new ResultNotInitializedException(nameof(resultTask)); + } + + return result; + } + + #endregion + + #region ValueOr + + public static T ValueOr(this in Result result, T fallback) + { + return result.State switch + { + ResultState.Success => result.Value, + ResultState.Error => fallback, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + public static T ValueOr(this in Result result, Func fallback) { return result.State switch { @@ -16,7 +89,17 @@ public static partial class ResultExtensions }; } - public static async Task Match(this Result result, Func> fallback) + public static async Task ValueOr(this Task> resultTask, T fallback) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => result.Value, + ResultState.Error => fallback, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + public static async Task ValueOr(this Result result, Func> fallback) { return result.State switch { @@ -25,7 +108,7 @@ public static partial class ResultExtensions _ => throw new ResultNotInitializedException(nameof(result)) }; } - public static async Task Match(this Task> resultTask, Func fallback) + public static async Task ValueOr(this Task> resultTask, Func fallback) { var result = await resultTask.ConfigureAwait(false); return result.State switch @@ -35,37 +118,7 @@ public static partial class ResultExtensions _ => throw new ResultNotInitializedException(nameof(resultTask)) }; } - public static async Task Match(this Task> resultTask, Func> fallback) - { - var result = await resultTask.ConfigureAwait(false); - return result.State switch - { - ResultState.Success => result.Value, - ResultState.Error => await fallback(result.Error!).ConfigureAwait(false), - _ => throw new ResultNotInitializedException(nameof(resultTask)) - }; - } - - public static async ValueTask Match(this Result result, Func> fallback) - { - return result.State switch - { - ResultState.Success => result.Value, - ResultState.Error => await fallback(result.Error!).ConfigureAwait(false), - _ => throw new ResultNotInitializedException(nameof(result)) - }; - } - public static async ValueTask Match(this ValueTask> resultTask, Func fallback) - { - var result = await resultTask.ConfigureAwait(false); - return result.State switch - { - ResultState.Success => result.Value, - ResultState.Error => fallback(result.Error!), - _ => throw new ResultNotInitializedException(nameof(resultTask)) - }; - } - public static async ValueTask Match(this ValueTask> resultTask, Func> fallback) + public static async Task ValueOr(this Task> resultTask, Func> fallback) { var result = await resultTask.ConfigureAwait(false); return result.State switch @@ -83,79 +136,95 @@ public static partial class ResultExtensions public static Result Merge(this IEnumerable results) { ImmutableArray.Builder? errors = null; - bool hasErrors = false; - foreach (var result in results.OrderBy(x => x.State)) + foreach (var result in results) { switch (result.State) { - case ResultState.Error: - hasErrors = true; - errors ??= ImmutableArray.CreateBuilder(); - ManyErrors.AppendSanitized(errors, result.Error!); + case ResultState.Success: break; - case ResultState.Success: - if (hasErrors) goto afterLoop; + case ResultState.Error: + errors ??= ImmutableArray.CreateBuilder(); + ManyErrors.AppendSanitized(errors, result.Error!); break; default: throw new ResultNotInitializedException(nameof(results)); } } - afterLoop: - return hasErrors - ? new(new ManyErrors(errors!.ToImmutable())) - : new(null); + + return errors is null + ? new(null) + : new(new ManyErrors(errors.ToImmutable())); } public static async Task Merge(this IEnumerable> tasks) { - var results = await Task.WhenAll(tasks).ConfigureAwait(false); + var taskList = tasks.ToArray(); + var results = new Result[taskList.Length]; + + // Wait for all tasks to complete (suppress AggregateException — we'll check each individually) + try { await Task.WhenAll(taskList).ConfigureAwait(false); } catch { /* handled per-task below */ } + + for (int i = 0; i < taskList.Length; i++) + { + var task = taskList[i]; + results[i] = task.Status switch + { + TaskStatus.RanToCompletion => task.Result, + TaskStatus.Faulted => Result.Failure(task.Exception!), + TaskStatus.Canceled => Result.Failure(Error.New("task_canceled", "Task was canceled")), + _ => Result.Failure(Error.New("task_unexpected", $"Task in unexpected state: {task.Status}")) + }; + } + return results.Merge(); } public static Result> Merge(this IEnumerable> results) { - ImmutableList.Builder? values = null; ImmutableArray.Builder? errors = null; - bool hasErrors = false; + ImmutableArray.Builder? values = null; - foreach (var result in results.OrderBy(x => x.State)) + foreach (var result in results) { switch (result.State) { + case ResultState.Success: + values ??= ImmutableArray.CreateBuilder(); + values.Add(result.Value); + break; + case ResultState.Error: - hasErrors = true; errors ??= ImmutableArray.CreateBuilder(); ManyErrors.AppendSanitized(errors, result.Error!); break; - case ResultState.Success: - if (hasErrors) goto afterLoop; - values ??= ImmutableList.CreateBuilder(); - values.Add(result.Value); - break; - default: throw new ResultNotInitializedException(nameof(results)); } } - afterLoop: - return hasErrors - ? new(new ManyErrors(errors!.ToImmutable())) - : new(values is not null ? values.ToImmutable() : ImmutableList.Empty); + + return errors is null + ? new(values?.ToImmutable() ?? Enumerable.Empty()) + : new(new ManyErrors(errors.ToImmutable())); } public static async Task>> Merge(this IEnumerable>> tasks) { - var taskList = tasks.ToList(); - var results = new Result[taskList.Count]; + var taskList = tasks.ToArray(); + var results = new Result[taskList.Length]; - for (int i = 0; i < taskList.Count; i++) + // Wait for all tasks to complete (suppress AggregateException — we'll check each individually) + try { await Task.WhenAll(taskList).ConfigureAwait(false); } catch { /* handled per-task below */ } + + for (int i = 0; i < taskList.Length; i++) { - var task = await Task.WhenAny(taskList); - taskList.Remove(task); - - results[i] = task.IsCompletedSuccessfully - ? task.Result - : task.Exception!; + var task = taskList[i]; + results[i] = task.Status switch + { + TaskStatus.RanToCompletion => task.Result, + TaskStatus.Faulted => Result.Failure(task.Exception!), + TaskStatus.Canceled => Result.Failure(Error.New("task_canceled", "Task was canceled")), + _ => Result.Failure(Error.New("task_unexpected", $"Task in unexpected state: {task.Status}")) + }; } return results.Merge(); diff --git a/Railway/Try.cs b/Railway/Try.cs index f8200e3..59bc103 100644 --- a/Railway/Try.cs +++ b/Railway/Try.cs @@ -1,5 +1,9 @@ namespace Just.Railway; +/// +/// Result based try/catch +/// +/// All extension methods are source generated. public static partial class Try { } diff --git a/Railway/ValueTaskExtensions/ResultValueTaskExtensions.cs b/Railway/ValueTaskExtensions/ResultValueTaskExtensions.cs new file mode 100644 index 0000000..aa6a05b --- /dev/null +++ b/Railway/ValueTaskExtensions/ResultValueTaskExtensions.cs @@ -0,0 +1,98 @@ +namespace Just.Railway.ValueTaskExtensions; + +public static partial class ResultValueTaskExtensions +{ + #region Finally + + public static async ValueTask ValueTaskFinally(this ValueTask resultTask, Action action) + { + var result = await resultTask; + switch (result.State) + { + case ResultState.Success: + case ResultState.Error: + action.Invoke(); + break; + + default: throw new ResultNotInitializedException(nameof(resultTask)); + } + + return result; + } + public static async ValueTask ValueTaskFinally(this Result result, Func action) + { + switch (result.State) + { + case ResultState.Success: + case ResultState.Error: + await action.Invoke(); + break; + + default: throw new ResultNotInitializedException(nameof(result)); + } + + return result; + } + public static async ValueTask ValueTaskFinally(this ValueTask resultTask, Func action) + { + var result = await resultTask; + switch (result.State) + { + case ResultState.Success: + case ResultState.Error: + await action.Invoke(); + break; + + default: throw new ResultNotInitializedException(nameof(resultTask)); + } + + return result; + } + + #endregion + + #region ValueOr + + + public static async ValueTask ValueTaskValueOr(this ValueTask> resultTask, T fallback) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => result.Value, + ResultState.Error => fallback, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + public static async ValueTask ValueTaskValueOr(this Result result, Func> fallback) + { + return result.State switch + { + ResultState.Success => result.Value, + ResultState.Error => await fallback(result.Error!).ConfigureAwait(false), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + public static async ValueTask ValueTaskValueOr(this ValueTask> resultTask, Func fallback) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => result.Value, + ResultState.Error => fallback(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + public static async ValueTask ValueTaskValueOr(this ValueTask> resultTask, Func> fallback) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => result.Value, + ResultState.Error => await fallback(result.Error!).ConfigureAwait(false), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion +} diff --git a/Raliway.Tests/ReflectionHelper/ReflectionHelperTests.cs b/Raliway.Tests/ReflectionHelper/ReflectionHelperTests.cs new file mode 100644 index 0000000..3a4ea23 --- /dev/null +++ b/Raliway.Tests/ReflectionHelper/ReflectionHelperTests.cs @@ -0,0 +1,173 @@ +namespace Raliway.Tests.ReflectionHelper; + +// --- Test types --- + +file sealed record EquatableRecord(string Name, int Value); + +file sealed class NonEquatableClass +{ + public string Name { get; } + public NonEquatableClass(string name) => Name = name; + public override bool Equals(object? obj) => obj is NonEquatableClass other && Name == other.Name; + public override int GetHashCode() => Name.GetHashCode(); +} + +file readonly struct EquatableStruct : IEquatable +{ + public int Value { get; } + public EquatableStruct(int value) => Value = value; + public bool Equals(EquatableStruct other) => Value == other.Value; + public override bool Equals(object? obj) => obj is EquatableStruct s && Equals(s); + public override int GetHashCode() => Value; +} + +// --- IsEqual tests --- + +public class IsEqual_Tests +{ + public class ValueType_IEquatable + { + [Fact] public void Equal_values() => + Just.Railway.ReflectionHelper.IsEqual(42, 42).ShouldBeTrue(); + + [Fact] public void Different_values() => + Just.Railway.ReflectionHelper.IsEqual(42, 43).ShouldBeFalse(); + + [Fact] public void Null_args() => + Just.Railway.ReflectionHelper.IsEqual(default(int), default(int)).ShouldBeTrue(); + } + + public class NullableValueType_IEquatable + { + [Fact] public void Both_have_same_value() => + Just.Railway.ReflectionHelper.IsEqual(42, 42).ShouldBeTrue(); + + [Fact] public void Different_values() => + Just.Railway.ReflectionHelper.IsEqual(42, 43).ShouldBeFalse(); + + [Fact] public void Both_null() => + Just.Railway.ReflectionHelper.IsEqual(null, null).ShouldBeTrue(); + + [Fact] public void One_null_one_value() => + Just.Railway.ReflectionHelper.IsEqual(42, null).ShouldBeFalse(); + + [Fact] public void Null_and_value_reversed() => + Just.Railway.ReflectionHelper.IsEqual(null, 42).ShouldBeFalse(); + } + + public class ReferenceType_IEquatable_String + { + [Fact] public void Equal_strings() => + Just.Railway.ReflectionHelper.IsEqual("hello", "hello").ShouldBeTrue(); + + [Fact] public void Different_strings() => + Just.Railway.ReflectionHelper.IsEqual("hello", "world").ShouldBeFalse(); + + [Fact] public void Both_null() => + Just.Railway.ReflectionHelper.IsEqual(null, null).ShouldBeTrue(); + + [Fact] public void Left_null_right_value() => + Just.Railway.ReflectionHelper.IsEqual(null, "hello").ShouldBeFalse(); + + [Fact] public void Left_value_right_null() => + Just.Railway.ReflectionHelper.IsEqual("hello", null).ShouldBeFalse(); + } + + public class ReferenceType_IEquatable_Error + { + [Fact] public void Equal_errors() => + Just.Railway.ReflectionHelper.IsEqual( + Error.New("type", "msg"), + Error.New("type", "msg") + ).ShouldBeTrue(); + + [Fact] public void Different_errors() => + Just.Railway.ReflectionHelper.IsEqual( + Error.New("type_a", "msg"), + Error.New("type_b", "msg") + ).ShouldBeFalse(); + + [Fact] public void Both_null() => + Just.Railway.ReflectionHelper.IsEqual(null, null).ShouldBeTrue(); + + [Fact] public void One_null() => + Just.Railway.ReflectionHelper.IsEqual(Error.New("type", "msg"), null).ShouldBeFalse(); + } + + public class RecordType_IEquatable + { + [Fact] public void Equal_records() => + Just.Railway.ReflectionHelper.IsEqual( + new EquatableRecord("a", 1), + new EquatableRecord("a", 1) + ).ShouldBeTrue(); + + [Fact] public void Different_records() => + Just.Railway.ReflectionHelper.IsEqual( + new EquatableRecord("a", 1), + new EquatableRecord("b", 2) + ).ShouldBeFalse(); + + [Fact] public void Both_null() => + Just.Railway.ReflectionHelper.IsEqual(null, null).ShouldBeTrue(); + } + + public class NonIEquatable_Fallback + { + [Fact] public void Equal_by_Equals_override() => + Just.Railway.ReflectionHelper.IsEqual( + new NonEquatableClass("a"), + new NonEquatableClass("a") + ).ShouldBeTrue(); + + [Fact] public void Not_equal() => + Just.Railway.ReflectionHelper.IsEqual( + new NonEquatableClass("a"), + new NonEquatableClass("b") + ).ShouldBeFalse(); + + [Fact] public void Both_null() => + Just.Railway.ReflectionHelper.IsEqual(null, null).ShouldBeTrue(); + + [Fact] public void Left_null() => + Just.Railway.ReflectionHelper.IsEqual(null, new NonEquatableClass("a")) + .ShouldBeFalse(); + } + + public class CustomEquatableStruct + { + [Fact] public void Equal() => + Just.Railway.ReflectionHelper.IsEqual( + new EquatableStruct(1), + new EquatableStruct(1) + ).ShouldBeTrue(); + + [Fact] public void Not_equal() => + Just.Railway.ReflectionHelper.IsEqual( + new EquatableStruct(1), + new EquatableStruct(2) + ).ShouldBeFalse(); + } + + public class CustomNullableEquatableStruct + { + [Fact] public void Both_have_same_value() => + Just.Railway.ReflectionHelper.IsEqual( + new EquatableStruct(1), + new EquatableStruct(1) + ).ShouldBeTrue(); + + [Fact] public void Different_values() => + Just.Railway.ReflectionHelper.IsEqual( + new EquatableStruct(1), + new EquatableStruct(2) + ).ShouldBeFalse(); + + [Fact] public void Both_null() => + Just.Railway.ReflectionHelper.IsEqual(null, null).ShouldBeTrue(); + + [Fact] public void One_null() => + Just.Railway.ReflectionHelper.IsEqual(new EquatableStruct(1), null) + .ShouldBeFalse(); + } +} diff --git a/Raliway.Tests/Results/Bugs.cs b/Raliway.Tests/Results/Bugs.cs new file mode 100644 index 0000000..1e5e78a --- /dev/null +++ b/Raliway.Tests/Results/Bugs.cs @@ -0,0 +1,185 @@ +namespace Raliway.Tests.Results; + +public class Bugs +{ + #region BUG-1: Result.Equals asymmetry + + [Fact] + public void Result_Success_ShouldNotEqual_Default() + { + // default(Result) has State=Bottom — it's uninitialized. + // Equals should throw because one operand is Bottom. + var success = Result.Success(); + Result bottom = default; + + // Should throw ResultNotInitializedException, NOT return true + Should.Throw(() => success.Equals(bottom)); + } + + [Fact] + public void Result_Default_ShouldNotEqual_Success() + { + Result bottom = default; + var success = Result.Success(); + + Should.Throw(() => bottom.Equals(success)); + } + + [Fact] + public void Result_Success_EqualsOperator_WithDefault() + { + var success = Result.Success(); + Result bottom = default; + + Should.Throw(() => success == bottom); + } + + [Fact] + public void Result_Error_Equals_WithBottom_ShouldThrow() + { + var error = Result.Failure("test"); + Result bottom = default; + + Should.Throw(() => error.Equals(bottom)); + } + + [Fact] + public void Result_Bottom_Equals_WithSuccess_ShouldThrow() + { + Result bottom = default; + var success = Result.Success(); + + Should.Throw(() => bottom.Equals(success)); + } + + #endregion + + #region BUG-2: Result.Equals asymmetry + + [Fact] + public void ResultOfT_Success_ShouldNotEqual_Default() + { + var success = Result.Success(42); + Result bottom = default; + + Should.Throw(() => success.Equals(bottom)); + } + + [Fact] + public void ResultOfT_Error_ShouldNotEqual_Default() + { + var error = Result.Failure("test"); + Result bottom = default; + + Should.Throw(() => error.Equals(bottom)); + } + + [Fact] + public void ResultOfT_Default_ShouldNotEqual_Success() + { + Result bottom = default; + var success = Result.Success(42); + + Should.Throw(() => bottom.Equals(success)); + } + + #endregion + + #region BUG-3: Task.WhenAny Merge — faulted task + + [Fact] + public async Task Merge_Tasks_WhenOneFaults_ShouldReturnError_NotThrow() + { + var tasks = new Task[] + { + Task.FromResult(Result.Success()), + Task.Run(() => + { + throw new InvalidOperationException("boom"); + #pragma warning disable CS0162 // Unreachable code + return Result.Success(); + #pragma warning restore CS0162 + }), + }; + + // Should not throw AggregateException escaping the Result error channel + var result = await tasks.Merge(); + + result.IsFailure.ShouldBeTrue(); + } + + [Fact] + public async Task Merge_Tasks_WhenOneIsCanceled_ShouldReturnError_NotThrow() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + var canceledTask = Task.FromCanceled(cts.Token); + + var tasks = new Task[] + { + Task.FromResult(Result.Success()), + canceledTask, + }; + + // Should not throw — canceled tasks have null Exception + var result = await tasks.Merge(); + + result.IsFailure.ShouldBeTrue(); + } + + #endregion + + #region BUG-4: Task.WhenAny Merge — canceled task + O(n²) + + [Fact] + public async Task MergeOfT_Tasks_WhenOneIsCanceled_ShouldReturnError_NotThrow() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + var canceledTask = Task.FromCanceled>(cts.Token); + + var tasks = new Task>[] + { + Task.FromResult(Result.Success(1)), + canceledTask, + }; + + // Should not crash on canceled task (task.Exception is null → null ref in implicit operator) + var result = await tasks.Merge(); + + result.IsFailure.ShouldBeTrue(); + } + + [Fact] + public async Task MergeOfT_Tasks_WhenOneFaults_ShouldReturnError_NotThrow() + { + var tasks = new Task>[] + { + Task.FromResult(Result.Success(1)), + Task.Run(() => + { + throw new InvalidOperationException("boom"); + #pragma warning disable CS0162 // Unreachable code + return Result.Success(2); + #pragma warning restore CS0162 + }), + }; + + var result = await tasks.Merge(); + + result.IsFailure.ShouldBeTrue(); + } + + #endregion + + #region BUG-5: Error.New null type + + [Fact] + public void Error_New_WithNullType_ShouldThrowArgumentNullException() + { + // Null type should throw immediately at construction, not crash later in CompareTo/IsSimilarTo + Should.Throw(() => Error.New(null!, "some message")); + } + + #endregion +} diff --git a/Raliway.Tests/Results/GeneralUsage.cs b/Raliway.Tests/Results/GeneralUsage.cs index 67470a9..16c8113 100644 --- a/Raliway.Tests/Results/GeneralUsage.cs +++ b/Raliway.Tests/Results/GeneralUsage.cs @@ -37,6 +37,7 @@ public class GeneralUsage var result = Result.Success() .Append(() => Result.Failure(error)) .Append("test") + .Tap(onSuccess: (i, s) => true.ShouldBeFalse("should not reach this path")) .Map((i, s) => { true.ShouldBeFalse("should not reach this path"); @@ -85,7 +86,7 @@ public class GeneralUsage return ""; } ); - + result.ShouldBe("TEST_1;SOME"); } diff --git a/Raliway.Tests/TryExtensions/Run.cs b/Raliway.Tests/TryExtensions/Run.cs new file mode 100644 index 0000000..3a249c6 --- /dev/null +++ b/Raliway.Tests/TryExtensions/Run.cs @@ -0,0 +1,558 @@ +namespace Raliway.Tests.TryExtensions; + +public class RunTests +{ + // ═══════════════════════════════════════════════════════════ + // Action (void) — sync + // ═══════════════════════════════════════════════════════════ + + public class Action_Void + { + [Fact] + public void No_args_success() + { + var result = Try.Run(() => { }); + result.IsSuccess.ShouldBeTrue(); + } + + [Fact] + public void No_args_exception() + { + var result = Try.Run(new Action(() => throw new InvalidOperationException("boom"))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + result.Error!.Type.ShouldBe("System.InvalidOperationException"); + } + + [Fact] + public void One_arg_success() + { + var captured = 0; + var result = Try.Run(x => captured = x, 42); + result.IsSuccess.ShouldBeTrue(); + captured.ShouldBe(42); + } + + [Fact] + public void One_arg_exception() + { + var result = Try.Run(new Action(_ => throw new InvalidOperationException("boom")), 42); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + + [Fact] + public void Five_args_success() + { + var captured = ""; + var result = Try.Run( + (a, b, c, d, e) => captured = $"{a}{b}{c}{d}{e}", + 1, "x", true, 3.14, 'z'); + result.IsSuccess.ShouldBeTrue(); + captured.ShouldBe("1xTrue3.14z"); + } + + [Fact] + public void Five_args_exception() + { + var result = Try.Run( + new Action((_, _, _, _, _) => throw new InvalidOperationException("boom")), + 1, "x", true, 3.14, 'z'); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + } + + // ═══════════════════════════════════════════════════════════ + // Func — sync + // ═══════════════════════════════════════════════════════════ + + public class Func_Result + { + [Fact] + public void No_args_returns_success() + { + var result = Try.Run(() => Result.Success()); + result.IsSuccess.ShouldBeTrue(); + } + + [Fact] + public void No_args_returns_failure() + { + var error = Error.New("nope"); + var result = Try.Run(() => Result.Failure(error)); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(error); + } + + [Fact] + public void No_args_exception_caught() + { + var result = Try.Run(new Func(() => throw new InvalidOperationException("boom"))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + + [Fact] + public void One_arg_passes_through() + { + var result = Try.Run(x => Result.Success(x), 42); + result.IsSuccess.ShouldBeTrue(); + } + } + + // ═══════════════════════════════════════════════════════════ + // Func — sync + // ═══════════════════════════════════════════════════════════ + + public class Func_T + { + [Fact] + public void No_args_success() + { + var result = Try.Run(() => 42); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe(42); + } + + [Fact] + public void No_args_exception() + { + var result = Try.Run(new Func(() => throw new InvalidOperationException("boom"))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + + [Fact] + public void One_arg_success() + { + var result = Try.Run(x => $"val:{x}", 42); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe("val:42"); + } + + [Fact] + public void One_arg_exception() + { + var result = Try.Run(new Func(_ => throw new InvalidOperationException("boom")), 42); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + + [Fact] + public void Null_value_is_preserved() + { + var result = Try.Run(() => (string?)null); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBeNull(); + } + } + + // ═══════════════════════════════════════════════════════════ + // Func> — sync + // ═══════════════════════════════════════════════════════════ + + public class Func_ResultT + { + [Fact] + public void No_args_returns_success() + { + var result = Try.Run(() => Result.Success(42)); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe(42); + } + + [Fact] + public void No_args_returns_failure() + { + var error = Error.New("nope"); + var result = Try.Run(() => Result.Failure(error)); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(error); + } + + [Fact] + public void No_args_exception_caught() + { + var result = Try.Run(new Func>(() => throw new InvalidOperationException("boom"))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + + [Fact] + public void One_arg_passes_through() + { + var result = Try.Run(x => Result.Success($"val:{x}"), 42); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe("val:42"); + } + } + + // ═══════════════════════════════════════════════════════════ + // Task async — Action (void) + // ═══════════════════════════════════════════════════════════ + + public class Task_Action + { + [Fact] + public async Task No_args_success() + { + var result = await Try.Run(() => Task.CompletedTask); + result.IsSuccess.ShouldBeTrue(); + } + + [Fact] + public async Task No_args_exception() + { + var result = await Try.Run(() => Task.FromException(new InvalidOperationException("boom"))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + + [Fact] + public async Task One_arg_success() + { + var captured = 0; + var result = await Try.Run(x => { captured = x; return Task.CompletedTask; }, 42); + result.IsSuccess.ShouldBeTrue(); + captured.ShouldBe(42); + } + } + + // ═══════════════════════════════════════════════════════════ + // Task async — Func> + // ═══════════════════════════════════════════════════════════ + + public class Task_Func_Result + { + [Fact] + public async Task No_args_returns_success() + { + var result = await Try.Run(() => Task.FromResult(Result.Success())); + result.IsSuccess.ShouldBeTrue(); + } + + [Fact] + public async Task No_args_returns_failure() + { + var error = Error.New("nope"); + var result = await Try.Run(() => Task.FromResult(Result.Failure(error))); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(error); + } + + [Fact] + public async Task No_args_exception_caught() + { + var result = await Try.Run(new Func>(() => throw new InvalidOperationException("boom"))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + } + + // ═══════════════════════════════════════════════════════════ + // Task async — Func> + // ═══════════════════════════════════════════════════════════ + + public class Task_Func_T + { + [Fact] + public async Task No_args_success() + { + var result = await Try.Run(() => Task.FromResult(42)); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe(42); + } + + [Fact] + public async Task No_args_exception() + { + var result = await Try.Run(new Func>(() => throw new InvalidOperationException("boom"))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + } + + // ═══════════════════════════════════════════════════════════ + // Task async — Func>> + // ═══════════════════════════════════════════════════════════ + + public class Task_Func_ResultT + { + [Fact] + public async Task No_args_returns_success() + { + var result = await Try.Run(() => Task.FromResult(Result.Success(42))); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe(42); + } + + [Fact] + public async Task No_args_returns_failure() + { + var error = Error.New("nope"); + var result = await Try.Run(() => Task.FromResult(Result.Failure(error))); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(error); + } + + [Fact] + public async Task No_args_exception_caught() + { + var result = await Try.Run(new Func>>(() => throw new InvalidOperationException("boom"))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + } + + // ═══════════════════════════════════════════════════════════ + // ValueTask async — Action (void) + // ═══════════════════════════════════════════════════════════ + + public class ValueTask_Action + { + [Fact] + public async Task No_args_success() + { + var result = await Try.RunValueTask(() => ValueTask.CompletedTask); + result.IsSuccess.ShouldBeTrue(); + } + + [Fact] + public async Task No_args_exception() + { + var result = await Try.RunValueTask(() => ValueTask.FromException(new InvalidOperationException("boom"))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + + [Fact] + public async Task One_arg_success() + { + var captured = 0; + var result = await Try.RunValueTask(x => { captured = x; return ValueTask.CompletedTask; }, 42); + result.IsSuccess.ShouldBeTrue(); + captured.ShouldBe(42); + } + } + + // ═══════════════════════════════════════════════════════════ + // ValueTask async — Func> + // ═══════════════════════════════════════════════════════════ + + public class ValueTask_Func_Result + { + [Fact] + public async Task No_args_returns_success() + { + var result = await Try.RunValueTask(() => ValueTask.FromResult(Result.Success())); + result.IsSuccess.ShouldBeTrue(); + } + + [Fact] + public async Task No_args_returns_failure() + { + var error = Error.New("nope"); + var result = await Try.RunValueTask(() => ValueTask.FromResult(Result.Failure(error))); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(error); + } + + [Fact] + public async Task No_args_exception_caught() + { + var result = await Try.RunValueTask(new Func>(() => throw new InvalidOperationException("boom"))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + } + + // ═══════════════════════════════════════════════════════════ + // ValueTask async — Func> + // ═══════════════════════════════════════════════════════════ + + public class ValueTask_Func_T + { + [Fact] + public async Task No_args_success() + { + var result = await Try.RunValueTask(() => ValueTask.FromResult(42)); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe(42); + } + + [Fact] + public async Task No_args_exception() + { + var result = await Try.RunValueTask(() => throw new InvalidOperationException("boom")); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + } + + // ═══════════════════════════════════════════════════════════ + // ValueTask async — Func>> + // ═══════════════════════════════════════════════════════════ + + public class ValueTask_Func_ResultT + { + [Fact] + public async Task No_args_returns_success() + { + var result = await Try.RunValueTask(() => ValueTask.FromResult(Result.Success(42))); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe(42); + } + + [Fact] + public async Task No_args_returns_failure() + { + var error = Error.New("nope"); + var result = await Try.RunValueTask(() => ValueTask.FromResult(Result.Failure(error))); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(error); + } + + [Fact] + public async Task No_args_exception_caught() + { + var result = await Try.RunValueTask(new Func>>(() => throw new InvalidOperationException("boom"))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("boom"); + } + } + + // ═══════════════════════════════════════════════════════════ + // AggregateException unwrapping + // ═══════════════════════════════════════════════════════════ + + public class AggregateException_Unwrapping + { + [Fact] + public void Single_inner_exception_is_unwrapped() + { + var result = Try.Run(new Func(() => throw new AggregateException(new InvalidOperationException("inner")))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Type.ShouldBe("System.InvalidOperationException"); + result.Error!.Message.ShouldBe("inner"); + } + + [Fact] + public void Multiple_inner_exceptions_become_ManyErrors() + { + var result = Try.Run(new Func(() => throw new AggregateException( + new InvalidOperationException("first"), + new ArgumentException("second")))); + result.IsFailure.ShouldBeTrue(); + result.Error!.Type.ShouldBe("many_errors"); + var many = (ManyErrors)result.Error!; + many.Count.ShouldBe(2); + } + } + + // ═══════════════════════════════════════════════════════════ + // Async exception in task body (not Task.FromException) + // ═══════════════════════════════════════════════════════════ + + public class Async_Exception_In_Body + { + [Fact] + public async Task Task_throws_in_async_body() + { + var result = await Try.Run(async () => + { + await Task.Yield(); + throw new InvalidOperationException("async boom"); + }); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("async boom"); + } + + [Fact] + public async Task ValueTask_throws_in_async_body() + { + var result = await Try.RunValueTask(async () => + { + await Task.Yield(); + throw new InvalidOperationException("async boom vt"); + }); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("async boom vt"); + } + + [Fact] + public async Task Task_FuncT_throws_in_async_body() + { + var result = await Try.Run(async () => + { + await Task.Yield(); + throw new InvalidOperationException("async boom"); + }); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("async boom"); + } + + [Fact] + public async Task ValueTask_ResultT_throws_in_async_body() + { + var result = await Try.RunValueTask(async () => + { + await Task.Yield(); + throw new InvalidOperationException("async boom vt"); + }); + result.IsFailure.ShouldBeTrue(); + result.Error!.Message.ShouldBe("async boom vt"); + } + } + + // ═══════════════════════════════════════════════════════════ + // Edge cases + // ═══════════════════════════════════════════════════════════ + + public class Edge_Cases + { + [Fact] + public void Reference_type_null_success_value_is_preserved() + { + var result = Try.Run(() => Result.Success(null)); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBeNull(); + } + + [Fact] + public void Large_ValueTuple_is_preserved() + { + var result = Try.Run(() => (1, 2, 3, 4, 5)); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe((1, 2, 3, 4, 5)); + } + + [Fact] + public async Task Async_large_ValueTuple_is_preserved() + { + var result = await Try.Run(() => Task.FromResult((1, 2, 3, 4, 5))); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe((1, 2, 3, 4, 5)); + } + + [Fact] + public void Three_args_all_passed_correctly() + { + var result = Try.Run( + (a, b, c) => $"{a}-{b}-{c}", + 1, "two", true); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe("1-two-True"); + } + + [Fact] + public async Task Task_with_three_args() + { + var result = await Try.Run( + (a, b, c) => Task.FromResult($"{a}-{b}-{c}"), + 1, "two", true); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe("1-two-True"); + } + } +}