added Extend extension methods generation
This commit is contained in:
@@ -13,6 +13,7 @@ public class ExtensionsMethodGenerator : IIncrementalGenerator
|
|||||||
new ResultMapExecutor(),
|
new ResultMapExecutor(),
|
||||||
new ResultBindExecutor(),
|
new ResultBindExecutor(),
|
||||||
new ResultTapExecutor(),
|
new ResultTapExecutor(),
|
||||||
|
new ResultExtendExecutor(),
|
||||||
new ResultTryRecoverExecutor(),
|
new ResultTryRecoverExecutor(),
|
||||||
new ResultAppendExecutor(),
|
new ResultAppendExecutor(),
|
||||||
new TryExtensionsExecutor(),
|
new TryExtensionsExecutor(),
|
||||||
|
|||||||
@@ -460,12 +460,4 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor
|
|||||||
}
|
}
|
||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static string JoinArguments(string arg1, string arg2) => (arg1, arg2) switch
|
|
||||||
{
|
|
||||||
("", "") => "",
|
|
||||||
(string arg, "") => arg,
|
|
||||||
("", string arg) => arg,
|
|
||||||
_ => $"{arg1}, {arg2}"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
160
Railway.SourceGenerator/ResultExtendExecutor.cs
Normal file
160
Railway.SourceGenerator/ResultExtendExecutor.cs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace Just.Railway.SourceGen;
|
||||||
|
|
||||||
|
internal sealed class ResultExtendExecutor : ResultExtensionsExecutor
|
||||||
|
{
|
||||||
|
protected override string ExtensionType => "Extend";
|
||||||
|
|
||||||
|
protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount)
|
||||||
|
{
|
||||||
|
if (argCount == 0 || argCount == Constants.MaxResultTupleSize)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var templateArgNames = Enumerable.Range(1, argCount)
|
||||||
|
.Select(i => $"T{i}")
|
||||||
|
.ToImmutableArray();
|
||||||
|
|
||||||
|
var expandedTemplateArgNames = templateArgNames.Add("R");
|
||||||
|
string resultTypeDef = GenerateResultTypeDef(templateArgNames);
|
||||||
|
string resultValueExpansion = GenerateResultValueExpansion(templateArgNames);
|
||||||
|
string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames);
|
||||||
|
string methodTemplateDecl = GenerateTemplateDecl(expandedTemplateArgNames);
|
||||||
|
string bindTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result<R>"));
|
||||||
|
|
||||||
|
sb.AppendLine($"#region {resultTypeDef}");
|
||||||
|
|
||||||
|
sb.AppendLine($$"""
|
||||||
|
[PureAttribute]
|
||||||
|
[GeneratedCodeAttribute("{{nameof(ResultExtendExecutor)}}", "1.0.0.0")]
|
||||||
|
public static {{resultExpandedTypeDef}} Extend{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func{{bindTemplateDecl}} extensionFunc)
|
||||||
|
{
|
||||||
|
if (result.State == ResultState.Bottom)
|
||||||
|
{
|
||||||
|
throw new ResultNotInitializedException(nameof(result));
|
||||||
|
}
|
||||||
|
else if (result.IsFailure)
|
||||||
|
{
|
||||||
|
return result.Error!;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = extensionFunc({{resultValueExpansion}});
|
||||||
|
if (extension.State == ResultState.Bottom)
|
||||||
|
{
|
||||||
|
throw new ResultNotInitializedException(nameof(extensionFunc));
|
||||||
|
}
|
||||||
|
else if (extension.IsFailure)
|
||||||
|
{
|
||||||
|
return extension.Error!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Success({{JoinArguments(resultValueExpansion, "extension.Value")}});
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
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 expandedTemplateArgNames = templateArgNames.Add("R");
|
||||||
|
string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames);
|
||||||
|
string methodTemplateDecl = GenerateTemplateDecl(expandedTemplateArgNames);
|
||||||
|
string bindTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result<R>"));
|
||||||
|
string asyncActionTemplateDecl = GenerateTemplateDecl(templateArgNames.Add($"{taskType}<Result<R>>"));
|
||||||
|
|
||||||
|
sb.AppendLine($$"""
|
||||||
|
[PureAttribute]
|
||||||
|
[GeneratedCodeAttribute("{{nameof(ResultExtendExecutor)}}", "1.0.0.0")]
|
||||||
|
public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{bindTemplateDecl}} extensionFunc)
|
||||||
|
{
|
||||||
|
var result = await resultTask.ConfigureAwait(false);
|
||||||
|
if (result.State == ResultState.Bottom)
|
||||||
|
{
|
||||||
|
throw new ResultNotInitializedException(nameof(resultTask));
|
||||||
|
}
|
||||||
|
else if (result.IsFailure)
|
||||||
|
{
|
||||||
|
return result.Error!;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = extensionFunc({{resultValueExpansion}});
|
||||||
|
if (extension.State == ResultState.Bottom)
|
||||||
|
{
|
||||||
|
throw new ResultNotInitializedException(nameof(extensionFunc));
|
||||||
|
}
|
||||||
|
else if (extension.IsFailure)
|
||||||
|
{
|
||||||
|
return extension.Error!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Success({{JoinArguments(resultValueExpansion, "extension.Value")}});
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
sb.AppendLine($$"""
|
||||||
|
[PureAttribute]
|
||||||
|
[GeneratedCodeAttribute("{{nameof(ResultExtendExecutor)}}", "1.0.0.0")]
|
||||||
|
public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} extensionFunc)
|
||||||
|
{
|
||||||
|
if (result.State == ResultState.Bottom)
|
||||||
|
{
|
||||||
|
throw new ResultNotInitializedException(nameof(result));
|
||||||
|
}
|
||||||
|
else if (result.IsFailure)
|
||||||
|
{
|
||||||
|
return result.Error!;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = await extensionFunc({{resultValueExpansion}}).ConfigureAwait(false);
|
||||||
|
if (extension.State == ResultState.Bottom)
|
||||||
|
{
|
||||||
|
throw new ResultNotInitializedException(nameof(extensionFunc));
|
||||||
|
}
|
||||||
|
else if (extension.IsFailure)
|
||||||
|
{
|
||||||
|
return extension.Error!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Success({{JoinArguments(resultValueExpansion, "extension.Value")}});
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
sb.AppendLine($$"""
|
||||||
|
[PureAttribute]
|
||||||
|
[GeneratedCodeAttribute("{{nameof(ResultExtendExecutor)}}", "1.0.0.0")]
|
||||||
|
public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} extensionFunc)
|
||||||
|
{
|
||||||
|
var result = await resultTask.ConfigureAwait(false);
|
||||||
|
if (result.State == ResultState.Bottom)
|
||||||
|
{
|
||||||
|
throw new ResultNotInitializedException(nameof(resultTask));
|
||||||
|
}
|
||||||
|
else if (result.IsFailure)
|
||||||
|
{
|
||||||
|
return result.Error!;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = await extensionFunc({{resultValueExpansion}}).ConfigureAwait(false);
|
||||||
|
if (extension.State == ResultState.Bottom)
|
||||||
|
{
|
||||||
|
throw new ResultNotInitializedException(nameof(extensionFunc));
|
||||||
|
}
|
||||||
|
else if (extension.IsFailure)
|
||||||
|
{
|
||||||
|
return extension.Error!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Success({{JoinArguments(resultValueExpansion, "extension.Value")}});
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,13 @@ internal abstract class ResultExtensionsExecutor : IGeneratorExecutor
|
|||||||
1 => $"Result<{string.Join(", ", templateArgNames)}>",
|
1 => $"Result<{string.Join(", ", templateArgNames)}>",
|
||||||
_ => $"Result<({string.Join(", ", templateArgNames)})>",
|
_ => $"Result<({string.Join(", ", templateArgNames)})>",
|
||||||
};
|
};
|
||||||
|
protected static string JoinArguments(string arg1, string arg2) => (arg1, arg2) switch
|
||||||
|
{
|
||||||
|
("", "") => "",
|
||||||
|
(string arg, "") => arg,
|
||||||
|
("", string arg) => arg,
|
||||||
|
_ => $"{arg1}, {arg2}"
|
||||||
|
};
|
||||||
|
|
||||||
protected static string GenerateResultValueExpansion(ImmutableArray<string> templateArgNames)
|
protected static string GenerateResultValueExpansion(ImmutableArray<string> templateArgNames)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
|
|||||||
message = Message;
|
message = Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Pure] internal virtual Error AccessUnsafe(int position) => this;
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] internal virtual Error AccessUnsafe(int position) => this;
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConverter(typeof(ExpectedErrorJsonConverter))]
|
[JsonConverter(typeof(ExpectedErrorJsonConverter))]
|
||||||
@@ -393,7 +393,7 @@ public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
|
|||||||
errors.Add(error);
|
errors.Add(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Pure] internal override Error AccessUnsafe(int position) => _errors[position];
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] internal override Error AccessUnsafe(int position) => _errors[position];
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ public sealed class ErrorJsonConverter : JsonConverter<Error>
|
|||||||
|
|
||||||
if (!(reader.TokenType == JsonTokenType.String))
|
if (!(reader.TokenType == JsonTokenType.String))
|
||||||
throw new JsonException("Unable to deserialize Error type.");
|
throw new JsonException("Unable to deserialize Error type.");
|
||||||
|
|
||||||
var propvalue = reader.GetString();
|
var propvalue = reader.GetString();
|
||||||
if (string.IsNullOrEmpty(propvalue))
|
if (string.IsNullOrEmpty(propvalue))
|
||||||
break;
|
break;
|
||||||
@@ -84,7 +84,7 @@ public sealed class ErrorJsonConverter : JsonConverter<Error>
|
|||||||
else if (!string.IsNullOrEmpty(propname))
|
else if (!string.IsNullOrEmpty(propname))
|
||||||
{
|
{
|
||||||
extensionData ??= ImmutableDictionary.CreateBuilder<string, string>();
|
extensionData ??= ImmutableDictionary.CreateBuilder<string, string>();
|
||||||
extensionData.Add(propname, propvalue);
|
extensionData[propname] = propvalue;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ internal enum ResultState : byte
|
|||||||
|
|
||||||
public readonly partial struct Result : IEquatable<Result>
|
public readonly partial struct Result : IEquatable<Result>
|
||||||
{
|
{
|
||||||
internal SuccessUnit Value => new();
|
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Simplified source generation")]
|
||||||
|
internal SuccessUnit Value
|
||||||
|
{
|
||||||
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get => new();
|
||||||
|
}
|
||||||
|
|
||||||
internal readonly Error? Error;
|
internal readonly Error? Error;
|
||||||
internal readonly ResultState State;
|
internal readonly ResultState State;
|
||||||
|
|
||||||
@@ -36,12 +42,18 @@ public readonly partial struct Result : IEquatable<Result>
|
|||||||
public static Result<(T1, T2, T3, T4, T5)> Success<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) => new((value1, value2, value3, value4, value5));
|
public static Result<(T1, T2, T3, T4, T5)> Success<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) => new((value1, value2, value3, value4, value5));
|
||||||
|
|
||||||
|
|
||||||
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static Result Failure(string error) => Error.New(error ?? throw new ArgumentNullException(nameof(error)));
|
||||||
|
|
||||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static Result Failure(Error error) => new(error ?? throw new ArgumentNullException(nameof(error)));
|
public static Result Failure(Error error) => new(error ?? throw new ArgumentNullException(nameof(error)));
|
||||||
|
|
||||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static Result Failure(Exception exception) => new(Error.New(exception) ?? throw new ArgumentNullException(nameof(exception)));
|
public static Result Failure(Exception exception) => new(Error.New(exception) ?? throw new ArgumentNullException(nameof(exception)));
|
||||||
|
|
||||||
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static Result<T> Failure<T>(string error) => Error.New(error ?? throw new ArgumentNullException(nameof(error)));
|
||||||
|
|
||||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static Result<T> Failure<T>(Error error) => new(error ?? throw new ArgumentNullException(nameof(error)));
|
public static Result<T> Failure<T>(Error error) => new(error ?? throw new ArgumentNullException(nameof(error)));
|
||||||
|
|
||||||
@@ -66,13 +78,13 @@ public readonly partial struct Result : IEquatable<Result>
|
|||||||
[Pure] public bool IsSuccess => Error is null;
|
[Pure] public bool IsSuccess => Error is null;
|
||||||
[Pure] public bool IsFailure => Error is not null;
|
[Pure] public bool IsFailure => Error is not null;
|
||||||
|
|
||||||
[Pure] public bool Success([MaybeNullWhen(false)]out SuccessUnit? u, [MaybeNullWhen(true), NotNullWhen(false)]out Error? error)
|
[Pure] public bool TryGetValue([MaybeNullWhen(false)]out SuccessUnit? u, [MaybeNullWhen(true), NotNullWhen(false)]out Error? error)
|
||||||
{
|
{
|
||||||
switch (State)
|
switch (State)
|
||||||
{
|
{
|
||||||
case ResultState.Success:
|
case ResultState.Success:
|
||||||
u = new SuccessUnit();
|
u = new SuccessUnit();
|
||||||
error = default;
|
error = null;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case ResultState.Error:
|
case ResultState.Error:
|
||||||
@@ -161,13 +173,13 @@ public readonly struct Result<T> : IEquatable<Result<T>>
|
|||||||
[Pure] public bool IsSuccess => State == ResultState.Success;
|
[Pure] public bool IsSuccess => State == ResultState.Success;
|
||||||
[Pure] public bool IsFailure => State == ResultState.Error;
|
[Pure] public bool IsFailure => State == ResultState.Error;
|
||||||
|
|
||||||
[Pure] public bool Success([MaybeNullWhen(false)]out T value, [MaybeNullWhen(true), NotNullWhen(false)]out Error? error)
|
[Pure] public bool TryGetValue([MaybeNullWhen(false)]out T value, [MaybeNullWhen(true), NotNullWhen(false)]out Error? error)
|
||||||
{
|
{
|
||||||
switch (State)
|
switch (State)
|
||||||
{
|
{
|
||||||
case ResultState.Success:
|
case ResultState.Success:
|
||||||
value = Value;
|
value = Value;
|
||||||
error = default;
|
error = null;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case ResultState.Error:
|
case ResultState.Error:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class GeneralUsage
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
Assert.Equal("TEST_1;SOME", result);
|
Assert.Equal("TEST_1;SOME", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ public class GeneralUsage
|
|||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var error = Error.New("test");
|
var error = Error.New("test");
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
|
||||||
var result = Result.Success()
|
var result = Result.Success()
|
||||||
@@ -94,7 +94,7 @@ public class GeneralUsage
|
|||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var error = Error.New("test");
|
var error = Error.New("test");
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
|
||||||
var result = await Result.Success()
|
var result = await Result.Success()
|
||||||
@@ -169,4 +169,47 @@ public class GeneralUsage
|
|||||||
Assert.True(result.IsFailure);
|
Assert.True(result.IsFailure);
|
||||||
Assert.Equal(error, result.Error);
|
Assert.Equal(error, result.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WhenExtendingSuccessWithSuccess_ShouldReturnSuccess()
|
||||||
|
{
|
||||||
|
var success = Result.Success(1)
|
||||||
|
.Append("2");
|
||||||
|
|
||||||
|
var result = success
|
||||||
|
.Extend((i, s) => Result.Success($"{i} + {s}"));
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal((1, "2", "1 + 2"), result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WhenExtendingFailureWithSuccess_ShouldNotEvaluateExtension()
|
||||||
|
{
|
||||||
|
var failure = Result.Success(1)
|
||||||
|
.Append(Result.Failure<string>("failure"));
|
||||||
|
|
||||||
|
var result = failure
|
||||||
|
.Extend((i, s) =>
|
||||||
|
{
|
||||||
|
Assert.Fail();
|
||||||
|
return Result.Success("");
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(result.IsFailure);
|
||||||
|
Assert.Equal(Error.New("failure"), result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WhenExtendingSuccessWithFailure_ShouldReturnFailure()
|
||||||
|
{
|
||||||
|
var success = Result.Success(1)
|
||||||
|
.Append("2");
|
||||||
|
|
||||||
|
var result = success
|
||||||
|
.Extend((i, s) => Result.Failure<string>("failure"));
|
||||||
|
|
||||||
|
Assert.True(result.IsFailure);
|
||||||
|
Assert.Equal(Error.New("failure"), result.Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user