Files
Just.Railway/Railway/Error.cs
JustFixMe 719b4e85f5
All checks were successful
.NET Test / test (push) Successful in 4m42s
.NET Publish / publish (push) Successful in 5m53s
extended recover tests
2023-12-18 20:55:48 +04:00

391 lines
13 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(new List<Error>()),
(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 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<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;
List<KeyValuePair<string, string>>? 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 ??= new List<KeyValuePair<string, string>>(4);
values.Add(new(keyString, valueString));
}
return values?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty;
}
}
[JsonConverter(typeof(ManyErrorsJsonConverter))]
public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
{
private readonly List<Error> _errors;
[Pure] public IEnumerable<Error> Errors { get => _errors; }
internal ManyErrors(List<Error> errors) => _errors = errors;
internal ManyErrors(Error head, Error tail)
{
_errors = new List<Error>(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<Error> 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<Error> 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<Error> GetEnumerator() => _errors.GetEnumerator();
[Pure] IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
[Serializable]
public sealed class ErrorException : Exception
{
public ErrorException(string type, string message) : base(message)
{
Type = type ?? nameof(ErrorException);
}
public string Type { get; }
}