From 04c682d6fdcb2311fc283a6643470773fd0d8f8d Mon Sep 17 00:00:00 2001 From: just Date: Thu, 16 Jul 2026 19:00:05 +0400 Subject: [PATCH] fixes and adjustments --- Railway/Error.cs | 46 +++++++++++++--- Railway/ReflectionHelper.cs | 19 +++++++ Railway/Result.cs | 35 ++++++++---- Railway/ResultExtensions.cs | 105 ++++++++++++++++++++++++++++++++---- 4 files changed, 177 insertions(+), 28 deletions(-) diff --git a/Railway/Error.cs b/Railway/Error.cs index 58e1df7..99978c9 100644 --- a/Railway/Error.cs +++ b/Railway/Error.cs @@ -9,6 +9,12 @@ public abstract class Error : IEquatable, IComparable { protected internal Error(){} + /// + /// A reusable singleton representing the absence of an error (null-object pattern). + /// + [Pure] + public static Error Empty => EmptyError.Instance; + /// /// Create an /// @@ -99,11 +105,33 @@ public abstract class Error : IEquatable, IComparable [Pure] public abstract bool IsExpected { get; } [Pure] public abstract bool IsExceptional { get; } - /// - /// A reusable singleton representing the absence of an error (null-object pattern). - /// - [Pure] - public static Error Empty => EmptyError.Instance; + [Pure] public Error WithExtensionData(IEnumerable> extensionData) + { + return this switch + { + { IsEmpty: true } => Empty, + ExpectedError expected => new ExpectedError(expected.Type, expected.Message) + { + ExtensionData = extensionData + .UnionBy(expected.ExtensionData, x => x.Key) + .ToImmutableDictionary(), + }, + ExceptionalError exceptional => new ExceptionalError(exceptional.Type, exceptional.Message, exceptional.Exception) + { + ExtensionData = extensionData + .UnionBy(exceptional.ExtensionData, x => x.Key) + .ToImmutableDictionary(), + }, + ManyErrors manyErrors => new ManyErrors(manyErrors.AccessUnsafe()) + { + ExtensionData = extensionData + .UnionBy(manyErrors.ExtensionData, x => x.Key) + .ToImmutableDictionary(), + }, + + _ => throw new NotImplementedException(), // should not reach this path + }; + } [Pure] public Error Append(Error? next) { @@ -179,7 +207,7 @@ public sealed class EmptyError : Error [Pure] public override bool IsExceptional => false; [Pure] - public override IEnumerable ToEnumerable() { yield break; } + public override IEnumerable ToEnumerable() => Enumerable.Empty(); [Pure] public override Exception ToException() => @@ -250,6 +278,11 @@ public sealed class ExceptionalError : Error Exception = exception; ExtensionData = ExtractExtensionData(exception); } + internal ExceptionalError(string type, string message, Exception? exception) + : this(type, message) + { + Exception = exception; + } public ExceptionalError(string type, string message) { @@ -457,6 +490,7 @@ public sealed class ManyErrors : Error, IEnumerable, IReadOnlyList errors.Add(error); } + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] internal ImmutableArray AccessUnsafe() => _errors; [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] internal override Error AccessUnsafe(int position) => _errors[position]; } diff --git a/Railway/ReflectionHelper.cs b/Railway/ReflectionHelper.cs index 3996a10..3b4fa6f 100644 --- a/Railway/ReflectionHelper.cs +++ b/Railway/ReflectionHelper.cs @@ -4,9 +4,22 @@ namespace Just.Railway; internal static class ReflectionHelper { + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static R ObjectCast(object value) where R: class => (R)value; + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsEqual(T? left, T? right) => TypeReflectionCache.IsEqualFunc(left, right); + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + /// + /// Test for potential nullability + /// + /// true if T is reference type or Nullable value type. + public static bool IsNullable() => TypeReflectionCache.IsNullable; + + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNullableStruct() => TypeReflectionCache.IsNullableStruct; + private static class TypeReflectionCache { public static readonly Func IsEqualFunc; @@ -30,8 +43,14 @@ internal static class ReflectionHelper { IsEqualFunc = static (left, right) => left is null ? right is null : left.Equals(right); } + + IsNullableStruct = isNullableStruct; + IsNullable = isNullableStruct || !type.IsValueType; } + public static bool IsNullable { get; } + public static bool IsNullableStruct { get; } + #pragma warning disable CS8604 // Possible null reference argument. [Pure] public static bool IsEqual(R? left, R? right) where R : notnull, IEquatable, T => left is null ? right is null : left.Equals(right); [Pure] public static bool IsEqualNullable(R? left, R? right) where R : struct, IEquatable => left is null ? right is null : right is not null && left.Value.Equals(right.Value); diff --git a/Railway/Result.cs b/Railway/Result.cs index 5e260ca..59a579e 100644 --- a/Railway/Result.cs +++ b/Railway/Result.cs @@ -45,6 +45,9 @@ public readonly partial struct Result : IEquatable [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static Result Failure(string error) => Error.New(error ?? throw new ArgumentNullException(nameof(error))); + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Result Failure(string type, string error) => Error.New(type, error ?? throw new ArgumentNullException(nameof(error))); + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static Result Failure(Error error) => new(error ?? throw new ArgumentNullException(nameof(error))); @@ -54,6 +57,9 @@ public readonly partial struct Result : IEquatable [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static Result Failure(string error) => Error.New(error ?? throw new ArgumentNullException(nameof(error))); + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Result Failure(string type, string error) => Error.New(type, error ?? throw new ArgumentNullException(nameof(error))); + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static Result Failure(Error error) => new(error ?? throw new ArgumentNullException(nameof(error))); @@ -76,7 +82,10 @@ public readonly partial struct Result : IEquatable public static explicit operator Result(SuccessUnit _) => new(null); [Pure] public bool IsSuccess => State == ResultState.Success; - [Pure] public bool IsFailure => State == ResultState.Error; + + [Pure] + [MemberNotNullWhen(true, nameof(Error))] + public bool IsFailure => State == ResultState.Error; [Pure] public bool TryGetValue([MaybeNullWhen(false)]out SuccessUnit? u, [MaybeNullWhen(true), NotNullWhen(false)]out Error? error) { @@ -97,17 +106,18 @@ public readonly partial struct Result : IEquatable } [Pure] public bool TryGetError([MaybeNullWhen(false)]out Error error) { - if (IsSuccess) + switch (State) { - error = default; - return false; + case ResultState.Success: + error = default; + return false; + + case ResultState.Error: + error = Error!; + return true; + + default: throw new ResultNotInitializedException(); } - if (IsFailure) - { - error = Error!; - return true; - } - throw new ResultNotInitializedException(); } [Pure] public override string ToString() => State switch @@ -127,9 +137,12 @@ public readonly partial struct Result : IEquatable [Pure] public override bool Equals(object? obj) => obj is Result other && Equals(other); [Pure] public bool Equals(Result other) { - if (State == ResultState.Bottom || other.State == ResultState.Bottom) + if (State == ResultState.Bottom) throw new ResultNotInitializedException(); + if (other.State == ResultState.Bottom) + throw new ResultNotInitializedException(nameof(other)); + return Error == other.Error; } [Pure] public static bool operator ==(Result left, Result right) => left.Equals(right); diff --git a/Railway/ResultExtensions.cs b/Railway/ResultExtensions.cs index 5315045..0264a86 100644 --- a/Railway/ResultExtensions.cs +++ b/Railway/ResultExtensions.cs @@ -4,6 +4,83 @@ namespace Just.Railway; public static partial class ResultExtensions { + #region MapError + + public static Result MapError(this in Result result, Func func) => result.State switch + { + ResultState.Error => func(result.Error!), + ResultState.Success => result, + _ => throw new ResultNotInitializedException(nameof(result)), + }; + public static Result MapError(this in Result result, Func func) => result.State switch + { + ResultState.Error => func(result.Error!), + ResultState.Success => result, + _ => throw new ResultNotInitializedException(nameof(result)), + }; + + public static async Task MapError(this Task resultTask, Func func) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Error => func(result.Error!), + ResultState.Success => result, + _ => throw new ResultNotInitializedException(nameof(result)), + }; + } + public static async Task MapError(this Result result, Func> func) + { + return result.State switch + { + ResultState.Error => await func(result.Error!).ConfigureAwait(false), + ResultState.Success => result, + _ => throw new ResultNotInitializedException(nameof(result)), + }; + } + public static async Task MapError(this Task resultTask, Func> func) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Error => await func(result.Error!), + ResultState.Success => result, + _ => throw new ResultNotInitializedException(nameof(result)), + }; + } + + public static async Task> MapError(this Task> resultTask, Func func) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Error => func(result.Error!), + ResultState.Success => result, + _ => throw new ResultNotInitializedException(nameof(result)), + }; + } + public static async Task> MapError(this Result result, Func> func) + { + return result.State switch + { + ResultState.Error => await func(result.Error!).ConfigureAwait(false), + ResultState.Success => result, + _ => throw new ResultNotInitializedException(nameof(result)), + }; + } + public static async Task> MapError(this Task> resultTask, Func> func) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Error => await func(result.Error!), + ResultState.Success => result, + _ => throw new ResultNotInitializedException(nameof(result)), + }; + } + + #endregion + #region Finally public static Result Finally(this in Result result, Action action) @@ -142,20 +219,23 @@ public static partial class ResultExtensions switch (result.State) { case ResultState.Success: - break; + continue; case ResultState.Error: errors ??= ImmutableArray.CreateBuilder(); ManyErrors.AppendSanitized(errors, result.Error!); - break; + continue; default: throw new ResultNotInitializedException(nameof(results)); } } - - return errors is null - ? new(null) - : new(new ManyErrors(errors.ToImmutable())); + + return errors switch + { + null => new Result(null), + { Count: 1 } => errors[0], + _ => new(new ManyErrors(errors.ToImmutable())) + }; } public static async Task Merge(this IEnumerable> tasks) { @@ -192,20 +272,23 @@ public static partial class ResultExtensions case ResultState.Success: values ??= ImmutableArray.CreateBuilder(); values.Add(result.Value); - break; + continue; case ResultState.Error: errors ??= ImmutableArray.CreateBuilder(); ManyErrors.AppendSanitized(errors, result.Error!); - break; + continue; default: throw new ResultNotInitializedException(nameof(results)); } } - return errors is null - ? new(values?.ToImmutable() ?? Enumerable.Empty()) - : new(new ManyErrors(errors.ToImmutable())); + return errors switch + { + null => new Result>(values?.ToImmutable() ?? Enumerable.Empty()), + { Count: 1 } => errors[0], + _ => new(new ManyErrors(errors.ToImmutable())) + }; } public static async Task>> Merge(this IEnumerable>> tasks) {