extensions rework pt1
.NET Test / .NET tests (push) Successful in 2m0s

This commit is contained in:
2026-07-13 22:20:47 +04:00
parent 46487a4794
commit 0e7986996f
30 changed files with 1476 additions and 160 deletions
@@ -2,11 +2,11 @@ using System.Collections.Generic;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace Just.Railway.SourceGen; namespace Just.Railway.SourceGen.Executors;
public sealed class EnsureExtensionsExecutor : IGeneratorExecutor public sealed class EnsureExtensionsExecutor : IGeneratorExecutor
{ {
public void Execute(SourceProductionContext context, Compilation source) public void Execute(IncrementalGeneratorPostInitializationContext context)
{ {
var methods = GenerateMethods(); var methods = GenerateMethods();
var code = $$""" var code = $$"""
@@ -3,11 +3,13 @@ using System.Linq;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; 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 ExtensionType => "Append";
protected override string NamespaceName => "Just.Railway";
protected override string PartialClassName => "ResultExtensions";
protected override void GenerateHelperMethods(StringBuilder sb) protected override void GenerateHelperMethods(StringBuilder sb)
{ {
@@ -89,19 +91,17 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor
"""); """);
GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion);
GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
if (argCount < Constants.MaxResultTupleSize) if (argCount < Constants.MaxResultTupleSize)
{ {
GenerateExpandedMethods(sb, templateArgNames, resultTypeDef, resultValueExpansion); GenerateExpandedMethods(sb, templateArgNames, resultTypeDef, resultValueExpansion);
GenerateExpandedAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); GenerateExpandedAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion);
GenerateExpandedAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
} }
sb.AppendLine("#endregion"); sb.AppendLine("#endregion");
} }
private void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion) protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
{ {
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames); string methodTemplateDecl = GenerateTemplateDecl(templateArgNames);
@@ -192,7 +192,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor
"""); """);
} }
private void GenerateExpandedAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion) protected static void GenerateExpandedAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
{ {
var expandedTemplateArgNames = templateArgNames.Add("TNext"); var expandedTemplateArgNames = templateArgNames.Add("TNext");
string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames); string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames);
@@ -3,11 +3,13 @@ using System.Linq;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; 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 ExtensionType => "Bind";
protected override string NamespaceName => "Just.Railway";
protected override string PartialClassName => "ResultExtensions";
protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) 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("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion);
GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
sb.AppendLine("#endregion"); sb.AppendLine("#endregion");
} }
private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion) protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
{ {
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("R")); string methodTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("R"));
string bindTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result<R>")); string bindTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result<R>"));
@@ -5,11 +5,11 @@ using System.Linq;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace Just.Railway.SourceGen; namespace Just.Railway.SourceGen.Executors.ResultExtensions.MainExtensions;
internal sealed class ResultCombineExecutor : IGeneratorExecutor internal sealed class ResultCombineExecutor : IGeneratorExecutor
{ {
public void Execute(SourceProductionContext context, Compilation source) public void Execute(IncrementalGeneratorPostInitializationContext context)
{ {
var methods = GenerateCombineMethods(); var methods = GenerateCombineMethods();
var code = $$""" var code = $$"""
@@ -3,11 +3,13 @@ using System.Linq;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; 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 ExtensionType => "Extend";
protected override string NamespaceName => "Just.Railway";
protected override string PartialClassName => "ResultExtensions";
protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) 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("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion);
GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
sb.AppendLine("#endregion"); sb.AppendLine("#endregion");
} }
private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion) protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
{ {
var expandedTemplateArgNames = templateArgNames.Add("R"); var expandedTemplateArgNames = templateArgNames.Add("R");
string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames); string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames);
@@ -1,13 +1,17 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Just.Railway.SourceGen.Executors.ResultExtensions;
using Microsoft.CodeAnalysis; 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 ExtensionType => "Map";
protected override string NamespaceName => "Just.Railway";
protected override string PartialClassName => "ResultExtensions";
protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount)
{ {
var templateArgNames = Enumerable.Range(1, 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("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion);
GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
sb.AppendLine("#endregion"); sb.AppendLine("#endregion");
} }
private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion) protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
{ {
var methodTemplateArgNames = templateArgNames.Add("R"); var methodTemplateArgNames = templateArgNames.Add("R");
string methodTemplateDecl = GenerateTemplateDecl(methodTemplateArgNames); string methodTemplateDecl = GenerateTemplateDecl(methodTemplateArgNames);
@@ -3,11 +3,13 @@ using System.Linq;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; 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 ExtensionType => "Match";
protected override string NamespaceName => "Just.Railway";
protected override string PartialClassName => "ResultExtensions";
protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) 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("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion);
GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
sb.AppendLine("#endregion"); sb.AppendLine("#endregion");
} }
private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion) protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
{ {
var methodTemplateArgNames = templateArgNames.Add("R"); var methodTemplateArgNames = templateArgNames.Add("R");
string methodTemplateDecl = GenerateTemplateDecl(methodTemplateArgNames); string methodTemplateDecl = GenerateTemplateDecl(methodTemplateArgNames);
@@ -3,11 +3,13 @@ using System.Linq;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; 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 ExtensionType => "Tap";
protected override string NamespaceName => "Just.Railway";
protected override string PartialClassName => "ResultExtensions";
protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount)
{ {
@@ -24,7 +26,7 @@ internal sealed class ResultTapExecutor : ResultExtensionsExecutor
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(ResultTapExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(ResultTapExecutor)}}", "1.0.0.0")]
public static ref readonly {{resultTypeDef}} Tap{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Action{{methodTemplateDecl}}? onSuccess = null, Action<Error>? onFailure = null) public static {{resultTypeDef}} Tap{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Action{{methodTemplateDecl}}? onSuccess = null, Action<Error>? onFailure = null)
{ {
switch (result.State) switch (result.State)
{ {
@@ -37,17 +39,16 @@ internal sealed class ResultTapExecutor : ResultExtensionsExecutor
default: throw new ResultNotInitializedException(nameof(result)); default: throw new ResultNotInitializedException(nameof(result));
} }
return ref result; return result;
} }
"""); """);
GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion);
GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
sb.AppendLine("#endregion"); sb.AppendLine("#endregion");
} }
private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion) protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
{ {
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames); string methodTemplateDecl = GenerateTemplateDecl(templateArgNames);
string asyncActionTemplateDecl = GenerateTemplateDecl(templateArgNames.Add(taskType)); string asyncActionTemplateDecl = GenerateTemplateDecl(templateArgNames.Add(taskType));
@@ -2,15 +2,18 @@ using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Text; 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 ExtensionType => "TryRecover";
protected override string NamespaceName => "Just.Railway";
protected override string PartialClassName => "ResultExtensions";
protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount) protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount)
{ {
if (argCount > 1) return; if (argCount > 1) return;
var templateArgNames = Enumerable.Repeat("T", argCount) var templateArgNames = Enumerable.Repeat("T", argCount)
.ToImmutableArray(); .ToImmutableArray();
@@ -34,11 +37,11 @@ internal sealed class ResultTryRecoverExecutor : ResultExtensionsExecutor
"""); """);
GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, methodTemplateDecl); GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, methodTemplateDecl);
GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, methodTemplateDecl);
sb.AppendLine("#endregion"); sb.AppendLine("#endregion");
} }
private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string methodTemplateDecl)
protected static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string methodTemplateDecl)
{ {
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
@@ -2,11 +2,11 @@ using System.Collections.Immutable;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace Just.Railway.SourceGen; namespace Just.Railway.SourceGen.Executors.ResultExtensions;
internal abstract class ResultExtensionsExecutor : IGeneratorExecutor internal abstract class ResultExtensionsExecutor : IGeneratorExecutor
{ {
public void Execute(SourceProductionContext context, Compilation source) public void Execute(IncrementalGeneratorPostInitializationContext context)
{ {
var methods = GenerateMethods(); var methods = GenerateMethods();
var code = $$""" var code = $$"""
@@ -16,15 +16,15 @@ internal abstract class ResultExtensionsExecutor : IGeneratorExecutor
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using System.CodeDom.Compiler; using System.CodeDom.Compiler;
namespace Just.Railway; namespace {{NamespaceName}};
public static partial class ResultExtensions public static partial class {{PartialClassName}}
{ {
{{methods}} {{methods}}
} }
"""; """;
context.AddSource($"ResultExtensions.{ExtensionType}.g.cs", code); context.AddSource($"{PartialClassName}.{ExtensionType}.g.cs", code);
} }
private string GenerateMethods() private string GenerateMethods()
@@ -87,6 +87,8 @@ internal abstract class ResultExtensionsExecutor : IGeneratorExecutor
} }
protected abstract string ExtensionType { get; } protected abstract string ExtensionType { get; }
protected abstract string NamespaceName { get; }
protected abstract string PartialClassName { get; }
protected abstract void GenerateMethodsForArgCount(StringBuilder sb, int argCount); protected abstract void GenerateMethodsForArgCount(StringBuilder sb, int argCount);
protected virtual void GenerateHelperMethods(StringBuilder sb) {} protected virtual void GenerateHelperMethods(StringBuilder sb) {}
} }
@@ -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<string> 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");
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -3,11 +3,11 @@ using System.Linq;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace Just.Railway.SourceGen; namespace Just.Railway.SourceGen.Executors;
public sealed class TryExtensionsExecutor : IGeneratorExecutor public sealed class TryExtensionsExecutor : IGeneratorExecutor
{ {
public void Execute(SourceProductionContext context, Compilation source) public void Execute(IncrementalGeneratorPostInitializationContext context)
{ {
var methods = GenerateMethods(); var methods = GenerateMethods();
var code = $$""" 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, "Task", string.Empty);
GenerateAsyncMethods(sb, templateArgNames, actionTemplateDecl, funcTemplateDecl, argumentsDeclExpansion, argumentsExpansion, "ValueTask"); GenerateAsyncMethods(sb, templateArgNames, actionTemplateDecl, funcTemplateDecl, argumentsDeclExpansion, argumentsExpansion, "ValueTask", "ValueTask");
sb.AppendLine("#endregion"); sb.AppendLine("#endregion");
} }
private static void GenerateAsyncMethods(StringBuilder sb, ImmutableArray<string> templateArgNames, string actionTemplateDecl, string funcTemplateDecl, string argumentsDeclExpansion, string argumentsExpansion, string taskType) private static void GenerateAsyncMethods(StringBuilder sb, ImmutableArray<string> templateArgNames, string actionTemplateDecl, string funcTemplateDecl, string argumentsDeclExpansion, string argumentsExpansion, string taskType, string methodSuffix)
{ {
string actionTaskTemplateDecl = GenerateTemplateDecl(templateArgNames.Add(taskType)); string actionTaskTemplateDecl = GenerateTemplateDecl(templateArgNames.Add(taskType));
string resultActionTaskTemplateDecl = GenerateTemplateDecl(templateArgNames.Add($"{taskType}<Result>")); string resultActionTaskTemplateDecl = GenerateTemplateDecl(templateArgNames.Add($"{taskType}<Result>"));
@@ -138,7 +138,7 @@ public sealed class TryExtensionsExecutor : IGeneratorExecutor
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<Result> Run{{actionTemplateDecl}}(Func{{actionTaskTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}}) public static async {{taskType}}<Result> Run{{methodSuffix}}{{actionTemplateDecl}}(Func{{actionTaskTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}})
{ {
try try
{ {
@@ -155,7 +155,7 @@ public sealed class TryExtensionsExecutor : IGeneratorExecutor
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<Result> Run{{actionTemplateDecl}}(Func{{resultActionTaskTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}}) public static async {{taskType}}<Result> Run{{methodSuffix}}{{actionTemplateDecl}}(Func{{resultActionTaskTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}})
{ {
try try
{ {
@@ -171,7 +171,7 @@ public sealed class TryExtensionsExecutor : IGeneratorExecutor
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<Result<TResult>> Run{{funcTemplateDecl}}(Func{{funcTaskTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}}) public static async {{taskType}}<Result<TResult>> Run{{methodSuffix}}{{funcTemplateDecl}}(Func{{funcTaskTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}})
{ {
try try
{ {
@@ -187,7 +187,7 @@ public sealed class TryExtensionsExecutor : IGeneratorExecutor
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<Result<TResult>> Run{{funcTemplateDecl}}(Func{{resultFuncTaskTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}}) public static async {{taskType}}<Result<TResult>> Run{{methodSuffix}}{{funcTemplateDecl}}(Func{{resultFuncTaskTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}})
{ {
try try
{ {
@@ -1,4 +1,7 @@
using System.Collections.Generic; 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; using Microsoft.CodeAnalysis;
namespace Just.Railway.SourceGen; namespace Just.Railway.SourceGen;
@@ -6,9 +9,10 @@ namespace Just.Railway.SourceGen;
[Generator] [Generator]
public class ExtensionsMethodGenerator : IIncrementalGenerator public class ExtensionsMethodGenerator : IIncrementalGenerator
{ {
private readonly IEnumerable<IGeneratorExecutor> _executors = new IGeneratorExecutor[] private readonly IEnumerable<IGeneratorExecutor> _executors =
{ [
new ResultCombineExecutor(), new ResultCombineExecutor(),
new ResultMatchExecutor(), new ResultMatchExecutor(),
new ResultMapExecutor(), new ResultMapExecutor(),
new ResultBindExecutor(), new ResultBindExecutor(),
@@ -16,15 +20,24 @@ public class ExtensionsMethodGenerator : IIncrementalGenerator
new ResultExtendExecutor(), new ResultExtendExecutor(),
new ResultTryRecoverExecutor(), new ResultTryRecoverExecutor(),
new ResultAppendExecutor(), new ResultAppendExecutor(),
new ResultValueTaskMatchExecutor(),
new ResultValueTaskMapExecutor(),
new ResultValueTaskBindExecutor(),
new ResultValueTaskTapExecutor(),
new ResultValueTaskExtendExecutor(),
new ResultValueTaskTryRecoverExecutor(),
new ResultValueTaskAppendExecutor(),
new TryExtensionsExecutor(), new TryExtensionsExecutor(),
new EnsureExtensionsExecutor(), new EnsureExtensionsExecutor(),
}; ];
public void Initialize(IncrementalGeneratorInitializationContext context) public void Initialize(IncrementalGeneratorInitializationContext context)
{ {
foreach (var executor in _executors) foreach (var executor in _executors)
{ {
context.RegisterSourceOutput(context.CompilationProvider, executor.Execute); context.RegisterPostInitializationOutput(executor.Execute);
} }
} }
} }
@@ -4,5 +4,5 @@ namespace Just.Railway.SourceGen;
internal interface IGeneratorExecutor internal interface IGeneratorExecutor
{ {
public abstract void Execute(SourceProductionContext context, Compilation source); public abstract void Execute(IncrementalGeneratorPostInitializationContext context);
} }
+4 -2
View File
@@ -206,6 +206,7 @@ public sealed class ExpectedError : Error
{ {
public ExpectedError(string type, string message) public ExpectedError(string type, string message)
{ {
ArgumentNullException.ThrowIfNull(type);
Type = type; Type = type;
Message = message; Message = message;
} }
@@ -252,6 +253,7 @@ public sealed class ExceptionalError : Error
public ExceptionalError(string type, string message) public ExceptionalError(string type, string message)
{ {
ArgumentNullException.ThrowIfNull(type);
Type = type; Type = type;
Message = message; Message = message;
} }
@@ -343,7 +345,7 @@ public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
[Pure] public override string Type => "many_errors"; [Pure] public override string Type => "many_errors";
private string? _lazyMessage = null; 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<Error> errors) [Pure] private static string ToFullArrayString(in ImmutableArray<Error> errors)
{ {
@@ -428,7 +430,7 @@ public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
} }
private int? _lazyHashCode = null; 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<Error> errors) private static int CalcHashCode(in ImmutableArray<Error> errors)
{ {
if (errors.IsEmpty) if (errors.IsEmpty)
-26
View File
@@ -7,13 +7,9 @@ internal static class ReflectionHelper
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsEqual<T>(T? left, T? right) => TypeReflectionCache<T>.IsEqualFunc(left, right); public static bool IsEqual<T>(T? left, T? right) => TypeReflectionCache<T>.IsEqualFunc(left, right);
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Compare<T>(T? left, T? right) => TypeReflectionCache<T>.CompareFunc(left, right);
private static class TypeReflectionCache<T> private static class TypeReflectionCache<T>
{ {
public static readonly Func<T?, T?, bool> IsEqualFunc; public static readonly Func<T?, T?, bool> IsEqualFunc;
public static readonly Func<T?, T?, int> CompareFunc;
static TypeReflectionCache() static TypeReflectionCache()
{ {
@@ -34,33 +30,11 @@ internal static class ReflectionHelper
{ {
IsEqualFunc = static (left, right) => left is null ? right is null : left.Equals(right); 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<T?, T?, int>)Delegate.CreateDelegate(typeof(Func<T?, T?, int>), 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. #pragma warning disable CS8604 // Possible null reference argument.
[Pure] public static bool IsEqual<R>(R? left, R? right) where R : notnull, IEquatable<R>, T => left is null ? right is null : left.Equals(right); [Pure] public static bool IsEqual<R>(R? left, R? right) where R : notnull, IEquatable<R>, T => left is null ? right is null : left.Equals(right);
[Pure] public static bool IsEqualNullable<R>(R? left, R? right) where R : struct, IEquatable<R> => left is null ? right is null : right is not null && left.Value.Equals(right.Value); [Pure] public static bool IsEqualNullable<R>(R? left, R? right) where R : struct, IEquatable<R> => left is null ? right is null : right is not null && left.Value.Equals(right.Value);
[Pure] public static int Compare<R>(R? left, R? right) where R : notnull, IComparable<R>, T => left is null
? right is null ? 0 : -1
: right is null ? 1 : left.CompareTo(right);
[Pure] public static int CompareNullable<R>(R? left, R? right) where R : struct, IComparable<R> => 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. #pragma warning restore CS8604 // Possible null reference argument.
} }
} }
+4 -4
View File
@@ -127,9 +127,9 @@ public readonly partial struct Result : IEquatable<Result>
[Pure] public override bool Equals(object? obj) => obj is Result other && Equals(other); [Pure] public override bool Equals(object? obj) => obj is Result other && Equals(other);
[Pure] public bool Equals(Result other) [Pure] public bool Equals(Result other)
{ {
if (State == ResultState.Bottom) if (State == ResultState.Bottom || other.State == ResultState.Bottom)
throw new ResultNotInitializedException(); throw new ResultNotInitializedException();
return Error == other.Error; return Error == other.Error;
} }
[Pure] public static bool operator ==(Result left, Result right) => left.Equals(right); [Pure] public static bool operator ==(Result left, Result right) => left.Equals(right);
@@ -260,9 +260,9 @@ public readonly struct Result<T> : IEquatable<Result<T>>
[Pure] public override bool Equals(object? obj) => obj is Result<T> other && Equals(other); [Pure] public override bool Equals(object? obj) => obj is Result<T> other && Equals(other);
[Pure] public bool Equals(Result<T> other) [Pure] public bool Equals(Result<T> other)
{ {
if (State == ResultState.Bottom) if (State == ResultState.Bottom || other.State == ResultState.Bottom)
throw new ResultNotInitializedException(); throw new ResultNotInitializedException();
if (IsSuccess != other.IsSuccess) if (IsSuccess != other.IsSuccess)
return false; return false;
+140 -71
View File
@@ -4,9 +4,82 @@ namespace Just.Railway;
public static partial class ResultExtensions public static partial class ResultExtensions
{ {
#region Match (with fallback) #region Finally
public static T Match<T>(this in Result<T> result, Func<Error, T> 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<Result> Finally(this Task<Result> 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<Result> Finally(this Result result, Func<Task> 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<Result> Finally(this Task<Result> resultTask, Func<Task> 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<T>(this in Result<T> result, T fallback)
{
return result.State switch
{
ResultState.Success => result.Value,
ResultState.Error => fallback,
_ => throw new ResultNotInitializedException(nameof(result))
};
}
public static T ValueOr<T>(this in Result<T> result, Func<Error, T> fallback)
{ {
return result.State switch return result.State switch
{ {
@@ -16,7 +89,17 @@ public static partial class ResultExtensions
}; };
} }
public static async Task<T> Match<T>(this Result<T> result, Func<Error, Task<T>> fallback) public static async Task<T> ValueOr<T>(this Task<Result<T>> 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<T> ValueOr<T>(this Result<T> result, Func<Error, Task<T>> fallback)
{ {
return result.State switch return result.State switch
{ {
@@ -25,7 +108,7 @@ public static partial class ResultExtensions
_ => throw new ResultNotInitializedException(nameof(result)) _ => throw new ResultNotInitializedException(nameof(result))
}; };
} }
public static async Task<T> Match<T>(this Task<Result<T>> resultTask, Func<Error, T> fallback) public static async Task<T> ValueOr<T>(this Task<Result<T>> resultTask, Func<Error, T> fallback)
{ {
var result = await resultTask.ConfigureAwait(false); var result = await resultTask.ConfigureAwait(false);
return result.State switch return result.State switch
@@ -35,37 +118,7 @@ public static partial class ResultExtensions
_ => throw new ResultNotInitializedException(nameof(resultTask)) _ => throw new ResultNotInitializedException(nameof(resultTask))
}; };
} }
public static async Task<T> Match<T>(this Task<Result<T>> resultTask, Func<Error, Task<T>> fallback) public static async Task<T> ValueOr<T>(this Task<Result<T>> resultTask, Func<Error, Task<T>> 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<T> Match<T>(this Result<T> result, Func<Error, ValueTask<T>> 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<T> Match<T>(this ValueTask<Result<T>> resultTask, Func<Error, T> 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<T> Match<T>(this ValueTask<Result<T>> resultTask, Func<Error, ValueTask<T>> fallback)
{ {
var result = await resultTask.ConfigureAwait(false); var result = await resultTask.ConfigureAwait(false);
return result.State switch return result.State switch
@@ -83,79 +136,95 @@ public static partial class ResultExtensions
public static Result Merge(this IEnumerable<Result> results) public static Result Merge(this IEnumerable<Result> results)
{ {
ImmutableArray<Error>.Builder? errors = null; ImmutableArray<Error>.Builder? errors = null;
bool hasErrors = false;
foreach (var result in results.OrderBy(x => x.State)) foreach (var result in results)
{ {
switch (result.State) switch (result.State)
{ {
case ResultState.Error: case ResultState.Success:
hasErrors = true;
errors ??= ImmutableArray.CreateBuilder<Error>();
ManyErrors.AppendSanitized(errors, result.Error!);
break; break;
case ResultState.Success: case ResultState.Error:
if (hasErrors) goto afterLoop; errors ??= ImmutableArray.CreateBuilder<Error>();
ManyErrors.AppendSanitized(errors, result.Error!);
break; break;
default: throw new ResultNotInitializedException(nameof(results)); default: throw new ResultNotInitializedException(nameof(results));
} }
} }
afterLoop:
return hasErrors return errors is null
? new(new ManyErrors(errors!.ToImmutable())) ? new(null)
: new(null); : new(new ManyErrors(errors.ToImmutable()));
} }
public static async Task<Result> Merge(this IEnumerable<Task<Result>> tasks) public static async Task<Result> Merge(this IEnumerable<Task<Result>> 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(); return results.Merge();
} }
public static Result<IEnumerable<T>> Merge<T>(this IEnumerable<Result<T>> results) public static Result<IEnumerable<T>> Merge<T>(this IEnumerable<Result<T>> results)
{ {
ImmutableList<T>.Builder? values = null;
ImmutableArray<Error>.Builder? errors = null; ImmutableArray<Error>.Builder? errors = null;
bool hasErrors = false; ImmutableArray<T>.Builder? values = null;
foreach (var result in results.OrderBy(x => x.State)) foreach (var result in results)
{ {
switch (result.State) switch (result.State)
{ {
case ResultState.Success:
values ??= ImmutableArray.CreateBuilder<T>();
values.Add(result.Value);
break;
case ResultState.Error: case ResultState.Error:
hasErrors = true;
errors ??= ImmutableArray.CreateBuilder<Error>(); errors ??= ImmutableArray.CreateBuilder<Error>();
ManyErrors.AppendSanitized(errors, result.Error!); ManyErrors.AppendSanitized(errors, result.Error!);
break; break;
case ResultState.Success:
if (hasErrors) goto afterLoop;
values ??= ImmutableList.CreateBuilder<T>();
values.Add(result.Value);
break;
default: throw new ResultNotInitializedException(nameof(results)); default: throw new ResultNotInitializedException(nameof(results));
} }
} }
afterLoop:
return hasErrors return errors is null
? new(new ManyErrors(errors!.ToImmutable())) ? new(values?.ToImmutable() ?? Enumerable.Empty<T>())
: new(values is not null ? values.ToImmutable() : ImmutableList<T>.Empty); : new(new ManyErrors(errors.ToImmutable()));
} }
public static async Task<Result<IEnumerable<T>>> Merge<T>(this IEnumerable<Task<Result<T>>> tasks) public static async Task<Result<IEnumerable<T>>> Merge<T>(this IEnumerable<Task<Result<T>>> tasks)
{ {
var taskList = tasks.ToList(); var taskList = tasks.ToArray();
var results = new Result<T>[taskList.Count]; var results = new Result<T>[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); var task = taskList[i];
taskList.Remove(task); results[i] = task.Status switch
{
results[i] = task.IsCompletedSuccessfully TaskStatus.RanToCompletion => task.Result,
? task.Result TaskStatus.Faulted => Result.Failure<T>(task.Exception!),
: task.Exception!; TaskStatus.Canceled => Result.Failure<T>(Error.New("task_canceled", "Task was canceled")),
_ => Result.Failure<T>(Error.New("task_unexpected", $"Task in unexpected state: {task.Status}"))
};
} }
return results.Merge(); return results.Merge();
+4
View File
@@ -1,5 +1,9 @@
namespace Just.Railway; namespace Just.Railway;
/// <summary>
/// Result based try/catch
/// </summary>
/// <remarks>All extension methods are source generated.</remarks>
public static partial class Try public static partial class Try
{ {
} }
@@ -0,0 +1,98 @@
namespace Just.Railway.ValueTaskExtensions;
public static partial class ResultValueTaskExtensions
{
#region Finally
public static async ValueTask<Result> ValueTaskFinally(this ValueTask<Result> 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<Result> ValueTaskFinally(this Result result, Func<ValueTask> 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<Result> ValueTaskFinally(this ValueTask<Result> resultTask, Func<ValueTask> 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<T> ValueTaskValueOr<T>(this ValueTask<Result<T>> 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<T> ValueTaskValueOr<T>(this Result<T> result, Func<Error, ValueTask<T>> 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<T> ValueTaskValueOr<T>(this ValueTask<Result<T>> resultTask, Func<Error, T> 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<T> ValueTaskValueOr<T>(this ValueTask<Result<T>> resultTask, Func<Error, ValueTask<T>> 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
}
@@ -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<EquatableStruct>
{
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<int?>(42, 42).ShouldBeTrue();
[Fact] public void Different_values() =>
Just.Railway.ReflectionHelper.IsEqual<int?>(42, 43).ShouldBeFalse();
[Fact] public void Both_null() =>
Just.Railway.ReflectionHelper.IsEqual<int?>(null, null).ShouldBeTrue();
[Fact] public void One_null_one_value() =>
Just.Railway.ReflectionHelper.IsEqual<int?>(42, null).ShouldBeFalse();
[Fact] public void Null_and_value_reversed() =>
Just.Railway.ReflectionHelper.IsEqual<int?>(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<string>(null, null).ShouldBeTrue();
[Fact] public void Left_null_right_value() =>
Just.Railway.ReflectionHelper.IsEqual<string>(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<Error>(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<EquatableRecord>(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<NonEquatableClass>(null, null).ShouldBeTrue();
[Fact] public void Left_null() =>
Just.Railway.ReflectionHelper.IsEqual<NonEquatableClass>(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<EquatableStruct?>(
new EquatableStruct(1),
new EquatableStruct(1)
).ShouldBeTrue();
[Fact] public void Different_values() =>
Just.Railway.ReflectionHelper.IsEqual<EquatableStruct?>(
new EquatableStruct(1),
new EquatableStruct(2)
).ShouldBeFalse();
[Fact] public void Both_null() =>
Just.Railway.ReflectionHelper.IsEqual<EquatableStruct?>(null, null).ShouldBeTrue();
[Fact] public void One_null() =>
Just.Railway.ReflectionHelper.IsEqual<EquatableStruct?>(new EquatableStruct(1), null)
.ShouldBeFalse();
}
}
+185
View File
@@ -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<ResultNotInitializedException>(() => success.Equals(bottom));
}
[Fact]
public void Result_Default_ShouldNotEqual_Success()
{
Result bottom = default;
var success = Result.Success();
Should.Throw<ResultNotInitializedException>(() => bottom.Equals(success));
}
[Fact]
public void Result_Success_EqualsOperator_WithDefault()
{
var success = Result.Success();
Result bottom = default;
Should.Throw<ResultNotInitializedException>(() => success == bottom);
}
[Fact]
public void Result_Error_Equals_WithBottom_ShouldThrow()
{
var error = Result.Failure("test");
Result bottom = default;
Should.Throw<ResultNotInitializedException>(() => error.Equals(bottom));
}
[Fact]
public void Result_Bottom_Equals_WithSuccess_ShouldThrow()
{
Result bottom = default;
var success = Result.Success();
Should.Throw<ResultNotInitializedException>(() => bottom.Equals(success));
}
#endregion
#region BUG-2: Result<T>.Equals asymmetry
[Fact]
public void ResultOfT_Success_ShouldNotEqual_Default()
{
var success = Result.Success(42);
Result<int> bottom = default;
Should.Throw<ResultNotInitializedException>(() => success.Equals(bottom));
}
[Fact]
public void ResultOfT_Error_ShouldNotEqual_Default()
{
var error = Result.Failure<int>("test");
Result<int> bottom = default;
Should.Throw<ResultNotInitializedException>(() => error.Equals(bottom));
}
[Fact]
public void ResultOfT_Default_ShouldNotEqual_Success()
{
Result<int> bottom = default;
var success = Result.Success(42);
Should.Throw<ResultNotInitializedException>(() => 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<Result>[]
{
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<Result>(cts.Token);
var tasks = new Task<Result>[]
{
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<T> canceled task + O(n²)
[Fact]
public async Task MergeOfT_Tasks_WhenOneIsCanceled_ShouldReturnError_NotThrow()
{
var cts = new CancellationTokenSource();
cts.Cancel();
var canceledTask = Task.FromCanceled<Result<int>>(cts.Token);
var tasks = new Task<Result<int>>[]
{
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<Result<int>>[]
{
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<ArgumentNullException>(() => Error.New(null!, "some message"));
}
#endregion
}
+2 -1
View File
@@ -37,6 +37,7 @@ public class GeneralUsage
var result = Result.Success() var result = Result.Success()
.Append(() => Result.Failure<int>(error)) .Append(() => Result.Failure<int>(error))
.Append("test") .Append("test")
.Tap(onSuccess: (i, s) => true.ShouldBeFalse("should not reach this path"))
.Map((i, s) => .Map((i, s) =>
{ {
true.ShouldBeFalse("should not reach this path"); true.ShouldBeFalse("should not reach this path");
@@ -85,7 +86,7 @@ public class GeneralUsage
return ""; return "";
} }
); );
result.ShouldBe("TEST_1;SOME"); result.ShouldBe("TEST_1;SOME");
} }
+558
View File
@@ -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<int>(x => captured = x, 42);
result.IsSuccess.ShouldBeTrue();
captured.ShouldBe(42);
}
[Fact]
public void One_arg_exception()
{
var result = Try.Run(new Action<int>(_ => 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<int, string, bool, double, char>(
(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<int, string, bool, double, char>((_, _, _, _, _) => throw new InvalidOperationException("boom")),
1, "x", true, 3.14, 'z');
result.IsFailure.ShouldBeTrue();
result.Error!.Message.ShouldBe("boom");
}
}
// ═══════════════════════════════════════════════════════════
// Func<Result> — 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<Result>(() => throw new InvalidOperationException("boom")));
result.IsFailure.ShouldBeTrue();
result.Error!.Message.ShouldBe("boom");
}
[Fact]
public void One_arg_passes_through()
{
var result = Try.Run<int>(x => Result.Success(x), 42);
result.IsSuccess.ShouldBeTrue();
}
}
// ═══════════════════════════════════════════════════════════
// Func<T> — 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<int>(() => 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<int, string>(_ => 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<Result<T>> — 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<int>(error));
result.IsFailure.ShouldBeTrue();
result.Error.ShouldBe(error);
}
[Fact]
public void No_args_exception_caught()
{
var result = Try.Run(new Func<Result<int>>(() => 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<Task<Result>>
// ═══════════════════════════════════════════════════════════
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<Task<Result>>(() => throw new InvalidOperationException("boom")));
result.IsFailure.ShouldBeTrue();
result.Error!.Message.ShouldBe("boom");
}
}
// ═══════════════════════════════════════════════════════════
// Task async — Func<Task<T>>
// ═══════════════════════════════════════════════════════════
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<Task<int>>(() => throw new InvalidOperationException("boom")));
result.IsFailure.ShouldBeTrue();
result.Error!.Message.ShouldBe("boom");
}
}
// ═══════════════════════════════════════════════════════════
// Task async — Func<Task<Result<T>>>
// ═══════════════════════════════════════════════════════════
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<int>(error)));
result.IsFailure.ShouldBeTrue();
result.Error.ShouldBe(error);
}
[Fact]
public async Task No_args_exception_caught()
{
var result = await Try.Run(new Func<Task<Result<int>>>(() => 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<ValueTask<Result>>
// ═══════════════════════════════════════════════════════════
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<ValueTask<Result>>(() => throw new InvalidOperationException("boom")));
result.IsFailure.ShouldBeTrue();
result.Error!.Message.ShouldBe("boom");
}
}
// ═══════════════════════════════════════════════════════════
// ValueTask async — Func<ValueTask<T>>
// ═══════════════════════════════════════════════════════════
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<int>(() => throw new InvalidOperationException("boom"));
result.IsFailure.ShouldBeTrue();
result.Error!.Message.ShouldBe("boom");
}
}
// ═══════════════════════════════════════════════════════════
// ValueTask async — Func<ValueTask<Result<T>>>
// ═══════════════════════════════════════════════════════════
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<int>(error)));
result.IsFailure.ShouldBeTrue();
result.Error.ShouldBe(error);
}
[Fact]
public async Task No_args_exception_caught()
{
var result = await Try.RunValueTask(new Func<ValueTask<Result<int>>>(() => 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<int>(() => 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<int>(() => 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<int>(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<int>(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<string?>(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");
}
}
}