Files
Just.Railway/Railway/Error.cs
just 595b63dc23
All checks were successful
.NET Test / test (push) Successful in 2m2s
support for dotnet9 compilation
2025-07-29 23:22:28 +04:00

416 lines
14 KiB
C#

using System.Collections;
using System.Collections.Immutable;
using System.Text;
namespace Just.Railway;
[JsonConverter(typeof(ErrorJsonConverter))]
public abstract class Error : IEquatable<Error>, IComparable<Error>
{
protected internal Error(){}
/// <summary>
/// Create an <see cref="ExceptionalError"/>
/// </summary>
/// <param name="thisException">Exception</param>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Error New(Exception thisException) => new ExceptionalError(thisException);
/// <summary>
/// Create a <see cref="ExceptionalError"/> with an overriden detail. This can be useful for sanitising the display message
/// when internally we're carrying the exception.
/// </summary>
/// <param name="message">Error detail</param>
/// <param name="thisException">Exception</param>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Error New(string message, Exception thisException) =>
new ExceptionalError(message, thisException);
/// <summary>
/// Create an <see cref="ExpectedError"/>
/// </summary>
/// <param name="message">Error detail</param>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Error New(string message, IEnumerable<KeyValuePair<string, string>>? extensionData = null) =>
new ExpectedError("error", message)
{
ExtensionData = extensionData?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty
};
/// <summary>
/// Create an <see cref="ExpectedError"/>
/// </summary>
/// <param name="type">Error code</param>
/// <param name="message">Error detail</param>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Error New(string type, string message, IEnumerable<KeyValuePair<string, string>>? extensionData = null) =>
new ExpectedError(type, message)
{
ExtensionData = extensionData?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty
};
/// <summary>
/// Create a <see cref="ManyErrors"/>
/// </summary>
/// <remarks>Collects many errors into a single <see cref="Error"/> type, called <see cref="ManyErrors"/></remarks>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Error Many(Error error1, Error error2) => (error1, error2) switch
{
(null, null) => new ManyErrors(ImmutableArray<Error>.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)
};
/// <summary>
/// Create a <see cref="ManyErrors"/>
/// </summary>
/// <remarks>Collects many errors into a single <see cref="Error"/> type, called <see cref="ManyErrors"/></remarks>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Error Many(params Error[] errors) => errors.Length switch
{
1 => errors[0],
_ => new ManyErrors(errors)
};
/// <summary>
/// Create a <see cref="ManyErrors"/>
/// </summary>
/// <remarks>Collects many errors into a single <see cref="Error"/> type, called <see cref="ManyErrors"/></remarks>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Error Many(IEnumerable<Error> errors) => new ManyErrors(errors);
[Pure] public abstract string Type { get; }
[Pure] public abstract string Message { get; }
[Pure] public ImmutableDictionary<string, string> ExtensionData { get; internal init; } = ImmutableDictionary<string, string>.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<Error> ToEnumerable();
/// <summary>
/// Gets the <see cref="Exception"/>
/// </summary>
/// <returns>New <see cref="ErrorException"/> constructed from current error</returns>
[Pure] public virtual Exception ToException() => new ErrorException(Type, Message);
/// <summary>
/// Compares error types
/// </summary>
/// <returns><see cref="true"/> when other error has the same type</returns>
[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<Error> 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<Error> ToEnumerable()
{
yield return this;
}
private static ImmutableDictionary<string, string> ExtractExtensionData(Exception exception)
{
if (!(exception.Data?.Count > 0))
return ImmutableDictionary<string, string>.Empty;
var values = GetGenericExtData(exception);
return values is not null ? values.ToImmutable() : ImmutableDictionary<string, string>.Empty;
}
private static ImmutableDictionary<string, string>.Builder? GetGenericExtData(Exception ex)
{
ImmutableDictionary<string, string>.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<string, string>();
values.Add(keyString, valueString);
}
return values;
}
}
[JsonConverter(typeof(ManyErrorsJsonConverter))]
public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
{
private readonly ImmutableArray<Error> _errors;
[Pure] public IEnumerable<Error> Errors { get => _errors; }
internal ManyErrors(ImmutableArray<Error> errors) => _errors = errors;
internal ManyErrors(Error head, Error tail)
{
var headCount = head.Count;
var tailCount = tail.Count;
var errors = ImmutableArray.CreateBuilder<Error>(headCount + tailCount);
if (headCount > 0)
AppendSanitized(errors, head);
if (tailCount > 0)
AppendSanitized(errors, tail);
_errors = errors.MoveToImmutable();
}
public ManyErrors(IEnumerable<Error> errors)
{
var unpackedErrors = ImmutableArray.CreateBuilder<Error>();
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<Error> 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<Error> 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<Error> errors)
{
if (errors.IsEmpty)
return 0;
var hash = new HashCode();
foreach (var err in errors)
{
hash.Add(err);
}
return hash.ToHashCode();
}
[Pure] public ImmutableArray<Error>.Enumerator GetEnumerator() => _errors.GetEnumerator();
[Pure] IEnumerator<Error> IEnumerable<Error>.GetEnumerator() => Errors.GetEnumerator();
[Pure] IEnumerator IEnumerable.GetEnumerator() => Errors.GetEnumerator();
internal static void AppendSanitized(ImmutableArray<Error>.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; }
}