diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..76dd9a1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "Just.Railway.sln" +} \ No newline at end of file diff --git a/JustDotNet.Railway.sln b/JustDotNet.Railway.sln new file mode 100644 index 0000000..f22dfd8 --- /dev/null +++ b/JustDotNet.Railway.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Railway", "Railway\Railway.csproj", "{23505B47-110B-4FB7-8615-741CC7F463D4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Raliway.Tests", "Raliway.Tests\Raliway.Tests.csproj", "{607F91E4-83A2-48C4-BAC2-2205BEE81D93}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {23505B47-110B-4FB7-8615-741CC7F463D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23505B47-110B-4FB7-8615-741CC7F463D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23505B47-110B-4FB7-8615-741CC7F463D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23505B47-110B-4FB7-8615-741CC7F463D4}.Release|Any CPU.Build.0 = Release|Any CPU + {607F91E4-83A2-48C4-BAC2-2205BEE81D93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index bcf9dbf..43f2278 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Base for Railway Programming in .NET 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 diff --git a/Railway/Ensure.cs b/Railway/Ensure.cs new file mode 100644 index 0000000..145b3ab --- /dev/null +++ b/Railway/Ensure.cs @@ -0,0 +1,323 @@ +namespace Just.Railway; + +public static class Ensure +{ + public delegate Error ErrorFactory(string valueExpression); + + public const string DefaultErrorType = "EnsureFailed"; + + [Pure] public static Ensure That(T value, [CallerArgumentExpression(nameof(value))]string valueExpression = "") => new(value, valueExpression); + [Pure] public static async Task> That(Task value, [CallerArgumentExpression(nameof(value))]string valueExpression = "") => new(await value.ConfigureAwait(false), valueExpression); + + [Pure] public static Result Result(this in Ensure ensure) => ensure.State switch + { + ResultState.Success => new(ensure.Value), + ResultState.Error => new(ensure.Error!), + _ => throw new EnsureNotInitializedException(nameof(ensure)) + }; + [Pure] + public static async Task> Result(this Task> ensureTask) + { + var ensure = await ensureTask.ConfigureAwait(false); + return ensure.State switch + { + ResultState.Success => new(ensure.Value), + ResultState.Error => new(ensure.Error!), + _ => throw new EnsureNotInitializedException(nameof(ensureTask)) + }; + } + [Pure] + public static Ensure Satisfies(this in Ensure ensure, Func 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 Satisfies(this in Ensure ensure, Func 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> Satisfies(this Task> ensureTask, Func requirement, Error error = default!) + { + 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> Satisfies(this Task> ensureTask, Func 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> Satisfies(this Ensure ensure, Func> 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> Satisfies(this Ensure ensure, Func> 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> Satisfies(this Task> ensureTask, Func> 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> Satisfies(this Task> ensureTask, Func> 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), + _ => throw new EnsureNotInitializedException(nameof(ensureTask)) + }; + } + + [Pure] + public static Ensure NotNull(this in Ensure 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> NotNull(this Task> 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 NotNull(this in Ensure 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> NotNull(this Task> 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 NotEmpty(this in Ensure 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> NotEmpty(this in Ensure> 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> NotEmpty(this in Ensure> 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> NotEmpty(this in Ensure> 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> NotEmpty(this in Ensure> 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> NotEmpty(this in Ensure> 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> NotEmpty(this in Ensure> 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 NotEmpty(this in Ensure 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 NotWhitespace(this in Ensure ensure, Error error = default!) + { + return ensure.State switch + { + ResultState.Success => string.IsNullOrWhiteSpace(ensure.Value) + ? new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty or consists exclusively of white-space characters."), ensure.ValueExpression) + : new(ensure.Value, ensure.ValueExpression), + ResultState.Error => new(ensure.Error!, ensure.ValueExpression), + _ => throw new EnsureNotInitializedException(nameof(ensure)) + }; + } +} + +public readonly struct Ensure +{ + internal readonly ResultState State; + internal readonly Error? Error; + internal readonly T Value; + internal readonly string ValueExpression; + + internal Ensure(T value, string valueExpression) + { + Value = value; + ValueExpression = valueExpression; + State = ResultState.Success; + } + + internal Ensure(Error error, string valueExpression) + { + Error = error; + ValueExpression = valueExpression; + Value = default!; + State = ResultState.Error; + } +} + +[Serializable] +public class EnsureNotInitializedException(string variableName = "this") : InvalidOperationException("Ensure was not properly initialized.") +{ + public string VariableName { get; } = variableName; +} diff --git a/Railway/Error.cs b/Railway/Error.cs new file mode 100644 index 0000000..1b9c7d2 --- /dev/null +++ b/Railway/Error.cs @@ -0,0 +1,385 @@ +using System.Collections; +using System.Runtime.Serialization; +using System.Text; + +namespace Just.Railway; + +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$$err")] +[JsonDerivedType(typeof(ExpectedError), typeDiscriminator: 0)] +[JsonDerivedType(typeof(ExceptionalError), typeDiscriminator: 1)] +[JsonDerivedType(typeof(ManyErrors))] +public abstract class Error : IEquatable, IComparable +{ + private IDictionary? _extensionData; + + protected internal Error(){} + + /// + /// Create an + /// + /// Exception + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Error New(Exception thisException) => new ExceptionalError(thisException); + /// + /// Create a with an overriden detail. This can be useful for sanitising the display message + /// when internally we're carrying the exception. + /// + /// Error detail + /// Exception + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Error New(string message, Exception thisException) => + new ExceptionalError(message, thisException); + /// + /// Create an + /// + /// Error detail + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Error New(string message) => + new ExpectedError("error", message); + /// + /// Create an + /// + /// Error code + /// Error detail + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Error New(string type, string message) => + new ExpectedError(type, message); + /// + /// Create a + /// + /// Collects many errors into a single type, called + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Error Many(Error error1, Error error2) => (error1, error2) switch + { + (null, null) => new ManyErrors(Enumerable.Empty()), + (Error err, null) => err, + (Error err, { IsEmpty: true }) => err, + (null, Error err) => err, + ({ IsEmpty: true }, Error err) => err, + (Error err1, Error err2) => new ManyErrors(err1, err2) + }; + /// + /// Create a + /// + /// Collects many errors into a single type, called + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Error Many(params Error[] errors) => errors.Length switch + { + 1 => errors[0], + _ => new ManyErrors(errors) + }; + /// + /// Create a + /// + /// Collects many errors into a single type, called + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Error Many(IEnumerable errors) => new ManyErrors(errors); + + [Pure] public abstract string Type { get; } + [Pure] public abstract string Message { get; } + [Pure, JsonExtensionData] public IDictionary ExtensionData + { + get => _extensionData ??= new Dictionary(); + init => _extensionData = value ?? new Dictionary(); + } + [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(); + _extensionData[name] = value; + } + } + } + + [Pure, JsonIgnore] public abstract int Count { get; } + [Pure, JsonIgnore] public abstract bool IsEmpty { get; } + [Pure, JsonIgnore] public abstract bool IsExpected { get; } + [Pure, JsonIgnore] public abstract bool IsExeptional { get; } + + [Pure] public Error Append(Error? next) + { + if (next is null || next.IsEmpty) + return this; + + if (this.IsEmpty) + return next; + + return new ManyErrors(this, next); + } + [Pure] + [return: NotNullIfNotNull(nameof(lhs))] + [return: NotNullIfNotNull(nameof(rhs))] + public static Error? operator +(Error? lhs, Error? rhs) => lhs is null ? rhs : lhs.Append(rhs); + + [Pure] public abstract IEnumerable ToEnumerable(); + + /// + /// Gets the + /// + /// New constructed from current error + [Pure] public virtual Exception ToException() => new ErrorException(Type, Message); + + /// + /// Compares error types + /// + /// when other error has the same type + [Pure] public virtual bool IsSimilarTo([NotNullWhen(true)] Error? other) => Type == other?.Type; + [Pure] public virtual bool Equals([NotNullWhen(true)] Error? other) => IsSimilarTo(other) && Message == other.Message; + [Pure] public static bool operator ==(Error? lhs, Error? rhs) => lhs is null ? rhs is null : lhs.Equals(rhs); + [Pure] public static bool operator !=(Error? lhs, Error? rhs) => !(lhs == rhs); + [Pure] public sealed override bool Equals(object? obj) => Equals(obj as Error); + [Pure] public override int GetHashCode() => HashCode.Combine(Type, Message); + + [Pure] public virtual int CompareTo(Error? other) + { + if (other is null) + { + return -1; + } + + var compareResult = string.Compare(Type, other.Type); + if (compareResult != 0) + { + return compareResult; + } + + return string.Compare(Message, other.Message); + } + + [Pure] public override string ToString() => Message; + [Pure] public void Deconstruct(out string type, out string message) + { + type = Type; + message = Message; + } +} + +public sealed class ExpectedError : Error +{ + [JsonConstructor] + public ExpectedError(string type, string message) + { + Type = type; + Message = message; + } + public ExpectedError(string message) + : this("error", message) + { + } + + [Pure] public override string Type { get; } + [Pure] public override string Message { get; } + + [Pure, JsonIgnore] public override int Count => 1; + [Pure, JsonIgnore] public override bool IsEmpty => false; + [Pure, JsonIgnore] public override bool IsExpected => true; + [Pure, JsonIgnore] public override bool IsExeptional => false; + + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public override IEnumerable ToEnumerable() + { + yield return this; + } +} + +public sealed class ExceptionalError : Error +{ + internal readonly Exception? Exception; + + internal ExceptionalError(Exception exception) + : this(exception.GetType().Name, exception.Message) + { + Exception = exception; + FillExtensionData(exception); + } + internal ExceptionalError(string message, Exception exception) + : this(exception.GetType().Name, message) + { + Exception = exception; + FillExtensionData(exception); + } + + [JsonConstructor] + public ExceptionalError(string type, string message) + { + Type = type; + Message = message; + } + + [Pure] public override string Type { get; } + [Pure] public override string Message { get; } + + [Pure, JsonIgnore] public override int Count => 1; + [Pure, JsonIgnore] public override bool IsEmpty => false; + [Pure, JsonIgnore] public override bool IsExpected => false; + [Pure, JsonIgnore] public override bool IsExeptional => true; + + [Pure] public override Exception ToException() => Exception ?? base.ToException(); + + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public override IEnumerable ToEnumerable() + { + yield return this; + } + + private void FillExtensionData(Exception exception) + { + foreach (var key in exception.Data.Keys) + { + var value = exception.Data[key]; + if (key is null || value is null) + continue; + this.ExtensionData[key.ToString() ?? string.Empty] = value; + } + } +} + +[DataContract] +public sealed class ManyErrors : Error, IEnumerable +{ + private readonly List _errors; + [Pure, DataMember] public IEnumerable Errors { get => _errors; } + + internal ManyErrors(Error head, Error tail) + { + _errors = new List(head.Count + tail.Count); + + if (head.Count == 1) + _errors.Add(head); + else if (head.Count > 1) + _errors.AddRange(head.ToEnumerable()); + + if (tail.Count == 1) + _errors.Add(tail); + else if (tail.Count > 1) + _errors.AddRange(tail.ToEnumerable()); + } + public ManyErrors(IEnumerable errors) + { + _errors = errors.SelectMany(x => x.ToEnumerable()) + .Where(x => !x.IsEmpty) + .ToList(); + } + + [Pure] public override string Type => "many_errors"; + [Pure] public override string Message => ToFullArrayString(); + [Pure] public override string ToString() => ToFullArrayString(); + + [Pure] private string ToFullArrayString() + { + var separator = Environment.NewLine; + var lastIndex = _errors.Count - 1; + + var sb = new StringBuilder(); + for (int i = 0; i < _errors.Count; i++) + { + sb.Append(_errors[i]); + if (i < lastIndex) + sb.Append(separator); + } + + return sb.ToString(); + } + + [Pure] public override int Count => _errors.Count; + [Pure, JsonIgnore] public override bool IsEmpty => _errors.Count == 0; + [Pure, JsonIgnore] public override bool IsExpected => _errors.All(static x => x.IsExpected); + [Pure, JsonIgnore] public override bool IsExeptional => _errors.Any(static x => x.IsExeptional); + + [Pure] public override Exception ToException() => new AggregateException(_errors.Select(static x => x.ToException())); + [Pure] public override IEnumerable ToEnumerable() => _errors; + + [Pure] public override int CompareTo(Error? other) + { + if (other is null) + return -1; + if (other.Count != _errors.Count) + return _errors.Count.CompareTo(other.Count); + + var compareResult = 0; + int i = 0; + foreach (var otherErr in other.ToEnumerable()) + { + var thisErr = _errors[i++]; + compareResult = thisErr.CompareTo(otherErr); + if (compareResult != 0) + { + return compareResult; + } + } + + return compareResult; + } + [Pure] public override bool IsSimilarTo([NotNullWhen(true)] Error? other) + { + if (other is null) + { + return false; + } + if (_errors.Count != other.Count) + { + return false; + } + int i = 0; + foreach (var otherErr in other.ToEnumerable()) + { + var thisErr = _errors[i++]; + if (!thisErr.IsSimilarTo(otherErr)) + { + return false; + } + } + return true; + } + [Pure] public override bool Equals([NotNullWhen(true)] Error? other) + { + if (other is null) + { + return false; + } + if (_errors.Count != other.Count) + { + return false; + } + int i = 0; + foreach (var otherErr in other.ToEnumerable()) + { + var thisErr = _errors[i++]; + if (!thisErr.Equals(otherErr)) + { + return false; + } + } + return true; + } + [Pure] public override int GetHashCode() + { + if (_errors.Count == 0) + return 0; + + var hash = new HashCode(); + foreach (var err in _errors) + { + hash.Add(err); + } + return hash.ToHashCode(); + } + + [Pure] public IEnumerator GetEnumerator() => _errors.GetEnumerator(); + [Pure] IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +[Serializable] +public sealed class ErrorException(string type, string message) : Exception(message) +{ + public string Type { get; } = type ?? nameof(ErrorException); +} diff --git a/Railway/Railway.csproj b/Railway/Railway.csproj new file mode 100644 index 0000000..c92b5f7 --- /dev/null +++ b/Railway/Railway.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + Just.Railway + Just.Railway + + + + + + + diff --git a/Railway/ReflectionHelper.cs b/Railway/ReflectionHelper.cs new file mode 100644 index 0000000..bd449da --- /dev/null +++ b/Railway/ReflectionHelper.cs @@ -0,0 +1,67 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Just.Railway; + +internal static class ReflectionHelper +{ + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsEqual(T? left, T? right) => TypeReflectionCache.IsEqualFunc(left, right); + + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Compare(T? left, T? right) => TypeReflectionCache.CompareFunc(left, right); +} + +file static class TypeReflectionCache +{ + public static readonly Func IsEqualFunc; + public static readonly Func CompareFunc; + + static TypeReflectionCache() + { + var type = typeof(T); + var isNullableStruct = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + var underlyingType = isNullableStruct ? type.GenericTypeArguments.First() : type; + var thisType = typeof(TypeReflectionCache); + + var equatableType = typeof(IEquatable<>).MakeGenericType(underlyingType); + if (equatableType.IsAssignableFrom(underlyingType)) + { + var isEqualFunc = thisType.GetMethod(isNullableStruct ? nameof(IsEqualNullable) : nameof(IsEqual), BindingFlags.Static | BindingFlags.Public) + !.MakeGenericMethod(underlyingType); + + IsEqualFunc = (Func)Delegate.CreateDelegate(typeof(Func), isEqualFunc); + } + else + { + 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)Delegate.CreateDelegate(typeof(Func), 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? 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); + + [Pure] public static int Compare(R? left, R? right) where R : notnull, IComparable, T => left is null + ? right is null ? 0 : -1 + : right is null ? 1 : left.CompareTo(right); + [Pure] public static int CompareNullable(R? left, R? right) where R : struct, IComparable => 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. +} diff --git a/Railway/Result.cs b/Railway/Result.cs new file mode 100644 index 0000000..0d0f664 --- /dev/null +++ b/Railway/Result.cs @@ -0,0 +1,1279 @@ + +namespace Just.Railway; + +internal enum ResultState : byte +{ + Bottom = 0, Error = 0b01, Success = 0b11, +} + +public readonly struct Result : IEquatable +{ + internal readonly Error? Error; + internal readonly ResultState State; + + internal Result(Error? error) + { + Error = error; + State = error is null ? ResultState.Success : ResultState.Error; + } + + [Pure] public static Result Success() => new(null); + [Pure] public static Result Success(T value) => new(value); + [Pure] public static Result Failure(Error error) => new(error ?? throw new ArgumentNullException(nameof(error))); + [Pure] public static Result Failure(Error error) => new(error ?? throw new ArgumentNullException(nameof(error))); + +#region Combine 2 Results + [Pure] public static Result Combine(in Result result1, in Result result2) + { + Error? error = null; + + if ((result1.State & result2.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + return new(error); + } + [Pure] public static Result<(T1, T2)> Combine(in Result result1, in Result result2) + { + Error? error = null; + + if ((result1.State & result2.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + return error is null + ? new((result1.Value, result2.Value)) + : new(error); + } + [Pure] public static Result Combine(in Result result1, in Result result2) + { + Error? error = null; + + if ((result1.State & result2.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + return error is null + ? new(result1.Value) + : new(error); + } + [Pure] public static Result Combine(in Result result1, in Result result2) + { + Error? error = null; + + if ((result1.State & result2.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + return error is null + ? new(result2.Value) + : new(error); + } +#endregion + +#region Combine 3 Results + [Pure] public static Result Combine(in Result result1, in Result result2, in Result result3) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + return new(error); + } + [Pure] public static Result<(T1, T2, T3)> Combine(in Result result1, in Result result2, in Result result3) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + return error is null + ? new((result1.Value, result2.Value, result3.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2)> Combine(in Result result1, in Result result2, in Result result3) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + return error is null + ? new((result1.Value, result2.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2)> Combine(in Result result1, in Result result2, in Result result3) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + return error is null + ? new((result2.Value, result3.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2)> Combine(in Result result1, in Result result2, in Result result3) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + return error is null + ? new((result1.Value, result3.Value)) + : new(error); + } + [Pure] public static Result Combine(in Result result1, in Result result2, in Result result3) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + return error is null + ? new(result1.Value) + : new(error); + } + [Pure] public static Result Combine(in Result result1, in Result result2, in Result result3) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + return error is null + ? new(result2.Value) + : new(error); + } + [Pure] public static Result Combine(in Result result1, in Result result2, in Result result3) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + return error is null + ? new(result3.Value) + : new(error); + } +#endregion + +#region Combine 4 Results + [Pure] public static Result Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return new(error); + } + [Pure] public static Result<(T1, T2, T3, T4)> Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new((result1.Value, result2.Value, result3.Value, result4.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2, T3)> Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new((result2.Value, result3.Value, result4.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2, T3)> Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new((result1.Value, result3.Value, result4.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2, T3)> Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new((result1.Value, result2.Value, result4.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2, T3)> Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new((result1.Value, result2.Value, result3.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2)> Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new((result3.Value, result4.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2)> Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new((result2.Value, result4.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2)> Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new((result2.Value, result3.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2)> Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new((result1.Value, result4.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2)> Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new((result1.Value, result3.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2)> Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new((result1.Value, result2.Value)) + : new(error); + } + [Pure] public static Result Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new(result4.Value) + : new(error); + } + [Pure] public static Result Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new(result3.Value) + : new(error); + } + [Pure] public static Result Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new(result2.Value) + : new(error); + } + [Pure] public static Result Combine(in Result result1, in Result result2, in Result result3, in Result result4) + { + Error? error = null; + + if ((result1.State & result2.State & result3.State & result4.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result1.State, result2.State, result3.State, result4.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2, ResultState r3, ResultState r4) + { + if (r1 == ResultState.Bottom) + yield return nameof(result1); + if (r2 == ResultState.Bottom) + yield return nameof(result2); + if (r3 == ResultState.Bottom) + yield return nameof(result3); + if (r4 == ResultState.Bottom) + yield return nameof(result4); + } + } + + if (result1.IsFailure) + { + error += result1.Error; + } + if (result2.IsFailure) + { + error += result2.Error; + } + if (result3.IsFailure) + { + error += result3.Error; + } + if (result4.IsFailure) + { + error += result4.Error; + } + return error is null + ? new(result1.Value) + : new(error); + } +#endregion + + [Pure] public static implicit operator Result(Error error) => new(error ?? throw new ArgumentNullException(nameof(error))); + [Pure] public static implicit operator Result(Result result) => result.State switch + { + ResultState.Success => new(new SuccessUnit()), + ResultState.Error => new(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + + [Pure] public bool IsSuccess => Error is null; + [Pure] public bool IsFailure => Error is not null; + + [Pure] public bool TryGetError([MaybeNullWhen(false)]out Error error) + { + if (IsSuccess) + { + error = default; + return false; + } + if (IsFailure) + { + error = Error!; + return true; + } + throw new ResultNotInitializedException(); + } + + [Pure] public override string ToString() => State switch + { + ResultState.Success => "", + ResultState.Error => Error!.ToString(), + _ => throw new ResultNotInitializedException() + }; + + [Pure] public override int GetHashCode() => State switch + { + ResultState.Success => 0, + ResultState.Error => Error!.GetHashCode(), + _ => throw new ResultNotInitializedException() + }; + + [Pure] public override bool Equals(object? obj) => obj is Result other && Equals(other); + [Pure] public bool Equals(Result other) + { + if (State == ResultState.Bottom) + throw new ResultNotInitializedException(); + + return Error == other.Error; + } + [Pure] public static bool operator ==(Result left, Result right) => left.Equals(right); + [Pure] public static bool operator !=(Result left, Result right) => !(left == right); +} + +public readonly struct Result : IEquatable> +{ + internal readonly Error? Error; + internal readonly T Value; + internal readonly ResultState State; + + internal Result(Error error) + { + Error = error ?? throw new ArgumentNullException(nameof(error)); + State = ResultState.Error; + Value = default!; + } + + internal Result(T value) + { + Value = value; + State = ResultState.Success; + } + + [Pure] public static explicit operator Result(Result result) => result.State switch + { + ResultState.Success => new(null), + ResultState.Error => new(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + [Pure] public static implicit operator Result(Error error) => new(error); + [Pure] public static implicit operator Result(T value) => new(value); + [Pure] public bool IsSuccess => State == ResultState.Success; + [Pure] public bool IsFailure => State == ResultState.Error; + + [Pure] public bool Unwrap([MaybeNullWhen(false)]out T value, [MaybeNullWhen(true)]out Error error) + { + switch (State) + { + case ResultState.Success: + value = Value; + error = default; + return true; + + case ResultState.Error: + value = default; + error = Error!; + return false; + + default: throw new ResultNotInitializedException(); + } + } + [Pure] public bool TryGetValue([MaybeNullWhen(false)]out T value) + { + switch (State) + { + case ResultState.Success: + value = Value; + return true; + + case ResultState.Error: + value = default; + return false; + + default: throw new ResultNotInitializedException(); + } + } + [Pure] public bool TryGetError([MaybeNullWhen(false)]out Error error) + { + switch (State) + { + case ResultState.Success: + error = default; + return false; + + case ResultState.Error: + error = Error!; + return true; + + default: throw new ResultNotInitializedException(); + } + } + + [Pure] public Result Cast() + { + switch (State) + { + case ResultState.Error: + return Error!; + + case ResultState.Success: + { + if (Value is R ret) + return ret; + + if (typeof(R).IsAssignableFrom(typeof(T)) && Value is null) + return default(R)!; + + return (R)(object)Value!; + } + + default: throw new ResultNotInitializedException(); + } + } + + [Pure] public override string ToString() => State switch + { + ResultState.Success => Value?.ToString() ?? "", + ResultState.Error => Error!.ToString(), + _ => throw new ResultNotInitializedException() + }; + + [Pure] public override int GetHashCode() => State switch + { + ResultState.Success => Value?.GetHashCode() ?? 0, + ResultState.Error => Error!.GetHashCode(), + _ => throw new ResultNotInitializedException() + }; + + [Pure] public override bool Equals(object? obj) => obj is Result other && Equals(other); + [Pure] public bool Equals(Result other) + { + if (State == ResultState.Bottom) + throw new ResultNotInitializedException(); + + if (IsSuccess != other.IsSuccess) + return false; + + return IsSuccess + ? ReflectionHelper.IsEqual(Value, other.Value) + : Error == other.Error; + } + [Pure] public static bool operator ==(Result left, Result right) => left.Equals(right); + [Pure] public static bool operator !=(Result left, Result right) => !(left == right); +} + +public readonly struct SuccessUnit : IEquatable +{ + public override bool Equals([NotNullWhen(true)] object? obj) => obj is SuccessUnit; + public bool Equals(SuccessUnit other) => true; + public override int GetHashCode() => 0; + public override string ToString() => "success"; + + public static bool operator ==(SuccessUnit left, SuccessUnit right) => left.Equals(right); + + public static bool operator !=(SuccessUnit left, SuccessUnit right) => !(left == right); +} + +[Serializable] +public class ResultNotInitializedException(string variableName = "this") : InvalidOperationException("Result was not properly initialized.") +{ + public string VariableName { get; } = variableName; +} diff --git a/Railway/ResultExtensions.cs b/Railway/ResultExtensions.cs new file mode 100644 index 0000000..64c5443 --- /dev/null +++ b/Railway/ResultExtensions.cs @@ -0,0 +1,1687 @@ +namespace Just.Railway; + +public static class ResultExtensions +{ + #region Match + + #region <> + [Pure] + public static R Match(this in Result result, Func onSuccess, Func onFailure) + { + return result.State switch + { + ResultState.Success => onSuccess(), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static Task Match(this in Result result, Func> onSuccess, Func> onFailure) + { + return result.State switch + { + ResultState.Success => onSuccess(), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] public static async Task Match(this Task resultTask, Func onSuccess, Func onFailure) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => onSuccess(), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + [Pure] + public static async Task Match(this Task resultTask, Func> onSuccess, Func> onFailure) + { + var result = await resultTask.ConfigureAwait(false); + var matchTask = result.State switch + { + ResultState.Success => onSuccess(), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + return await matchTask.ConfigureAwait(false); + } + + #endregion + + #region + [Pure] + public static R Match(this in Result result, Func onSuccess, Func onFailure) + { + return result.State switch + { + ResultState.Success => onSuccess(result.Value), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static Task Match(this in Result result, Func> onSuccess, Func> onFailure) + { + return result.State switch + { + ResultState.Success => onSuccess(result.Value), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task Match(this Task> resultTask, Func onSuccess, Func onFailure) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => onSuccess(result.Value), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + [Pure] + public static async Task Match(this Task> resultTask, Func> onSuccess, Func> onFailure) + { + var result = await resultTask.ConfigureAwait(false); + var matchTask = result.State switch + { + ResultState.Success => onSuccess(result.Value), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + return await matchTask.ConfigureAwait(false); + } + + #endregion + + #region + [Pure] + public static R Match(this in Result<(T1, T2)> result, Func onSuccess, Func onFailure) + { + return result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static Task Match(this in Result<(T1, T2)> result, Func> onSuccess, Func> onFailure) + { + return result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task Match(this Task> resultTask, Func onSuccess, Func onFailure) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + [Pure] + public static async Task Match(this Task> resultTask, Func> onSuccess, Func> onFailure) + { + var result = await resultTask.ConfigureAwait(false); + var matchTask = result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + return await matchTask.ConfigureAwait(false); + } + + #endregion + + #region + [Pure] + public static R Match(this in Result<(T1, T2, T3)> result, Func onSuccess, Func onFailure) + { + return result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2, result.Value.Item3), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static Task Match(this in Result<(T1, T2, T3)> result, Func> onSuccess, Func> onFailure) + { + return result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2, result.Value.Item3), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task Match(this Task> resultTask, Func onSuccess, Func onFailure) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2, result.Value.Item3), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + [Pure] + public static async Task Match(this Task> resultTask, Func> onSuccess, Func> onFailure) + { + var result = await resultTask.ConfigureAwait(false); + var matchTask = result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2, result.Value.Item3), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + return await matchTask.ConfigureAwait(false); + } + + #endregion + + #region + [Pure] + public static R Match(this in Result<(T1, T2, T3, T4)> result, Func onSuccess, Func onFailure) + { + return result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static Task Match(this in Result<(T1, T2, T3, T4)> result, Func> onSuccess, Func> onFailure) + { + return result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task Match(this Task> resultTask, Func onSuccess, Func onFailure) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + [Pure] + public static async Task Match(this Task> resultTask, Func> onSuccess, Func> onFailure) + { + var result = await resultTask.ConfigureAwait(false); + var matchTask = result.State switch + { + ResultState.Success => onSuccess(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4), + ResultState.Error => onFailure(result.Error!), + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + return await matchTask.ConfigureAwait(false); + } + + #endregion + + #endregion + + #region Map + + #region <> + + [Pure] + public static Result Map(this in Result result, Func mapping) + { + return result.State switch + { + ResultState.Success => mapping(), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task> Map(this Result result, Func> mapping) + { + return result.State switch + { + ResultState.Success => await mapping().ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task> Map(this Task resultTask, Func mapping) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => mapping(), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + [Pure] + public static async Task> Map(this Task resultTask, Func> mapping) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await mapping().ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #region + + [Pure] + public static Result Map(this in Result result, Func mapping) + { + return result.State switch + { + ResultState.Success => mapping(result.Value), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task> Map(this Result result, Func> mapping) + { + return result.State switch + { + ResultState.Success => await mapping(result.Value).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task> Map(this Task> resultTask, Func mapping) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => mapping(result.Value), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + [Pure] + public static async Task> Map(this Task> resultTask, Func> mapping) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await mapping(result.Value).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #region + + [Pure] + public static Result Map(this in Result<(T1, T2)> result, Func mapping) + { + return result.State switch + { + ResultState.Success => mapping(result.Value.Item1, result.Value.Item2), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task> Map(this Result<(T1, T2)> result, Func> mapping) + { + return result.State switch + { + ResultState.Success => await mapping(result.Value.Item1, result.Value.Item2).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task> Map(this Task> resultTask, Func mapping) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => mapping(result.Value.Item1, result.Value.Item2), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + [Pure] + public static async Task> Map(this Task> resultTask, Func> mapping) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await mapping(result.Value.Item1, result.Value.Item2).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #region + + [Pure] + public static Result Map(this in Result<(T1, T2, T3)> result, Func mapping) + { + return result.State switch + { + ResultState.Success => mapping(result.Value.Item1, result.Value.Item2, result.Value.Item3), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task> Map(this Result<(T1, T2, T3)> result, Func> mapping) + { + return result.State switch + { + ResultState.Success => await mapping(result.Value.Item1, result.Value.Item2, result.Value.Item3).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task> Map(this Task> resultTask, Func mapping) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => mapping(result.Value.Item1, result.Value.Item2, result.Value.Item3), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + [Pure] + public static async Task> Map(this Task> resultTask, Func> mapping) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await mapping(result.Value.Item1, result.Value.Item2, result.Value.Item3).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #region + + [Pure] + public static Result Map(this in Result<(T1, T2, T3, T4)> result, Func mapping) + { + return result.State switch + { + ResultState.Success => mapping(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task> Map(this Result<(T1, T2, T3, T4)> result, Func> mapping) + { + return result.State switch + { + ResultState.Success => await mapping(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static async Task> Map(this Task> resultTask, Func mapping) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => mapping(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + [Pure] + public static async Task> Map(this Task> resultTask, Func> mapping) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await mapping(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #endregion + + #region Bind + + #region <> + + [Pure] + public static Result Bind(this in Result result, Func binding) + { + return result.State switch + { + ResultState.Success => binding(), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static Task Bind(this in Result result, Func> binding) + { + return result.State switch + { + ResultState.Success => binding(), + ResultState.Error => Task.FromResult(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task Bind(this Task resultTask, Func binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => binding(), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + [Pure] + public static async Task Bind(this Task resultTask, Func> binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await binding().ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + [Pure] + public static Result Bind(this in Result result, Func> binding) + { + return result.State switch + { + ResultState.Success => binding(), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static Task> Bind(this in Result result, Func>> binding) + { + return result.State switch + { + ResultState.Success => binding(), + ResultState.Error => Task.FromResult>(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Bind(this Task resultTask, Func> binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => binding(), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + [Pure] + public static async Task> Bind(this Task resultTask, Func>> binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await binding().ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #region + + [Pure] + public static Result Bind(this in Result result, Func> binding) + { + return result.State switch + { + ResultState.Success => binding(result.Value), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static Task> Bind(this in Result result, Func>> binding) + { + return result.State switch + { + ResultState.Success => binding(result.Value), + ResultState.Error => Task.FromResult>(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Bind(this Task> resultTask, Func> binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => binding(result.Value), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + [Pure] + public static async Task> Bind(this Task> resultTask, Func>> binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await binding(result.Value).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #region + + [Pure] + public static Result Bind(this in Result<(T1, T2)> result, Func> binding) + { + return result.State switch + { + ResultState.Success => binding(result.Value.Item1, result.Value.Item2), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static Task> Bind(this in Result<(T1, T2)> result, Func>> binding) + { + return result.State switch + { + ResultState.Success => binding(result.Value.Item1, result.Value.Item2), + ResultState.Error => Task.FromResult>(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Bind(this Task> resultTask, Func> binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => binding(result.Value.Item1, result.Value.Item2), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + [Pure] + public static async Task> Bind(this Task> resultTask, Func>> binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await binding(result.Value.Item1, result.Value.Item2).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #region + + [Pure] + public static Result Bind(this in Result<(T1, T2, T3)> result, Func> binding) + { + return result.State switch + { + ResultState.Success => binding(result.Value.Item1, result.Value.Item2, result.Value.Item3), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static Task> Bind(this in Result<(T1, T2, T3)> result, Func>> binding) + { + return result.State switch + { + ResultState.Success => binding(result.Value.Item1, result.Value.Item2, result.Value.Item3), + ResultState.Error => Task.FromResult>(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Bind(this Task> resultTask, Func> binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => binding(result.Value.Item1, result.Value.Item2, result.Value.Item3), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + [Pure] + public static async Task> Bind(this Task> resultTask, Func>> binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await binding(result.Value.Item1, result.Value.Item2, result.Value.Item3).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #region + + [Pure] + public static Result Bind(this in Result<(T1, T2, T3, T4)> result, Func> binding) + { + return result.State switch + { + ResultState.Success => binding(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static Task> Bind(this in Result<(T1, T2, T3, T4)> result, Func>> binding) + { + return result.State switch + { + ResultState.Success => binding(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4), + ResultState.Error => Task.FromResult>(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Bind(this Task> resultTask, Func> binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => binding(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + [Pure] + public static async Task> Bind(this Task> resultTask, Func>> binding) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await binding(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4).ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + #endregion + + #region Append + + #region <> + + [Pure] public static Result Append(this in Result result, Result next) + { + Error? error = null; + + if ((result.State & next.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result); + if (r2 == ResultState.Bottom) + yield return nameof(next); + } + } + + if (result.IsFailure) + { + error += result.Error; + } + if (next.IsFailure) + { + error += next.Error; + } + return error is null + ? new(null) + : new(error); + } + + #endregion + + #region + + [Pure] public static Result Append(this in Result result, T value) + { + return result.State switch + { + ResultState.Success => value, + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] public static Result Append(this in Result result, Result next) + { + Error? error = null; + + if ((result.State & next.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result); + if (r2 == ResultState.Bottom) + yield return nameof(next); + } + } + + if (result.IsFailure) + { + error += result.Error; + } + if (next.IsFailure) + { + error += next.Error; + } + return error is null + ? new(next.Value) + : new(error); + } + [Pure] public static Result Append(this in Result result, Result next) + { + Error? error = null; + + if ((result.State & next.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result); + if (r2 == ResultState.Bottom) + yield return nameof(next); + } + } + + if (result.IsFailure) + { + error += result.Error; + } + if (next.IsFailure) + { + error += next.Error; + } + return error is null + ? new(result.Value) + : new(error); + } + [Pure] + public static Result Append(this in Result result, Func> next) + { + return result.State switch + { + ResultState.Success => next(), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + + [Pure] + public static Task> Append(this in Result result, Func>> next) + { + return result.State switch + { + ResultState.Success => next(), + ResultState.Error => Task.FromResult>(result.Error!), + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Append(this Task resultTask, Func>> next) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => await next().ConfigureAwait(false), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + [Pure] + public static async Task> Append(this Task resultTask, Func> next) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => next(), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #region + + [Pure] public static Result<(T1, T2)> Append(this in Result result, T2 value) + { + return result.State switch + { + ResultState.Success => (result.Value, value), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] public static Result<(T1, T2)> Append(this in Result result, Result next) + { + Error? error = null; + + if ((result.State & next.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result); + if (r2 == ResultState.Bottom) + yield return nameof(next); + } + } + + if (result.IsFailure) + { + error += result.Error; + } + if (next.IsFailure) + { + error += next.Error; + } + return error is null + ? new((result.Value, next.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2)> Append(this in Result<(T1, T2)> result, Result next) + { + Error? error = null; + + if ((result.State & next.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result); + if (r2 == ResultState.Bottom) + yield return nameof(next); + } + } + + if (result.IsFailure) + { + error += result.Error; + } + if (next.IsFailure) + { + error += next.Error; + } + return error is null + ? new(result.Value) + : new(error); + } + [Pure] + public static Result<(T1, T2)> Append(this in Result result, Func> next) + { + return result.State switch + { + ResultState.Success => result.Append(next()), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Append(this Result result, Func>> next) + { + return result.State switch + { + ResultState.Success => result.Append(await next().ConfigureAwait(false)), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Append(this Task> resultTask, Func>> next) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => result.Append(await next().ConfigureAwait(false)), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + [Pure] + public static async Task> Append(this Task> resultTask, Func> next) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => result.Append(next()), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #region + + [Pure] public static Result<(T1, T2, T3)> Append(this in Result<(T1, T2)> result, T3 value) + { + return result.State switch + { + ResultState.Success => (result.Value.Item1, result.Value.Item2, value), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] public static Result<(T1, T2, T3)> Append(this in Result<(T1, T2)> result, Result next) + { + Error? error = null; + + if ((result.State & next.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result); + if (r2 == ResultState.Bottom) + yield return nameof(next); + } + } + + if (result.IsFailure) + { + error += result.Error; + } + if (next.IsFailure) + { + error += next.Error; + } + return error is null + ? new((result.Value.Item1, result.Value.Item2, next.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2, T3)> Append(this in Result<(T1, T2, T3)> result, Result next) + { + Error? error = null; + + if ((result.State & next.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result); + if (r2 == ResultState.Bottom) + yield return nameof(next); + } + } + + if (result.IsFailure) + { + error += result.Error; + } + if (next.IsFailure) + { + error += next.Error; + } + return error is null + ? new(result.Value) + : new(error); + } + [Pure] + public static Result<(T1, T2, T3)> Append(this in Result<(T1, T2)> result, Func> next) + { + return result.State switch + { + ResultState.Success => result.Append(next()), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Append(this Result<(T1, T2)> result, Func>> next) + { + return result.State switch + { + ResultState.Success => result.Append(await next().ConfigureAwait(false)), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Append(this Task> resultTask, Func>> next) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => result.Append(await next().ConfigureAwait(false)), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + [Pure] + public static async Task> Append(this Task> resultTask, Func> next) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => result.Append(next()), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #region + + [Pure] public static Result<(T1, T2, T3, T4)> Append(this in Result<(T1, T2, T3)> result, T4 value) + { + return result.State switch + { + ResultState.Success => (result.Value.Item1, result.Value.Item2, result.Value.Item3, value), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] public static Result<(T1, T2, T3, T4)> Append(this in Result<(T1, T2, T3)> result, Result next) + { + Error? error = null; + + if ((result.State & next.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result); + if (r2 == ResultState.Bottom) + yield return nameof(next); + } + } + + if (result.IsFailure) + { + error += result.Error; + } + if (next.IsFailure) + { + error += next.Error; + } + return error is null + ? new((result.Value.Item1, result.Value.Item2, result.Value.Item3, next.Value)) + : new(error); + } + [Pure] public static Result<(T1, T2, T3, T4)> Append(this in Result<(T1, T2, T3, T4)> result, Result next) + { + Error? error = null; + + if ((result.State & next.State) == ResultState.Bottom) + { + throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State))); + + static IEnumerable GetBottom(ResultState r1, ResultState r2) + { + if (r1 == ResultState.Bottom) + yield return nameof(result); + if (r2 == ResultState.Bottom) + yield return nameof(next); + } + } + + if (result.IsFailure) + { + error += result.Error; + } + if (next.IsFailure) + { + error += next.Error; + } + return error is null + ? new(result.Value) + : new(error); + } + [Pure] + public static Result<(T1, T2, T3, T4)> Append(this in Result<(T1, T2, T3)> result, Func> next) + { + return result.State switch + { + ResultState.Success => result.Append(next()), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Append(this Result<(T1, T2, T3)> result, Func>> next) + { + return result.State switch + { + ResultState.Success => result.Append(await next().ConfigureAwait(false)), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(result)) + }; + } + [Pure] + public static async Task> Append(this Task> resultTask, Func>> next) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => result.Append(await next().ConfigureAwait(false)), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + [Pure] + public static async Task> Append(this Task> resultTask, Func> next) + { + var result = await resultTask.ConfigureAwait(false); + return result.State switch + { + ResultState.Success => result.Append(next()), + ResultState.Error => result.Error!, + _ => throw new ResultNotInitializedException(nameof(resultTask)) + }; + } + + #endregion + + #endregion + + #region Tap + [Pure] + public static ref readonly Result Tap(this in Result result, Action? onSuccess = null, Action? onFailure = null) + { + switch (result.State) + { + case ResultState.Success: + onSuccess?.Invoke(); + break; + case ResultState.Error: + onFailure?.Invoke(result.Error!); + break; + + default: throw new ResultNotInitializedException(nameof(result)); + } + + return ref result; + } + [Pure] + public static async Task Tap(this Task resultTask, Action? onSuccess = null, Action? onFailure = null) + { + var result = await resultTask.ConfigureAwait(false); + switch (result.State) + { + case ResultState.Success: + onSuccess?.Invoke(); + break; + case ResultState.Error: + onFailure?.Invoke(result.Error!); + break; + + default: throw new ResultNotInitializedException(nameof(resultTask)); + } + + return result; + } + [Pure] + public static async Task Tap(this Result result, Func? onSuccess = null, Func? onFailure = null) + { + switch (result.State) + { + case ResultState.Success: + if (onSuccess is not null) + await onSuccess.Invoke().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; + } + [Pure] + public static async Task Tap(this Task resultTask, Func? onSuccess = null, Func? onFailure = null) + { + var result = await resultTask.ConfigureAwait(false); + switch (result.State) + { + case ResultState.Success: + if (onSuccess is not null) + await onSuccess.Invoke().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; + } + + [Pure] + public static ref readonly Result Tap(this in Result result, Action? onSuccess = null, Action? onFailure = null) + { + switch (result.State) + { + case ResultState.Success: + onSuccess?.Invoke(result.Value!); + break; + case ResultState.Error: + onFailure?.Invoke(result.Error!); + break; + + default: throw new ResultNotInitializedException(nameof(result)); + } + return ref result; + } + [Pure] + public static async Task> Tap(this Task> resultTask, Action? onSuccess = null, Action? onFailure = null) + { + var result = await resultTask.ConfigureAwait(false); + switch (result.State) + { + case ResultState.Success: + onSuccess?.Invoke(result.Value!); + break; + case ResultState.Error: + onFailure?.Invoke(result.Error!); + break; + + default: throw new ResultNotInitializedException(nameof(resultTask)); + } + return result; + } + [Pure] + public static async Task> Tap(this Result result, Func? onSuccess = null, Func? onFailure = null) + { + switch (result.State) + { + case ResultState.Success: + if (onSuccess is not null) + await onSuccess.Invoke(result.Value!).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; + } + [Pure] + public static async Task> Tap(this Task> resultTask, Func? onSuccess = null, Func? onFailure = null) + { + var result = await resultTask.ConfigureAwait(false); + switch (result.State) + { + case ResultState.Success: + if (onSuccess is not null) + await onSuccess.Invoke(result.Value!).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; + } + + [Pure] + public static ref readonly Result<(T1, T2)> Tap(this in Result<(T1, T2)> result, Action? onSuccess = null, Action? onFailure = null) + { + switch (result.State) + { + case ResultState.Success: + onSuccess?.Invoke(result.Value.Item1, result.Value.Item2); + break; + case ResultState.Error: + onFailure?.Invoke(result.Error!); + break; + + default: throw new ResultNotInitializedException(nameof(result)); + } + return ref result; + } + [Pure] + public static async Task> Tap(this Task> resultTask, Action? onSuccess = null, Action? onFailure = null) + { + var result = await resultTask.ConfigureAwait(false); + switch (result.State) + { + case ResultState.Success: + onSuccess?.Invoke(result.Value.Item1, result.Value.Item2); + break; + case ResultState.Error: + onFailure?.Invoke(result.Error!); + break; + + default: throw new ResultNotInitializedException(nameof(resultTask)); + } + return result; + } + [Pure] + public static async Task> Tap(this Result<(T1, T2)> result, Func? onSuccess = null, Func? onFailure = null) + { + switch (result.State) + { + case ResultState.Success: + if (onSuccess is not null) + await onSuccess.Invoke(result.Value.Item1, result.Value.Item2).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; + } + [Pure] + public static async Task> Tap(this Task> resultTask, Func? onSuccess = null, Func? onFailure = null) + { + var result = await resultTask.ConfigureAwait(false); + switch (result.State) + { + case ResultState.Success: + if (onSuccess is not null) + await onSuccess.Invoke(result.Value.Item1, result.Value.Item2).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; + } + + [Pure] + public static ref readonly Result<(T1, T2, T3)> Tap(this in Result<(T1, T2, T3)> result, Action? onSuccess = null, Action? onFailure = null) + { + switch (result.State) + { + case ResultState.Success: + onSuccess?.Invoke(result.Value.Item1, result.Value.Item2, result.Value.Item3); + break; + case ResultState.Error: + onFailure?.Invoke(result.Error!); + break; + + default: throw new ResultNotInitializedException(nameof(result)); + } + return ref result; + } + [Pure] + public static async Task> Tap(this Task> resultTask, Action? onSuccess = null, Action? onFailure = null) + { + var result = await resultTask.ConfigureAwait(false); + switch (result.State) + { + case ResultState.Success: + onSuccess?.Invoke(result.Value.Item1, result.Value.Item2, result.Value.Item3); + break; + case ResultState.Error: + onFailure?.Invoke(result.Error!); + break; + + default: throw new ResultNotInitializedException(nameof(resultTask)); + } + return result; + } + [Pure] + public static async Task> Tap(this Result<(T1, T2, T3)> result, Func? onSuccess = null, Func? onFailure = null) + { + switch (result.State) + { + case ResultState.Success: + if (onSuccess is not null) + await onSuccess.Invoke(result.Value.Item1, result.Value.Item2, result.Value.Item3).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; + } + [Pure] + public static async Task> Tap(this Task> resultTask, Func? onSuccess = null, Func? onFailure = null) + { + var result = await resultTask.ConfigureAwait(false); + switch (result.State) + { + case ResultState.Success: + if (onSuccess is not null) + await onSuccess.Invoke(result.Value.Item1, result.Value.Item2, result.Value.Item3).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; + } + + [Pure] + public static ref readonly Result<(T1, T2, T3, T4)> Tap(this in Result<(T1, T2, T3, T4)> result, Action? onSuccess = null, Action? onFailure = null) + { + switch (result.State) + { + case ResultState.Success: + onSuccess?.Invoke(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4); + break; + case ResultState.Error: + onFailure?.Invoke(result.Error!); + break; + + default: throw new ResultNotInitializedException(nameof(result)); + } + return ref result; + } + [Pure] + public static async Task> Tap(this Task> resultTask, Action? onSuccess = null, Action? onFailure = null) + { + var result = await resultTask.ConfigureAwait(false); + switch (result.State) + { + case ResultState.Success: + onSuccess?.Invoke(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4); + break; + case ResultState.Error: + onFailure?.Invoke(result.Error!); + break; + + default: throw new ResultNotInitializedException(nameof(resultTask)); + } + return result; + } + [Pure] + public static async Task> Tap(this Result<(T1, T2, T3, T4)> result, Func? onSuccess = null, Func? onFailure = null) + { + switch (result.State) + { + case ResultState.Success: + if (onSuccess is not null) + await onSuccess.Invoke(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4).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; + } + [Pure] + public static async Task> Tap(this Task> resultTask, Func? onSuccess = null, Func? onFailure = null) + { + var result = await resultTask.ConfigureAwait(false); + switch (result.State) + { + case ResultState.Success: + if (onSuccess is not null) + await onSuccess.Invoke(result.Value.Item1, result.Value.Item2, result.Value.Item3, result.Value.Item4).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; + } +#endregion + + #region Merge + + public static Result> Merge(this IEnumerable> results) + { + List? values = null; + List? errors = null; + bool hasErrors = false; + + foreach (var result in results.OrderBy(x => x.State)) + { + switch (result.State) + { + case ResultState.Error: + hasErrors = true; + errors ??= []; + errors.Add(result.Error!); + break; + + case ResultState.Success: + if (hasErrors) goto afterLoop; + values ??= []; + values.Add(result.Value); + break; + + default: throw new ResultNotInitializedException(nameof(results)); + } + } + afterLoop: + return hasErrors + ? new(new ManyErrors(errors!)) + : new((IEnumerable?)values ?? Array.Empty()); + } + public static async Task>> Merge(this IEnumerable>> tasks) + { + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + return results.Merge(); + } + + #endregion +} diff --git a/Railway/Usings.cs b/Railway/Usings.cs new file mode 100644 index 0000000..0b12390 --- /dev/null +++ b/Railway/Usings.cs @@ -0,0 +1,5 @@ +global using System.Diagnostics.CodeAnalysis; +global using System.Diagnostics.Contracts; +global using System.Runtime.CompilerServices; +global using System.Text.Json; +global using System.Text.Json.Serialization; diff --git a/Raliway.Tests/EnsureExtensions/Satisfy.cs b/Raliway.Tests/EnsureExtensions/Satisfy.cs new file mode 100644 index 0000000..588f2af --- /dev/null +++ b/Raliway.Tests/EnsureExtensions/Satisfy.cs @@ -0,0 +1,55 @@ +namespace Raliway.Tests.EnsureExtensions; + +public class Satisfy +{ + [Fact] + public void WhenRequirementWasSatisfied_ShouldBeSuccessful() + { + var result = Ensure.That(69) + .Satisfies(i => i < 100) + .Result(); + + Assert.True(result.IsSuccess); + Assert.Equal(69, result.Value); + } + [Fact] + public void WhenRequirementWasNotSatisfied_ShouldBeFailureWithDefaultError() + { + var error = Error.New(Ensure.DefaultErrorType, "Value {69} does not satisfy the requirement."); + var result = Ensure.That(69) + .Satisfies(i => i > 100) + .Result(); + + Assert.True(result.IsFailure); + Assert.Equal(error, result.Error); + } + + [Fact] + public void WhenAllRequirementsWasSatisfied_ShouldBeSuccessful() + { + var result = Ensure.That("69") + .NotNull() + .NotEmpty() + .NotWhitespace() + .Satisfies(s => s == "69") + .Result(); + + Assert.True(result.IsSuccess); + Assert.Equal("69", result.Value); + } + + [Fact] + public void WhenAnyRequirementWasNotSatisfied_ShouldBeFailureWithFirstError() + { + var error = Error.New(Ensure.DefaultErrorType, "Value {(string?)\" \"} is empty or consists exclusively of white-space characters."); + var result = Ensure.That((string?)" ") + .NotNull() + .NotEmpty() + .NotWhitespace() + .Satisfies(s => s == "69") + .Result(); + + Assert.True(result.IsFailure); + Assert.Equal(error, result.Error); + } +} diff --git a/Raliway.Tests/Errors/Serialization.cs b/Raliway.Tests/Errors/Serialization.cs new file mode 100644 index 0000000..19792b2 --- /dev/null +++ b/Raliway.Tests/Errors/Serialization.cs @@ -0,0 +1,45 @@ +namespace Railway.Tests.Errors; + +public class Serialization +{ + [Fact] + public void WhenSerializingManyErrors() + { + // Given + Error many_errors = new ManyErrors(new Error[]{ + new ExpectedError("err1", "msg1"){ + ExtensionData = { + ["ext"] = "ext_value" + } + }, + new ExceptionalError(new Exception("msg2")), + }); + // When + 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\"}]", + result); + } + + [Fact] + public void WhenDeserializingManyErrors() + { + // Given + var json = "[{\"$$err\":0,\"Type\":\"err1\",\"Message\":\"msg1\",\"ext\":\"ext_value\"},{\"$$err\":1,\"Type\":\"Exception\",\"Message\":\"msg2\"}]"; + // When + var result = JsonSerializer.Deserialize(json); + // Then + Assert.True(result?.Length == 2); + Assert.Equal( + expected: new ManyErrors(new Error[]{ + new ExpectedError("err1", "msg1"), + new ExceptionalError(new Exception("msg2")), + }), + result + ); + Assert.Equal( + expected: "ext_value", + result[0].ExtensionData["ext"].ToString()); + } +} diff --git a/Raliway.Tests/Raliway.Tests.csproj b/Raliway.Tests/Raliway.Tests.csproj new file mode 100644 index 0000000..396ba8a --- /dev/null +++ b/Raliway.Tests/Raliway.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + Just.Railway.Tests + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Raliway.Tests/Results/GeneralUsage.cs b/Raliway.Tests/Results/GeneralUsage.cs new file mode 100644 index 0000000..14e1593 --- /dev/null +++ b/Raliway.Tests/Results/GeneralUsage.cs @@ -0,0 +1,142 @@ +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(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(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() + { + // Given + + // When + var result = Result.Success() + .Append(() => Result.Success(1)) + .Append("test") + .Map((i, s) => $"{s}_{i}") + .Append("some") + .Bind((s1, s2) => Result.Success(string.Join(';', s1, s2))) + .Match( + onSuccess: s => s.ToUpper(), + onFailure: _ => + { + Assert.Fail(); + return ""; + } + ); + + Assert.Equal("TEST_1;SOME", result); + } + + [Fact] + public void ChainedResultExtensions_WhenThereIsAnError() + { + // Given + + // When + var error = Error.New("test"); + + + var result = Result.Success() + .Append(() => Result.Failure(error)) + .Append("test") + .Map((i, s) => + { + Assert.Fail(); + return Result.Success(""); + }) + .Append("some") + .Bind((s1, s2) => + { + Assert.Fail(); + return Result.Success(""); + }) + .Match( + onSuccess: _ => + { + Assert.Fail(); + return ""; + }, + onFailure: err => + { + Assert.Equal(error, err); + return "satisfied"; + } + ); + + // Then + Assert.Equal("satisfied", result); + } +} diff --git a/Raliway.Tests/Usings.cs b/Raliway.Tests/Usings.cs new file mode 100644 index 0000000..999ce38 --- /dev/null +++ b/Raliway.Tests/Usings.cs @@ -0,0 +1,3 @@ +global using System.Text.Json; +global using Xunit; +global using Just.Railway;