+51
-3
@@ -58,7 +58,7 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
|
|||||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static Error Many(Error error1, Error error2) => (error1, error2) switch
|
public static Error Many(Error error1, Error error2) => (error1, error2) switch
|
||||||
{
|
{
|
||||||
(null, null) => new ManyErrors(ImmutableArray<Error>.Empty),
|
(null, null) => EmptyError.Instance,
|
||||||
(Error err, null) => err,
|
(Error err, null) => err,
|
||||||
(Error err, { IsEmpty: true }) => err,
|
(Error err, { IsEmpty: true }) => err,
|
||||||
(null, Error err) => err,
|
(null, Error err) => err,
|
||||||
@@ -70,8 +70,10 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Collects many errors into a single <see cref="Error"/> type, called <see cref="ManyErrors"/></remarks>
|
/// <remarks>Collects many errors into a single <see cref="Error"/> type, called <see cref="ManyErrors"/></remarks>
|
||||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static Error Many(params Error[] errors) => errors.Length switch
|
public static Error Many(params Error[] errors) => errors?.Length switch
|
||||||
{
|
{
|
||||||
|
null => EmptyError.Instance,
|
||||||
|
0 => EmptyError.Instance,
|
||||||
1 => errors[0],
|
1 => errors[0],
|
||||||
_ => new ManyErrors(errors)
|
_ => new ManyErrors(errors)
|
||||||
};
|
};
|
||||||
@@ -80,7 +82,11 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Collects many errors into a single <see cref="Error"/> type, called <see cref="ManyErrors"/></remarks>
|
/// <remarks>Collects many errors into a single <see cref="Error"/> type, called <see cref="ManyErrors"/></remarks>
|
||||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static Error Many(IEnumerable<Error> errors) => new ManyErrors(errors);
|
public static Error Many(IEnumerable<Error> errors)
|
||||||
|
{
|
||||||
|
var result = new ManyErrors(errors);
|
||||||
|
return result.IsEmpty ? EmptyError.Instance : result;
|
||||||
|
}
|
||||||
|
|
||||||
[Pure] public abstract string Type { get; }
|
[Pure] public abstract string Type { get; }
|
||||||
[Pure] public abstract string Message { get; }
|
[Pure] public abstract string Message { get; }
|
||||||
@@ -93,6 +99,12 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
|
|||||||
[Pure] public abstract bool IsExpected { get; }
|
[Pure] public abstract bool IsExpected { get; }
|
||||||
[Pure] public abstract bool IsExceptional { get; }
|
[Pure] public abstract bool IsExceptional { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A reusable singleton representing the absence of an error (null-object pattern).
|
||||||
|
/// </summary>
|
||||||
|
[Pure]
|
||||||
|
public static Error Empty => EmptyError.Instance;
|
||||||
|
|
||||||
[Pure] public Error Append(Error? next)
|
[Pure] public Error Append(Error? next)
|
||||||
{
|
{
|
||||||
if (next is null || next.IsEmpty)
|
if (next is null || next.IsEmpty)
|
||||||
@@ -153,6 +165,42 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
|
|||||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] internal virtual Error AccessUnsafe(int position) => this;
|
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] internal virtual Error AccessUnsafe(int position) => this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class EmptyError : Error
|
||||||
|
{
|
||||||
|
public static readonly EmptyError Instance = new();
|
||||||
|
|
||||||
|
private EmptyError() { }
|
||||||
|
|
||||||
|
[Pure] public override string Type => "empty";
|
||||||
|
[Pure] public override string Message => "";
|
||||||
|
[Pure] public override int Count => 0;
|
||||||
|
[Pure] public override bool IsEmpty => true;
|
||||||
|
[Pure] public override bool IsExpected => true;
|
||||||
|
[Pure] public override bool IsExceptional => false;
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public override IEnumerable<Error> ToEnumerable() { yield break; }
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public override Exception ToException() =>
|
||||||
|
new ErrorException(Type, Message);
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public override int CompareTo(Error? other) =>
|
||||||
|
other is EmptyError ? 0 : -1;
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public override bool Equals(Error? other) =>
|
||||||
|
ReferenceEquals(this, other) || (other?.IsEmpty == true);
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public override int GetHashCode() => 0;
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public override bool IsSimilarTo(Error? other) =>
|
||||||
|
other is EmptyError || (other?.IsEmpty == true);
|
||||||
|
}
|
||||||
|
|
||||||
[JsonConverter(typeof(ExpectedErrorJsonConverter))]
|
[JsonConverter(typeof(ExpectedErrorJsonConverter))]
|
||||||
public sealed class ExpectedError : Error
|
public sealed class ExpectedError : Error
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,11 +18,19 @@ public sealed class ErrorJsonConverter : JsonConverter<Error>
|
|||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, Error value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, Error value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
|
if (value is EmptyError)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (value is ManyErrors manyErrors)
|
if (value is ManyErrors manyErrors)
|
||||||
{
|
{
|
||||||
writer.WriteStartArray();
|
writer.WriteStartArray();
|
||||||
foreach (var err in manyErrors)
|
foreach (var err in manyErrors)
|
||||||
{
|
{
|
||||||
|
if (err.IsEmpty)
|
||||||
|
continue;
|
||||||
WriteOne(writer, err);
|
WriteOne(writer, err);
|
||||||
}
|
}
|
||||||
writer.WriteEndArray();
|
writer.WriteEndArray();
|
||||||
|
|||||||
@@ -145,14 +145,14 @@ public class Construction
|
|||||||
public class Many_Factory
|
public class Many_Factory
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void With_both_null_returns_empty_ManyErrors()
|
public void With_both_null_returns_EmptyError()
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
// When
|
// When
|
||||||
var error = Error.Many(null!, null!);
|
var error = Error.Many(null!, null!);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
error.ShouldBeOfType<ManyErrors>();
|
error.ShouldBeOfType<EmptyError>();
|
||||||
error.IsEmpty.ShouldBeTrue();
|
error.IsEmpty.ShouldBeTrue();
|
||||||
error.Count.ShouldBe(0);
|
error.Count.ShouldBe(0);
|
||||||
}
|
}
|
||||||
@@ -216,14 +216,14 @@ public class Construction
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Params_with_empty_array_returns_empty_ManyErrors()
|
public void Params_with_empty_array_returns_EmptyError()
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
// When
|
// When
|
||||||
var result = Error.Many(Array.Empty<Error>());
|
var result = Error.Many(Array.Empty<Error>());
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
result.ShouldBeOfType<ManyErrors>();
|
result.ShouldBeOfType<EmptyError>();
|
||||||
result.IsEmpty.ShouldBeTrue();
|
result.IsEmpty.ShouldBeTrue();
|
||||||
result.Count.ShouldBe(0);
|
result.Count.ShouldBe(0);
|
||||||
}
|
}
|
||||||
@@ -365,9 +365,10 @@ public class Construction
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ManyErrors_IsEmpty_when_empty()
|
public void EmptyError_IsEmpty()
|
||||||
{
|
{
|
||||||
var error = Error.Many(Array.Empty<Error>());
|
var error = Error.Many(Array.Empty<Error>());
|
||||||
|
error.ShouldBeOfType<EmptyError>();
|
||||||
error.IsEmpty.ShouldBeTrue();
|
error.IsEmpty.ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,4 +570,167 @@ public class Construction
|
|||||||
message.ShouldBe("custom message");
|
message.ShouldBe("custom message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class EmptyError_Tests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Error_Empty_returns_EmptyError_singleton()
|
||||||
|
{
|
||||||
|
Error.Empty.ShouldBeOfType<EmptyError>();
|
||||||
|
Error.Empty.ShouldBeSameAs(EmptyError.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Properties_are_correct()
|
||||||
|
{
|
||||||
|
var error = Error.Empty;
|
||||||
|
error.Type.ShouldBe("empty");
|
||||||
|
error.Message.ShouldBe("");
|
||||||
|
error.Count.ShouldBe(0);
|
||||||
|
error.IsEmpty.ShouldBeTrue();
|
||||||
|
error.IsExpected.ShouldBeTrue();
|
||||||
|
error.IsExceptional.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToEnumerable_returns_empty()
|
||||||
|
{
|
||||||
|
Error.Empty.ToEnumerable().ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToException_returns_ErrorException()
|
||||||
|
{
|
||||||
|
var ex = Error.Empty.ToException();
|
||||||
|
ex.ShouldBeOfType<ErrorException>();
|
||||||
|
var errEx = (ErrorException)ex;
|
||||||
|
errEx.Type.ShouldBe("empty");
|
||||||
|
errEx.Message.ShouldBe("");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Deconstruct_produces_type_and_message()
|
||||||
|
{
|
||||||
|
var (type, message) = Error.Empty;
|
||||||
|
type.ShouldBe("empty");
|
||||||
|
message.ShouldBe("");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtensionData_is_empty()
|
||||||
|
{
|
||||||
|
Error.Empty.ExtensionData.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Indexer_returns_null()
|
||||||
|
{
|
||||||
|
Error.Empty["anything"].ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Equals_self()
|
||||||
|
{
|
||||||
|
Error.Empty.Equals(Error.Empty).ShouldBeTrue();
|
||||||
|
Error.Empty.Equals(EmptyError.Instance).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Equals_other_empty_error()
|
||||||
|
{
|
||||||
|
// An empty ManyErrors is also IsEmpty=true, so EmptyError equals it
|
||||||
|
var emptyMany = new ManyErrors(Array.Empty<Error>());
|
||||||
|
Error.Empty.Equals(emptyMany).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Equals_non_empty_is_false()
|
||||||
|
{
|
||||||
|
var realError = Error.New("test");
|
||||||
|
Error.Empty.Equals(realError).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsSimilarTo_empty_error()
|
||||||
|
{
|
||||||
|
var emptyMany = new ManyErrors(Array.Empty<Error>());
|
||||||
|
Error.Empty.IsSimilarTo(emptyMany).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsSimilarTo_non_empty_is_false()
|
||||||
|
{
|
||||||
|
Error.Empty.IsSimilarTo(Error.New("test")).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CompareTo_sorts_before_non_empty()
|
||||||
|
{
|
||||||
|
Error.Empty.CompareTo(Error.New("test")).ShouldBeLessThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CompareTo_self_is_zero()
|
||||||
|
{
|
||||||
|
Error.Empty.CompareTo(Error.Empty).ShouldBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToString_returns_empty_message()
|
||||||
|
{
|
||||||
|
Error.Empty.ToString().ShouldBe("");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Operator_equals()
|
||||||
|
{
|
||||||
|
(Error.Empty == EmptyError.Instance).ShouldBeTrue();
|
||||||
|
(Error.Empty == Error.New("test")).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Operator_not_equals()
|
||||||
|
{
|
||||||
|
(Error.Empty != Error.New("test")).ShouldBeTrue();
|
||||||
|
(Error.Empty != EmptyError.Instance).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Append_EmptyError_returns_other()
|
||||||
|
{
|
||||||
|
var real = Error.New("test");
|
||||||
|
Error.Empty.Append(real).ShouldBe(real);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Append_to_EmptyError_returns_real()
|
||||||
|
{
|
||||||
|
var real = Error.New("test");
|
||||||
|
real.Append(Error.Empty).ShouldBe(real);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Operator_plus_EmptyError()
|
||||||
|
{
|
||||||
|
var real = Error.New("test");
|
||||||
|
(Error.Empty + real).ShouldBe(real);
|
||||||
|
(real + Error.Empty).ShouldBe(real);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Many_with_EmptyError_absorbs_it()
|
||||||
|
{
|
||||||
|
var real = Error.New("test");
|
||||||
|
var result = Error.Many(Error.Empty, real);
|
||||||
|
result.ShouldBeOfType<ExpectedError>();
|
||||||
|
result.ShouldBe(real);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetHashCode_is_stable()
|
||||||
|
{
|
||||||
|
Error.Empty.GetHashCode().ShouldBe(0);
|
||||||
|
EmptyError.Instance.GetHashCode().ShouldBe(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,19 +152,31 @@ public class Serialization_RoundTrip
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Empty_ManyErrors_round_trip()
|
public void EmptyError_serializes_to_null()
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
Error original = Error.Many(Array.Empty<Error>());
|
var error = Error.Empty;
|
||||||
|
|
||||||
// When
|
// When
|
||||||
var json = JsonSerializer.Serialize(original);
|
var json = JsonSerializer.Serialize(error);
|
||||||
var deserialized = JsonSerializer.Deserialize<Error>(json);
|
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
deserialized.ShouldBeOfType<ManyErrors>();
|
json.ShouldBe("null");
|
||||||
deserialized!.IsEmpty.ShouldBeTrue();
|
}
|
||||||
deserialized.Count.ShouldBe(0);
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyError_filtered_from_ManyErrors_serialization()
|
||||||
|
{
|
||||||
|
// Given
|
||||||
|
var real = Error.New("real_error");
|
||||||
|
var errors = Error.Many(Error.Empty, real);
|
||||||
|
|
||||||
|
// When
|
||||||
|
var json = JsonSerializer.Serialize(errors);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
json.ShouldNotContain("empty");
|
||||||
|
json.ShouldContain("real_error");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user