diff --git a/.editorconfig b/.editorconfig index 861f73d..04559f5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,66 +1,72 @@ -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 + +# SA1516: Elements should be separated by blank line +dotnet_diagnostic.SA1516.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.ToErrorOrExtensions.cs b/src/ErrorOr.ToErrorOrExtensions.cs deleted file mode 100644 index c7297a9..0000000 --- a/src/ErrorOr.ToErrorOrExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace ErrorOr; - -public static partial class ErrorOrExtensions -{ - /// - /// Creates an instance with the given . - /// - public static ErrorOr ToErrorOr(this TValue value) - { - return value; - } - - /// - /// Creates an instance with the given . - /// - public static ErrorOr ToErrorOr(this Error error) - { - return error; - } - - /// - /// Creates an instance with the given . - /// - public static ErrorOr ToErrorOr(this List error) - { - return error; - } -} diff --git a/src/ErrorOr.cs b/src/ErrorOr.cs deleted file mode 100644 index 6614182..0000000 --- a/src/ErrorOr.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace ErrorOr; - -/// -/// A discriminated union of errors or a value. -/// -public readonly partial record struct ErrorOr : IErrorOr -{ - private readonly TValue? _value = default; - private readonly List? _errors = null; - - /// - /// 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! : KnownErrors.CachedNoErrorsList; - - /// - /// 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; - } - - /// - /// Gets the value. - /// - public TValue Value => _value!; - - /// - /// Gets the first error. - /// - public Error FirstError - { - get - { - if (!IsError) - { - return KnownErrors.NoFirstError; - } - - 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) - { - _value = value; - IsError = false; - } -} 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/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.Else.cs b/src/ErrorOr/ErrorOr.Else.cs similarity index 96% rename from src/ErrorOr.Else.cs rename to src/ErrorOr/ErrorOr.Else.cs index 6e706e9..125256b 100644 --- a/src/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.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 96% rename from src/ErrorOr.Equality.cs rename to src/ErrorOr/ErrorOr.Equality.cs index b23b669..5521344 100644 --- a/src/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.FailIf.cs b/src/ErrorOr/ErrorOr.FailIf.cs similarity index 94% rename from src/ErrorOr.FailIf.cs rename to src/ErrorOr/ErrorOr.FailIf.cs index c59fab0..1d6baaf 100644 --- a/src/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.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 60% rename from src/ErrorOr.ImplicitConverters.cs rename to src/ErrorOr/ErrorOr.ImplicitConverters.cs index d2e155e..982f770 100644 --- a/src/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 { /// @@ -24,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) { return new ErrorOr(errors); @@ -32,8 +31,15 @@ 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) { - return new ErrorOr(errors.ToList()); + if (errors is null) + { + throw new ArgumentNullException(nameof(errors)); + } + + return new ErrorOr([.. errors]); } } diff --git a/src/ErrorOr.Match.cs b/src/ErrorOr/ErrorOr.Match.cs similarity index 96% rename from src/ErrorOr.Match.cs rename to src/ErrorOr/ErrorOr.Match.cs index db76d48..5769e29 100644 --- a/src/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.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 95% rename from src/ErrorOr.Switch.cs rename to src/ErrorOr/ErrorOr.Switch.cs index 1fabdbc..a374ae8 100644 --- a/src/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.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 95% rename from src/ErrorOr.Then.cs rename to src/ErrorOr/ErrorOr.Then.cs index 5f24448..7282687 100644 --- a/src/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.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/ErrorOr.ToErrorOrExtensions.cs b/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs new file mode 100644 index 0000000..8b9c80f --- /dev/null +++ b/src/ErrorOr/ErrorOr.ToErrorOrExtensions.cs @@ -0,0 +1,40 @@ +namespace ErrorOr; + +public static partial class ErrorOrExtensions +{ + /// + /// Creates an instance with the given . + /// + public static ErrorOr ToErrorOr(this TValue value) + { + return value; + } + + /// + /// Creates an instance with the given . + /// + public static ErrorOr ToErrorOr(this Error error) + { + return error; + } + + /// + /// Creates an instance with the given . + /// + /// Thrown when is null. + /// Thrown when is an empty list. + 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/src/ErrorOr/ErrorOr.cs b/src/ErrorOr/ErrorOr.cs new file mode 100644 index 0000000..b958157 --- /dev/null +++ b/src/ErrorOr/ErrorOr.cs @@ -0,0 +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) + { + 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/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 97% rename from src/Error.cs rename to src/Errors/Error.cs index d308e72..c42b9f6 100644 --- a/src/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,29 @@ 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 +202,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/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/KnownErrors.cs deleted file mode 100644 index cfa7220..0000000 --- a/src/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/Types.cs b/src/Results/Results.cs similarity index 70% rename from src/Types.cs rename to src/Results/Results.cs index 1b80330..c0b7f85 100644 --- a/src/Types.cs +++ b/src/Results/Results.cs @@ -1,17 +1,19 @@ -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; + +#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 +{ + 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..4853611 --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,7 @@ +[*.cs] + +# 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 diff --git a/tests/ErrorOr.ElseAsyncTests.cs b/tests/ErrorOr/ErrorOr.ElseAsyncTests.cs similarity index 74% rename from tests/ErrorOr.ElseAsyncTests.cs rename to tests/ErrorOr/ErrorOr.ElseAsyncTests.cs index ad4bb07..df7db96 100644 --- a/tests/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.ElseTests.cs b/tests/ErrorOr/ErrorOr.ElseTests.cs similarity index 74% rename from tests/ErrorOr.ElseTests.cs rename to tests/ErrorOr/ErrorOr.ElseTests.cs index c2cf37c..a262a62 100644 --- a/tests/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.EqualityTests.cs b/tests/ErrorOr/ErrorOr.EqualityTests.cs similarity index 94% rename from tests/ErrorOr.EqualityTests.cs rename to tests/ErrorOr/ErrorOr.EqualityTests.cs index 5cb0f31..c7716a0 100644 --- a/tests/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.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 62% rename from tests/ErrorOrTests.cs rename to tests/ErrorOr/ErrorOr.InstantiationTests.cs index 82add1f..77d17ca 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); @@ -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] @@ -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] @@ -172,27 +173,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] @@ -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] @@ -295,11 +297,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 +333,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; @@ -344,4 +346,65 @@ public void ImplicitCastErrorArray_WhenAccessingFirstError_ShouldReturnFirstErro errorOrPerson.IsError.Should().BeTrue(); errorOrPerson.FirstError.Should().Be(errors[0]); } + + [Fact] + public void CreateErrorOr_WhenUsingEmptyConstructor_ShouldThrow() + { + // Act + Func> action = () => new ErrorOr(); + + // Assert + action.Should().ThrowExactly(); + } + + [Fact] + public void CreateErrorOr_WhenEmptyErrorsList_ShouldThrow() + { + // Act + Func> errorOrInt = () => new List(); + + // Assert + var exception = errorOrInt.Should().ThrowExactly().Which; + 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"); + } + + [Fact] + public void CreateErrorOr_WhenEmptyErrorsArray_ShouldThrow() + { + // Act + Func> errorOrInt = () => Array.Empty(); + + // Assert + var exception = errorOrInt.Should().ThrowExactly().Which; + 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"); + } + + [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"); + } + + [Fact] + public void CreateErrorOr_WhenValueIsNull_ShouldThrowArgumentNullException() + { + Func> act = () => default(int?); + + act.Should().ThrowExactly() + .And.ParamName.Should().Be("value"); + } } 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 66% rename from tests/ErrorOr.ThenAsyncTests.cs rename to tests/ErrorOr/ErrorOr.ThenAsyncTests.cs index f3b0305..5dd7c48 100644 --- a/tests/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.ThenTests.cs b/tests/ErrorOr/ErrorOr.ThenTests.cs similarity index 66% rename from tests/ErrorOr.ThenTests.cs rename to tests/ErrorOr/ErrorOr.ThenTests.cs index 0e91bb9..2555896 100644 --- a/tests/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.ToErrorOrTests.cs b/tests/ErrorOr/ErrorOr.ToErrorOrTests.cs similarity index 69% rename from tests/ErrorOr.ToErrorOrTests.cs rename to tests/ErrorOr/ErrorOr.ToErrorOrTests.cs index e07fbde..71202df 100644 --- a/tests/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(); @@ -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); + } } 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/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 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;