using System.Collections; using System.Collections.Immutable; using System.Text; namespace Just.Railway; [JsonConverter(typeof(ErrorJsonConverter))] public abstract class Error : IEquatable, IComparable { 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, IEnumerable>? extensionData = null) => new ExpectedError("error", message) { ExtensionData = extensionData?.ToImmutableDictionary() ?? ImmutableDictionary.Empty }; /// /// Create an /// /// Error code /// Error detail [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static Error New(string type, string message, IEnumerable>? extensionData = null) => new ExpectedError(type, message) { ExtensionData = extensionData?.ToImmutableDictionary() ?? ImmutableDictionary.Empty }; /// /// 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([]), (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] public ImmutableDictionary ExtensionData { get; internal init; } = ImmutableDictionary.Empty; [Pure] public string? this[string key] => ExtensionData.TryGetValue(key, out var value) == true ? value : null; [Pure] public abstract int Count { get; } [Pure] public abstract bool IsEmpty { get; } [Pure] public abstract bool IsExpected { get; } [Pure] 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; } } [JsonConverter(typeof(ExpectedErrorJsonConverter))] public sealed class ExpectedError : Error { 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] public override int Count => 1; [Pure] public override bool IsEmpty => false; [Pure] public override bool IsExpected => true; [Pure] public override bool IsExeptional => false; [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public override IEnumerable ToEnumerable() { yield return this; } } [JsonConverter(typeof(ExceptionalErrorJsonConverter))] public sealed class ExceptionalError : Error { internal readonly Exception? Exception; internal ExceptionalError(Exception exception) : this(exception.GetType().FullName ?? exception.GetType().Name, exception.Message) { Exception = exception; ExtensionData = ExtractExtensionData(exception); } internal ExceptionalError(string message, Exception exception) : this(exception.GetType().FullName ?? exception.GetType().Name, message) { Exception = exception; ExtensionData = ExtractExtensionData(exception); } public ExceptionalError(string type, string message) { Type = type; Message = message; } [Pure] public override string Type { get; } [Pure] public override string Message { get; } [Pure] public override int Count => 1; [Pure] public override bool IsEmpty => false; [Pure] public override bool IsExpected => false; [Pure] 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 static ImmutableDictionary ExtractExtensionData(Exception exception) { if (!(exception.Data?.Count > 0)) return ImmutableDictionary.Empty; List>? values = null; foreach (var key in exception.Data.Keys) { if (key is null) continue; var value = exception.Data[key]; if (value is null) continue; var keyString = key.ToString(); var valueString = value.ToString(); if (string.IsNullOrEmpty(keyString) || string.IsNullOrEmpty(valueString)) continue; values ??= []; values.Add(new(keyString, valueString)); } return values?.ToImmutableDictionary() ?? ImmutableDictionary.Empty; } } [JsonConverter(typeof(ManyErrorsJsonConverter))] public sealed class ManyErrors : Error, IEnumerable, IReadOnlyList { private readonly List _errors; [Pure] public IEnumerable Errors { get => _errors; } internal ManyErrors(List errors) => _errors = 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] public override bool IsEmpty => _errors.Count == 0; [Pure] public override bool IsExpected => _errors.All(static x => x.IsExpected); [Pure] public override bool IsExeptional => _errors.Any(static x => x.IsExeptional); [Pure] public Error this[int index] => _errors[index]; [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); }