+4
-2
@@ -206,6 +206,7 @@ public sealed class ExpectedError : Error
|
||||
{
|
||||
public ExpectedError(string type, string message)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
Type = type;
|
||||
Message = message;
|
||||
}
|
||||
@@ -252,6 +253,7 @@ public sealed class ExceptionalError : Error
|
||||
|
||||
public ExceptionalError(string type, string message)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
Type = type;
|
||||
Message = message;
|
||||
}
|
||||
@@ -343,7 +345,7 @@ public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
|
||||
[Pure] public override string Type => "many_errors";
|
||||
|
||||
private string? _lazyMessage = null;
|
||||
[Pure] public override string Message => _lazyMessage ??= ToFullArrayString(_errors);
|
||||
[Pure] public override string Message => _lazyMessage ??= ToFullArrayString(_errors); // not thread safe. wontfix
|
||||
|
||||
[Pure] private static string ToFullArrayString(in ImmutableArray<Error> errors)
|
||||
{
|
||||
@@ -428,7 +430,7 @@ public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
|
||||
}
|
||||
|
||||
private int? _lazyHashCode = null;
|
||||
[Pure] public override int GetHashCode() => _lazyHashCode ??= CalcHashCode(_errors);
|
||||
[Pure] public override int GetHashCode() => _lazyHashCode ??= CalcHashCode(_errors); // not thread safe. wontfix
|
||||
private static int CalcHashCode(in ImmutableArray<Error> errors)
|
||||
{
|
||||
if (errors.IsEmpty)
|
||||
|
||||
@@ -7,13 +7,9 @@ internal static class ReflectionHelper
|
||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsEqual<T>(T? left, T? right) => TypeReflectionCache<T>.IsEqualFunc(left, right);
|
||||
|
||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Compare<T>(T? left, T? right) => TypeReflectionCache<T>.CompareFunc(left, right);
|
||||
|
||||
private static class TypeReflectionCache<T>
|
||||
{
|
||||
public static readonly Func<T?, T?, bool> IsEqualFunc;
|
||||
public static readonly Func<T?, T?, int> CompareFunc;
|
||||
|
||||
static TypeReflectionCache()
|
||||
{
|
||||
@@ -34,33 +30,11 @@ internal static class ReflectionHelper
|
||||
{
|
||||
IsEqualFunc = static (left, right) => left is null ? right is null : left.Equals(right);
|
||||
}
|
||||
|
||||
var comparableType = typeof(IComparable<>).MakeGenericType(underlyingType);
|
||||
if (comparableType.IsAssignableFrom(underlyingType))
|
||||
{
|
||||
var compareFunc = thisType.GetMethod(isNullableStruct ? nameof(CompareNullable) : nameof(Compare), BindingFlags.Static | BindingFlags.Public)
|
||||
!.MakeGenericMethod(underlyingType);
|
||||
|
||||
CompareFunc = (Func<T?, T?, int>)Delegate.CreateDelegate(typeof(Func<T?, T?, int>), compareFunc);
|
||||
}
|
||||
else
|
||||
{
|
||||
CompareFunc = static (left, right) => left is null
|
||||
? right is null ? 0 : -1
|
||||
: right is null ? 1 : left.GetHashCode().CompareTo(right.GetHashCode());
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CS8604 // Possible null reference argument.
|
||||
[Pure] public static bool IsEqual<R>(R? left, R? right) where R : notnull, IEquatable<R>, T => left is null ? right is null : left.Equals(right);
|
||||
[Pure] public static bool IsEqualNullable<R>(R? left, R? right) where R : struct, IEquatable<R> => left is null ? right is null : right is not null && left.Value.Equals(right.Value);
|
||||
|
||||
[Pure] public static int Compare<R>(R? left, R? right) where R : notnull, IComparable<R>, T => left is null
|
||||
? right is null ? 0 : -1
|
||||
: right is null ? 1 : left.CompareTo(right);
|
||||
[Pure] public static int CompareNullable<R>(R? left, R? right) where R : struct, IComparable<R> => left is null
|
||||
? right is null ? 0 : -1
|
||||
: right is null ? 1 : left.Value.CompareTo(right.Value);
|
||||
#pragma warning restore CS8604 // Possible null reference argument.
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -127,9 +127,9 @@ public readonly partial struct Result : IEquatable<Result>
|
||||
[Pure] public override bool Equals(object? obj) => obj is Result other && Equals(other);
|
||||
[Pure] public bool Equals(Result other)
|
||||
{
|
||||
if (State == ResultState.Bottom)
|
||||
if (State == ResultState.Bottom || other.State == ResultState.Bottom)
|
||||
throw new ResultNotInitializedException();
|
||||
|
||||
|
||||
return Error == other.Error;
|
||||
}
|
||||
[Pure] public static bool operator ==(Result left, Result right) => left.Equals(right);
|
||||
@@ -260,9 +260,9 @@ public readonly struct Result<T> : IEquatable<Result<T>>
|
||||
[Pure] public override bool Equals(object? obj) => obj is Result<T> other && Equals(other);
|
||||
[Pure] public bool Equals(Result<T> other)
|
||||
{
|
||||
if (State == ResultState.Bottom)
|
||||
if (State == ResultState.Bottom || other.State == ResultState.Bottom)
|
||||
throw new ResultNotInitializedException();
|
||||
|
||||
|
||||
if (IsSuccess != other.IsSuccess)
|
||||
return false;
|
||||
|
||||
|
||||
+140
-71
@@ -4,9 +4,82 @@ namespace Just.Railway;
|
||||
|
||||
public static partial class ResultExtensions
|
||||
{
|
||||
#region Match (with fallback)
|
||||
#region Finally
|
||||
|
||||
public static T Match<T>(this in Result<T> result, Func<Error, T> fallback)
|
||||
public static Result Finally(this in Result result, Action action)
|
||||
{
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
case ResultState.Error:
|
||||
action.Invoke();
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<Result> Finally(this Task<Result> resultTask, Action action)
|
||||
{
|
||||
var result = await resultTask;
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
case ResultState.Error:
|
||||
action.Invoke();
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(resultTask));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
public static async Task<Result> Finally(this Result result, Func<Task> action)
|
||||
{
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
case ResultState.Error:
|
||||
await action.Invoke();
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
public static async Task<Result> Finally(this Task<Result> resultTask, Func<Task> action)
|
||||
{
|
||||
var result = await resultTask;
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
case ResultState.Error:
|
||||
await action.Invoke();
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(resultTask));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ValueOr
|
||||
|
||||
public static T ValueOr<T>(this in Result<T> result, T fallback)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => result.Value,
|
||||
ResultState.Error => fallback,
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
public static T ValueOr<T>(this in Result<T> result, Func<Error, T> fallback)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
@@ -16,7 +89,17 @@ public static partial class ResultExtensions
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<T> Match<T>(this Result<T> result, Func<Error, Task<T>> fallback)
|
||||
public static async Task<T> ValueOr<T>(this Task<Result<T>> resultTask, T fallback)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => result.Value,
|
||||
ResultState.Error => fallback,
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
public static async Task<T> ValueOr<T>(this Result<T> result, Func<Error, Task<T>> fallback)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
@@ -25,7 +108,7 @@ public static partial class ResultExtensions
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
public static async Task<T> Match<T>(this Task<Result<T>> resultTask, Func<Error, T> fallback)
|
||||
public static async Task<T> ValueOr<T>(this Task<Result<T>> resultTask, Func<Error, T> fallback)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
@@ -35,37 +118,7 @@ public static partial class ResultExtensions
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
public static async Task<T> Match<T>(this Task<Result<T>> resultTask, Func<Error, Task<T>> fallback)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => result.Value,
|
||||
ResultState.Error => await fallback(result.Error!).ConfigureAwait(false),
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
|
||||
public static async ValueTask<T> Match<T>(this Result<T> result, Func<Error, ValueTask<T>> fallback)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => result.Value,
|
||||
ResultState.Error => await fallback(result.Error!).ConfigureAwait(false),
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
public static async ValueTask<T> Match<T>(this ValueTask<Result<T>> resultTask, Func<Error, T> fallback)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => result.Value,
|
||||
ResultState.Error => fallback(result.Error!),
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
public static async ValueTask<T> Match<T>(this ValueTask<Result<T>> resultTask, Func<Error, ValueTask<T>> fallback)
|
||||
public static async Task<T> ValueOr<T>(this Task<Result<T>> resultTask, Func<Error, Task<T>> fallback)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
@@ -83,79 +136,95 @@ public static partial class ResultExtensions
|
||||
public static Result Merge(this IEnumerable<Result> results)
|
||||
{
|
||||
ImmutableArray<Error>.Builder? errors = null;
|
||||
bool hasErrors = false;
|
||||
|
||||
foreach (var result in results.OrderBy(x => x.State))
|
||||
foreach (var result in results)
|
||||
{
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Error:
|
||||
hasErrors = true;
|
||||
errors ??= ImmutableArray.CreateBuilder<Error>();
|
||||
ManyErrors.AppendSanitized(errors, result.Error!);
|
||||
case ResultState.Success:
|
||||
break;
|
||||
|
||||
case ResultState.Success:
|
||||
if (hasErrors) goto afterLoop;
|
||||
case ResultState.Error:
|
||||
errors ??= ImmutableArray.CreateBuilder<Error>();
|
||||
ManyErrors.AppendSanitized(errors, result.Error!);
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(results));
|
||||
}
|
||||
}
|
||||
afterLoop:
|
||||
return hasErrors
|
||||
? new(new ManyErrors(errors!.ToImmutable()))
|
||||
: new(null);
|
||||
|
||||
return errors is null
|
||||
? new(null)
|
||||
: new(new ManyErrors(errors.ToImmutable()));
|
||||
}
|
||||
public static async Task<Result> Merge(this IEnumerable<Task<Result>> tasks)
|
||||
{
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
var taskList = tasks.ToArray();
|
||||
var results = new Result[taskList.Length];
|
||||
|
||||
// Wait for all tasks to complete (suppress AggregateException — we'll check each individually)
|
||||
try { await Task.WhenAll(taskList).ConfigureAwait(false); } catch { /* handled per-task below */ }
|
||||
|
||||
for (int i = 0; i < taskList.Length; i++)
|
||||
{
|
||||
var task = taskList[i];
|
||||
results[i] = task.Status switch
|
||||
{
|
||||
TaskStatus.RanToCompletion => task.Result,
|
||||
TaskStatus.Faulted => Result.Failure(task.Exception!),
|
||||
TaskStatus.Canceled => Result.Failure(Error.New("task_canceled", "Task was canceled")),
|
||||
_ => Result.Failure(Error.New("task_unexpected", $"Task in unexpected state: {task.Status}"))
|
||||
};
|
||||
}
|
||||
|
||||
return results.Merge();
|
||||
}
|
||||
|
||||
public static Result<IEnumerable<T>> Merge<T>(this IEnumerable<Result<T>> results)
|
||||
{
|
||||
ImmutableList<T>.Builder? values = null;
|
||||
ImmutableArray<Error>.Builder? errors = null;
|
||||
bool hasErrors = false;
|
||||
ImmutableArray<T>.Builder? values = null;
|
||||
|
||||
foreach (var result in results.OrderBy(x => x.State))
|
||||
foreach (var result in results)
|
||||
{
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
values ??= ImmutableArray.CreateBuilder<T>();
|
||||
values.Add(result.Value);
|
||||
break;
|
||||
|
||||
case ResultState.Error:
|
||||
hasErrors = true;
|
||||
errors ??= ImmutableArray.CreateBuilder<Error>();
|
||||
ManyErrors.AppendSanitized(errors, result.Error!);
|
||||
break;
|
||||
|
||||
case ResultState.Success:
|
||||
if (hasErrors) goto afterLoop;
|
||||
values ??= ImmutableList.CreateBuilder<T>();
|
||||
values.Add(result.Value);
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(results));
|
||||
}
|
||||
}
|
||||
afterLoop:
|
||||
return hasErrors
|
||||
? new(new ManyErrors(errors!.ToImmutable()))
|
||||
: new(values is not null ? values.ToImmutable() : ImmutableList<T>.Empty);
|
||||
|
||||
return errors is null
|
||||
? new(values?.ToImmutable() ?? Enumerable.Empty<T>())
|
||||
: new(new ManyErrors(errors.ToImmutable()));
|
||||
}
|
||||
public static async Task<Result<IEnumerable<T>>> Merge<T>(this IEnumerable<Task<Result<T>>> tasks)
|
||||
{
|
||||
var taskList = tasks.ToList();
|
||||
var results = new Result<T>[taskList.Count];
|
||||
var taskList = tasks.ToArray();
|
||||
var results = new Result<T>[taskList.Length];
|
||||
|
||||
for (int i = 0; i < taskList.Count; i++)
|
||||
// Wait for all tasks to complete (suppress AggregateException — we'll check each individually)
|
||||
try { await Task.WhenAll(taskList).ConfigureAwait(false); } catch { /* handled per-task below */ }
|
||||
|
||||
for (int i = 0; i < taskList.Length; i++)
|
||||
{
|
||||
var task = await Task.WhenAny(taskList);
|
||||
taskList.Remove(task);
|
||||
|
||||
results[i] = task.IsCompletedSuccessfully
|
||||
? task.Result
|
||||
: task.Exception!;
|
||||
var task = taskList[i];
|
||||
results[i] = task.Status switch
|
||||
{
|
||||
TaskStatus.RanToCompletion => task.Result,
|
||||
TaskStatus.Faulted => Result.Failure<T>(task.Exception!),
|
||||
TaskStatus.Canceled => Result.Failure<T>(Error.New("task_canceled", "Task was canceled")),
|
||||
_ => Result.Failure<T>(Error.New("task_unexpected", $"Task in unexpected state: {task.Status}"))
|
||||
};
|
||||
}
|
||||
|
||||
return results.Merge();
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
namespace Just.Railway;
|
||||
|
||||
/// <summary>
|
||||
/// Result based try/catch
|
||||
/// </summary>
|
||||
/// <remarks>All extension methods are source generated.</remarks>
|
||||
public static partial class Try
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
namespace Just.Railway.ValueTaskExtensions;
|
||||
|
||||
public static partial class ResultValueTaskExtensions
|
||||
{
|
||||
#region Finally
|
||||
|
||||
public static async ValueTask<Result> ValueTaskFinally(this ValueTask<Result> resultTask, Action action)
|
||||
{
|
||||
var result = await resultTask;
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
case ResultState.Error:
|
||||
action.Invoke();
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(resultTask));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
public static async ValueTask<Result> ValueTaskFinally(this Result result, Func<ValueTask> action)
|
||||
{
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
case ResultState.Error:
|
||||
await action.Invoke();
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
public static async ValueTask<Result> ValueTaskFinally(this ValueTask<Result> resultTask, Func<ValueTask> action)
|
||||
{
|
||||
var result = await resultTask;
|
||||
switch (result.State)
|
||||
{
|
||||
case ResultState.Success:
|
||||
case ResultState.Error:
|
||||
await action.Invoke();
|
||||
break;
|
||||
|
||||
default: throw new ResultNotInitializedException(nameof(resultTask));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ValueOr
|
||||
|
||||
|
||||
public static async ValueTask<T> ValueTaskValueOr<T>(this ValueTask<Result<T>> resultTask, T fallback)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => result.Value,
|
||||
ResultState.Error => fallback,
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
public static async ValueTask<T> ValueTaskValueOr<T>(this Result<T> result, Func<Error, ValueTask<T>> fallback)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => result.Value,
|
||||
ResultState.Error => await fallback(result.Error!).ConfigureAwait(false),
|
||||
_ => throw new ResultNotInitializedException(nameof(result))
|
||||
};
|
||||
}
|
||||
public static async ValueTask<T> ValueTaskValueOr<T>(this ValueTask<Result<T>> resultTask, Func<Error, T> fallback)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => result.Value,
|
||||
ResultState.Error => fallback(result.Error!),
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
public static async ValueTask<T> ValueTaskValueOr<T>(this ValueTask<Result<T>> resultTask, Func<Error, ValueTask<T>> fallback)
|
||||
{
|
||||
var result = await resultTask.ConfigureAwait(false);
|
||||
return result.State switch
|
||||
{
|
||||
ResultState.Success => result.Value,
|
||||
ResultState.Error => await fallback(result.Error!).ConfigureAwait(false),
|
||||
_ => throw new ResultNotInitializedException(nameof(resultTask))
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user