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(ImmutableArray.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] 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 sealed override string ToString() => Message; [Pure] public void Deconstruct(out string type, out string message) { type = Type; message = Message; } [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] internal virtual Error AccessUnsafe(int position) => this; } [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; [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] internal static string ToErrorType(Type exceptionType) => exceptionType.FullName ?? exceptionType.Name; internal ExceptionalError(Exception exception) : this(ToErrorType(exception.GetType()), exception.Message) { Exception = exception; ExtensionData = ExtractExtensionData(exception); } internal ExceptionalError(string message, Exception exception) : this(ToErrorType(exception.GetType()), 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; var values = GetGenericExtData(exception); return values is not null ? values.ToImmutable() : ImmutableDictionary.Empty; } private static ImmutableDictionary.Builder? GetGenericExtData(Exception ex) { ImmutableDictionary.Builder? values = null; foreach (var key in ex.Data.Keys) { if (key is null) continue; var value = ex.Data[key]; if (value is null) continue; var keyString = key.ToString(); var valueString = value.ToString(); if (string.IsNullOrEmpty(keyString) || string.IsNullOrEmpty(valueString)) continue; values ??= ImmutableDictionary.CreateBuilder(); values.Add(keyString, valueString); } return values; } } [JsonConverter(typeof(ManyErrorsJsonConverter))] public sealed class ManyErrors : Error, IEnumerable, IReadOnlyList { private readonly ImmutableArray _errors; [Pure] public IEnumerable Errors { get => _errors; } internal ManyErrors(ImmutableArray errors) => _errors = errors; internal ManyErrors(Error head, Error tail) { var headCount = head.Count; var tailCount = tail.Count; var errors = ImmutableArray.CreateBuilder(headCount + tailCount); if (headCount > 0) AppendSanitized(errors, head); if (tailCount > 0) AppendSanitized(errors, tail); _errors = errors.MoveToImmutable(); } public ManyErrors(IEnumerable errors) { var unpackedErrors = ImmutableArray.CreateBuilder(); foreach (var err in errors) { if (err.IsEmpty) continue; AppendSanitized(unpackedErrors, err); } _errors = unpackedErrors.ToImmutable(); } [Pure] public override string Type => "many_errors"; private string? _lazyMessage = null; [Pure] public override string Message => _lazyMessage ??= ToFullArrayString(_errors); [Pure] private static string ToFullArrayString(in ImmutableArray errors) { var separator = Environment.NewLine; var sb = new StringBuilder(); for (int i = 0; i < errors.Length; i++) { sb.Append(errors[i]); sb.Append(separator); } sb.Remove(sb.Length - separator.Length, separator.Length); return sb.ToString(); } [Pure] public override int Count => _errors.Length; [Pure] public override bool IsEmpty => _errors.IsEmpty; [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.Length) return _errors.Length.CompareTo(other.Count); for (int i = 0; i < _errors.Length; i++) { var compareResult = _errors[i].CompareTo(other.AccessUnsafe(i)); if (compareResult != 0) { return compareResult; } } return 0; } [Pure] public override bool IsSimilarTo([NotNullWhen(true)] Error? other) { if (other is null) { return false; } if (_errors.Length != other.Count) { return false; } for (int i = 0; i < _errors.Length; i++) { if (!_errors[i].IsSimilarTo(other.AccessUnsafe(i))) { return false; } } return true; } [Pure] public override bool Equals([NotNullWhen(true)] Error? other) { if (other is null) { return false; } if (_errors.Length != other.Count) { return false; } for (int i = 0; i < _errors.Length; i++) { if (!_errors[i].Equals(other.AccessUnsafe(i))) { return false; } } return true; } private int? _lazyHashCode = null; [Pure] public override int GetHashCode() => _lazyHashCode ??= CalcHashCode(_errors); private static int CalcHashCode(in ImmutableArray errors) { if (errors.IsEmpty) return 0; var hash = new HashCode(); foreach (var err in errors) { hash.Add(err); } return hash.ToHashCode(); } [Pure] public ImmutableArray.Enumerator GetEnumerator() => _errors.GetEnumerator(); [Pure] IEnumerator IEnumerable.GetEnumerator() => Errors.GetEnumerator(); [Pure] IEnumerator IEnumerable.GetEnumerator() => Errors.GetEnumerator(); internal static void AppendSanitized(ImmutableArray.Builder errors, Error error) { if (error is ManyErrors many) errors.AddRange(many._errors); else errors.Add(error); } [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] internal override Error AccessUnsafe(int position) => _errors[position]; } [Serializable] public sealed class ErrorException : Exception { public ErrorException(string type, string message) : base(message) { Type = type ?? nameof(ErrorException); } public string Type { get; } }