+51
-3
@@ -58,7 +58,7 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
|
||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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, { IsEmpty: true }) => err,
|
||||
(null, Error err) => err,
|
||||
@@ -70,8 +70,10 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
|
||||
/// </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
|
||||
public static Error Many(params Error[] errors) => errors?.Length switch
|
||||
{
|
||||
null => EmptyError.Instance,
|
||||
0 => EmptyError.Instance,
|
||||
1 => errors[0],
|
||||
_ => new ManyErrors(errors)
|
||||
};
|
||||
@@ -80,7 +82,11 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
|
||||
/// </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);
|
||||
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 Message { get; }
|
||||
@@ -93,6 +99,12 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
|
||||
[Pure] public abstract bool IsExpected { 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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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))]
|
||||
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)
|
||||
{
|
||||
if (value is EmptyError)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is ManyErrors manyErrors)
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
foreach (var err in manyErrors)
|
||||
{
|
||||
if (err.IsEmpty)
|
||||
continue;
|
||||
WriteOne(writer, err);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
@@ -145,14 +145,14 @@ public class Construction
|
||||
public class Many_Factory
|
||||
{
|
||||
[Fact]
|
||||
public void With_both_null_returns_empty_ManyErrors()
|
||||
public void With_both_null_returns_EmptyError()
|
||||
{
|
||||
// Given
|
||||
// When
|
||||
var error = Error.Many(null!, null!);
|
||||
|
||||
// Then
|
||||
error.ShouldBeOfType<ManyErrors>();
|
||||
error.ShouldBeOfType<EmptyError>();
|
||||
error.IsEmpty.ShouldBeTrue();
|
||||
error.Count.ShouldBe(0);
|
||||
}
|
||||
@@ -216,14 +216,14 @@ public class Construction
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Params_with_empty_array_returns_empty_ManyErrors()
|
||||
public void Params_with_empty_array_returns_EmptyError()
|
||||
{
|
||||
// Given
|
||||
// When
|
||||
var result = Error.Many(Array.Empty<Error>());
|
||||
|
||||
// Then
|
||||
result.ShouldBeOfType<ManyErrors>();
|
||||
result.ShouldBeOfType<EmptyError>();
|
||||
result.IsEmpty.ShouldBeTrue();
|
||||
result.Count.ShouldBe(0);
|
||||
}
|
||||
@@ -365,9 +365,10 @@ public class Construction
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManyErrors_IsEmpty_when_empty()
|
||||
public void EmptyError_IsEmpty()
|
||||
{
|
||||
var error = Error.Many(Array.Empty<Error>());
|
||||
error.ShouldBeOfType<EmptyError>();
|
||||
error.IsEmpty.ShouldBeTrue();
|
||||
}
|
||||
|
||||
@@ -569,4 +570,167 @@ public class Construction
|
||||
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]
|
||||
public void Empty_ManyErrors_round_trip()
|
||||
public void EmptyError_serializes_to_null()
|
||||
{
|
||||
// Given
|
||||
Error original = Error.Many(Array.Empty<Error>());
|
||||
var error = Error.Empty;
|
||||
|
||||
// When
|
||||
var json = JsonSerializer.Serialize(original);
|
||||
var deserialized = JsonSerializer.Deserialize<Error>(json);
|
||||
var json = JsonSerializer.Serialize(error);
|
||||
|
||||
// Then
|
||||
deserialized.ShouldBeOfType<ManyErrors>();
|
||||
deserialized!.IsEmpty.ShouldBeTrue();
|
||||
deserialized.Count.ShouldBe(0);
|
||||
json.ShouldBe("null");
|
||||
}
|
||||
|
||||
[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]
|
||||
|
||||
Reference in New Issue
Block a user