From 09e09ad7071c5a5d1c2c1f564f5c1f142c12ea19 Mon Sep 17 00:00:00 2001 From: Amichai Mantinband Date: Thu, 9 May 2024 14:17:43 +0300 Subject: [PATCH 01/12] Migrate from Errors to Exceptions for invalid use of library --- .editorconfig | 135 +++++++++--------- src/{ => ErrorOr}/ErrorOr.Else.cs | 0 src/{ => ErrorOr}/ErrorOr.ElseExtensions.cs | 0 src/{ => ErrorOr}/ErrorOr.Equality.cs | 0 src/{ => ErrorOr}/ErrorOr.FailIf.cs | 0 src/{ => ErrorOr}/ErrorOr.FailIfExtensions.cs | 0 .../ErrorOr.ImplicitConverters.cs | 10 ++ src/{ => ErrorOr}/ErrorOr.Match.cs | 0 src/{ => ErrorOr}/ErrorOr.MatchExtensions.cs | 0 src/{ => ErrorOr}/ErrorOr.Switch.cs | 0 src/{ => ErrorOr}/ErrorOr.SwitchExtensions.cs | 0 src/{ => ErrorOr}/ErrorOr.Then.cs | 0 src/{ => ErrorOr}/ErrorOr.ThenExtensions.cs | 0 .../ErrorOr.ToErrorOrExtensions.cs | 0 src/{ => ErrorOr}/ErrorOr.cs | 8 ++ src/{ => ErrorOr}/ErrorOrFactory.cs | 0 src/{ => ErrorOr}/IErrorOr.cs | 0 src/{ => Errors}/Error.cs | 0 src/{ => Errors}/ErrorType.cs | 0 src/{ => Errors}/KnownErrors.cs | 0 src/{ => Results}/Types.cs | 0 tests/{ => ErrorOr}/ErrorOr.ElseAsyncTests.cs | 0 tests/{ => ErrorOr}/ErrorOr.ElseTests.cs | 0 tests/{ => ErrorOr}/ErrorOr.EqualityTests.cs | 0 .../{ => ErrorOr}/ErrorOr.FailIfAsyncTests.cs | 0 tests/{ => ErrorOr}/ErrorOr.FailIfTests.cs | 0 .../ErrorOr.InstantiationTests.cs} | 34 ++++- .../{ => ErrorOr}/ErrorOr.MatchAsyncTests.cs | 0 tests/{ => ErrorOr}/ErrorOr.MatchTests.cs | 0 .../{ => ErrorOr}/ErrorOr.SwitchAsyncTests.cs | 0 tests/{ => ErrorOr}/ErrorOr.SwitchTests.cs | 0 tests/{ => ErrorOr}/ErrorOr.ThenAsyncTests.cs | 0 tests/{ => ErrorOr}/ErrorOr.ThenTests.cs | 0 tests/{ => ErrorOr}/ErrorOr.ToErrorOrTests.cs | 0 tests/{ => Errors}/Error.EqualityTests.cs | 0 tests/{ => Errors}/ErrorTests.cs | 0 36 files changed, 120 insertions(+), 67 deletions(-) rename src/{ => ErrorOr}/ErrorOr.Else.cs (100%) rename src/{ => ErrorOr}/ErrorOr.ElseExtensions.cs (100%) rename src/{ => ErrorOr}/ErrorOr.Equality.cs (100%) rename src/{ => ErrorOr}/ErrorOr.FailIf.cs (100%) rename src/{ => ErrorOr}/ErrorOr.FailIfExtensions.cs (100%) rename src/{ => ErrorOr}/ErrorOr.ImplicitConverters.cs (71%) rename src/{ => ErrorOr}/ErrorOr.Match.cs (100%) rename src/{ => ErrorOr}/ErrorOr.MatchExtensions.cs (100%) rename src/{ => ErrorOr}/ErrorOr.Switch.cs (100%) rename src/{ => ErrorOr}/ErrorOr.SwitchExtensions.cs (100%) rename src/{ => ErrorOr}/ErrorOr.Then.cs (100%) rename src/{ => ErrorOr}/ErrorOr.ThenExtensions.cs (100%) rename src/{ => ErrorOr}/ErrorOr.ToErrorOrExtensions.cs (100%) rename src/{ => ErrorOr}/ErrorOr.cs (83%) rename src/{ => ErrorOr}/ErrorOrFactory.cs (100%) rename src/{ => ErrorOr}/IErrorOr.cs (100%) rename src/{ => Errors}/Error.cs (100%) rename src/{ => Errors}/ErrorType.cs (100%) rename src/{ => Errors}/KnownErrors.cs (100%) rename src/{ => Results}/Types.cs (100%) rename tests/{ => ErrorOr}/ErrorOr.ElseAsyncTests.cs (100%) rename tests/{ => ErrorOr}/ErrorOr.ElseTests.cs (100%) rename tests/{ => ErrorOr}/ErrorOr.EqualityTests.cs (100%) rename tests/{ => ErrorOr}/ErrorOr.FailIfAsyncTests.cs (100%) rename tests/{ => ErrorOr}/ErrorOr.FailIfTests.cs (100%) rename tests/{ErrorOrTests.cs => ErrorOr/ErrorOr.InstantiationTests.cs} (88%) rename tests/{ => ErrorOr}/ErrorOr.MatchAsyncTests.cs (100%) rename tests/{ => ErrorOr}/ErrorOr.MatchTests.cs (100%) rename tests/{ => ErrorOr}/ErrorOr.SwitchAsyncTests.cs (100%) rename tests/{ => ErrorOr}/ErrorOr.SwitchTests.cs (100%) rename tests/{ => ErrorOr}/ErrorOr.ThenAsyncTests.cs (100%) rename tests/{ => ErrorOr}/ErrorOr.ThenTests.cs (100%) rename tests/{ => ErrorOr}/ErrorOr.ToErrorOrTests.cs (100%) rename tests/{ => Errors}/Error.EqualityTests.cs (100%) rename tests/{ => Errors}/ErrorTests.cs (100%) diff --git a/.editorconfig b/.editorconfig index 861f73d..083abc2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,66 +1,69 @@ -root = true - -[*] -indent_style = space -indent_size = 4 -end_of_line = crlf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -# SA1615: Element return value should be documented. -dotnet_diagnostic.SA1615.severity = none - -# SA1611: Element parameters must be documented. -dotnet_diagnostic.SA1611.severity = none - -# SA1633: File must have header. -dotnet_diagnostic.SA1633.severity = none - -# SA1633: Generic type parameters must be documented. -dotnet_diagnostic.SA1618.severity = none - -# SA1133: Each attribute should be placed in its own set of square brackets. -dotnet_diagnostic.SA1133.severity = none - -# SA1600: Elements must be documented. -dotnet_diagnostic.SA1600.severity = none - -# SA1601: Partial elementes should be documented. -dotnet_diagnostic.SA1601.severity = none - -# SA1313: Parameter names must begin with lower case letter. -dotnet_diagnostic.SA1313.severity = none - -# SA1009: Closing parenthesis should be followed by a space. -dotnet_diagnostic.SA1009.severity = none - -# SA1000: The keyword 'new' should be followed by a space. -dotnet_diagnostic.SA1000.severity = none - -# SA1101: Prefix local calls with this. -dotnet_diagnostic.SA1101.severity = none - -# SA1309: Field should not begin with an underscore. -dotnet_diagnostic.SA1309.severity = none - -# SA1602: Enumeration items should be documented. -dotnet_diagnostic.SA1602.severity = none - -# CS1591: Missing XML comment for publicly visible type or member. -dotnet_diagnostic.CS1591.severity = none - -# SA1200: Using directive should appear within a namespace declaration. -dotnet_diagnostic.SA1200.severity = none - -# IDE0008: Use explicit type -csharp_style_var_when_type_is_apparent = true - -# IDE0130: Namespace does not match folder structure -dotnet_style_namespace_match_folder = false - -# IDE0023: Use block body for operators -csharp_style_expression_bodied_operators = when_on_single_line - -# IDE0130: Namespace does not match folder structure -dotnet_diagnostic.IDE0130.severity = none +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +# SA1615: Element return value should be documented. +dotnet_diagnostic.SA1615.severity = none + +# SA1611: Element parameters must be documented. +dotnet_diagnostic.SA1611.severity = none + +# SA1633: File must have header. +dotnet_diagnostic.SA1633.severity = none + +# SA1633: Generic type parameters must be documented. +dotnet_diagnostic.SA1618.severity = none + +# SA1133: Each attribute should be placed in its own set of square brackets. +dotnet_diagnostic.SA1133.severity = none + +# SA1600: Elements must be documented. +dotnet_diagnostic.SA1600.severity = none + +# SA1601: Partial elementes should be documented. +dotnet_diagnostic.SA1601.severity = none + +# SA1313: Parameter names must begin with lower case letter. +dotnet_diagnostic.SA1313.severity = none + +# SA1009: Closing parenthesis should be followed by a space. +dotnet_diagnostic.SA1009.severity = none + +# SA1000: The keyword 'new' should be followed by a space. +dotnet_diagnostic.SA1000.severity = none + +# SA1101: Prefix local calls with this. +dotnet_diagnostic.SA1101.severity = none + +# SA1309: Field should not begin with an underscore. +dotnet_diagnostic.SA1309.severity = none + +# SA1602: Enumeration items should be documented. +dotnet_diagnostic.SA1602.severity = none + +# CS1591: Missing XML comment for publicly visible type or member. +dotnet_diagnostic.CS1591.severity = none + +# SA1200: Using directive should appear within a namespace declaration. +dotnet_diagnostic.SA1200.severity = none + +# IDE0008: Use explicit type +csharp_style_var_when_type_is_apparent = true + +# IDE0130: Namespace does not match folder structure +dotnet_style_namespace_match_folder = false + +# IDE0023: Use block body for operators +csharp_style_expression_bodied_operators = when_on_single_line + +# IDE0130: Namespace does not match folder structure +dotnet_diagnostic.IDE0130.severity = none + +# SA1642: Constructor summary documentation should begin with standard text +dotnet_diagnostic.SA1642.severity = none diff --git a/src/ErrorOr.Else.cs b/src/ErrorOr/ErrorOr.Else.cs similarity index 100% rename from src/ErrorOr.Else.cs rename to src/ErrorOr/ErrorOr.Else.cs diff --git a/src/ErrorOr.ElseExtensions.cs b/src/ErrorOr/ErrorOr.ElseExtensions.cs similarity index 100% rename from src/ErrorOr.ElseExtensions.cs rename to src/ErrorOr/ErrorOr.ElseExtensions.cs diff --git a/src/ErrorOr.Equality.cs b/src/ErrorOr/ErrorOr.Equality.cs similarity index 100% rename from src/ErrorOr.Equality.cs rename to src/ErrorOr/ErrorOr.Equality.cs diff --git a/src/ErrorOr.FailIf.cs b/src/ErrorOr/ErrorOr.FailIf.cs similarity index 100% rename from src/ErrorOr.FailIf.cs rename to src/ErrorOr/ErrorOr.FailIf.cs diff --git a/src/ErrorOr.FailIfExtensions.cs b/src/ErrorOr/ErrorOr.FailIfExtensions.cs similarity index 100% rename from src/ErrorOr.FailIfExtensions.cs rename to src/ErrorOr/ErrorOr.FailIfExtensions.cs diff --git a/src/ErrorOr.ImplicitConverters.cs b/src/ErrorOr/ErrorOr.ImplicitConverters.cs similarity index 71% rename from src/ErrorOr.ImplicitConverters.cs rename to src/ErrorOr/ErrorOr.ImplicitConverters.cs index d2e155e..d022487 100644 --- a/src/ErrorOr.ImplicitConverters.cs +++ b/src/ErrorOr/ErrorOr.ImplicitConverters.cs @@ -26,6 +26,11 @@ namespace ErrorOr; /// public static implicit operator ErrorOr(List errors) { + if (errors.Count == 0) + { + throw new InvalidOperationException("Cannot create an ErrorOr from an empty list of errors. Provide at least one error."); + } + return new ErrorOr(errors); } @@ -34,6 +39,11 @@ namespace ErrorOr; /// public static implicit operator ErrorOr(Error[] errors) { + if (errors.Length == 0) + { + throw new InvalidOperationException("Cannot create an ErrorOr from an empty list of errors. Provide at least one error."); + } + return new ErrorOr(errors.ToList()); } } diff --git a/src/ErrorOr.Match.cs b/src/ErrorOr/ErrorOr.Match.cs similarity index 100% rename from src/ErrorOr.Match.cs rename to src/ErrorOr/ErrorOr.Match.cs diff --git a/src/ErrorOr.MatchExtensions.cs b/src/ErrorOr/ErrorOr.MatchExtensions.cs similarity index 100% rename from src/ErrorOr.MatchExtensions.cs rename to src/ErrorOr/ErrorOr.MatchExtensions.cs diff --git a/src/ErrorOr.Switch.cs b/src/ErrorOr/ErrorOr.Switch.cs similarity index 100% rename from src/ErrorOr.Switch.cs rename to src/ErrorOr/ErrorOr.Switch.cs diff --git a/src/ErrorOr.SwitchExtensions.cs b/src/ErrorOr/ErrorOr.SwitchExtensions.cs similarity index 100% rename from src/ErrorOr.SwitchExtensions.cs rename to src/ErrorOr/ErrorOr.SwitchExtensions.cs diff --git a/src/ErrorOr.Then.cs b/src/ErrorOr/ErrorOr.Then.cs similarity index 100% rename from src/ErrorOr.Then.cs rename to src/ErrorOr/ErrorOr.Then.cs diff --git a/src/ErrorOr.ThenExtensions.cs b/src/ErrorOr/ErrorOr.ThenExtensions.cs similarity index 100% rename from src/ErrorOr.ThenExtensions.cs rename to src/ErrorOr/ErrorOr.ThenExtensions.cs diff --git a/src/ErrorOr.ToErrorOrExtensions.cs b/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs similarity index 100% rename from src/ErrorOr.ToErrorOrExtensions.cs rename to src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs diff --git a/src/ErrorOr.cs b/src/ErrorOr/ErrorOr.cs similarity index 83% rename from src/ErrorOr.cs rename to src/ErrorOr/ErrorOr.cs index 6614182..15866f3 100644 --- a/src/ErrorOr.cs +++ b/src/ErrorOr/ErrorOr.cs @@ -7,6 +7,14 @@ namespace ErrorOr; /// public readonly partial record struct ErrorOr : IErrorOr { + /// + /// Prevents a default struct from being created. + /// + public ErrorOr() + { + throw new InvalidOperationException("Default construction of ErrorOr is invalid. Please use provided factory methods to instantiate."); + } + private readonly TValue? _value = default; private readonly List? _errors = null; diff --git a/src/ErrorOrFactory.cs b/src/ErrorOr/ErrorOrFactory.cs similarity index 100% rename from src/ErrorOrFactory.cs rename to src/ErrorOr/ErrorOrFactory.cs diff --git a/src/IErrorOr.cs b/src/ErrorOr/IErrorOr.cs similarity index 100% rename from src/IErrorOr.cs rename to src/ErrorOr/IErrorOr.cs diff --git a/src/Error.cs b/src/Errors/Error.cs similarity index 100% rename from src/Error.cs rename to src/Errors/Error.cs diff --git a/src/ErrorType.cs b/src/Errors/ErrorType.cs similarity index 100% rename from src/ErrorType.cs rename to src/Errors/ErrorType.cs diff --git a/src/KnownErrors.cs b/src/Errors/KnownErrors.cs similarity index 100% rename from src/KnownErrors.cs rename to src/Errors/KnownErrors.cs diff --git a/src/Types.cs b/src/Results/Types.cs similarity index 100% rename from src/Types.cs rename to src/Results/Types.cs diff --git a/tests/ErrorOr.ElseAsyncTests.cs b/tests/ErrorOr/ErrorOr.ElseAsyncTests.cs similarity index 100% rename from tests/ErrorOr.ElseAsyncTests.cs rename to tests/ErrorOr/ErrorOr.ElseAsyncTests.cs diff --git a/tests/ErrorOr.ElseTests.cs b/tests/ErrorOr/ErrorOr.ElseTests.cs similarity index 100% rename from tests/ErrorOr.ElseTests.cs rename to tests/ErrorOr/ErrorOr.ElseTests.cs diff --git a/tests/ErrorOr.EqualityTests.cs b/tests/ErrorOr/ErrorOr.EqualityTests.cs similarity index 100% rename from tests/ErrorOr.EqualityTests.cs rename to tests/ErrorOr/ErrorOr.EqualityTests.cs diff --git a/tests/ErrorOr.FailIfAsyncTests.cs b/tests/ErrorOr/ErrorOr.FailIfAsyncTests.cs similarity index 100% rename from tests/ErrorOr.FailIfAsyncTests.cs rename to tests/ErrorOr/ErrorOr.FailIfAsyncTests.cs diff --git a/tests/ErrorOr.FailIfTests.cs b/tests/ErrorOr/ErrorOr.FailIfTests.cs similarity index 100% rename from tests/ErrorOr.FailIfTests.cs rename to tests/ErrorOr/ErrorOr.FailIfTests.cs diff --git a/tests/ErrorOrTests.cs b/tests/ErrorOr/ErrorOr.InstantiationTests.cs similarity index 88% rename from tests/ErrorOrTests.cs rename to tests/ErrorOr/ErrorOr.InstantiationTests.cs index 82add1f..e33fd1a 100644 --- a/tests/ErrorOrTests.cs +++ b/tests/ErrorOr/ErrorOr.InstantiationTests.cs @@ -3,7 +3,7 @@ namespace Tests; using ErrorOr; using FluentAssertions; -public class ErrorOrTests +public class ErrorOrInstantiationTests { private record Person(string Name); @@ -344,4 +344,36 @@ public void ImplicitCastErrorArray_WhenAccessingFirstError_ShouldReturnFirstErro errorOrPerson.IsError.Should().BeTrue(); errorOrPerson.FirstError.Should().Be(errors[0]); } + + [Fact] + public void CreateErrorOr_WhenUsingEmptyConstructor_ShouldThrow() + { + // Act +#pragma warning disable SA1129 // Do not use default value type constructor + Func> errorOrInt = () => new ErrorOr(); +#pragma warning restore SA1129 // Do not use default value type constructor + + // Assert + errorOrInt.Should().ThrowExactly(); + } + + [Fact] + public void CreateErrorOr_WhenEmptyErrorsList_ShouldThrow() + { + // Act + Func> errorOrInt = () => new List(); + + // Assert + errorOrInt.Should().ThrowExactly(); + } + + [Fact] + public void CreateErrorOr_WhenEmptyErrorsArray_ShouldThrow() + { + // Act + Func> errorOrInt = () => Array.Empty(); + + // Assert + errorOrInt.Should().ThrowExactly(); + } } diff --git a/tests/ErrorOr.MatchAsyncTests.cs b/tests/ErrorOr/ErrorOr.MatchAsyncTests.cs similarity index 100% rename from tests/ErrorOr.MatchAsyncTests.cs rename to tests/ErrorOr/ErrorOr.MatchAsyncTests.cs diff --git a/tests/ErrorOr.MatchTests.cs b/tests/ErrorOr/ErrorOr.MatchTests.cs similarity index 100% rename from tests/ErrorOr.MatchTests.cs rename to tests/ErrorOr/ErrorOr.MatchTests.cs diff --git a/tests/ErrorOr.SwitchAsyncTests.cs b/tests/ErrorOr/ErrorOr.SwitchAsyncTests.cs similarity index 100% rename from tests/ErrorOr.SwitchAsyncTests.cs rename to tests/ErrorOr/ErrorOr.SwitchAsyncTests.cs diff --git a/tests/ErrorOr.SwitchTests.cs b/tests/ErrorOr/ErrorOr.SwitchTests.cs similarity index 100% rename from tests/ErrorOr.SwitchTests.cs rename to tests/ErrorOr/ErrorOr.SwitchTests.cs diff --git a/tests/ErrorOr.ThenAsyncTests.cs b/tests/ErrorOr/ErrorOr.ThenAsyncTests.cs similarity index 100% rename from tests/ErrorOr.ThenAsyncTests.cs rename to tests/ErrorOr/ErrorOr.ThenAsyncTests.cs diff --git a/tests/ErrorOr.ThenTests.cs b/tests/ErrorOr/ErrorOr.ThenTests.cs similarity index 100% rename from tests/ErrorOr.ThenTests.cs rename to tests/ErrorOr/ErrorOr.ThenTests.cs diff --git a/tests/ErrorOr.ToErrorOrTests.cs b/tests/ErrorOr/ErrorOr.ToErrorOrTests.cs similarity index 100% rename from tests/ErrorOr.ToErrorOrTests.cs rename to tests/ErrorOr/ErrorOr.ToErrorOrTests.cs diff --git a/tests/Error.EqualityTests.cs b/tests/Errors/Error.EqualityTests.cs similarity index 100% rename from tests/Error.EqualityTests.cs rename to tests/Errors/Error.EqualityTests.cs diff --git a/tests/ErrorTests.cs b/tests/Errors/ErrorTests.cs similarity index 100% rename from tests/ErrorTests.cs rename to tests/Errors/ErrorTests.cs From 62ee3b46ad23ca95662fcb1216810970c57c16b0 Mon Sep 17 00:00:00 2001 From: Amichai Mantinband Date: Thu, 9 May 2024 16:16:40 +0300 Subject: [PATCH 02/12] Update packages and update all unexpected scnearios to throw an exception --- .editorconfig | 3 + Directory.Build.props | 4 +- src/ErrorOr.csproj | 3 + src/ErrorOr/ErrorOr.Else.cs | 3 - src/ErrorOr/ErrorOr.Equality.cs | 118 +++---- src/ErrorOr/ErrorOr.FailIf.cs | 3 - src/ErrorOr/ErrorOr.ImplicitConverters.cs | 3 - src/ErrorOr/ErrorOr.Match.cs | 3 - src/ErrorOr/ErrorOr.Switch.cs | 3 - src/ErrorOr/ErrorOr.Then.cs | 3 - src/ErrorOr/ErrorOr.cs | 57 +-- src/Errors/Error.cs | 60 ++-- src/Errors/KnownErrors.cs | 16 - src/Results/Types.cs | 37 +- tests/.editorconfig | 4 + tests/ErrorOr/ErrorOr.ElseAsyncTests.cs | 44 ++- tests/ErrorOr/ErrorOr.ElseTests.cs | 72 ++-- tests/ErrorOr/ErrorOr.EqualityTests.cs | 362 ++++++++++---------- tests/ErrorOr/ErrorOr.InstantiationTests.cs | 70 ++-- tests/ErrorOr/ErrorOr.ThenAsyncTests.cs | 12 +- tests/ErrorOr/ErrorOr.ThenTests.cs | 30 +- tests/ErrorOr/ErrorOr.ToErrorOrTests.cs | 2 +- tests/ErrorOr/TestUtils.cs | 14 + tests/Tests.csproj | 10 +- tests/Usings.cs | 2 +- 25 files changed, 454 insertions(+), 484 deletions(-) delete mode 100644 src/Errors/KnownErrors.cs create mode 100644 tests/.editorconfig create mode 100644 tests/ErrorOr/TestUtils.cs diff --git a/.editorconfig b/.editorconfig index 083abc2..4ba6a4a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -67,3 +67,6 @@ dotnet_diagnostic.IDE0130.severity = none # SA1642: Constructor summary documentation should begin with standard text dotnet_diagnostic.SA1642.severity = none + +# SA1649: File name should match first type name +dotnet_diagnostic.SA1649.severity = none diff --git a/Directory.Build.props b/Directory.Build.props index 5506a74..980e822 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ - 10.0 + 12.0 true enable enable @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/ErrorOr.csproj b/src/ErrorOr.csproj index 335bf03..598f2cf 100644 --- a/src/ErrorOr.csproj +++ b/src/ErrorOr.csproj @@ -4,6 +4,8 @@ netstandard2.0;net8.0 enable enable + true + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml @@ -24,6 +26,7 @@ + diff --git a/src/ErrorOr/ErrorOr.Else.cs b/src/ErrorOr/ErrorOr.Else.cs index 6e706e9..125256b 100644 --- a/src/ErrorOr/ErrorOr.Else.cs +++ b/src/ErrorOr/ErrorOr.Else.cs @@ -1,8 +1,5 @@ namespace ErrorOr; -/// -/// A discriminated union of errors or a value. -/// public readonly partial record struct ErrorOr : IErrorOr { /// diff --git a/src/ErrorOr/ErrorOr.Equality.cs b/src/ErrorOr/ErrorOr.Equality.cs index b23b669..5521344 100644 --- a/src/ErrorOr/ErrorOr.Equality.cs +++ b/src/ErrorOr/ErrorOr.Equality.cs @@ -1,59 +1,59 @@ -namespace ErrorOr; - -public readonly partial record struct ErrorOr -{ - public bool Equals(ErrorOr other) - { - if (!IsError) - { - return !other.IsError && EqualityComparer.Default.Equals(_value, other._value); - } - - return other.IsError && CheckIfErrorsAreEqual(_errors, other._errors); - } - - private static bool CheckIfErrorsAreEqual(List errors1, List errors2) - { - // This method is currently implemented with strict ordering in mind, so the errors - // of the two lists need to be in the exact same order. - // This avoids allocating a hash set. We could provide a dedicated EqualityComparer for - // ErrorOr when arbitrary orders should be supported. - if (ReferenceEquals(errors1, errors2)) - { - return true; - } - - if (errors1.Count != errors2.Count) - { - return false; - } - - for (var i = 0; i < errors1.Count; i++) - { - if (!errors1[i].Equals(errors2[i])) - { - return false; - } - } - - return true; - } - - public override int GetHashCode() - { - if (!IsError) - { - return _value.GetHashCode(); - } - -#pragma warning disable SA1129 // HashCode needs to be instantiated this way - var hashCode = new HashCode(); -#pragma warning restore SA1129 - for (var i = 0; i < _errors.Count; i++) - { - hashCode.Add(_errors[i]); - } - - return hashCode.ToHashCode(); - } -} +namespace ErrorOr; + +public readonly partial record struct ErrorOr +{ + public bool Equals(ErrorOr other) + { + if (!IsError) + { + return !other.IsError && EqualityComparer.Default.Equals(_value, other._value); + } + + return other.IsError && CheckIfErrorsAreEqual(_errors, other._errors); + } + + public override int GetHashCode() + { + if (!IsError) + { + return _value.GetHashCode(); + } + +#pragma warning disable SA1129 // HashCode needs to be instantiated this way + var hashCode = new HashCode(); +#pragma warning restore SA1129 + for (var i = 0; i < _errors.Count; i++) + { + hashCode.Add(_errors[i]); + } + + return hashCode.ToHashCode(); + } + + private static bool CheckIfErrorsAreEqual(List errors1, List errors2) + { + // This method is currently implemented with strict ordering in mind, so the errors + // of the two lists need to be in the exact same order. + // This avoids allocating a hash set. We could provide a dedicated EqualityComparer for + // ErrorOr when arbitrary orders should be supported. + if (ReferenceEquals(errors1, errors2)) + { + return true; + } + + if (errors1.Count != errors2.Count) + { + return false; + } + + for (var i = 0; i < errors1.Count; i++) + { + if (!errors1[i].Equals(errors2[i])) + { + return false; + } + } + + return true; + } +} diff --git a/src/ErrorOr/ErrorOr.FailIf.cs b/src/ErrorOr/ErrorOr.FailIf.cs index c59fab0..1d6baaf 100644 --- a/src/ErrorOr/ErrorOr.FailIf.cs +++ b/src/ErrorOr/ErrorOr.FailIf.cs @@ -1,8 +1,5 @@ namespace ErrorOr; -/// -/// A discriminated union of errors or a value. -/// public readonly partial record struct ErrorOr : IErrorOr { /// diff --git a/src/ErrorOr/ErrorOr.ImplicitConverters.cs b/src/ErrorOr/ErrorOr.ImplicitConverters.cs index d022487..f617707 100644 --- a/src/ErrorOr/ErrorOr.ImplicitConverters.cs +++ b/src/ErrorOr/ErrorOr.ImplicitConverters.cs @@ -1,8 +1,5 @@ namespace ErrorOr; -/// -/// A discriminated union of errors or a value. -/// public readonly partial record struct ErrorOr : IErrorOr { /// diff --git a/src/ErrorOr/ErrorOr.Match.cs b/src/ErrorOr/ErrorOr.Match.cs index db76d48..5769e29 100644 --- a/src/ErrorOr/ErrorOr.Match.cs +++ b/src/ErrorOr/ErrorOr.Match.cs @@ -1,8 +1,5 @@ namespace ErrorOr; -/// -/// A discriminated union of errors or a value. -/// public readonly partial record struct ErrorOr : IErrorOr { /// diff --git a/src/ErrorOr/ErrorOr.Switch.cs b/src/ErrorOr/ErrorOr.Switch.cs index 1fabdbc..a374ae8 100644 --- a/src/ErrorOr/ErrorOr.Switch.cs +++ b/src/ErrorOr/ErrorOr.Switch.cs @@ -1,8 +1,5 @@ namespace ErrorOr; -/// -/// A discriminated union of errors or a value. -/// public readonly partial record struct ErrorOr : IErrorOr { /// diff --git a/src/ErrorOr/ErrorOr.Then.cs b/src/ErrorOr/ErrorOr.Then.cs index 5f24448..7282687 100644 --- a/src/ErrorOr/ErrorOr.Then.cs +++ b/src/ErrorOr/ErrorOr.Then.cs @@ -1,8 +1,5 @@ namespace ErrorOr; -/// -/// A discriminated union of errors or a value. -/// public readonly partial record struct ErrorOr : IErrorOr { /// diff --git a/src/ErrorOr/ErrorOr.cs b/src/ErrorOr/ErrorOr.cs index 15866f3..99b517d 100644 --- a/src/ErrorOr/ErrorOr.cs +++ b/src/ErrorOr/ErrorOr.cs @@ -5,8 +5,12 @@ namespace ErrorOr; /// /// A discriminated union of errors or a value. /// +/// The type of the underlying . public readonly partial record struct ErrorOr : IErrorOr { + private readonly TValue? _value = default; + private readonly List? _errors = null; + /// /// Prevents a default struct from being created. /// @@ -15,8 +19,23 @@ public ErrorOr() throw new InvalidOperationException("Default construction of ErrorOr is invalid. Please use provided factory methods to instantiate."); } - private readonly TValue? _value = default; - private readonly List? _errors = null; + private ErrorOr(Error error) + { + _errors = new List { error }; + IsError = true; + } + + private ErrorOr(List errors) + { + _errors = errors; + IsError = true; + } + + private ErrorOr(TValue value) + { + _value = value; + IsError = false; + } /// /// Gets a value indicating whether the state is error. @@ -30,20 +49,12 @@ public ErrorOr() /// /// Gets the list of errors. If the state is not error, the list will contain a single error representing the state. /// - public List Errors => IsError ? _errors! : KnownErrors.CachedNoErrorsList; + public List Errors => IsError ? _errors! : throw new InvalidOperationException("The Errors property cannot be accessed when no errors have been recorded. Check IsError before accessing Errors."); /// /// Gets the list of errors. If the state is not error, the list will be empty. /// - public List ErrorsOrEmptyList => IsError ? _errors! : KnownErrors.CachedEmptyErrorsList; - - /// - /// Creates an from a list of errors. - /// - public static ErrorOr From(List errors) - { - return errors; - } + public List ErrorsOrEmptyList => IsError ? _errors! : []; /// /// Gets the value. @@ -59,28 +70,18 @@ public Error FirstError { if (!IsError) { - return KnownErrors.NoFirstError; + throw new InvalidOperationException("The FirstError property cannot be accessed when no errors have been recorded. Check IsError before accessing FirstError."); } return _errors![0]; } } - private ErrorOr(Error error) - { - _errors = new List { error }; - IsError = true; - } - - private ErrorOr(List errors) - { - _errors = errors; - IsError = true; - } - - private ErrorOr(TValue value) + /// + /// Creates an from a list of errors. + /// + public static ErrorOr From(List errors) { - _value = value; - IsError = false; + return errors; } } diff --git a/src/Errors/Error.cs b/src/Errors/Error.cs index d308e72..a925326 100644 --- a/src/Errors/Error.cs +++ b/src/Errors/Error.cs @@ -5,6 +5,15 @@ namespace ErrorOr; /// public readonly record struct Error { + private Error(string code, string description, ErrorType type, Dictionary? metadata) + { + Code = code; + Description = description; + Type = type; + NumericType = (int)type; + Metadata = metadata; + } + /// /// Gets the unique error code. /// @@ -129,15 +138,6 @@ namespace ErrorOr; Dictionary? metadata = null) => new(code, description, (ErrorType)type, metadata); - private Error(string code, string description, ErrorType type, Dictionary? metadata) - { - Code = code; - Description = description; - Type = type; - NumericType = (int)type; - Metadata = metadata; - } - public bool Equals(Error other) { if (Type != other.Type || @@ -156,6 +156,27 @@ public bool Equals(Error other) return other.Metadata is not null && CompareMetadata(Metadata, other.Metadata); } + public override int GetHashCode() => + Metadata is null ? HashCode.Combine(Code, Description, Type, NumericType) : ComposeHashCode(); + + private int ComposeHashCode() + { +#pragma warning disable SA1129 // HashCode needs to be instantiated this way + var hashCode = new HashCode(); +#pragma warning restore SA1129 + hashCode.Add(Code); + hashCode.Add(Description); + hashCode.Add(Type); + hashCode.Add(NumericType); + foreach (var keyValuePair in Metadata!) + { + hashCode.Add(keyValuePair.Key); + hashCode.Add(keyValuePair.Value); + } + + return hashCode.ToHashCode(); + } + private static bool CompareMetadata(Dictionary metadata, Dictionary otherMetadata) { if (ReferenceEquals(metadata, otherMetadata)) @@ -179,25 +200,4 @@ private static bool CompareMetadata(Dictionary metadata, Diction return true; } - - public override int GetHashCode() => - Metadata is null ? HashCode.Combine(Code, Description, Type, NumericType) : ComposeHashCode(); - - private int ComposeHashCode() - { -#pragma warning disable SA1129 // HashCode needs to be instantiated this way - var hashCode = new HashCode(); -#pragma warning restore SA1129 - hashCode.Add(Code); - hashCode.Add(Description); - hashCode.Add(Type); - hashCode.Add(NumericType); - foreach (var keyValuePair in Metadata!) - { - hashCode.Add(keyValuePair.Key); - hashCode.Add(keyValuePair.Value); - } - - return hashCode.ToHashCode(); - } } diff --git a/src/Errors/KnownErrors.cs b/src/Errors/KnownErrors.cs deleted file mode 100644 index cfa7220..0000000 --- a/src/Errors/KnownErrors.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace ErrorOr; - -internal static class KnownErrors -{ - public static Error NoFirstError { get; } = Error.Unexpected( - code: "ErrorOr.NoFirstError", - description: "First error cannot be retrieved from a successful ErrorOr."); - - public static Error NoErrors { get; } = Error.Unexpected( - code: "ErrorOr.NoErrors", - description: "Error list cannot be retrieved from a successful ErrorOr."); - - public static List CachedNoErrorsList { get; } = new (1) { NoErrors }; - - public static List CachedEmptyErrorsList { get; } = new (0); -} diff --git a/src/Results/Types.cs b/src/Results/Types.cs index 1b80330..f2864e2 100644 --- a/src/Results/Types.cs +++ b/src/Results/Types.cs @@ -1,17 +1,20 @@ -namespace ErrorOr; - -public readonly record struct Success; -public readonly record struct Created; -public readonly record struct Deleted; -public readonly record struct Updated; - -public static class Result -{ - public static Success Success => default; - - public static Created Created => default; - - public static Deleted Deleted => default; - - public static Updated Updated => default; -} +namespace ErrorOr; + +public readonly record struct Success; + +public readonly record struct Created; + +public readonly record struct Deleted; + +public readonly record struct Updated; + +public static class Result +{ + public static Success Success => default; + + public static Created Created => default; + + public static Deleted Deleted => default; + + public static Updated Updated => default; +} diff --git a/tests/.editorconfig b/tests/.editorconfig new file mode 100644 index 0000000..6f819ab --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# SA1201: Elements should appear in the correct order +dotnet_diagnostic.SA1201.severity = none diff --git a/tests/ErrorOr/ErrorOr.ElseAsyncTests.cs b/tests/ErrorOr/ErrorOr.ElseAsyncTests.cs index ad4bb07..df7db96 100644 --- a/tests/ErrorOr/ErrorOr.ElseAsyncTests.cs +++ b/tests/ErrorOr/ErrorOr.ElseAsyncTests.cs @@ -13,8 +13,8 @@ public async Task CallingElseAsyncWithValueFunc_WhenIsSuccess_ShouldNotInvokeEls // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .ThenAsync(Convert.ToIntAsync) + .ThenAsync(Convert.ToStringAsync) .ElseAsync(errors => Task.FromResult($"Error count: {errors.Count}")); // Assert @@ -30,8 +30,8 @@ public async Task CallingElseAsyncWithValueFunc_WhenIsError_ShouldInvokeElseFunc // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .ThenAsync(Convert.ToIntAsync) + .ThenAsync(Convert.ToStringAsync) .ElseAsync(errors => Task.FromResult($"Error count: {errors.Count}")); // Assert @@ -47,8 +47,8 @@ public async Task CallingElseAsyncWithValue_WhenIsSuccess_ShouldNotReturnElseVal // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .ThenAsync(Convert.ToIntAsync) + .ThenAsync(Convert.ToStringAsync) .ElseAsync(Task.FromResult("oh no")); // Assert @@ -64,8 +64,8 @@ public async Task CallingElseAsyncWithValue_WhenIsError_ShouldInvokeElseFunc() // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .ThenAsync(Convert.ToIntAsync) + .ThenAsync(Convert.ToStringAsync) .ElseAsync(Task.FromResult("oh no")); // Assert @@ -81,8 +81,8 @@ public async Task CallingElseAsyncWithError_WhenIsError_ShouldReturnElseError() // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .ThenAsync(Convert.ToIntAsync) + .ThenAsync(Convert.ToStringAsync) .ElseAsync(Task.FromResult(Error.Unexpected())); // Assert @@ -98,8 +98,8 @@ public async Task CallingElseAsyncWithError_WhenIsSuccess_ShouldNotReturnElseErr // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .ThenAsync(Convert.ToIntAsync) + .ThenAsync(Convert.ToStringAsync) .ElseAsync(Task.FromResult(Error.Unexpected())); // Assert @@ -115,8 +115,8 @@ public async Task CallingElseAsyncWithErrorFunc_WhenIsError_ShouldReturnElseErro // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .ThenAsync(Convert.ToIntAsync) + .ThenAsync(Convert.ToStringAsync) .ElseAsync(errors => Task.FromResult(Error.Unexpected())); // Assert @@ -132,8 +132,8 @@ public async Task CallingElseAsyncWithErrorFunc_WhenIsSuccess_ShouldNotReturnEls // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .ThenAsync(Convert.ToIntAsync) + .ThenAsync(Convert.ToStringAsync) .ElseAsync(errors => Task.FromResult(Error.Unexpected())); // Assert @@ -149,8 +149,8 @@ public async Task CallingElseAsyncWithErrorFunc_WhenIsError_ShouldReturnElseErro // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .ThenAsync(Convert.ToIntAsync) + .ThenAsync(Convert.ToStringAsync) .ElseAsync(errors => Task.FromResult(new List { Error.Unexpected() })); // Assert @@ -166,16 +166,12 @@ public async Task CallingElseAsyncWithErrorFunc_WhenIsSuccess_ShouldNotReturnEls // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .ThenAsync(Convert.ToIntAsync) + .ThenAsync(Convert.ToStringAsync) .ElseAsync(errors => Task.FromResult(new List { Error.Unexpected() })); // Assert result.IsError.Should().BeFalse(); result.Value.Should().Be(errorOrString.Value); } - - private static Task> ConvertToStringAsync(int num) => Task.FromResult(ErrorOrFactory.From(num.ToString())); - - private static Task> ConvertToIntAsync(string str) => Task.FromResult(ErrorOrFactory.From(int.Parse(str))); } diff --git a/tests/ErrorOr/ErrorOr.ElseTests.cs b/tests/ErrorOr/ErrorOr.ElseTests.cs index c2cf37c..a262a62 100644 --- a/tests/ErrorOr/ErrorOr.ElseTests.cs +++ b/tests/ErrorOr/ErrorOr.ElseTests.cs @@ -13,8 +13,8 @@ public void CallingElseWithValueFunc_WhenIsSuccess_ShouldNotInvokeElseFunc() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) + .Then(Convert.ToInt) + .Then(Convert.ToString) .Else(errors => $"Error count: {errors.Count}"); // Assert @@ -30,8 +30,8 @@ public void CallingElseWithValueFunc_WhenIsError_ShouldInvokeElseFunc() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) + .Then(Convert.ToInt) + .Then(Convert.ToString) .Else(errors => $"Error count: {errors.Count}"); // Assert @@ -47,8 +47,8 @@ public void CallingElseWithValue_WhenIsSuccess_ShouldNotReturnElseValue() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) + .Then(Convert.ToInt) + .Then(Convert.ToString) .Else("oh no"); // Assert @@ -64,8 +64,8 @@ public void CallingElseWithValue_WhenIsError_ShouldInvokeElseFunc() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) + .Then(Convert.ToInt) + .Then(Convert.ToString) .Else("oh no"); // Assert @@ -81,8 +81,8 @@ public void CallingElseWithError_WhenIsError_ShouldReturnElseError() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) + .Then(Convert.ToInt) + .Then(Convert.ToString) .Else(Error.Unexpected()); // Assert @@ -98,8 +98,8 @@ public void CallingElseWithError_WhenIsSuccess_ShouldNotReturnElseError() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) + .Then(Convert.ToInt) + .Then(Convert.ToString) .Else(Error.Unexpected()); // Assert @@ -115,8 +115,8 @@ public void CallingElseWithErrorsFunc_WhenIsError_ShouldReturnElseError() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) + .Then(Convert.ToInt) + .Then(Convert.ToString) .Else(errors => Error.Unexpected()); // Assert @@ -132,8 +132,8 @@ public void CallingElseWithErrorsFunc_WhenIsSuccess_ShouldNotReturnElseError() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) + .Then(Convert.ToInt) + .Then(Convert.ToString) .Else(errors => Error.Unexpected()); // Assert @@ -149,9 +149,9 @@ public void CallingElseWithErrorsFunc_WhenIsError_ShouldReturnElseErrors() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) - .Else(errors => new() { Error.Unexpected() }); + .Then(Convert.ToInt) + .Then(Convert.ToString) + .Else(errors => [Error.Unexpected()]); // Assert result.IsError.Should().BeTrue(); @@ -166,9 +166,9 @@ public void CallingElseWithErrorsFunc_WhenIsSuccess_ShouldNotReturnElseErrors() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) - .Else(errors => new() { Error.Unexpected() }); + .Then(Convert.ToInt) + .Then(Convert.ToString) + .Else(errors => [Error.Unexpected()]); // Assert result.IsError.Should().BeFalse(); @@ -183,8 +183,8 @@ public async Task CallingElseWithValueAfterThenAsync_WhenIsError_ShouldInvokeEls // Act ErrorOr result = await errorOrString - .Then(str => ConvertToInt(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .Then(Convert.ToInt) + .ThenAsync(Convert.ToStringAsync) .Else("oh no"); // Assert @@ -200,8 +200,8 @@ public async Task CallingElseWithValueFuncAfterThenAsync_WhenIsError_ShouldInvok // Act ErrorOr result = await errorOrString - .Then(str => ConvertToInt(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .Then(Convert.ToInt) + .ThenAsync(Convert.ToStringAsync) .Else(errors => $"Error count: {errors.Count}"); // Assert @@ -217,8 +217,8 @@ public async Task CallingElseWithErrorAfterThenAsync_WhenIsError_ShouldReturnEls // Act ErrorOr result = await errorOrString - .Then(str => ConvertToInt(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .Then(Convert.ToInt) + .ThenAsync(Convert.ToStringAsync) .Else(Error.Unexpected()); // Assert @@ -234,8 +234,8 @@ public async Task CallingElseWithErrorFuncAfterThenAsync_WhenIsError_ShouldRetur // Act ErrorOr result = await errorOrString - .Then(str => ConvertToInt(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .Then(Convert.ToInt) + .ThenAsync(Convert.ToStringAsync) .Else(errors => Error.Unexpected()); // Assert @@ -251,18 +251,12 @@ public async Task CallingElseWithErrorFuncAfterThenAsync_WhenIsError_ShouldRetur // Act ErrorOr result = await errorOrString - .Then(str => ConvertToInt(str)) - .ThenAsync(num => ConvertToStringAsync(num)) - .Else(errors => new() { Error.Unexpected() }); + .Then(Convert.ToInt) + .ThenAsync(Convert.ToStringAsync) + .Else(errors => [Error.Unexpected()]); // Assert result.IsError.Should().BeTrue(); result.FirstError.Type.Should().Be(ErrorType.Unexpected); } - - private static ErrorOr ConvertToString(int num) => num.ToString(); - - private static ErrorOr ConvertToInt(string str) => int.Parse(str); - - private static Task> ConvertToStringAsync(int num) => Task.FromResult(ErrorOrFactory.From(num.ToString())); } diff --git a/tests/ErrorOr/ErrorOr.EqualityTests.cs b/tests/ErrorOr/ErrorOr.EqualityTests.cs index 5cb0f31..c7716a0 100644 --- a/tests/ErrorOr/ErrorOr.EqualityTests.cs +++ b/tests/ErrorOr/ErrorOr.EqualityTests.cs @@ -1,181 +1,181 @@ -using ErrorOr; -using FluentAssertions; - -namespace Tests; - -public sealed class ErrorOrEqualityTests -{ - // ReSharper disable once NotAccessedPositionalProperty.Local -- we require this property for these tests - private record Person(string Name); - - public static readonly TheoryData DifferentErrors = - new () - { - { - // Different number of entries - new[] - { - Error.Validation("User.Name", "Name is too short"), - }, - new[] - { - Error.Validation("User.Name", "Name is too short"), - Error.Validation("User.Age", "User is too young"), - } - }, - { - // Different errors - new[] - { - Error.Validation("User.Name", "Name is too short"), - }, - new[] - { - Error.Validation("User.Age", "User is too young"), - } - }, - }; - - public static readonly TheoryData Names = new () { "Amichai", "feO2x" }; - - public static readonly TheoryData DifferentNames = - new () { { "Amichai", "feO2x" }, { "Tyrion", "Cersei" } }; - - [Fact] - public void Equals_WhenTwoInstancesHaveTheSameErrorsCollection_ShouldReturnTrue() - { - var errors = new List - { - Error.Validation("User.Name", "Name is too short"), - Error.Validation("User.Age", "User is too young"), - }; - ErrorOr errorOrPerson1 = errors; - ErrorOr errorOrPerson2 = errors; - - var result = errorOrPerson1.Equals(errorOrPerson2); - - result.Should().BeTrue(); - } - - [Fact] - public void Equals_WhenTwoInstancesHaveDifferentErrorCollectionsWithSameErrors_ShouldReturnTrue() - { - var errors1 = new[] - { - Error.Validation("User.Name", "Name is too short"), - Error.Validation("User.Age", "User is too young"), - }; - var errors2 = new[] - { - Error.Validation("User.Name", "Name is too short"), - Error.Validation("User.Age", "User is too young"), - }; - ErrorOr errorOrPerson1 = errors1; - ErrorOr errorOrPerson2 = errors2; - - var result = errorOrPerson1.Equals(errorOrPerson2); - - result.Should().BeTrue(); - } - - [Theory] - [MemberData(nameof(DifferentErrors))] - public void Equals_WhenTwoInstancesHaveDifferentErrors_ShouldReturnFalse(Error[] errors1, Error[] errors2) - { - ErrorOr errorOrPerson1 = errors1; - ErrorOr errorOrPerson2 = errors2; - - var result = errorOrPerson1.Equals(errorOrPerson2); - - result.Should().BeFalse(); - } - - [Theory] - [MemberData(nameof(Names))] - public void Equals_WhenTwoInstancesHaveEqualValues_ShouldReturnTrue(string name) - { - ErrorOr errorOrPerson1 = new Person(name); - ErrorOr errorOrPerson2 = new Person(name); - - var result = errorOrPerson1.Equals(errorOrPerson2); - - result.Should().BeTrue(); - } - - [Theory] - [MemberData(nameof(DifferentNames))] - public void Equals_WhenTwoInstancesHaveDifferentValues_ShouldReturnFalse(string name1, string name2) - { - ErrorOr errorOrPerson1 = new Person(name1); - ErrorOr errorOrPerson2 = new Person(name2); - - var result = errorOrPerson1.Equals(errorOrPerson2); - - result.Should().BeFalse(); - } - - [Theory] - [MemberData(nameof(Names))] - public void GetHashCode_WhenTwoInstancesHaveEqualValues_ShouldReturnSameHashCode(string name) - { - ErrorOr errorOrPerson1 = new Person(name); - ErrorOr errorOrPerson2 = new Person(name); - - var hashCode1 = errorOrPerson1.GetHashCode(); - var hashCode2 = errorOrPerson2.GetHashCode(); - - hashCode1.Should().Be(hashCode2); - } - - [Theory] - [MemberData(nameof(DifferentNames))] - public void GetHashCode_WhenTwoInstanceHaveDifferentValues_ShouldReturnDifferentHashCodes( - string name1, - string name2) - { - ErrorOr errorOrPerson1 = new Person(name1); - ErrorOr errorOrPerson2 = new Person(name2); - - var hashCode1 = errorOrPerson1.GetHashCode(); - var hashCode2 = errorOrPerson2.GetHashCode(); - - hashCode1.Should().NotBe(hashCode2); - } - - [Fact] - public void GetHashCode_WhenTwoInstancesHaveEqualErrors_ShouldReturnSameHashCode() - { - var errors1 = new[] - { - Error.Validation("User.Name", "Name is too short"), - Error.Validation("User.Age", "User is too young"), - }; - var errors2 = new[] - { - Error.Validation("User.Name", "Name is too short"), - Error.Validation("User.Age", "User is too young"), - }; - ErrorOr errorOrPerson1 = errors1; - ErrorOr errorOrPerson2 = errors2; - - var hashCode1 = errorOrPerson1.GetHashCode(); - var hashCode2 = errorOrPerson2.GetHashCode(); - - hashCode1.Should().Be(hashCode2); - } - - [Theory] - [MemberData(nameof(DifferentErrors))] - public void GetHashCode_WhenTwoInstancesHaveDifferentErrors_ShouldReturnDifferentHashCodes( - Error[] errors1, - Error[] errors2) - { - ErrorOr errorOrPerson1 = errors1; - ErrorOr errorOrPerson2 = errors2; - - var hashCode1 = errorOrPerson1.GetHashCode(); - var hashCode2 = errorOrPerson2.GetHashCode(); - - hashCode1.Should().NotBe(hashCode2); - } -} +using ErrorOr; +using FluentAssertions; + +namespace Tests; + +public sealed class ErrorOrEqualityTests +{ + // ReSharper disable once NotAccessedPositionalProperty.Local -- we require this property for these tests + private record Person(string Name); + + public static readonly TheoryData DifferentErrors = + new() + { + { + // Different number of entries + new[] + { + Error.Validation("User.Name", "Name is too short"), + }, + new[] + { + Error.Validation("User.Name", "Name is too short"), + Error.Validation("User.Age", "User is too young"), + } + }, + { + // Different errors + new[] + { + Error.Validation("User.Name", "Name is too short"), + }, + new[] + { + Error.Validation("User.Age", "User is too young"), + } + }, + }; + + public static readonly TheoryData Names = new() { "Amichai", "feO2x" }; + + public static readonly TheoryData DifferentNames = + new() { { "Amichai", "feO2x" }, { "Tyrion", "Cersei" } }; + + [Fact] + public void Equals_WhenTwoInstancesHaveTheSameErrorsCollection_ShouldReturnTrue() + { + var errors = new List + { + Error.Validation("User.Name", "Name is too short"), + Error.Validation("User.Age", "User is too young"), + }; + ErrorOr errorOrPerson1 = errors; + ErrorOr errorOrPerson2 = errors; + + var result = errorOrPerson1.Equals(errorOrPerson2); + + result.Should().BeTrue(); + } + + [Fact] + public void Equals_WhenTwoInstancesHaveDifferentErrorCollectionsWithSameErrors_ShouldReturnTrue() + { + var errors1 = new[] + { + Error.Validation("User.Name", "Name is too short"), + Error.Validation("User.Age", "User is too young"), + }; + var errors2 = new[] + { + Error.Validation("User.Name", "Name is too short"), + Error.Validation("User.Age", "User is too young"), + }; + ErrorOr errorOrPerson1 = errors1; + ErrorOr errorOrPerson2 = errors2; + + var result = errorOrPerson1.Equals(errorOrPerson2); + + result.Should().BeTrue(); + } + + [Theory] + [MemberData(nameof(DifferentErrors))] + public void Equals_WhenTwoInstancesHaveDifferentErrors_ShouldReturnFalse(Error[] errors1, Error[] errors2) + { + ErrorOr errorOrPerson1 = errors1; + ErrorOr errorOrPerson2 = errors2; + + var result = errorOrPerson1.Equals(errorOrPerson2); + + result.Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(Names))] + public void Equals_WhenTwoInstancesHaveEqualValues_ShouldReturnTrue(string name) + { + ErrorOr errorOrPerson1 = new Person(name); + ErrorOr errorOrPerson2 = new Person(name); + + var result = errorOrPerson1.Equals(errorOrPerson2); + + result.Should().BeTrue(); + } + + [Theory] + [MemberData(nameof(DifferentNames))] + public void Equals_WhenTwoInstancesHaveDifferentValues_ShouldReturnFalse(string name1, string name2) + { + ErrorOr errorOrPerson1 = new Person(name1); + ErrorOr errorOrPerson2 = new Person(name2); + + var result = errorOrPerson1.Equals(errorOrPerson2); + + result.Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(Names))] + public void GetHashCode_WhenTwoInstancesHaveEqualValues_ShouldReturnSameHashCode(string name) + { + ErrorOr errorOrPerson1 = new Person(name); + ErrorOr errorOrPerson2 = new Person(name); + + var hashCode1 = errorOrPerson1.GetHashCode(); + var hashCode2 = errorOrPerson2.GetHashCode(); + + hashCode1.Should().Be(hashCode2); + } + + [Theory] + [MemberData(nameof(DifferentNames))] + public void GetHashCode_WhenTwoInstanceHaveDifferentValues_ShouldReturnDifferentHashCodes( + string name1, + string name2) + { + ErrorOr errorOrPerson1 = new Person(name1); + ErrorOr errorOrPerson2 = new Person(name2); + + var hashCode1 = errorOrPerson1.GetHashCode(); + var hashCode2 = errorOrPerson2.GetHashCode(); + + hashCode1.Should().NotBe(hashCode2); + } + + [Fact] + public void GetHashCode_WhenTwoInstancesHaveEqualErrors_ShouldReturnSameHashCode() + { + var errors1 = new[] + { + Error.Validation("User.Name", "Name is too short"), + Error.Validation("User.Age", "User is too young"), + }; + var errors2 = new[] + { + Error.Validation("User.Name", "Name is too short"), + Error.Validation("User.Age", "User is too young"), + }; + ErrorOr errorOrPerson1 = errors1; + ErrorOr errorOrPerson2 = errors2; + + var hashCode1 = errorOrPerson1.GetHashCode(); + var hashCode2 = errorOrPerson2.GetHashCode(); + + hashCode1.Should().Be(hashCode2); + } + + [Theory] + [MemberData(nameof(DifferentErrors))] + public void GetHashCode_WhenTwoInstancesHaveDifferentErrors_ShouldReturnDifferentHashCodes( + Error[] errors1, + Error[] errors2) + { + ErrorOr errorOrPerson1 = errors1; + ErrorOr errorOrPerson2 = errors2; + + var hashCode1 = errorOrPerson1.GetHashCode(); + var hashCode2 = errorOrPerson2.GetHashCode(); + + hashCode1.Should().NotBe(hashCode2); + } +} diff --git a/tests/ErrorOr/ErrorOr.InstantiationTests.cs b/tests/ErrorOr/ErrorOr.InstantiationTests.cs index e33fd1a..039371d 100644 --- a/tests/ErrorOr/ErrorOr.InstantiationTests.cs +++ b/tests/ErrorOr/ErrorOr.InstantiationTests.cs @@ -11,7 +11,7 @@ private record Person(string Name); public void CreateFromFactory_WhenAccessingValue_ShouldReturnValue() { // Arrange - IEnumerable value = new[] { "value" }; + IEnumerable value = ["value"]; // Act ErrorOr> errorOrPerson = ErrorOrFactory.From(value); @@ -22,24 +22,24 @@ public void CreateFromFactory_WhenAccessingValue_ShouldReturnValue() } [Fact] - public void CreateFromFactory_WhenAccessingErrors_ShouldReturnUnexpectedError() + public void CreateFromFactory_WhenAccessingErrors_ShouldThrow() { // Arrange - IEnumerable value = new[] { "value" }; + IEnumerable value = ["value"]; ErrorOr> errorOrPerson = ErrorOrFactory.From(value); // Act - List errors = errorOrPerson.Errors; + Func> errors = () => errorOrPerson.Errors; // Assert - errors.Should().ContainSingle().Which.Type.Should().Be(ErrorType.Unexpected); + errors.Should().ThrowExactly(); } [Fact] public void CreateFromFactory_WhenAccessingErrorsOrEmptyList_ShouldReturnEmptyList() { // Arrange - IEnumerable value = new[] { "value" }; + IEnumerable value = ["value"]; ErrorOr> errorOrPerson = ErrorOrFactory.From(value); // Act @@ -50,24 +50,24 @@ public void CreateFromFactory_WhenAccessingErrorsOrEmptyList_ShouldReturnEmptyLi } [Fact] - public void CreateFromFactory_WhenAccessingFirstError_ShouldReturnUnexpectedError() + public void CreateFromFactory_WhenAccessingFirstError_ShouldThrow() { // Arrange - IEnumerable value = new[] { "value" }; + IEnumerable value = ["value"]; ErrorOr> errorOrPerson = ErrorOrFactory.From(value); // Act - Error firstError = errorOrPerson.FirstError; + Func action = () => errorOrPerson.FirstError; // Assert - firstError.Type.Should().Be(ErrorType.Unexpected); + action.Should().ThrowExactly(); } [Fact] public void CreateFromValue_WhenAccessingValue_ShouldReturnValue() { // Arrange - IEnumerable value = new[] { "value" }; + IEnumerable value = ["value"]; // Act ErrorOr> errorOrPerson = ErrorOrFactory.From(value); @@ -78,24 +78,24 @@ public void CreateFromValue_WhenAccessingValue_ShouldReturnValue() } [Fact] - public void CreateFromValue_WhenAccessingErrors_ShouldReturnUnexpectedError() + public void CreateFromValue_WhenAccessingErrors_ShouldThrow() { // Arrange - IEnumerable value = new[] { "value" }; + IEnumerable value = ["value"]; ErrorOr> errorOrPerson = ErrorOrFactory.From(value); // Act - List errors = errorOrPerson.Errors; + Func> action = () => errorOrPerson.Errors; // Assert - errors.Should().ContainSingle().Which.Type.Should().Be(ErrorType.Unexpected); + action.Should().ThrowExactly(); } [Fact] public void CreateFromValue_WhenAccessingErrorsOrEmptyList_ShouldReturnEmptyList() { // Arrange - IEnumerable value = new[] { "value" }; + IEnumerable value = ["value"]; ErrorOr> errorOrPerson = ErrorOrFactory.From(value); // Act @@ -106,17 +106,17 @@ public void CreateFromValue_WhenAccessingErrorsOrEmptyList_ShouldReturnEmptyList } [Fact] - public void CreateFromValue_WhenAccessingFirstError_ShouldReturnUnexpectedError() + public void CreateFromValue_WhenAccessingFirstError_ShouldThrow() { // Arrange - IEnumerable value = new[] { "value" }; + IEnumerable value = ["value"]; ErrorOr> errorOrPerson = ErrorOrFactory.From(value); // Act - Error firstError = errorOrPerson.FirstError; + Func action = () => errorOrPerson.FirstError; // Assert - firstError.Type.Should().Be(ErrorType.Unexpected); + action.Should().ThrowExactly(); } [Fact] @@ -172,27 +172,27 @@ public void ImplicitCastResult_WhenAccessingResult_ShouldReturnValue() } [Fact] - public void ImplicitCastResult_WhenAccessingErrors_ShouldReturnUnexpectedError() + public void ImplicitCastResult_WhenAccessingErrors_ShouldThrow() { ErrorOr errorOrPerson = new Person("Amichai"); // Act - List errors = errorOrPerson.Errors; + Func> action = () => errorOrPerson.Errors; // Assert - errors.Should().ContainSingle().Which.Type.Should().Be(ErrorType.Unexpected); + action.Should().ThrowExactly(); } [Fact] - public void ImplicitCastResult_WhenAccessingFirstError_ShouldReturnUnexpectedError() + public void ImplicitCastResult_WhenAccessingFirstError_ShouldThrow() { ErrorOr errorOrPerson = new Person("Amichai"); // Act - Error firstError = errorOrPerson.FirstError; + Func action = () => errorOrPerson.FirstError; // Assert - firstError.Type.Should().Be(ErrorType.Unexpected); + action.Should().ThrowExactly(); } [Fact] @@ -295,11 +295,11 @@ public void ImplicitCastErrorList_WhenAccessingErrors_ShouldReturnErrorList() public void ImplicitCastErrorArray_WhenAccessingErrors_ShouldReturnErrorArray() { // Arrange - Error[] errors = new[] - { + Error[] errors = + [ Error.Validation("User.Name", "Name is too short"), Error.Validation("User.Age", "User is too young"), - }; + ]; // Act ErrorOr errorOrPerson = errors; @@ -331,11 +331,11 @@ public void ImplicitCastErrorList_WhenAccessingFirstError_ShouldReturnFirstError public void ImplicitCastErrorArray_WhenAccessingFirstError_ShouldReturnFirstError() { // Arrange - Error[] errors = new[] - { + Error[] errors = + [ Error.Validation("User.Name", "Name is too short"), Error.Validation("User.Age", "User is too young"), - }; + ]; // Act ErrorOr errorOrPerson = errors; @@ -349,12 +349,10 @@ public void ImplicitCastErrorArray_WhenAccessingFirstError_ShouldReturnFirstErro public void CreateErrorOr_WhenUsingEmptyConstructor_ShouldThrow() { // Act -#pragma warning disable SA1129 // Do not use default value type constructor - Func> errorOrInt = () => new ErrorOr(); -#pragma warning restore SA1129 // Do not use default value type constructor + Func> action = () => new ErrorOr(); // Assert - errorOrInt.Should().ThrowExactly(); + action.Should().ThrowExactly(); } [Fact] diff --git a/tests/ErrorOr/ErrorOr.ThenAsyncTests.cs b/tests/ErrorOr/ErrorOr.ThenAsyncTests.cs index f3b0305..5dd7c48 100644 --- a/tests/ErrorOr/ErrorOr.ThenAsyncTests.cs +++ b/tests/ErrorOr/ErrorOr.ThenAsyncTests.cs @@ -13,10 +13,10 @@ public async Task CallingThenAsync_WhenIsSuccess_ShouldInvokeNextThen() // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) + .ThenAsync(Convert.ToIntAsync) .ThenAsync(num => Task.FromResult(num * 2)) .ThenDoAsync(num => Task.Run(() => { _ = 5; })) - .ThenAsync(num => ConvertToStringAsync(num)); + .ThenAsync(Convert.ToStringAsync); // Assert result.IsError.Should().BeFalse(); @@ -31,17 +31,13 @@ public async Task CallingThenAsync_WhenIsError_ShouldReturnErrors() // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) + .ThenAsync(Convert.ToIntAsync) .ThenAsync(num => Task.FromResult(num * 2)) .ThenDoAsync(num => Task.Run(() => { _ = 5; })) - .ThenAsync(num => ConvertToStringAsync(num)); + .ThenAsync(Convert.ToStringAsync); // Assert result.IsError.Should().BeTrue(); result.FirstError.Should().BeEquivalentTo(errorOrString.FirstError); } - - private static Task> ConvertToStringAsync(int num) => Task.FromResult(ErrorOrFactory.From(num.ToString())); - - private static Task> ConvertToIntAsync(string str) => Task.FromResult(ErrorOrFactory.From(int.Parse(str))); } diff --git a/tests/ErrorOr/ErrorOr.ThenTests.cs b/tests/ErrorOr/ErrorOr.ThenTests.cs index 0e91bb9..2555896 100644 --- a/tests/ErrorOr/ErrorOr.ThenTests.cs +++ b/tests/ErrorOr/ErrorOr.ThenTests.cs @@ -13,9 +13,9 @@ public void CallingThen_WhenIsSuccess_ShouldInvokeGivenFunc() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) + .Then(Convert.ToInt) .Then(num => num * 2) - .Then(num => ConvertToString(num)); + .Then(Convert.ToString); // Assert result.IsError.Should().BeFalse(); @@ -31,7 +31,7 @@ public void CallingThen_WhenIsSuccess_ShouldInvokeGivenAction() // Act ErrorOr result = errorOrString .ThenDo(str => { _ = 5; }) - .Then(str => ConvertToInt(str)) + .Then(Convert.ToInt) .ThenDo(str => { _ = 5; }); // Assert @@ -47,10 +47,10 @@ public void CallingThen_WhenIsError_ShouldReturnErrors() // Act ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) + .Then(Convert.ToInt) .Then(num => num * 2) .ThenDo(str => { _ = 5; }) - .Then(num => ConvertToString(num)); + .Then(Convert.ToString); // Assert result.IsError.Should().BeTrue(); @@ -65,11 +65,11 @@ public async Task CallingThenAfterThenAsync_WhenIsSuccess_ShouldInvokeGivenFunc( // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) + .ThenAsync(Convert.ToIntAsync) .Then(num => num * 2) - .ThenAsync(num => ConvertToStringAsync(num)) - .Then(str => ConvertToInt(str)) - .ThenAsync(num => ConvertToStringAsync(num)) + .ThenAsync(Convert.ToStringAsync) + .Then(Convert.ToInt) + .ThenAsync(Convert.ToStringAsync) .ThenDo(num => { _ = 5; }); // Assert @@ -85,19 +85,11 @@ public async Task CallingThenAfterThenAsync_WhenIsError_ShouldReturnErrors() // Act ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .Then(num => ConvertToString(num)); + .ThenAsync(Convert.ToIntAsync) + .Then(Convert.ToString); // Assert result.IsError.Should().BeTrue(); result.FirstError.Should().BeEquivalentTo(errorOrString.FirstError); } - - private static ErrorOr ConvertToString(int num) => num.ToString(); - - private static ErrorOr ConvertToInt(string str) => int.Parse(str); - - private static Task> ConvertToIntAsync(string str) => Task.FromResult(ErrorOrFactory.From(int.Parse(str))); - - private static Task> ConvertToStringAsync(int num) => Task.FromResult(ErrorOrFactory.From(num.ToString())); } diff --git a/tests/ErrorOr/ErrorOr.ToErrorOrTests.cs b/tests/ErrorOr/ErrorOr.ToErrorOrTests.cs index e07fbde..2890bf0 100644 --- a/tests/ErrorOr/ErrorOr.ToErrorOrTests.cs +++ b/tests/ErrorOr/ErrorOr.ToErrorOrTests.cs @@ -37,7 +37,7 @@ public void ErrorToErrorOr_WhenAccessingFirstError_ShouldReturnSameError() public void ListOfErrorsToErrorOr_WhenAccessingErrors_ShouldReturnSameErrors() { // Arrange - List errors = new List { Error.Unauthorized(), Error.Validation() }; + List errors = [Error.Unauthorized(), Error.Validation()]; // Act ErrorOr result = errors.ToErrorOr(); diff --git a/tests/ErrorOr/TestUtils.cs b/tests/ErrorOr/TestUtils.cs new file mode 100644 index 0000000..77e60c1 --- /dev/null +++ b/tests/ErrorOr/TestUtils.cs @@ -0,0 +1,14 @@ +using ErrorOr; + +namespace Tests; + +public static class Convert +{ + public static ErrorOr ToString(int num) => num.ToString(); + + public static ErrorOr ToInt(string str) => int.Parse(str); + + public static Task> ToIntAsync(string str) => Task.FromResult(ErrorOrFactory.From(int.Parse(str))); + + public static Task> ToStringAsync(int num) => Task.FromResult(ErrorOrFactory.From(num.ToString())); +} diff --git a/tests/Tests.csproj b/tests/Tests.csproj index f702f25..8170f9a 100644 --- a/tests/Tests.csproj +++ b/tests/Tests.csproj @@ -9,14 +9,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Usings.cs b/tests/Usings.cs index 8c927eb..e106559 100644 --- a/tests/Usings.cs +++ b/tests/Usings.cs @@ -1 +1 @@ -global using Xunit; \ No newline at end of file +global using Xunit; From a76df266594a2de42a462ff25924c795866f7e33 Mon Sep 17 00:00:00 2001 From: Amichai Mantinband Date: Thu, 9 May 2024 16:25:55 +0300 Subject: [PATCH 03/12] Resolve code review comments --- .editorconfig | 4 ++-- src/ErrorOr/ErrorOr.ImplicitConverters.cs | 2 +- src/ErrorOr/ErrorOr.cs | 2 +- src/Errors/Error.cs | 2 ++ src/Results/{Types.cs => Results.cs} | 5 ++--- tests/.editorconfig | 3 +++ 6 files changed, 11 insertions(+), 7 deletions(-) rename src/Results/{Types.cs => Results.cs} (70%) diff --git a/.editorconfig b/.editorconfig index 4ba6a4a..04559f5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -68,5 +68,5 @@ dotnet_diagnostic.IDE0130.severity = none # SA1642: Constructor summary documentation should begin with standard text dotnet_diagnostic.SA1642.severity = none -# SA1649: File name should match first type name -dotnet_diagnostic.SA1649.severity = none +# SA1516: Elements should be separated by blank line +dotnet_diagnostic.SA1516.severity = none diff --git a/src/ErrorOr/ErrorOr.ImplicitConverters.cs b/src/ErrorOr/ErrorOr.ImplicitConverters.cs index f617707..c93639a 100644 --- a/src/ErrorOr/ErrorOr.ImplicitConverters.cs +++ b/src/ErrorOr/ErrorOr.ImplicitConverters.cs @@ -38,7 +38,7 @@ namespace ErrorOr; { if (errors.Length == 0) { - throw new InvalidOperationException("Cannot create an ErrorOr from an empty list of errors. Provide at least one error."); + throw new InvalidOperationException("Cannot create an ErrorOr from an empty array of errors. Provide at least one error."); } return new ErrorOr(errors.ToList()); diff --git a/src/ErrorOr/ErrorOr.cs b/src/ErrorOr/ErrorOr.cs index 99b517d..3569d8e 100644 --- a/src/ErrorOr/ErrorOr.cs +++ b/src/ErrorOr/ErrorOr.cs @@ -21,7 +21,7 @@ public ErrorOr() private ErrorOr(Error error) { - _errors = new List { error }; + _errors = [error]; IsError = true; } diff --git a/src/Errors/Error.cs b/src/Errors/Error.cs index a925326..c42b9f6 100644 --- a/src/Errors/Error.cs +++ b/src/Errors/Error.cs @@ -164,10 +164,12 @@ private int ComposeHashCode() #pragma warning disable SA1129 // HashCode needs to be instantiated this way var hashCode = new HashCode(); #pragma warning restore SA1129 + hashCode.Add(Code); hashCode.Add(Description); hashCode.Add(Type); hashCode.Add(NumericType); + foreach (var keyValuePair in Metadata!) { hashCode.Add(keyValuePair.Key); diff --git a/src/Results/Types.cs b/src/Results/Results.cs similarity index 70% rename from src/Results/Types.cs rename to src/Results/Results.cs index f2864e2..c0b7f85 100644 --- a/src/Results/Types.cs +++ b/src/Results/Results.cs @@ -1,11 +1,10 @@ namespace ErrorOr; +#pragma warning disable SA1649 // File name should match first type name public readonly record struct Success; - +#pragma warning restore SA1649 // File name should match first type name public readonly record struct Created; - public readonly record struct Deleted; - public readonly record struct Updated; public static class Result diff --git a/tests/.editorconfig b/tests/.editorconfig index 6f819ab..4853611 100644 --- a/tests/.editorconfig +++ b/tests/.editorconfig @@ -2,3 +2,6 @@ # SA1201: Elements should appear in the correct order dotnet_diagnostic.SA1201.severity = none + +# SA1649: File name should match first type name +dotnet_diagnostic.SA1649.severity = none From d534d0e02a04d7e80d5a216e35576f3727a6e7de Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Thu, 9 May 2024 19:10:00 +0200 Subject: [PATCH 04/12] perf: IsError is now a computed property This change saves us 8 bits in the ErrorOr struct which leads to less execution time when an instance is copied by value. As the struct is now properly encapsulated from a Design by Contract point of view, I simply choose _errors being null as the indicator for errors not being present. Signed-off-by: Kenny Pflug --- src/ErrorOr/ErrorOr.cs | 171 ++++++++++++++++++++--------------------- 1 file changed, 84 insertions(+), 87 deletions(-) diff --git a/src/ErrorOr/ErrorOr.cs b/src/ErrorOr/ErrorOr.cs index 3569d8e..727982b 100644 --- a/src/ErrorOr/ErrorOr.cs +++ b/src/ErrorOr/ErrorOr.cs @@ -1,87 +1,84 @@ -using System.Diagnostics.CodeAnalysis; - -namespace ErrorOr; - -/// -/// A discriminated union of errors or a value. -/// -/// The type of the underlying . -public readonly partial record struct ErrorOr : IErrorOr -{ - private readonly TValue? _value = default; - private readonly List? _errors = null; - - /// - /// Prevents a default struct from being created. - /// - public ErrorOr() - { - throw new InvalidOperationException("Default construction of ErrorOr is invalid. Please use provided factory methods to instantiate."); - } - - private ErrorOr(Error error) - { - _errors = [error]; - IsError = true; - } - - private ErrorOr(List errors) - { - _errors = errors; - IsError = true; - } - - private ErrorOr(TValue value) - { - _value = value; - IsError = false; - } - - /// - /// Gets a value indicating whether the state is error. - /// - [MemberNotNullWhen(true, nameof(_errors))] - [MemberNotNullWhen(true, nameof(Errors))] - [MemberNotNullWhen(false, nameof(Value))] - [MemberNotNullWhen(false, nameof(_value))] - public bool IsError { get; } - - /// - /// Gets the list of errors. If the state is not error, the list will contain a single error representing the state. - /// - public List Errors => IsError ? _errors! : throw new InvalidOperationException("The Errors property cannot be accessed when no errors have been recorded. Check IsError before accessing Errors."); - - /// - /// Gets the list of errors. If the state is not error, the list will be empty. - /// - public List ErrorsOrEmptyList => IsError ? _errors! : []; - - /// - /// Gets the value. - /// - public TValue Value => _value!; - - /// - /// Gets the first error. - /// - public Error FirstError - { - get - { - if (!IsError) - { - throw new InvalidOperationException("The FirstError property cannot be accessed when no errors have been recorded. Check IsError before accessing FirstError."); - } - - return _errors![0]; - } - } - - /// - /// Creates an from a list of errors. - /// - public static ErrorOr From(List errors) - { - return errors; - } -} +using System.Diagnostics.CodeAnalysis; + +namespace ErrorOr; + +/// +/// A discriminated union of errors or a value. +/// +/// The type of the underlying . +public readonly partial record struct ErrorOr : IErrorOr +{ + private readonly TValue? _value = default; + private readonly List? _errors = null; + + /// + /// Prevents a default struct from being created. + /// + public ErrorOr() + { + throw new InvalidOperationException("Default construction of ErrorOr is invalid. Please use provided factory methods to instantiate."); + } + + private ErrorOr(Error error) + { + _errors = [error]; + } + + private ErrorOr(List errors) + { + _errors = errors; + } + + private ErrorOr(TValue value) + { + _value = value; + } + + /// + /// Gets a value indicating whether the state is error. + /// + [MemberNotNullWhen(true, nameof(_errors))] + [MemberNotNullWhen(true, nameof(Errors))] + [MemberNotNullWhen(false, nameof(Value))] + [MemberNotNullWhen(false, nameof(_value))] + public bool IsError => _errors is not null; + + /// + /// Gets the list of errors. If the state is not error, the list will contain a single error representing the state. + /// + public List Errors => IsError ? _errors! : throw new InvalidOperationException("The Errors property cannot be accessed when no errors have been recorded. Check IsError before accessing Errors."); + + /// + /// Gets the list of errors. If the state is not error, the list will be empty. + /// + public List ErrorsOrEmptyList => IsError ? _errors! : []; + + /// + /// Gets the value. + /// + public TValue Value => _value!; + + /// + /// Gets the first error. + /// + public Error FirstError + { + get + { + if (!IsError) + { + throw new InvalidOperationException("The FirstError property cannot be accessed when no errors have been recorded. Check IsError before accessing FirstError."); + } + + return _errors![0]; + } + } + + /// + /// Creates an from a list of errors. + /// + public static ErrorOr From(List errors) + { + return errors; + } +} From 015ddd77002b1f55c460ef96a5afb5c13db1c775 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Thu, 9 May 2024 19:13:13 +0200 Subject: [PATCH 05/12] perf: reuse same empty error list in ErrorsOrEmptyList Instead of instantiating a new list every time ErrorsOrEmptyList is called with no errors being present, I reused a cached empty list. I also removed the unnecessary null-forgiving operators (the compiler uses the MemberNotNullWhenAttribute on IsError to verify that we checked for null beforehand). Signed-off-by: Kenny Pflug --- src/ErrorOr/EmptyErrors.cs | 6 ++++++ src/ErrorOr/ErrorOr.cs | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/ErrorOr/EmptyErrors.cs diff --git a/src/ErrorOr/EmptyErrors.cs b/src/ErrorOr/EmptyErrors.cs new file mode 100644 index 0000000..7a79e64 --- /dev/null +++ b/src/ErrorOr/EmptyErrors.cs @@ -0,0 +1,6 @@ +namespace ErrorOr; + +internal static class EmptyErrors +{ + public static List Instance { get; } = []; +} diff --git a/src/ErrorOr/ErrorOr.cs b/src/ErrorOr/ErrorOr.cs index 727982b..56808ca 100644 --- a/src/ErrorOr/ErrorOr.cs +++ b/src/ErrorOr/ErrorOr.cs @@ -46,12 +46,12 @@ private ErrorOr(TValue value) /// /// Gets the list of errors. If the state is not error, the list will contain a single error representing the state. /// - public List Errors => IsError ? _errors! : throw new InvalidOperationException("The Errors property cannot be accessed when no errors have been recorded. Check IsError before accessing Errors."); + public List Errors => IsError ? _errors : throw new InvalidOperationException("The Errors property cannot be accessed when no errors have been recorded. Check IsError before accessing Errors."); /// /// Gets the list of errors. If the state is not error, the list will be empty. /// - public List ErrorsOrEmptyList => IsError ? _errors! : []; + public List ErrorsOrEmptyList => IsError ? _errors : EmptyErrors.Instance; /// /// Gets the value. @@ -70,7 +70,7 @@ public Error FirstError throw new InvalidOperationException("The FirstError property cannot be accessed when no errors have been recorded. Check IsError before accessing FirstError."); } - return _errors![0]; + return _errors[0]; } } From 1f43ff314e0358efa0c55d0745380b5f8cac0d30 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Thu, 9 May 2024 19:22:19 +0200 Subject: [PATCH 06/12] feat: Value will now throw when errors are present The Value property is now properly encapsulated. When errors are present, the property getter will throw, similar to other properties like Errors or FirstError. Signed-off-by: Kenny Pflug --- src/ErrorOr/ErrorOr.cs | 13 ++++++++++++- tests/ErrorOr/ErrorOr.InstantiationTests.cs | 14 ++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/ErrorOr/ErrorOr.cs b/src/ErrorOr/ErrorOr.cs index 56808ca..4a06061 100644 --- a/src/ErrorOr/ErrorOr.cs +++ b/src/ErrorOr/ErrorOr.cs @@ -56,7 +56,18 @@ private ErrorOr(TValue value) /// /// Gets the value. /// - public TValue Value => _value!; + public TValue Value + { + get + { + if (IsError) + { + throw new InvalidOperationException("The Value property cannot be accessed when errors have been recorded. Check IsError before accessing Value."); + } + + return _value; + } + } /// /// Gets the first error. diff --git a/tests/ErrorOr/ErrorOr.InstantiationTests.cs b/tests/ErrorOr/ErrorOr.InstantiationTests.cs index 039371d..bfcef5f 100644 --- a/tests/ErrorOr/ErrorOr.InstantiationTests.cs +++ b/tests/ErrorOr/ErrorOr.InstantiationTests.cs @@ -144,17 +144,18 @@ public void CreateFromErrorList_WhenAccessingErrorsOrEmptyList_ShouldReturnError } [Fact] - public void CreateFromErrorList_WhenAccessingValue_ShouldReturnDefault() + public void CreateFromErrorList_WhenAccessingValue_ShouldThrowInvalidOperationException() { // Arrange List errors = new() { Error.Validation("User.Name", "Name is too short") }; ErrorOr errorOrPerson = ErrorOr.From(errors); // Act - Person value = errorOrPerson.Value; + var act = () => errorOrPerson.Value; // Assert - value.Should().Be(default); + act.Should().Throw() + .And.Message.Should().Be("The Value property cannot be accessed when errors have been recorded. Check IsError before accessing Value."); } [Fact] @@ -247,16 +248,17 @@ public void ImplicitCastSingleError_WhenAccessingErrors_ShouldReturnErrorList() } [Fact] - public void ImplicitCastError_WhenAccessingValue_ShouldReturnDefault() + public void ImplicitCastError_WhenAccessingValue_ShouldThrowInvalidOperationException() { // Arrange ErrorOr errorOrPerson = Error.Validation("User.Name", "Name is too short"); // Act - Person value = errorOrPerson.Value; + var act = () => errorOrPerson.Value; // Assert - value.Should().Be(default); + act.Should().Throw() + .And.Message.Should().Be("The Value property cannot be accessed when errors have been recorded. Check IsError before accessing Value."); } [Fact] From 73be4f798e08b56f57862cb58e915e0e036a8c3a Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Thu, 9 May 2024 19:32:41 +0200 Subject: [PATCH 07/12] feat: add additional null checks to implicit conversions When clients have turned off, the C# compiler won't hint that null shouldn't be passed as the errors array/list. By explicitly checking for null, we eliminate these error cases. Signed-off-by: Kenny Pflug --- src/ErrorOr/ErrorOr.ImplicitConverters.cs | 102 +++++++++++--------- tests/ErrorOr/ErrorOr.InstantiationTests.cs | 18 ++++ 2 files changed, 74 insertions(+), 46 deletions(-) diff --git a/src/ErrorOr/ErrorOr.ImplicitConverters.cs b/src/ErrorOr/ErrorOr.ImplicitConverters.cs index c93639a..0345f7f 100644 --- a/src/ErrorOr/ErrorOr.ImplicitConverters.cs +++ b/src/ErrorOr/ErrorOr.ImplicitConverters.cs @@ -1,46 +1,56 @@ -namespace ErrorOr; - -public readonly partial record struct ErrorOr : IErrorOr -{ - /// - /// Creates an from a value. - /// - public static implicit operator ErrorOr(TValue value) - { - return new ErrorOr(value); - } - - /// - /// Creates an from an error. - /// - public static implicit operator ErrorOr(Error error) - { - return new ErrorOr(error); - } - - /// - /// Creates an from a list of errors. - /// - public static implicit operator ErrorOr(List errors) - { - if (errors.Count == 0) - { - throw new InvalidOperationException("Cannot create an ErrorOr from an empty list of errors. Provide at least one error."); - } - - return new ErrorOr(errors); - } - - /// - /// Creates an from a list of errors. - /// - public static implicit operator ErrorOr(Error[] errors) - { - if (errors.Length == 0) - { - throw new InvalidOperationException("Cannot create an ErrorOr from an empty array of errors. Provide at least one error."); - } - - return new ErrorOr(errors.ToList()); - } -} +namespace ErrorOr; + +public readonly partial record struct ErrorOr : IErrorOr +{ + /// + /// Creates an from a value. + /// + public static implicit operator ErrorOr(TValue value) + { + return new ErrorOr(value); + } + + /// + /// Creates an from an error. + /// + public static implicit operator ErrorOr(Error error) + { + return new ErrorOr(error); + } + + /// + /// Creates an from a list of errors. + /// + public static implicit operator ErrorOr(List errors) + { + if (errors is null) + { + throw new ArgumentNullException(nameof(errors)); + } + + if (errors.Count == 0) + { + throw new InvalidOperationException("Cannot create an ErrorOr from an empty list of errors. Provide at least one error."); + } + + return new ErrorOr(errors); + } + + /// + /// Creates an from a list of errors. + /// + public static implicit operator ErrorOr(Error[] errors) + { + if (errors is null) + { + throw new ArgumentNullException(nameof(errors)); + } + + if (errors.Length == 0) + { + throw new InvalidOperationException("Cannot create an ErrorOr from an empty array of errors. Provide at least one error."); + } + + return new ErrorOr(errors.ToList()); + } +} diff --git a/tests/ErrorOr/ErrorOr.InstantiationTests.cs b/tests/ErrorOr/ErrorOr.InstantiationTests.cs index bfcef5f..54ab4ef 100644 --- a/tests/ErrorOr/ErrorOr.InstantiationTests.cs +++ b/tests/ErrorOr/ErrorOr.InstantiationTests.cs @@ -376,4 +376,22 @@ public void CreateErrorOr_WhenEmptyErrorsArray_ShouldThrow() // Assert errorOrInt.Should().ThrowExactly(); } + + [Fact] + public void CreateErrorOr_WhenNullIsPassedAsErrorsList_ShouldThrowArgumentNullException() + { + Func> act = () => default(List)!; + + act.Should().ThrowExactly() + .And.ParamName.Should().Be("errors"); + } + + [Fact] + public void CreateErrorOr_WhenNullIsPassedAsErrorsArray_ShouldThrowArgumentNullException() + { + Func> act = () => default(Error[])!; + + act.Should().ThrowExactly() + .And.ParamName.Should().Be("errors"); + } } From 6fffb3ee13c453497639654a30b3c216c52300ae Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Thu, 9 May 2024 19:34:29 +0200 Subject: [PATCH 08/12] docs: add XML docs for exceptions Signed-off-by: Kenny Pflug --- src/ErrorOr/ErrorOr.ImplicitConverters.cs | 4 ++++ src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ErrorOr/ErrorOr.ImplicitConverters.cs b/src/ErrorOr/ErrorOr.ImplicitConverters.cs index 0345f7f..2dc51e3 100644 --- a/src/ErrorOr/ErrorOr.ImplicitConverters.cs +++ b/src/ErrorOr/ErrorOr.ImplicitConverters.cs @@ -21,6 +21,8 @@ namespace ErrorOr; /// /// Creates an from a list of errors. /// + /// Thrown when is null. + /// Thrown when is an empty list. public static implicit operator ErrorOr(List errors) { if (errors is null) @@ -39,6 +41,8 @@ namespace ErrorOr; /// /// Creates an from a list of errors. /// + /// Thrown when is null. + /// Thrown when is an empty array. public static implicit operator ErrorOr(Error[] errors) { if (errors is null) diff --git a/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs b/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs index c7297a9..416995d 100644 --- a/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs +++ b/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs @@ -19,10 +19,12 @@ public static ErrorOr ToErrorOr(this Error error) } /// - /// Creates an instance with the given . + /// Creates an instance with the given . /// - public static ErrorOr ToErrorOr(this List error) + /// Thrown when is null. + /// Thrown when is an empty list. + public static ErrorOr ToErrorOr(this List errors) { - return error; + return errors; } } From 5298abc4558564d5b3637ffd90db222a9b13d056 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Thu, 9 May 2024 19:39:06 +0200 Subject: [PATCH 09/12] feat: add ToErrorOr extension method for error arrays Signed-off-by: Kenny Pflug --- src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs | 10 ++++++++++ tests/ErrorOr/ErrorOr.ToErrorOrTests.cs | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs b/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs index 416995d..15e4fee 100644 --- a/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs +++ b/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs @@ -27,4 +27,14 @@ public static ErrorOr ToErrorOr(this List errors) { return errors; } + + /// + /// Creates an instance with the given . + /// + /// Thrown when is null. + /// Thrown when is an empty array. + public static ErrorOr ToErrorOr(this Error[] errors) + { + return errors; + } } diff --git a/tests/ErrorOr/ErrorOr.ToErrorOrTests.cs b/tests/ErrorOr/ErrorOr.ToErrorOrTests.cs index 2890bf0..71202df 100644 --- a/tests/ErrorOr/ErrorOr.ToErrorOrTests.cs +++ b/tests/ErrorOr/ErrorOr.ToErrorOrTests.cs @@ -46,4 +46,15 @@ public void ListOfErrorsToErrorOr_WhenAccessingErrors_ShouldReturnSameErrors() result.IsError.Should().BeTrue(); result.Errors.Should().BeEquivalentTo(errors); } + + [Fact] + public void ArrayOfErrorsToErrorOr_WhenAccessingErrors_ShouldReturnSimilarErrors() + { + Error[] errors = [Error.Unauthorized(), Error.Validation()]; + + ErrorOr result = errors.ToErrorOr(); + + result.IsError.Should().BeTrue(); + result.Errors.Should().Equal(errors); + } } From 3359933480e7234c962b9ffee6c92e2faf2926c8 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Thu, 9 May 2024 19:40:50 +0200 Subject: [PATCH 10/12] feat: ArgumentException is now thrown when empty errors list/array is passed Signed-off-by: Kenny Pflug --- src/ErrorOr/ErrorOr.ImplicitConverters.cs | 8 ++++---- src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs | 4 ++-- tests/ErrorOr/ErrorOr.InstantiationTests.cs | 8 ++++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/ErrorOr/ErrorOr.ImplicitConverters.cs b/src/ErrorOr/ErrorOr.ImplicitConverters.cs index 2dc51e3..79b9eea 100644 --- a/src/ErrorOr/ErrorOr.ImplicitConverters.cs +++ b/src/ErrorOr/ErrorOr.ImplicitConverters.cs @@ -22,7 +22,7 @@ namespace ErrorOr; /// Creates an from a list of errors. /// /// Thrown when is null. - /// Thrown when is an empty list. + /// Thrown when is an empty list. public static implicit operator ErrorOr(List errors) { if (errors is null) @@ -32,7 +32,7 @@ namespace ErrorOr; if (errors.Count == 0) { - throw new InvalidOperationException("Cannot create an ErrorOr from an empty list of errors. Provide at least one error."); + throw new ArgumentException("Cannot create an ErrorOr from an empty list of errors. Provide at least one error.", nameof(errors)); } return new ErrorOr(errors); @@ -42,7 +42,7 @@ namespace ErrorOr; /// Creates an from a list of errors. /// /// Thrown when is null. - /// Thrown when is an empty array. + /// Thrown when is an empty array. public static implicit operator ErrorOr(Error[] errors) { if (errors is null) @@ -52,7 +52,7 @@ namespace ErrorOr; if (errors.Length == 0) { - throw new InvalidOperationException("Cannot create an ErrorOr from an empty array of errors. Provide at least one error."); + throw new ArgumentException("Cannot create an ErrorOr from an empty array of errors. Provide at least one error.", nameof(errors)); } return new ErrorOr(errors.ToList()); diff --git a/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs b/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs index 15e4fee..8b9c80f 100644 --- a/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs +++ b/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs @@ -22,7 +22,7 @@ public static ErrorOr ToErrorOr(this Error error) /// Creates an instance with the given . /// /// Thrown when is null. - /// Thrown when is an empty list. + /// Thrown when is an empty list. public static ErrorOr ToErrorOr(this List errors) { return errors; @@ -32,7 +32,7 @@ public static ErrorOr ToErrorOr(this List errors) /// Creates an instance with the given . /// /// Thrown when is null. - /// Thrown when is an empty array. + /// Thrown when is an empty array. public static ErrorOr ToErrorOr(this Error[] errors) { return errors; diff --git a/tests/ErrorOr/ErrorOr.InstantiationTests.cs b/tests/ErrorOr/ErrorOr.InstantiationTests.cs index 54ab4ef..85b8967 100644 --- a/tests/ErrorOr/ErrorOr.InstantiationTests.cs +++ b/tests/ErrorOr/ErrorOr.InstantiationTests.cs @@ -364,7 +364,9 @@ public void CreateErrorOr_WhenEmptyErrorsList_ShouldThrow() Func> errorOrInt = () => new List(); // Assert - errorOrInt.Should().ThrowExactly(); + var exception = errorOrInt.Should().ThrowExactly().Which; + exception.Message.Should().Be("Cannot create an ErrorOr from an empty list of errors. Provide at least one error. (Parameter 'errors')"); + exception.ParamName.Should().Be("errors"); } [Fact] @@ -374,7 +376,9 @@ public void CreateErrorOr_WhenEmptyErrorsArray_ShouldThrow() Func> errorOrInt = () => Array.Empty(); // Assert - errorOrInt.Should().ThrowExactly(); + var exception = errorOrInt.Should().ThrowExactly().Which; + exception.Message.Should().Be("Cannot create an ErrorOr from an empty array of errors. Provide at least one error. (Parameter 'errors')"); + exception.ParamName.Should().Be("errors"); } [Fact] From 61d1540b30697afe8f8483c4eae0d46a3e44a3bd Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Thu, 9 May 2024 20:02:04 +0200 Subject: [PATCH 11/12] docs: add additional exception XML comments to ErrorOr Signed-off-by: Kenny Pflug --- src/ErrorOr/ErrorOr.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ErrorOr/ErrorOr.cs b/src/ErrorOr/ErrorOr.cs index 4a06061..7df9fb4 100644 --- a/src/ErrorOr/ErrorOr.cs +++ b/src/ErrorOr/ErrorOr.cs @@ -14,6 +14,7 @@ namespace ErrorOr; /// /// Prevents a default struct from being created. /// + /// Thrown when this method is called. public ErrorOr() { throw new InvalidOperationException("Default construction of ErrorOr is invalid. Please use provided factory methods to instantiate."); @@ -46,6 +47,7 @@ private ErrorOr(TValue value) /// /// Gets the list of errors. If the state is not error, the list will contain a single error representing the state. /// + /// Thrown when no errors are present. public List Errors => IsError ? _errors : throw new InvalidOperationException("The Errors property cannot be accessed when no errors have been recorded. Check IsError before accessing Errors."); /// @@ -56,6 +58,7 @@ private ErrorOr(TValue value) /// /// Gets the value. /// + /// Thrown when no value is present. public TValue Value { get @@ -72,6 +75,7 @@ public TValue Value /// /// Gets the first error. /// + /// Thrown when no errors are present. public Error FirstError { get From fcc40992b639a7db75eb7de745d8db7d6ff70506 Mon Sep 17 00:00:00 2001 From: Amichai Mantinband Date: Sun, 12 May 2024 11:32:21 +0300 Subject: [PATCH 12/12] Restrict Value to be non-nullable --- src/ErrorOr/ErrorOr.ImplicitConverters.cs | 105 +++++----- src/ErrorOr/ErrorOr.cs | 213 +++++++++++--------- tests/ErrorOr/ErrorOr.InstantiationTests.cs | 13 +- 3 files changed, 170 insertions(+), 161 deletions(-) diff --git a/src/ErrorOr/ErrorOr.ImplicitConverters.cs b/src/ErrorOr/ErrorOr.ImplicitConverters.cs index 79b9eea..982f770 100644 --- a/src/ErrorOr/ErrorOr.ImplicitConverters.cs +++ b/src/ErrorOr/ErrorOr.ImplicitConverters.cs @@ -1,60 +1,45 @@ -namespace ErrorOr; - -public readonly partial record struct ErrorOr : IErrorOr -{ - /// - /// Creates an from a value. - /// - public static implicit operator ErrorOr(TValue value) - { - return new ErrorOr(value); - } - - /// - /// Creates an from an error. - /// - public static implicit operator ErrorOr(Error error) - { - return new ErrorOr(error); - } - - /// - /// Creates an from a list of errors. - /// - /// Thrown when is null. - /// Thrown when is an empty list. - public static implicit operator ErrorOr(List errors) - { - if (errors is null) - { - throw new ArgumentNullException(nameof(errors)); - } - - if (errors.Count == 0) - { - throw new ArgumentException("Cannot create an ErrorOr from an empty list of errors. Provide at least one error.", nameof(errors)); - } - - return new ErrorOr(errors); - } - - /// - /// Creates an from a list of errors. - /// - /// Thrown when is null. - /// Thrown when is an empty array. - public static implicit operator ErrorOr(Error[] errors) - { - if (errors is null) - { - throw new ArgumentNullException(nameof(errors)); - } - - if (errors.Length == 0) - { - throw new ArgumentException("Cannot create an ErrorOr from an empty array of errors. Provide at least one error.", nameof(errors)); - } - - return new ErrorOr(errors.ToList()); - } -} +namespace ErrorOr; + +public readonly partial record struct ErrorOr : IErrorOr +{ + /// + /// Creates an from a value. + /// + public static implicit operator ErrorOr(TValue value) + { + return new ErrorOr(value); + } + + /// + /// Creates an from an error. + /// + public static implicit operator ErrorOr(Error error) + { + return new ErrorOr(error); + } + + /// + /// Creates an from a list of errors. + /// + /// Thrown when is null. + /// Thrown when is an empty list. + public static implicit operator ErrorOr(List errors) + { + return new ErrorOr(errors); + } + + /// + /// Creates an from a list of errors. + /// + /// Thrown when is null. + /// Thrown when is an empty array. + public static implicit operator ErrorOr(Error[] errors) + { + if (errors is null) + { + throw new ArgumentNullException(nameof(errors)); + } + + return new ErrorOr([.. errors]); + } +} diff --git a/src/ErrorOr/ErrorOr.cs b/src/ErrorOr/ErrorOr.cs index 7df9fb4..b958157 100644 --- a/src/ErrorOr/ErrorOr.cs +++ b/src/ErrorOr/ErrorOr.cs @@ -1,99 +1,114 @@ -using System.Diagnostics.CodeAnalysis; - -namespace ErrorOr; - -/// -/// A discriminated union of errors or a value. -/// -/// The type of the underlying . -public readonly partial record struct ErrorOr : IErrorOr -{ - private readonly TValue? _value = default; - private readonly List? _errors = null; - - /// - /// Prevents a default struct from being created. - /// - /// Thrown when this method is called. - public ErrorOr() - { - throw new InvalidOperationException("Default construction of ErrorOr is invalid. Please use provided factory methods to instantiate."); - } - - private ErrorOr(Error error) - { - _errors = [error]; - } - - private ErrorOr(List errors) - { - _errors = errors; - } - - private ErrorOr(TValue value) - { - _value = value; - } - - /// - /// Gets a value indicating whether the state is error. - /// - [MemberNotNullWhen(true, nameof(_errors))] - [MemberNotNullWhen(true, nameof(Errors))] - [MemberNotNullWhen(false, nameof(Value))] - [MemberNotNullWhen(false, nameof(_value))] - public bool IsError => _errors is not null; - - /// - /// Gets the list of errors. If the state is not error, the list will contain a single error representing the state. - /// - /// Thrown when no errors are present. - public List Errors => IsError ? _errors : throw new InvalidOperationException("The Errors property cannot be accessed when no errors have been recorded. Check IsError before accessing Errors."); - - /// - /// Gets the list of errors. If the state is not error, the list will be empty. - /// - public List ErrorsOrEmptyList => IsError ? _errors : EmptyErrors.Instance; - - /// - /// Gets the value. - /// - /// Thrown when no value is present. - public TValue Value - { - get - { - if (IsError) - { - throw new InvalidOperationException("The Value property cannot be accessed when errors have been recorded. Check IsError before accessing Value."); - } - - return _value; - } - } - - /// - /// Gets the first error. - /// - /// Thrown when no errors are present. - public Error FirstError - { - get - { - if (!IsError) - { - throw new InvalidOperationException("The FirstError property cannot be accessed when no errors have been recorded. Check IsError before accessing FirstError."); - } - - return _errors[0]; - } - } - - /// - /// Creates an from a list of errors. - /// - public static ErrorOr From(List errors) - { - return errors; - } -} +using System.Diagnostics.CodeAnalysis; + +namespace ErrorOr; + +/// +/// A discriminated union of errors or a value. +/// +/// The type of the underlying . +public readonly partial record struct ErrorOr : IErrorOr +{ + private readonly TValue? _value = default; + private readonly List? _errors = null; + + /// + /// Prevents a default struct from being created. + /// + /// Thrown when this method is called. + public ErrorOr() + { + throw new InvalidOperationException("Default construction of ErrorOr is invalid. Please use provided factory methods to instantiate."); + } + + private ErrorOr(Error error) + { + _errors = [error]; + } + + private ErrorOr(List errors) + { + if (errors is null) + { + throw new ArgumentNullException(nameof(errors)); + } + + if (errors is null || errors.Count == 0) + { + throw new ArgumentException("Cannot create an ErrorOr from an empty collection of errors. Provide at least one error.", nameof(errors)); + } + + _errors = errors; + } + + private ErrorOr(TValue value) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + _value = value; + } + + /// + /// Gets a value indicating whether the state is error. + /// + [MemberNotNullWhen(true, nameof(_errors))] + [MemberNotNullWhen(true, nameof(Errors))] + [MemberNotNullWhen(false, nameof(Value))] + [MemberNotNullWhen(false, nameof(_value))] + public bool IsError => _errors is not null; + + /// + /// Gets the list of errors. If the state is not error, the list will contain a single error representing the state. + /// + /// Thrown when no errors are present. + public List Errors => IsError ? _errors : throw new InvalidOperationException("The Errors property cannot be accessed when no errors have been recorded. Check IsError before accessing Errors."); + + /// + /// Gets the list of errors. If the state is not error, the list will be empty. + /// + public List ErrorsOrEmptyList => IsError ? _errors : EmptyErrors.Instance; + + /// + /// Gets the value. + /// + /// Thrown when no value is present. + public TValue Value + { + get + { + if (IsError) + { + throw new InvalidOperationException("The Value property cannot be accessed when errors have been recorded. Check IsError before accessing Value."); + } + + return _value; + } + } + + /// + /// Gets the first error. + /// + /// Thrown when no errors are present. + public Error FirstError + { + get + { + if (!IsError) + { + throw new InvalidOperationException("The FirstError property cannot be accessed when no errors have been recorded. Check IsError before accessing FirstError."); + } + + return _errors[0]; + } + } + + /// + /// Creates an from a list of errors. + /// + public static ErrorOr From(List errors) + { + return errors; + } +} diff --git a/tests/ErrorOr/ErrorOr.InstantiationTests.cs b/tests/ErrorOr/ErrorOr.InstantiationTests.cs index 85b8967..77d17ca 100644 --- a/tests/ErrorOr/ErrorOr.InstantiationTests.cs +++ b/tests/ErrorOr/ErrorOr.InstantiationTests.cs @@ -365,7 +365,7 @@ public void CreateErrorOr_WhenEmptyErrorsList_ShouldThrow() // Assert var exception = errorOrInt.Should().ThrowExactly().Which; - exception.Message.Should().Be("Cannot create an ErrorOr from an empty list of errors. Provide at least one error. (Parameter 'errors')"); + exception.Message.Should().Be("Cannot create an ErrorOr from an empty collection of errors. Provide at least one error. (Parameter 'errors')"); exception.ParamName.Should().Be("errors"); } @@ -377,7 +377,7 @@ public void CreateErrorOr_WhenEmptyErrorsArray_ShouldThrow() // Assert var exception = errorOrInt.Should().ThrowExactly().Which; - exception.Message.Should().Be("Cannot create an ErrorOr from an empty array of errors. Provide at least one error. (Parameter 'errors')"); + exception.Message.Should().Be("Cannot create an ErrorOr from an empty collection of errors. Provide at least one error. (Parameter 'errors')"); exception.ParamName.Should().Be("errors"); } @@ -398,4 +398,13 @@ public void CreateErrorOr_WhenNullIsPassedAsErrorsArray_ShouldThrowArgumentNullE act.Should().ThrowExactly() .And.ParamName.Should().Be("errors"); } + + [Fact] + public void CreateErrorOr_WhenValueIsNull_ShouldThrowArgumentNullException() + { + Func> act = () => default(int?); + + act.Should().ThrowExactly() + .And.ParamName.Should().Be("value"); + } }