Compare commits
11 Commits
v1.0.0-rc1
...
v1.0.0-rc4
| Author | SHA1 | Date | |
|---|---|---|---|
| b8ea74ec5b | |||
| 9ae185342b | |||
| bb8c2135b5 | |||
| 26a1c604d5 | |||
| 036b34d3c0 | |||
| f39b899514 | |||
| b79192ec6c | |||
| 0452b4f867 | |||
| 6726ba07b3 | |||
| ac18863426 | |||
| db2fa49ff5 |
@@ -24,9 +24,6 @@ jobs:
|
||||
- name: Setup nuget source
|
||||
run: dotnet nuget add source --name gitea_registry https://gitea.jstdev.ru/api/packages/just/nuget/index.json
|
||||
|
||||
- name: Parsing release tag
|
||||
run: echo ${{ gitea.ref_name }} | sed -E 's|^(v([0-9]+(\.[0-9]+){2}))(-([a-z0-9]+)){1}|/p:ReleaseVersion=\2 /p:VersionSuffix=\5|; s|^(v([0-9]+(\.[0-9]+){2}))$|/p:ReleaseVersion=\2|'
|
||||
|
||||
- name: Create the package
|
||||
env:
|
||||
RELEASE_VERSION: ${{ gitea.ref_name }}
|
||||
|
||||
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Railway", "Railway\Railway.
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Raliway.Tests", "Raliway.Tests\Raliway.Tests.csproj", "{607F91E4-83A2-48C4-BAC2-2205BEE81D93}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Railway.SourceGenerator", "Railway.SourceGenerator\Railway.SourceGenerator.csproj", "{1A3B8F0A-7A30-4AA8-BC15-47FA2D75B6BF}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -24,5 +26,9 @@ Global
|
||||
{607F91E4-83A2-48C4-BAC2-2205BEE81D93}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{607F91E4-83A2-48C4-BAC2-2205BEE81D93}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{607F91E4-83A2-48C4-BAC2-2205BEE81D93}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1A3B8F0A-7A30-4AA8-BC15-47FA2D75B6BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1A3B8F0A-7A30-4AA8-BC15-47FA2D75B6BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1A3B8F0A-7A30-4AA8-BC15-47FA2D75B6BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1A3B8F0A-7A30-4AA8-BC15-47FA2D75B6BF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
106
README.md
106
README.md
@@ -4,6 +4,108 @@ This library uses features of C# to achieve railway-oriented programming.
|
||||
|
||||
The desire is to make somewhat user-friendly experience while using result-object pattern.
|
||||
|
||||
## Contents
|
||||
## Features
|
||||
|
||||
_Coming soon..._
|
||||
- Immutable ```Error``` class
|
||||
- ```Result``` object
|
||||
- A bunch of extensions to use result-object pattern with
|
||||
- ```Try``` extensions to wrap function calls with result-object
|
||||
- ```Ensure``` extensions to utilize result-object in validation scenarios
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Install from local Gitea package repository
|
||||
|
||||
```sh
|
||||
# Setup NuGet registry from the command line
|
||||
dotnet nuget add source --name gitea_jstdev https://gitea.jstdev.ru/api/packages/just/nuget/index.json
|
||||
# then install the package using NuGet
|
||||
dotnet add package --source gitea_jstdev Just.Railway
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Error
|
||||
|
||||
```csharp
|
||||
using Just.Railway;
|
||||
Error expectedError = Error.New(type: "Some Error", message: "Some error detail");
|
||||
Error exceptionalError = Error.New(new Exception("Some Exception"));
|
||||
Error manyErrors = Error.Many(expectedError, exceptionalError);
|
||||
// the same result while using .Append(..) or +
|
||||
manyErrors = expectedError.Append(exceptionalError);
|
||||
manyErrors = expectedError + exceptionalError;
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> You can easily serialize/deserialize Error to and from JSON
|
||||
|
||||
### Result
|
||||
|
||||
#### As return value:
|
||||
|
||||
```csharp
|
||||
Result Foo()
|
||||
{
|
||||
// ...
|
||||
if (SomeCondition())
|
||||
return Result.Failure(Error.New("Some Error"));
|
||||
// or just: return Error.New("Some Error");
|
||||
// ...
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
Result<T> Bar()
|
||||
{
|
||||
T value;
|
||||
// ...
|
||||
if (SomeCondition())
|
||||
return Error.New("Some Error");
|
||||
// ...
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
#### Consume Result object
|
||||
|
||||
```csharp
|
||||
Result<int> result = GetResult();
|
||||
|
||||
var value = result
|
||||
.Append("new")
|
||||
.Map((i, s) => $"{s} result {i}")
|
||||
.Match(
|
||||
onSuccess: x => x,
|
||||
onFailure: err => err.ToString()
|
||||
);
|
||||
// value: "new result 1"
|
||||
|
||||
Result<int> GetResult() => Result.Success(1);
|
||||
```
|
||||
|
||||
### Try
|
||||
|
||||
```csharp
|
||||
Result result = Try.Run(SomeAction);
|
||||
// you can pass up to 5 arguments like this
|
||||
result = Try.Run(SomeActionWithArguments, 1, 2.0, "3");
|
||||
|
||||
// you also can call functions
|
||||
Result<int> resultWithValue = Try.Run(SomeFunction);
|
||||
|
||||
void SomeAction() {}
|
||||
void SomeActionWithArguments(int a1, double a2, string? a3) {}
|
||||
int SomeFunction() => 1;
|
||||
```
|
||||
|
||||
### Ensure
|
||||
|
||||
```csharp
|
||||
var value = GetValue();
|
||||
Result<int> result = Ensure.That(value)
|
||||
.NotNull()
|
||||
.Satisfies(i => i < 100)
|
||||
.Result();
|
||||
|
||||
int? GetValue() => 1;
|
||||
```
|
||||
|
||||
6
Railway.SourceGenerator/Constants.cs
Normal file
6
Railway.SourceGenerator/Constants.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
internal static class Constants
|
||||
{
|
||||
public const int MaxResultTupleSize = 5;
|
||||
}
|
||||
209
Railway.SourceGenerator/EnsureExtensionsExecutor.cs
Normal file
209
Railway.SourceGenerator/EnsureExtensionsExecutor.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
public sealed class EnsureExtensionsExecutor : IGeneratorExecutor
|
||||
{
|
||||
public void Execute(SourceProductionContext context, Compilation source)
|
||||
{
|
||||
var methods = GenerateMethods();
|
||||
var code = $$"""
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.CodeDom.Compiler;
|
||||
|
||||
namespace Just.Railway;
|
||||
|
||||
public static partial class Ensure
|
||||
{
|
||||
{{methods}}
|
||||
}
|
||||
""";
|
||||
|
||||
context.AddSource("Ensure.Extensions.g.cs", code);
|
||||
}
|
||||
|
||||
private string GenerateMethods()
|
||||
{
|
||||
List<(string ErrorParameterDecl, string ErrorValueExpr)> errorGenerationDefinitions =
|
||||
[
|
||||
("Error error = default!", "error"),
|
||||
("ErrorFactory errorFactory", "errorFactory(ensure.ValueExpression)")
|
||||
];
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"#region Satisfies");
|
||||
errorGenerationDefinitions.ForEach(def => GenerateSatisfiesExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
|
||||
sb.AppendLine("#endregion");
|
||||
|
||||
sb.AppendLine($"#region NotNull");
|
||||
errorGenerationDefinitions.ForEach(def => GenerateNotNullExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
|
||||
sb.AppendLine("#endregion");
|
||||
|
||||
sb.AppendLine($"#region NotEmpty");
|
||||
errorGenerationDefinitions.ForEach(def => GenerateNotEmptyExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
|
||||
sb.AppendLine("#endregion");
|
||||
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void GenerateNotEmptyExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
|
||||
{
|
||||
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is empty.\")";
|
||||
List<(string TemplateDef, string CollectionType, string NotEmptyTest)> typeOverloads =
|
||||
[
|
||||
("<T>", "IEnumerable<T>", "ensure.Value?.Any() == true"),
|
||||
("<T>", "ICollection<T>", "ensure.Value?.Count > 0"),
|
||||
("<T>", "IReadOnlyCollection<T>", "ensure.Value?.Count > 0"),
|
||||
("<T>", "IList<T>", "ensure.Value?.Count > 0"),
|
||||
("<T>", "IReadOnlyList<T>", "ensure.Value?.Count > 0"),
|
||||
("<T>", "ISet<T>", "ensure.Value?.Count > 0"),
|
||||
("<T>", "IReadOnlySet<T>", "ensure.Value?.Count > 0"),
|
||||
("<TKey,TValue>", "IDictionary<TKey,TValue>", "ensure.Value?.Count > 0"),
|
||||
("<TKey,TValue>", "IReadOnlyDictionary<TKey,TValue>", "ensure.Value?.Count > 0"),
|
||||
("<T>", "T[]", "ensure.Value?.Length > 0"),
|
||||
("<T>", "List<T>", "ensure.Value?.Count > 0"),
|
||||
("<T>", "Queue<T>", "ensure.Value?.Count > 0"),
|
||||
("<T>", "HashSet<T>", "ensure.Value?.Count > 0"),
|
||||
("", "string", "!string.IsNullOrEmpty(ensure.Value)"),
|
||||
];
|
||||
|
||||
typeOverloads.ForEach(def => sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static Ensure<{{def.CollectionType}}> NotEmpty{{def.TemplateDef}}(this in Ensure<{{def.CollectionType}}> ensure, {{errorParameterDecl}})
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => {{def.NotEmptyTest}}
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
"""));
|
||||
}
|
||||
|
||||
private void GenerateNotNullExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
|
||||
{
|
||||
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is null.\")";
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static Ensure<T> NotNull<T>(this in Ensure<T?> ensure, {{errorParameterDecl}})
|
||||
where T : struct
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value.HasValue
|
||||
? new(ensure.Value.Value, ensure.ValueExpression)
|
||||
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
""");
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static Ensure<T> NotNull<T>(this in Ensure<T?> ensure, {{errorParameterDecl}})
|
||||
where T : notnull
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value is not null
|
||||
? new(ensure.Value!, ensure.ValueExpression)
|
||||
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
private void GenerateSatisfiesExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
|
||||
{
|
||||
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} does not satisfy the requirement.\")";
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static Ensure<T> Satisfies<T>(this in Ensure<T> ensure, Func<T, bool> requirement, {{errorParameterDecl}})
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => requirement(ensure.Value)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
GenerateSatisfiesAsyncExtensions(sb, "Task", errorParameterDecl, errorValueExpr, defaultErrorExpr);
|
||||
GenerateSatisfiesAsyncExtensions(sb, "ValueTask", errorParameterDecl, errorValueExpr, defaultErrorExpr);
|
||||
}
|
||||
|
||||
private void GenerateSatisfiesAsyncExtensions(StringBuilder sb, string taskType, string errorParameterDecl, string errorValueExpr, string defaultErrorExpr)
|
||||
{
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Ensure<T>> Satisfies<T>(this {{taskType}}<Ensure<T>> ensureTask, Func<T, bool> requirement, {{errorParameterDecl}})
|
||||
{
|
||||
var ensure = await ensureTask.ConfigureAwait(false);
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => requirement(ensure.Value)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensureTask))
|
||||
};
|
||||
}
|
||||
""");
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Ensure<T>> Satisfies<T>(this Ensure<T> ensure, Func<T, {{taskType}}<bool>> requirement, {{errorParameterDecl}})
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => await requirement(ensure.Value).ConfigureAwait(false)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
""");
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Ensure<T>> Satisfies<T>(this {{taskType}}<Ensure<T>> ensureTask, Func<T, {{taskType}}<bool>> requirement, {{errorParameterDecl}})
|
||||
{
|
||||
var ensure = await ensureTask.ConfigureAwait(false);
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => await requirement(ensure.Value).ConfigureAwait(false)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensureTask))
|
||||
};
|
||||
}
|
||||
""");
|
||||
}
|
||||
}
|
||||
32
Railway.SourceGenerator/ExtensionsMethodGenerator.cs
Normal file
32
Railway.SourceGenerator/ExtensionsMethodGenerator.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
[Generator]
|
||||
public class ExtensionsMethodGenerator : IIncrementalGenerator
|
||||
{
|
||||
private readonly IEnumerable<IGeneratorExecutor> _executors = new IGeneratorExecutor[]
|
||||
{
|
||||
new ResultCombineExecutor(),
|
||||
new ResultMatchExecutor(),
|
||||
new ResultMapExecutor(),
|
||||
new ResultBindExecutor(),
|
||||
new ResultTapExecutor(),
|
||||
new ResultAppendExecutor(),
|
||||
new TryExtensionsExecutor(),
|
||||
new EnsureExtensionsExecutor(),
|
||||
};
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
foreach (var executor in _executors)
|
||||
{
|
||||
context.RegisterSourceOutput(context.CompilationProvider, executor.Execute);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Railway.SourceGenerator/IGeneratorExecutor.cs
Normal file
9
Railway.SourceGenerator/IGeneratorExecutor.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
internal interface IGeneratorExecutor
|
||||
{
|
||||
public abstract void Execute(SourceProductionContext context, Compilation source);
|
||||
}
|
||||
20
Railway.SourceGenerator/Railway.SourceGenerator.csproj
Normal file
20
Railway.SourceGenerator/Railway.SourceGenerator.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Just.Railway.SourceGen</RootNamespace>
|
||||
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
473
Railway.SourceGenerator/ResultAppendExecutor.cs
Normal file
473
Railway.SourceGenerator/ResultAppendExecutor.cs
Normal file
@@ -0,0 +1,473 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
internal sealed class ResultAppendExecutor : ResultExtensionsExecutor
|
||||
{
|
||||
protected override string ExtensionType => "Append";
|
||||
|
||||
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);
|
||||
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames);
|
||||
|
||||
sb.AppendLine($"#region {resultTypeDef}");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static {{resultTypeDef}} Append{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Result next)
|
||||
{
|
||||
Error? error = null;
|
||||
if ((result.State & next.State) == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State)));
|
||||
}
|
||||
|
||||
if (result.IsFailure)
|
||||
{
|
||||
error += result.Error;
|
||||
}
|
||||
if (next.IsFailure)
|
||||
{
|
||||
error += next.Error;
|
||||
}
|
||||
|
||||
return error is null
|
||||
? Result.Success({{resultValueExpansion}})
|
||||
: error;
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static {{resultTypeDef}} Append{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func<Result> nextFunc)
|
||||
{
|
||||
if (result.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(result));
|
||||
}
|
||||
else if (result.IsFailure)
|
||||
{
|
||||
return result.Error!;
|
||||
}
|
||||
|
||||
var next = nextFunc();
|
||||
if (next.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(nextFunc));
|
||||
}
|
||||
else if (next.IsFailure)
|
||||
{
|
||||
return next.Error!;
|
||||
}
|
||||
|
||||
return Result.Success({{resultValueExpansion}});
|
||||
}
|
||||
""");
|
||||
|
||||
GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
|
||||
if (argCount < Constants.MaxResultTupleSize)
|
||||
{
|
||||
GenerateExpandedMethods(sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
GenerateExpandedAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
GenerateExpandedAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
}
|
||||
|
||||
sb.AppendLine("#endregion");
|
||||
}
|
||||
|
||||
private void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
|
||||
{
|
||||
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames);
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultTypeDef}}> Append{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<Result> nextFunc)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
if (result.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(result));
|
||||
}
|
||||
else if (result.IsFailure)
|
||||
{
|
||||
return result.Error!;
|
||||
}
|
||||
|
||||
var next = nextFunc();
|
||||
if (next.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(nextFunc));
|
||||
}
|
||||
else if (next.IsFailure)
|
||||
{
|
||||
return next.Error!;
|
||||
}
|
||||
|
||||
return Result.Success({{resultValueExpansion}});
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultTypeDef}}> Append{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func<{{taskType}}<Result>> nextFunc)
|
||||
{
|
||||
if (result.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(result));
|
||||
}
|
||||
else if (result.IsFailure)
|
||||
{
|
||||
return result.Error!;
|
||||
}
|
||||
|
||||
var next = await nextFunc().ConfigureAwait(false);
|
||||
if (next.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(nextFunc));
|
||||
}
|
||||
else if (next.IsFailure)
|
||||
{
|
||||
return next.Error!;
|
||||
}
|
||||
|
||||
return Result.Success({{resultValueExpansion}});
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultTypeDef}}> Append{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<{{taskType}}<Result>> nextFunc)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
if (result.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(resultTask));
|
||||
}
|
||||
else if (result.IsFailure)
|
||||
{
|
||||
return result.Error!;
|
||||
}
|
||||
|
||||
var next = await nextFunc().ConfigureAwait(false);
|
||||
if (next.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(nextFunc));
|
||||
}
|
||||
else if (next.IsFailure)
|
||||
{
|
||||
return next.Error!;
|
||||
}
|
||||
|
||||
return Result.Success({{resultValueExpansion}});
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
private void GenerateExpandedAsyncMethods(string taskType, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
|
||||
{
|
||||
var expandedTemplateArgNames = templateArgNames.Add("TNext");
|
||||
string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames);
|
||||
string methodExpandedTemplateDecl = GenerateTemplateDecl(expandedTemplateArgNames);
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, TNext next)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => Result.Success({{JoinArguments(resultValueExpansion, "next")}}),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Result<TNext> next)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
if ((result.State & next.State) == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State)));
|
||||
}
|
||||
|
||||
Error? error = null;
|
||||
if (result.IsFailure)
|
||||
{
|
||||
error += result.Error;
|
||||
}
|
||||
if (next.IsFailure)
|
||||
{
|
||||
error += next.Error;
|
||||
}
|
||||
|
||||
return error is null
|
||||
? Result.Success({{JoinArguments(resultValueExpansion, "next.Value")}})
|
||||
: error;
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<TNext> nextFunc)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => Result.Success({{JoinArguments(resultValueExpansion, "nextFunc()")}}),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{resultTypeDef}} result, Func<{{taskType}}<TNext>> nextFunc)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => Result.Success({{JoinArguments(resultValueExpansion, "await nextFunc().ConfigureAwait(false)")}}),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<{{taskType}}<TNext>> nextFunc)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => Result.Success({{JoinArguments(resultValueExpansion, "await nextFunc().ConfigureAwait(false)")}}),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<Result<TNext>> nextFunc)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
if (result.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(resultTask));
|
||||
}
|
||||
else if (result.IsFailure)
|
||||
{
|
||||
return result.Error!;
|
||||
}
|
||||
|
||||
var next = nextFunc();
|
||||
if (next.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(nextFunc));
|
||||
}
|
||||
else if (next.IsFailure)
|
||||
{
|
||||
return next.Error!;
|
||||
}
|
||||
|
||||
return Result.Success({{JoinArguments(resultValueExpansion, "next.Value")}});
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{resultTypeDef}} result, Func<{{taskType}}<Result<TNext>>> nextFunc)
|
||||
{
|
||||
if (result.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(result));
|
||||
}
|
||||
else if (result.IsFailure)
|
||||
{
|
||||
return result.Error!;
|
||||
}
|
||||
|
||||
var next = await nextFunc().ConfigureAwait(false);
|
||||
if (next.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(nextFunc));
|
||||
}
|
||||
else if (next.IsFailure)
|
||||
{
|
||||
return next.Error!;
|
||||
}
|
||||
|
||||
return Result.Success({{JoinArguments(resultValueExpansion, "next.Value")}});
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<{{taskType}}<Result<TNext>>> nextFunc)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
if (result.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(resultTask));
|
||||
}
|
||||
else if (result.IsFailure)
|
||||
{
|
||||
return result.Error!;
|
||||
}
|
||||
|
||||
var next = await nextFunc().ConfigureAwait(false);
|
||||
if (next.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(nextFunc));
|
||||
}
|
||||
else if (next.IsFailure)
|
||||
{
|
||||
return next.Error!;
|
||||
}
|
||||
|
||||
return Result.Success({{JoinArguments(resultValueExpansion, "next.Value")}});
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
private static void GenerateExpandedMethods(StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
|
||||
{
|
||||
var expandedTemplateArgNames = templateArgNames.Add("TNext");
|
||||
string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames);
|
||||
string methodExpandedTemplateDecl = GenerateTemplateDecl(expandedTemplateArgNames);
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static {{resultExpandedTypeDef}} Append{{methodExpandedTemplateDecl}}(this in {{resultTypeDef}} result, TNext next)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => Result.Success({{JoinArguments(resultValueExpansion, "next")}}),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static {{resultExpandedTypeDef}} Append{{methodExpandedTemplateDecl}}(this in {{resultTypeDef}} result, Func<TNext> nextFunc)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => Result.Success({{JoinArguments(resultValueExpansion, "nextFunc()")}}),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static {{resultExpandedTypeDef}} Append{{methodExpandedTemplateDecl}}(this in {{resultTypeDef}} result, Result<TNext> next)
|
||||
{
|
||||
Error? error = null;
|
||||
if ((result.State & next.State) == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State)));
|
||||
}
|
||||
|
||||
if (result.IsFailure)
|
||||
{
|
||||
error += result.Error;
|
||||
}
|
||||
if (next.IsFailure)
|
||||
{
|
||||
error += next.Error;
|
||||
}
|
||||
|
||||
return error is null
|
||||
? Result.Success({{JoinArguments(resultValueExpansion, "next.Value")}})
|
||||
: error;
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
|
||||
public static {{resultExpandedTypeDef}} Append{{methodExpandedTemplateDecl}}(this in {{resultTypeDef}} result, Func<Result<TNext>> nextFunc)
|
||||
{
|
||||
if (result.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(result));
|
||||
}
|
||||
else if (result.IsFailure)
|
||||
{
|
||||
return result.Error!;
|
||||
}
|
||||
|
||||
var next = nextFunc();
|
||||
if (next.State == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(nameof(nextFunc));
|
||||
}
|
||||
else if (next.IsFailure)
|
||||
{
|
||||
return next.Error!;
|
||||
}
|
||||
|
||||
return Result.Success({{JoinArguments(resultValueExpansion, "next.Value")}});
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
internal static string JoinArguments(string arg1, string arg2) => (arg1, arg2) switch
|
||||
{
|
||||
("", "") => "",
|
||||
(string arg, "") => arg,
|
||||
("", string arg) => arg,
|
||||
_ => $"{arg1}, {arg2}"
|
||||
};
|
||||
}
|
||||
95
Railway.SourceGenerator/ResultBindExecutor.cs
Normal file
95
Railway.SourceGenerator/ResultBindExecutor.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
internal sealed class ResultBindExecutor : ResultExtensionsExecutor
|
||||
{
|
||||
protected override string ExtensionType => "Bind";
|
||||
|
||||
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);
|
||||
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("R"));
|
||||
string bindTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result<R>"));
|
||||
|
||||
sb.AppendLine($"#region {resultTypeDef}");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultBindExecutor)}}", "1.0.0.0")]
|
||||
public static Result<R> Bind{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func{{bindTemplateDecl}} binding)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => binding({{resultValueExpansion}}),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
|
||||
sb.AppendLine("#endregion");
|
||||
}
|
||||
|
||||
private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
|
||||
{
|
||||
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("R"));
|
||||
string bindTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result<R>"));
|
||||
string asyncActionTemplateDecl = GenerateTemplateDecl(templateArgNames.Add($"{taskType}<Result<R>>"));
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultBindExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Result<R>> Bind{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{bindTemplateDecl}} binding)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => binding({{resultValueExpansion}}),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultBindExecutor)}}", "1.0.0.0")]
|
||||
public static {{taskType}}<Result<R>> Bind{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} binding)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => binding({{resultValueExpansion}}),
|
||||
ResultState.Error => {{taskType}}.FromResult<Result<R>>(result.Error!),
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultBindExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Result<R>> Bind{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} binding)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => await binding({{resultValueExpansion}}).ConfigureAwait(false),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
""");
|
||||
}
|
||||
}
|
||||
196
Railway.SourceGenerator/ResultCombineExecutor.cs
Normal file
196
Railway.SourceGenerator/ResultCombineExecutor.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
internal sealed class ResultCombineExecutor : IGeneratorExecutor
|
||||
{
|
||||
public void Execute(SourceProductionContext context, Compilation source)
|
||||
{
|
||||
var methods = GenerateCombineMethods();
|
||||
var code = $$"""
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.CodeDom.Compiler;
|
||||
|
||||
namespace Just.Railway;
|
||||
|
||||
public readonly partial struct Result
|
||||
{
|
||||
{{methods}}
|
||||
}
|
||||
""";
|
||||
|
||||
context.AddSource("Result.Combine.g.cs", code);
|
||||
}
|
||||
|
||||
|
||||
private string GenerateCombineMethods()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
for (int i = 2; i <= Constants.MaxResultTupleSize; i++)
|
||||
{
|
||||
GenerateCombineMethodsForArgCount(sb, argCount: i);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void GenerateCombineMethodsForArgCount(StringBuilder sb, int argCount)
|
||||
{
|
||||
sb.AppendLine($"#region Combine {argCount} Results");
|
||||
|
||||
GenerateGetBottomMethod(sb, argCount);
|
||||
|
||||
var permutations = 1 << argCount;
|
||||
var argsResultTupleSizes = new ImmutableArray<int>[permutations];
|
||||
|
||||
Span<int> templateCounts = stackalloc int[argCount];
|
||||
for (int i = 0; i < permutations; i++)
|
||||
{
|
||||
templateCounts.Fill(0);
|
||||
for (int j = 0; j < argCount; j++)
|
||||
{
|
||||
templateCounts[j] = (i & (1 << j)) > 0 ? 1 : 0;
|
||||
}
|
||||
argsResultTupleSizes[i] = templateCounts.ToImmutableArray();
|
||||
}
|
||||
|
||||
foreach (var argResultTupleSizes in argsResultTupleSizes)
|
||||
{
|
||||
sb.AppendLine(GenerateCombineMethodBody(argResultTupleSizes));
|
||||
}
|
||||
|
||||
sb.AppendLine("#endregion");
|
||||
|
||||
}
|
||||
|
||||
private static void GenerateGetBottomMethod(StringBuilder sb, int argCount)
|
||||
{
|
||||
var args = Enumerable.Range(1, argCount)
|
||||
.Select(i => $"result{i}")
|
||||
.ToImmutableArray();
|
||||
var argsDecl = string.Join(", ", args.Select(x => $"ResultState {x}"));
|
||||
sb.AppendLine($"[GeneratedCodeAttribute(\"{nameof(ResultCombineExecutor)}\", \"1.0.0.0\")]");
|
||||
sb.AppendLine($"private static IEnumerable<string> GetBottom({argsDecl})");
|
||||
sb.AppendLine("{");
|
||||
foreach (var arg in args)
|
||||
{
|
||||
sb.AppendLine($" if ({arg} == ResultState.Bottom) yield return \"{arg}\";");
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
}
|
||||
|
||||
private string GenerateCombineMethodBody(ImmutableArray<int> argResultTupleSizes)
|
||||
{
|
||||
var resultTupleSize = argResultTupleSizes.Sum();
|
||||
|
||||
var paramNames = Enumerable.Range(1, argResultTupleSizes.Length)
|
||||
.Select(i => $"result{i}")
|
||||
.ToImmutableArray();
|
||||
var templateArgNames = Enumerable.Range(1, resultTupleSize)
|
||||
.Select(i => $"T{i}")
|
||||
.ToImmutableArray();
|
||||
|
||||
string templateDecl = templateArgNames.IsEmpty
|
||||
? string.Empty
|
||||
: $"<{string.Join(", ", templateArgNames)}>";
|
||||
string resultTypeDecl = GetResultTypeDecl(templateArgNames);
|
||||
string paramDecl;
|
||||
{
|
||||
var paramDeclBuilder = new StringBuilder();
|
||||
int currentTemplateArg = 0;
|
||||
for (int i = 0; i < argResultTupleSizes.Length; i++)
|
||||
{
|
||||
var argResultTupleSize = argResultTupleSizes[i];
|
||||
string currentParamType = GetResultTypeDecl(templateArgNames.Slice(currentTemplateArg, argResultTupleSize));
|
||||
currentTemplateArg += argResultTupleSize;
|
||||
paramDeclBuilder.Append($"in {currentParamType} {paramNames[i]}, ");
|
||||
}
|
||||
paramDeclBuilder.Remove(paramDeclBuilder.Length-2, 2);
|
||||
paramDecl = paramDeclBuilder.ToString();
|
||||
}
|
||||
|
||||
var paramNameStates = paramNames.Select(x => $"{x}.State")
|
||||
.ToImmutableArray();
|
||||
string bottomStateCheck = string.Join(" & ", paramNameStates);
|
||||
string statesSeparatedList = string.Join(", ", paramNameStates);
|
||||
|
||||
string failureChecks;
|
||||
{
|
||||
var failureChecksBuilder = new StringBuilder();
|
||||
foreach (var paramName in paramNames)
|
||||
{
|
||||
failureChecksBuilder.AppendLine($" if ({paramName}.IsFailure) error += {paramName}.Error;");
|
||||
}
|
||||
failureChecks = failureChecksBuilder.ToString();
|
||||
}
|
||||
string resultExpansion;
|
||||
switch (resultTupleSize)
|
||||
{
|
||||
case 0:
|
||||
resultExpansion = "null";
|
||||
break;
|
||||
|
||||
case 1:
|
||||
resultExpansion = $"{paramNames[argResultTupleSizes.IndexOf(1)]}.Value";
|
||||
break;
|
||||
|
||||
default:
|
||||
var resultExpansionBuilder = new StringBuilder();
|
||||
resultExpansionBuilder.Append("(");
|
||||
for (int i = 0; i < argResultTupleSizes.Length; i++)
|
||||
{
|
||||
if (argResultTupleSizes[i] == 0) continue;
|
||||
if (argResultTupleSizes[i] == 1)
|
||||
{
|
||||
resultExpansionBuilder.Append($"{paramNames[i]}.Value, ");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int valueIndex = 1; valueIndex <= argResultTupleSizes[i]; valueIndex++)
|
||||
{
|
||||
resultExpansionBuilder.Append($"{paramNames[i]}.Value.Item{valueIndex}, ");
|
||||
}
|
||||
}
|
||||
resultExpansionBuilder.Remove(resultExpansionBuilder.Length - 2, 2);
|
||||
resultExpansionBuilder.Append(")");
|
||||
resultExpansion = resultExpansionBuilder.ToString();
|
||||
break;
|
||||
}
|
||||
|
||||
string returnExpr = $"return error is null ? new({resultExpansion}) : new(error);";
|
||||
var method = $$"""
|
||||
[GeneratedCodeAttribute("{{nameof(ResultCombineExecutor)}}", "1.0.0.0")]
|
||||
[PureAttribute]
|
||||
public static {{resultTypeDecl}} Combine{{templateDecl}}({{paramDecl}})
|
||||
{
|
||||
if (({{bottomStateCheck}}) == ResultState.Bottom)
|
||||
{
|
||||
throw new ResultNotInitializedException(string.Join(';', GetBottom({{statesSeparatedList}})));
|
||||
}
|
||||
Error? error = null;
|
||||
{{failureChecks}}
|
||||
{{returnExpr}}
|
||||
}
|
||||
""";
|
||||
return method;
|
||||
|
||||
static string GetResultTypeDecl(IReadOnlyList<string> templateArgNames)
|
||||
{
|
||||
return templateArgNames.Count switch
|
||||
{
|
||||
0 => "Result",
|
||||
1 => $"Result<{templateArgNames[0]}>",
|
||||
_ => $"Result<({string.Join(", ", templateArgNames)})>"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
85
Railway.SourceGenerator/ResultExtensionsExecutor.cs
Normal file
85
Railway.SourceGenerator/ResultExtensionsExecutor.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
internal abstract class ResultExtensionsExecutor : IGeneratorExecutor
|
||||
{
|
||||
public void Execute(SourceProductionContext context, Compilation source)
|
||||
{
|
||||
var methods = GenerateMethods();
|
||||
var code = $$"""
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.CodeDom.Compiler;
|
||||
|
||||
namespace Just.Railway;
|
||||
|
||||
public static partial class ResultExtensions
|
||||
{
|
||||
{{methods}}
|
||||
}
|
||||
""";
|
||||
|
||||
context.AddSource($"ResultExtensions.{ExtensionType}.g.cs", code);
|
||||
}
|
||||
|
||||
private string GenerateMethods()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
GenerateHelperMethods(sb);
|
||||
for (int i = 0; i <= Constants.MaxResultTupleSize; i++)
|
||||
{
|
||||
GenerateMethodsForArgCount(sb, argCount: i);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
protected static string GenerateTemplateDecl(ImmutableArray<string> templateArgNames) => templateArgNames.Length > 0
|
||||
? $"<{string.Join(", ", templateArgNames)}>"
|
||||
: string.Empty;
|
||||
|
||||
protected static string GenerateResultTypeDef(ImmutableArray<string> templateArgNames) => templateArgNames.Length switch
|
||||
{
|
||||
0 => "Result",
|
||||
1 => $"Result<{string.Join(", ", templateArgNames)}>",
|
||||
_ => $"Result<({string.Join(", ", templateArgNames)})>",
|
||||
};
|
||||
|
||||
protected static string GenerateResultValueExpansion(ImmutableArray<string> templateArgNames)
|
||||
{
|
||||
string resultExpansion;
|
||||
|
||||
switch (templateArgNames.Length)
|
||||
{
|
||||
case 0:
|
||||
resultExpansion = string.Empty;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
resultExpansion = "result.Value";
|
||||
break;
|
||||
|
||||
default:
|
||||
var resultExpansionBuilder = new StringBuilder();
|
||||
for (int i = 1; i <= templateArgNames.Length; i++)
|
||||
{
|
||||
resultExpansionBuilder.Append($"result.Value.Item{i}, ");
|
||||
}
|
||||
resultExpansionBuilder.Remove(resultExpansionBuilder.Length - 2, 2);
|
||||
resultExpansion = resultExpansionBuilder.ToString();
|
||||
break;
|
||||
}
|
||||
|
||||
return resultExpansion;
|
||||
}
|
||||
|
||||
protected abstract string ExtensionType { get; }
|
||||
protected abstract void GenerateMethodsForArgCount(StringBuilder sb, int argCount);
|
||||
protected virtual void GenerateHelperMethods(StringBuilder sb) {}
|
||||
}
|
||||
93
Railway.SourceGenerator/ResultMapExecutor.cs
Normal file
93
Railway.SourceGenerator/ResultMapExecutor.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
internal sealed class ResultMapExecutor : ResultExtensionsExecutor
|
||||
{
|
||||
protected override string ExtensionType => "Map";
|
||||
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);
|
||||
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("R"));
|
||||
|
||||
sb.AppendLine($"#region {resultTypeDef}");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultMapExecutor)}}", "1.0.0.0")]
|
||||
public static Result<R> Map{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func{{methodTemplateDecl}} mapping)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => mapping({{resultValueExpansion}}),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
|
||||
sb.AppendLine("#endregion");
|
||||
}
|
||||
|
||||
private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
|
||||
{
|
||||
var methodTemplateArgNames = templateArgNames.Add("R");
|
||||
string methodTemplateDecl = GenerateTemplateDecl(methodTemplateArgNames);
|
||||
string asyncActionTemplateDecl = GenerateTemplateDecl(templateArgNames.Add($"{taskType}<R>"));
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultMapExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Result<R>> Map{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{methodTemplateDecl}} mapping)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => mapping({{resultValueExpansion}}),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultMapExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Result<R>> Map{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} mapping)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => await mapping({{resultValueExpansion}}).ConfigureAwait(false),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultMapExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Result<R>> Map{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} mapping)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => await mapping({{resultValueExpansion}}).ConfigureAwait(false),
|
||||
ResultState.Error => result.Error!,
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
""");
|
||||
}
|
||||
}
|
||||
97
Railway.SourceGenerator/ResultMatchExecutor.cs
Normal file
97
Railway.SourceGenerator/ResultMatchExecutor.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
internal sealed class ResultMatchExecutor : ResultExtensionsExecutor
|
||||
{
|
||||
protected override string ExtensionType => "Match";
|
||||
|
||||
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);
|
||||
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("R"));
|
||||
|
||||
sb.AppendLine($"#region {resultTypeDef}");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultMatchExecutor)}}", "1.0.0.0")]
|
||||
public static R Match{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func{{methodTemplateDecl}} onSuccess, Func<Error, R> onFailure)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => onSuccess({{resultValueExpansion}}),
|
||||
ResultState.Error => onFailure(result.Error!),
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
|
||||
sb.AppendLine("#endregion");
|
||||
}
|
||||
|
||||
private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
|
||||
{
|
||||
var methodTemplateArgNames = templateArgNames.Add("R");
|
||||
string methodTemplateDecl = GenerateTemplateDecl(methodTemplateArgNames);
|
||||
string asyncActionTemplateDecl = GenerateTemplateDecl(templateArgNames.Add($"{taskType}<R>"));
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultMatchExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<R> Match{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{methodTemplateDecl}} onSuccess, Func<Error, R> onFailure)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => onSuccess({{resultValueExpansion}}),
|
||||
ResultState.Error => onFailure(result.Error!),
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultMatchExecutor)}}", "1.0.0.0")]
|
||||
public static {{taskType}}<R> Match{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} onSuccess, Func<Error, {{taskType}}<R>> onFailure)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => onSuccess({{resultValueExpansion}}),
|
||||
ResultState.Error => onFailure(result.Error!),
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultMatchExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<R> Match{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} onSuccess, Func<Error, {{taskType}}<R>> onFailure)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
var matchTask = result.State switch
|
||||
{
|
||||
ResultState.Success => onSuccess({{resultValueExpansion}}),
|
||||
ResultState.Error => onFailure(result.Error!),
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
return await matchTask.ConfigureAwait(false);
|
||||
}
|
||||
""");
|
||||
}
|
||||
}
|
||||
121
Railway.SourceGenerator/ResultTapExecutor.cs
Normal file
121
Railway.SourceGenerator/ResultTapExecutor.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
internal sealed class ResultTapExecutor : ResultExtensionsExecutor
|
||||
{
|
||||
protected override string ExtensionType => "Tap";
|
||||
|
||||
protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount)
|
||||
{
|
||||
var templateArgNames = Enumerable.Range(1, argCount)
|
||||
.Select(i => $"T{i}")
|
||||
.ToImmutableArray();
|
||||
|
||||
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames);
|
||||
string resultTypeDef = GenerateResultTypeDef(templateArgNames);
|
||||
string resultValueExpansion = GenerateResultValueExpansion(templateArgNames);
|
||||
|
||||
sb.AppendLine($"#region {resultTypeDef}");
|
||||
|
||||
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<Error>? onFailure = null)
|
||||
{
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
onSuccess?.Invoke({{resultValueExpansion}});
|
||||
break;
|
||||
case ResultState.Error:
|
||||
onFailure?.Invoke(result.Error!);
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(result));
|
||||
}
|
||||
return ref result;
|
||||
}
|
||||
""");
|
||||
|
||||
GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
|
||||
|
||||
sb.AppendLine("#endregion");
|
||||
}
|
||||
|
||||
private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
|
||||
{
|
||||
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames);
|
||||
string asyncActionTemplateDecl = GenerateTemplateDecl(templateArgNames.Add(taskType));
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultTapExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultTypeDef}}> Tap{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Action{{methodTemplateDecl}}? onSuccess = null, Action<Error>? onFailure = null)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
onSuccess?.Invoke({{resultValueExpansion}});
|
||||
break;
|
||||
case ResultState.Error:
|
||||
onFailure?.Invoke(result.Error!);
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(resultTask));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultTapExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultTypeDef}}> Tap{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}}? onSuccess = null, Func<Error, {{taskType}}>? onFailure = null)
|
||||
{
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
if (onSuccess is not null)
|
||||
await onSuccess.Invoke({{resultValueExpansion}}).ConfigureAwait(false);
|
||||
break;
|
||||
case ResultState.Error:
|
||||
if (onFailure is not null)
|
||||
await onFailure.Invoke(result.Error!).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(ResultTapExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<{{resultTypeDef}}> Tap{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}}? onSuccess = null, Func<Error, {{taskType}}>? onFailure = null)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
if (onSuccess is not null)
|
||||
await onSuccess.Invoke({{resultValueExpansion}}).ConfigureAwait(false);
|
||||
break;
|
||||
case ResultState.Error:
|
||||
if (onFailure is not null)
|
||||
await onFailure.Invoke(result.Error!).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(resultTask));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
""");
|
||||
}
|
||||
}
|
||||
211
Railway.SourceGenerator/TryExtensionsExecutor.cs
Normal file
211
Railway.SourceGenerator/TryExtensionsExecutor.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Just.Railway.SourceGen;
|
||||
|
||||
public sealed class TryExtensionsExecutor : IGeneratorExecutor
|
||||
{
|
||||
public void Execute(SourceProductionContext context, Compilation source)
|
||||
{
|
||||
var methods = GenerateMethods();
|
||||
var code = $$"""
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.CodeDom.Compiler;
|
||||
|
||||
namespace Just.Railway;
|
||||
|
||||
public static partial class Try
|
||||
{
|
||||
{{methods}}
|
||||
}
|
||||
""";
|
||||
|
||||
context.AddSource("Try.Run.g.cs", code);
|
||||
}
|
||||
|
||||
private string GenerateMethods()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i <= Constants.MaxResultTupleSize; i++)
|
||||
{
|
||||
GenerateMethodsForArgCount(sb, argCount: i);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void GenerateMethodsForArgCount(StringBuilder sb, int argCount)
|
||||
{
|
||||
var templateArgNames = Enumerable.Range(1, argCount)
|
||||
.Select(i => $"T{i}")
|
||||
.ToImmutableArray();
|
||||
var argNames = Enumerable.Range(1, argCount)
|
||||
.Select(i => $"arg{i}")
|
||||
.ToImmutableArray();
|
||||
|
||||
string actionTemplateDecl = GenerateTemplateDecl(templateArgNames);
|
||||
string resultActionTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result"));
|
||||
string funcTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("TResult"));
|
||||
string resultFuncTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result<TResult>"));
|
||||
string argumentsDeclExpansion = string.Join(", ", templateArgNames.Zip(argNames, (t, n) => $"{t} {n}"));
|
||||
string argumentsExpansion = string.Join(", ", argNames);
|
||||
|
||||
sb.AppendLine($"#region <{string.Join(", ", templateArgNames)}>");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static Result Run{{actionTemplateDecl}}(Action{{actionTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}})
|
||||
{
|
||||
try
|
||||
{
|
||||
action({{argumentsExpansion}});
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Error.New(ex);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static Result Run{{actionTemplateDecl}}(Func{{resultActionTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}})
|
||||
{
|
||||
try
|
||||
{
|
||||
return action({{argumentsExpansion}});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Error.New(ex);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static Result<TResult> Run{{funcTemplateDecl}}(Func{{funcTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}})
|
||||
{
|
||||
try
|
||||
{
|
||||
return Result.Success(func({{argumentsExpansion}}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Error.New(ex);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static Result<TResult> Run{{funcTemplateDecl}}(Func{{resultFuncTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}})
|
||||
{
|
||||
try
|
||||
{
|
||||
return func({{argumentsExpansion}});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Error.New(ex);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
GenerateAsyncMethods(sb, templateArgNames, actionTemplateDecl, funcTemplateDecl, argumentsDeclExpansion, argumentsExpansion, "Task");
|
||||
GenerateAsyncMethods(sb, templateArgNames, actionTemplateDecl, funcTemplateDecl, argumentsDeclExpansion, argumentsExpansion, "ValueTask");
|
||||
|
||||
sb.AppendLine("#endregion");
|
||||
}
|
||||
|
||||
private static void GenerateAsyncMethods(StringBuilder sb, ImmutableArray<string> templateArgNames, string actionTemplateDecl, string funcTemplateDecl, string argumentsDeclExpansion, string argumentsExpansion, string taskType)
|
||||
{
|
||||
string actionTaskTemplateDecl = GenerateTemplateDecl(templateArgNames.Add(taskType));
|
||||
string resultActionTaskTemplateDecl = GenerateTemplateDecl(templateArgNames.Add($"{taskType}<Result>"));
|
||||
string funcTaskTemplateDecl = GenerateTemplateDecl(templateArgNames.Add($"{taskType}<TResult>"));
|
||||
string resultFuncTaskTemplateDecl = GenerateTemplateDecl(templateArgNames.Add($"{taskType}<Result<TResult>>"));
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Result> Run{{actionTemplateDecl}}(Func{{actionTaskTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}})
|
||||
{
|
||||
try
|
||||
{
|
||||
await action({{argumentsExpansion}}).ConfigureAwait(false);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Error.New(ex);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Result> Run{{actionTemplateDecl}}(Func{{resultActionTaskTemplateDecl}} action{{TrailingArguments(argumentsDeclExpansion)}})
|
||||
{
|
||||
try
|
||||
{
|
||||
return await action({{argumentsExpansion}}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Error.New(ex);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Result<TResult>> Run{{funcTemplateDecl}}(Func{{funcTaskTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}})
|
||||
{
|
||||
try
|
||||
{
|
||||
return Result.Success(await func({{argumentsExpansion}}).ConfigureAwait(false));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Error.New(ex);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
sb.AppendLine($$"""
|
||||
[PureAttribute]
|
||||
[GeneratedCodeAttribute("{{nameof(TryExtensionsExecutor)}}", "1.0.0.0")]
|
||||
public static async {{taskType}}<Result<TResult>> Run{{funcTemplateDecl}}(Func{{resultFuncTaskTemplateDecl}} func{{TrailingArguments(argumentsDeclExpansion)}})
|
||||
{
|
||||
try
|
||||
{
|
||||
return await func({{argumentsExpansion}}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Error.New(ex);
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
private static string TrailingArguments(string argumentsExpansion) => string.IsNullOrEmpty(argumentsExpansion)
|
||||
? string.Empty
|
||||
: $", {argumentsExpansion}";
|
||||
private static string GenerateTemplateDecl(ImmutableArray<string> templateArgNames) => templateArgNames.Length > 0
|
||||
? $"<{string.Join(", ", templateArgNames)}>"
|
||||
: string.Empty;
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
namespace Just.Railway;
|
||||
|
||||
public static class Ensure
|
||||
public static partial class Ensure
|
||||
{
|
||||
public delegate Error ErrorFactory(string valueExpression);
|
||||
|
||||
public const string DefaultErrorType = "EnsureFailed";
|
||||
|
||||
[Pure] public static Ensure<T> That<T>(T value, [CallerArgumentExpression(nameof(value))]string valueExpression = "") => new(value, valueExpression);
|
||||
[Pure] public static async Task<Ensure<T>> That<T>(Task<T> value, [CallerArgumentExpression(nameof(value))]string valueExpression = "") => new(await value.ConfigureAwait(false), valueExpression);
|
||||
|
||||
[Pure] public static Result<T> Result<T>(this in Ensure<T> ensure) => ensure.State switch
|
||||
{
|
||||
@@ -15,8 +14,7 @@ public static class Ensure
|
||||
ResultState.Error => new(ensure.Error!),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
[Pure]
|
||||
public static async Task<Result<T>> Result<T>(this Task<Ensure<T>> ensureTask)
|
||||
[Pure] public static async Task<Result<T>> Result<T>(this Task<Ensure<T>> ensureTask)
|
||||
{
|
||||
var ensure = await ensureTask.ConfigureAwait(false);
|
||||
return ensure.State switch
|
||||
@@ -26,261 +24,18 @@ public static class Ensure
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensureTask))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static Ensure<T> Satisfies<T>(this in Ensure<T> ensure, Func<T, bool> requirement, Error error = default!)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => requirement(ensure.Value)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} does not satisfy the requirement."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static Ensure<T> Satisfies<T>(this in Ensure<T> ensure, Func<T, bool> requirement, ErrorFactory errorFactory)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => requirement(ensure.Value)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(errorFactory(ensure.ValueExpression), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static async Task<Ensure<T>> Satisfies<T>(this Task<Ensure<T>> ensureTask, Func<T, bool> requirement, Error error = default!)
|
||||
[Pure] public static async ValueTask<Result<T>> Result<T>(this ValueTask<Ensure<T>> ensureTask)
|
||||
{
|
||||
var ensure = await ensureTask.ConfigureAwait(false);
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => requirement(ensure.Value)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} does not satisfy the requirement."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensureTask))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static async Task<Ensure<T>> Satisfies<T>(this Task<Ensure<T>> ensureTask, Func<T, bool> requirement, ErrorFactory errorFactory)
|
||||
{
|
||||
var ensure = await ensureTask.ConfigureAwait(false);
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => requirement(ensure.Value)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(errorFactory(ensure.ValueExpression), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensureTask))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static async Task<Ensure<T>> Satisfies<T>(this Ensure<T> ensure, Func<T, Task<bool>> requirement, Error error = default!)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => await requirement(ensure.Value).ConfigureAwait(false)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} does not satisfy the requirement."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static async Task<Ensure<T>> Satisfies<T>(this Ensure<T> ensure, Func<T, Task<bool>> requirement, ErrorFactory errorFactory)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => await requirement(ensure.Value).ConfigureAwait(false)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(errorFactory(ensure.ValueExpression), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static async Task<Ensure<T>> Satisfies<T>(this Task<Ensure<T>> ensureTask, Func<T, Task<bool>> requirement, Error error = default!)
|
||||
{
|
||||
var ensure = await ensureTask.ConfigureAwait(false);
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => await requirement(ensure.Value).ConfigureAwait(false)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} does not satisfy the requirement."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensureTask))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static async Task<Ensure<T>> Satisfies<T>(this Task<Ensure<T>> ensureTask, Func<T, Task<bool>> requirement, ErrorFactory errorFactory)
|
||||
{
|
||||
var ensure = await ensureTask.ConfigureAwait(false);
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => await requirement(ensure.Value).ConfigureAwait(false)
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(errorFactory(ensure.ValueExpression), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
ResultState.Success => new(ensure.Value),
|
||||
ResultState.Error => new(ensure.Error!),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensureTask))
|
||||
};
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public static Ensure<T> NotNull<T>(this in Ensure<T?> ensure, Error error = default!)
|
||||
where T : struct
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value.HasValue
|
||||
? new(ensure.Value.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is null."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static async Task<Ensure<T>> NotNull<T>(this Task<Ensure<T?>> ensureTask, Error error = default!)
|
||||
where T : struct
|
||||
{
|
||||
var ensure = await ensureTask.ConfigureAwait(false);
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value.HasValue
|
||||
? new(ensure.Value.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is null."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensureTask))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static Ensure<T> NotNull<T>(this in Ensure<T?> ensure, Error error = default!)
|
||||
where T : notnull
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value is not null
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is null."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static async Task<Ensure<T>> NotNull<T>(this Task<Ensure<T?>> ensureTask, Error error = default!)
|
||||
where T : notnull
|
||||
{
|
||||
var ensure = await ensureTask.ConfigureAwait(false);
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value is not null
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is null."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensureTask))
|
||||
};
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public static Ensure<T[]> NotEmpty<T>(this in Ensure<T[]> ensure, Error error = default!)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value is not null && ensure.Value.Length > 0
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static Ensure<List<T>> NotEmpty<T>(this in Ensure<List<T>> ensure, Error error = default!)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value is not null && ensure.Value.Count > 0
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static Ensure<IReadOnlyCollection<T>> NotEmpty<T>(this in Ensure<IReadOnlyCollection<T>> ensure, Error error = default!)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value is not null && ensure.Value.Count > 0
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static Ensure<ICollection<T>> NotEmpty<T>(this in Ensure<ICollection<T>> ensure, Error error = default!)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value is not null && ensure.Value.Count > 0
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static Ensure<IReadOnlyList<T>> NotEmpty<T>(this in Ensure<IReadOnlyList<T>> ensure, Error error = default!)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value is not null && ensure.Value.Count > 0
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static Ensure<IList<T>> NotEmpty<T>(this in Ensure<IList<T>> ensure, Error error = default!)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value is not null && ensure.Value.Count > 0
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static Ensure<IEnumerable<T>> NotEmpty<T>(this in Ensure<IEnumerable<T>> ensure, Error error = default!)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => ensure.Value is not null && ensure.Value.Any()
|
||||
? new(ensure.Value, ensure.ValueExpression)
|
||||
: new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
[Pure]
|
||||
public static Ensure<string> NotEmpty(this in Ensure<string> ensure, Error error = default!)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
ResultState.Success => string.IsNullOrEmpty(ensure.Value)
|
||||
? new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression)
|
||||
: new(ensure.Value, ensure.ValueExpression),
|
||||
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
|
||||
_ => throw new EnsureNotInitializedException(nameof(ensure))
|
||||
};
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public static Ensure<string> NotWhitespace(this in Ensure<string> ensure, Error error = default!)
|
||||
[Pure] public static Ensure<string> NotWhitespace(this in Ensure<string> ensure, Error error = default!)
|
||||
{
|
||||
return ensure.State switch
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Immutable;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
|
||||
@@ -10,8 +11,6 @@ namespace Just.Railway;
|
||||
[JsonDerivedType(typeof(ManyErrors))]
|
||||
public abstract class Error : IEquatable<Error>, IComparable<Error>
|
||||
{
|
||||
private IDictionary<string, object>? _extensionData;
|
||||
|
||||
protected internal Error(){}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,28 +76,8 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
|
||||
|
||||
[Pure] public abstract string Type { get; }
|
||||
[Pure] public abstract string Message { get; }
|
||||
[Pure, JsonExtensionData] public IDictionary<string, object> ExtensionData
|
||||
{
|
||||
get => _extensionData ??= new Dictionary<string, object>();
|
||||
init => _extensionData = value ?? new Dictionary<string, object>();
|
||||
}
|
||||
[Pure] public object? this[string name]
|
||||
{
|
||||
get => _extensionData?.TryGetValue(name, out var val) == true ? val : null;
|
||||
|
||||
set
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
_extensionData?.Remove(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_extensionData ??= new Dictionary<string, object>();
|
||||
_extensionData[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
[Pure, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ImmutableDictionary<string, string>? ExtensionData { get; init; }
|
||||
[Pure] public string? this[string key] => ExtensionData?.TryGetValue(key, out var value) == true ? value : null;
|
||||
|
||||
[Pure, JsonIgnore] public abstract int Count { get; }
|
||||
[Pure, JsonIgnore] public abstract bool IsEmpty { get; }
|
||||
@@ -199,13 +178,13 @@ public sealed class ExceptionalError : Error
|
||||
: this(exception.GetType().Name, exception.Message)
|
||||
{
|
||||
Exception = exception;
|
||||
FillExtensionData(exception);
|
||||
ExtensionData = ExtractExtensionData(exception);
|
||||
}
|
||||
internal ExceptionalError(string message, Exception exception)
|
||||
: this(exception.GetType().Name, message)
|
||||
{
|
||||
Exception = exception;
|
||||
FillExtensionData(exception);
|
||||
ExtensionData = ExtractExtensionData(exception);
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
@@ -231,15 +210,28 @@ public sealed class ExceptionalError : Error
|
||||
yield return this;
|
||||
}
|
||||
|
||||
private void FillExtensionData(Exception exception)
|
||||
private static ImmutableDictionary<string, string>? ExtractExtensionData(Exception exception)
|
||||
{
|
||||
if (!(exception.Data?.Count > 0))
|
||||
return null;
|
||||
|
||||
List<KeyValuePair<string, string>>? values = null;
|
||||
|
||||
foreach (var key in exception.Data.Keys)
|
||||
{
|
||||
if (key is null) continue;
|
||||
|
||||
var value = exception.Data[key];
|
||||
if (key is null || value is null)
|
||||
continue;
|
||||
this.ExtensionData[key.ToString() ?? string.Empty] = value;
|
||||
if (value is null) continue;
|
||||
|
||||
var keyString = key.ToString();
|
||||
var valueString = value.ToString();
|
||||
if (string.IsNullOrEmpty(keyString) || string.IsNullOrEmpty(valueString)) continue;
|
||||
|
||||
values ??= [];
|
||||
values.Add(new(keyString, valueString));
|
||||
}
|
||||
return values?.ToImmutableDictionary();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<AssemblyName>Just.Railway</AssemblyName>
|
||||
<RootNamespace>Just.Railway</RootNamespace>
|
||||
|
||||
<Authors>JustFixMe</Authors>
|
||||
<Copyright>Copyright (c) 2023 JustFixMe</Copyright>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<RepositoryUrl>https://gitea.jstdev.ru/just/Just.Railway/</RepositoryUrl>
|
||||
|
||||
<EmitCompilerGeneratedFiles Condition="'$(Configuration)'=='Debug'">true</EmitCompilerGeneratedFiles>
|
||||
<ReleaseVersion Condition=" '$(ReleaseVersion)' == '' ">1.0.0</ReleaseVersion>
|
||||
<VersionSuffix Condition=" '$(VersionSuffix)' != '' ">$(VersionSuffix)</VersionSuffix>
|
||||
<VersionPrefix Condition=" '$(VersionSuffix)' != '' ">$(ReleaseVersion)</VersionPrefix>
|
||||
@@ -19,4 +24,10 @@
|
||||
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Railway.SourceGenerator\Railway.SourceGenerator.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
1096
Railway/Result.cs
1096
Railway/Result.cs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
5
Railway/Try.cs
Normal file
5
Railway/Try.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace Just.Railway;
|
||||
|
||||
public static partial class Try
|
||||
{
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Railway.Tests.Errors;
|
||||
|
||||
public class Serialization
|
||||
@@ -8,9 +10,8 @@ public class Serialization
|
||||
// Given
|
||||
Error many_errors = new ManyErrors(new Error[]{
|
||||
new ExpectedError("err1", "msg1"){
|
||||
ExtensionData = {
|
||||
["ext"] = "ext_value"
|
||||
}
|
||||
ExtensionData = ImmutableDictionary<string, string>.Empty
|
||||
.Add("ext", "ext_value"),
|
||||
},
|
||||
new ExceptionalError(new Exception("msg2")),
|
||||
});
|
||||
@@ -18,7 +19,7 @@ public class Serialization
|
||||
var result = JsonSerializer.Serialize(many_errors);
|
||||
// Then
|
||||
Assert.Equal(
|
||||
expected: "[{\"$$err\":0,\"Type\":\"err1\",\"Message\":\"msg1\",\"ext\":\"ext_value\"},{\"$$err\":1,\"Type\":\"Exception\",\"Message\":\"msg2\"}]",
|
||||
expected: "[{\"$$err\":0,\"Type\":\"err1\",\"Message\":\"msg1\",\"ExtensionData\":{\"ext\":\"ext_value\"}},{\"$$err\":1,\"Type\":\"Exception\",\"Message\":\"msg2\"}]",
|
||||
result);
|
||||
}
|
||||
|
||||
@@ -26,7 +27,7 @@ public class Serialization
|
||||
public void WhenDeserializingManyErrors()
|
||||
{
|
||||
// Given
|
||||
var json = "[{\"$$err\":0,\"Type\":\"err1\",\"Message\":\"msg1\",\"ext\":\"ext_value\"},{\"$$err\":1,\"Type\":\"Exception\",\"Message\":\"msg2\"}]";
|
||||
var json = "[{\"$$err\":0,\"Type\":\"err1\",\"Message\":\"msg1\",\"ExtensionData\":{\"ext1\":\"ext_value1\",\"ext2\":\"ext_value2\"}},{\"$$err\":1,\"Type\":\"Exception\",\"Message\":\"msg2\"}]";
|
||||
// When
|
||||
var result = JsonSerializer.Deserialize<Error[]>(json);
|
||||
// Then
|
||||
@@ -39,7 +40,10 @@ public class Serialization
|
||||
result
|
||||
);
|
||||
Assert.Equal(
|
||||
expected: "ext_value",
|
||||
result[0].ExtensionData["ext"].ToString());
|
||||
expected: "ext_value1",
|
||||
result[0]["ext1"]);
|
||||
Assert.Equal(
|
||||
expected: "ext_value2",
|
||||
result[0]["ext2"]);
|
||||
}
|
||||
}
|
||||
|
||||
78
Raliway.Tests/Results/Combine.cs
Normal file
78
Raliway.Tests/Results/Combine.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
namespace Raliway.Tests.Results;
|
||||
|
||||
public class Combine
|
||||
{
|
||||
[Fact]
|
||||
public void TwoResultCombination_WhenThereIsAnError()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Success(1);
|
||||
var result2 = Result.Failure(Error.New("some error"));
|
||||
// When
|
||||
var result = Result.Combine(result1, result2);
|
||||
// Then
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Equal(result2.Error, result.Error);
|
||||
}
|
||||
[Fact]
|
||||
public void TwoResultCombination_WhenThereAreTwoErrors()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Failure<byte>(Error.New("1"));
|
||||
var result2 = Result.Failure(Error.New("2"));
|
||||
// When
|
||||
var result = Result.Combine(result1, result2);
|
||||
// Then
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Equal(result1.Error + result2.Error, result.Error);
|
||||
}
|
||||
[Fact]
|
||||
public void TwoResultCombination_WhenThereIsNoError()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Success(1);
|
||||
var result2 = Result.Success(3.14);
|
||||
// When
|
||||
var result = Result.Combine(result1, result2);
|
||||
// Then
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
[Fact]
|
||||
public void ThreeResultCombination_WhenThereIsAnError()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Success(1);
|
||||
var result2 = Result.Success(3.14);
|
||||
var result3 = Result.Failure(Error.New("some error"));
|
||||
// When
|
||||
Result<(int, double)> result = Result.Combine(result1, result2, result3);
|
||||
// Then
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Equal(result3.Error, result.Error);
|
||||
}
|
||||
[Fact]
|
||||
public void ThreeResultCombination_WhenThereAreTwoErrors()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Failure<int?>(Error.New("1"));
|
||||
var result2 = Result.Success(3.14);
|
||||
var result3 = Result.Failure(Error.New("3"));
|
||||
// When
|
||||
Result<(int?, double)> result = Result.Combine(result1, result2, result3);
|
||||
// Then
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Equal(result1.Error + result3.Error, result.Error);
|
||||
}
|
||||
[Fact]
|
||||
public void ThreeResultCombination_WhenThereIsNoError()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Success(1);
|
||||
var result2 = Result.Success(3.14);
|
||||
var result3 = Result.Success();
|
||||
// When
|
||||
var result = Result.Combine(result1, result2, result3);
|
||||
// Then
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
}
|
||||
@@ -2,80 +2,6 @@ namespace Raliway.Tests.Results;
|
||||
|
||||
public class GeneralUsage
|
||||
{
|
||||
[Fact]
|
||||
public void TwoResultCombination_WhenThereIsAnError()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Success(1);
|
||||
var result2 = Result.Failure(Error.New("some error"));
|
||||
// When
|
||||
var result = Result.Combine(result1, result2);
|
||||
// Then
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Equal(result2.Error, result.Error);
|
||||
}
|
||||
[Fact]
|
||||
public void TwoResultCombination_WhenThereAreTwoErrors()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Failure<byte>(Error.New("1"));
|
||||
var result2 = Result.Failure(Error.New("2"));
|
||||
// When
|
||||
var result = Result.Combine(result1, result2);
|
||||
// Then
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Equal(result1.Error + result2.Error, result.Error);
|
||||
}
|
||||
[Fact]
|
||||
public void TwoResultCombination_WhenThereIsNoError()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Success(1);
|
||||
var result2 = Result.Success(3.14);
|
||||
// When
|
||||
var result = Result.Combine(result1, result2);
|
||||
// Then
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
[Fact]
|
||||
public void ThreeResultCombination_WhenThereIsAnError()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Success(1);
|
||||
var result2 = Result.Success(3.14);
|
||||
var result3 = Result.Failure(Error.New("some error"));
|
||||
// When
|
||||
Result<(int, double)> result = Result.Combine(result1, result2, result3);
|
||||
// Then
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Equal(result3.Error, result.Error);
|
||||
}
|
||||
[Fact]
|
||||
public void ThreeResultCombination_WhenThereAreTwoErrors()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Failure<int?>(Error.New("1"));
|
||||
var result2 = Result.Success(3.14);
|
||||
var result3 = Result.Failure(Error.New("3"));
|
||||
// When
|
||||
Result<(int?, double)> result = Result.Combine(result1, result2, result3);
|
||||
// Then
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Equal(result1.Error + result3.Error, result.Error);
|
||||
}
|
||||
[Fact]
|
||||
public void ThreeResultCombination_WhenThereIsNoError()
|
||||
{
|
||||
// Given
|
||||
var result1 = Result.Success(1);
|
||||
var result2 = Result.Success(3.14);
|
||||
var result3 = Result.Success();
|
||||
// When
|
||||
var result = Result.Combine(result1, result2, result3);
|
||||
// Then
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChainedResultExtensions_WhenThereIsNoError()
|
||||
{
|
||||
@@ -104,10 +30,9 @@ public class GeneralUsage
|
||||
public void ChainedResultExtensions_WhenThereIsAnError()
|
||||
{
|
||||
// Given
|
||||
|
||||
// When
|
||||
var error = Error.New("test");
|
||||
|
||||
// When
|
||||
|
||||
var result = Result.Success()
|
||||
.Append(() => Result.Failure<int>(error))
|
||||
@@ -115,7 +40,7 @@ public class GeneralUsage
|
||||
.Map((i, s) =>
|
||||
{
|
||||
Assert.Fail();
|
||||
return Result.Success("");
|
||||
return "";
|
||||
})
|
||||
.Append("some")
|
||||
.Bind((s1, s2) =>
|
||||
@@ -139,4 +64,68 @@ public class GeneralUsage
|
||||
// Then
|
||||
Assert.Equal("satisfied", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChainedResultAsyncExtensions_WhenThereIsNoError()
|
||||
{
|
||||
// Given
|
||||
|
||||
// When
|
||||
var result = await Result.Success()
|
||||
.Append(() => ValueTask.FromResult(Result.Success(1)))
|
||||
.Append("test")
|
||||
.Map((i, s) => $"{s}_{i}")
|
||||
.Append("some")
|
||||
.Bind(async (s1, s2) => await ValueTask.FromResult(Result.Success(string.Join(';', s1, s2))))
|
||||
.Match(
|
||||
onSuccess: s => s.ToUpper(),
|
||||
onFailure: _ =>
|
||||
{
|
||||
Assert.Fail();
|
||||
return "";
|
||||
}
|
||||
);
|
||||
|
||||
Assert.Equal("TEST_1;SOME", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChainedResultAsyncExtensions_WhenThereIsAnError()
|
||||
{
|
||||
// Given
|
||||
var error = Error.New("test");
|
||||
|
||||
// When
|
||||
|
||||
var result = await Result.Success()
|
||||
.Append(() => Task.FromResult(Result.Failure<int>(error)))
|
||||
.Append("test")
|
||||
.Map((i, s) =>
|
||||
{
|
||||
Assert.Fail();
|
||||
return "";
|
||||
})
|
||||
.Append("some")
|
||||
.Bind(async (s1, s2) =>
|
||||
{
|
||||
Assert.Fail();
|
||||
await Task.CompletedTask;
|
||||
return Result.Success("");
|
||||
})
|
||||
.Match(
|
||||
onSuccess: _ =>
|
||||
{
|
||||
Assert.Fail();
|
||||
return "";
|
||||
},
|
||||
onFailure: err =>
|
||||
{
|
||||
Assert.Equal(error, err);
|
||||
return "satisfied";
|
||||
}
|
||||
);
|
||||
|
||||
// Then
|
||||
Assert.Equal("satisfied", result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user