diff --git a/Code/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthTests.cs b/Code/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthTests.cs new file mode 100644 index 0000000..de12c50 --- /dev/null +++ b/Code/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Immutable; +using FluentAssertions; +using Light.GuardClauses.Exceptions; +using Xunit; + +namespace Light.GuardClauses.Tests.CollectionAssertions; + +public static class MustHaveLengthTests +{ + [Theory] + [InlineData(3, 2)] + [InlineData(5, 6)] + [InlineData(0, 1)] + [InlineData(10, 11)] + public static void ImmutableArrayInvalidLength(int arrayLength, int expectedLength) + { + var array = ImmutableArray.CreateRange(new int[arrayLength]); + + var act = () => array.MustHaveLength(expectedLength, nameof(array)); + + act.Should().Throw() + .And.Message.Should().Contain( + $"{nameof(array)} must have length {expectedLength}, but it actually has length {arrayLength}." + ); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(3)] + [InlineData(6)] + public static void ImmutableArrayValidLength(int arrayLength) + { + var array = ImmutableArray.CreateRange(new string[arrayLength]); + + var result = array.MustHaveLength(arrayLength); + + result.Should().Equal(array); + } + + [Fact] + public static void ImmutableArrayCustomException() + { + var exception = new Exception(); + var array = ImmutableArray.Create("a", "b", "c"); + + var act = () => array.MustHaveLength(4, (_, _) => exception); + + act.Should().Throw().Which.Should().BeSameAs(exception); + } + + [Fact] + public static void ImmutableArrayNoCustomException() + { + var array = ImmutableArray.Create(1, 2, 3, 4); + + var result = array.MustHaveLength(4, (_, _) => null); + + result.Should().Equal(array); + } + + [Fact] + public static void ImmutableArrayCustomMessage() + { + var array = ImmutableArray.Empty; + + var act = () => array.MustHaveLength(1, message: "Custom error message"); + + act.Should().Throw() + .And.Message.Should().Contain("Custom error message"); + } + + [Fact] + public static void ImmutableArrayCallerArgumentExpression() + { + var myArray = ImmutableArray.Create("foo", "bar"); + + var act = () => myArray.MustHaveLength(10); + + act.Should().Throw() + .WithParameterName("myArray"); + } + + [Fact] + public static void ImmutableArrayDefaultArray() + { + var defaultArray = default(ImmutableArray); + + var act = () => defaultArray.MustHaveLength(5, nameof(defaultArray)); + + act.Should().Throw() + .And.Message.Should().Contain($"{nameof(defaultArray)} must have length 5, but it actually has length 0."); + } +} diff --git a/Code/Light.GuardClauses/Check.MustHaveLength.cs b/Code/Light.GuardClauses/Check.MustHaveLength.cs index a7d8d4f..9277d9a 100644 --- a/Code/Light.GuardClauses/Check.MustHaveLength.cs +++ b/Code/Light.GuardClauses/Check.MustHaveLength.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Immutable; using System.Runtime.CompilerServices; using JetBrains.Annotations; using Light.GuardClauses.ExceptionFactory; @@ -145,4 +146,52 @@ ReadOnlySpanExceptionFactory exceptionFactory return parameter; } + + /// + /// Ensures that the immutable array has the specified length, or otherwise throws an . + /// + /// The immutable array to be checked. + /// The length that the immutable array must have. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when does not have the specified length. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ImmutableArray MustHaveLength( + this ImmutableArray parameter, + int length, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + var actualLength = parameter.IsDefault ? 0 : parameter.Length; + if (actualLength != length) + { + Throw.InvalidImmutableArrayLength(parameter, length, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the immutable array has the specified length, or otherwise throws your custom exception. + /// + /// The immutable array to be checked. + /// The length that the immutable array must have. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when does not have the specified length. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ImmutableArray MustHaveLength( + this ImmutableArray parameter, + int length, + Func, int, Exception> exceptionFactory + ) + { + var actualLength = parameter.IsDefault ? 0 : parameter.Length; + if (actualLength != length) + { + Throw.CustomException(exceptionFactory, parameter, length); + } + + return parameter; + } } diff --git a/Code/Light.GuardClauses/ExceptionFactory/Throw.ImmutableArray.cs b/Code/Light.GuardClauses/ExceptionFactory/Throw.ImmutableArray.cs new file mode 100644 index 0000000..0d76b3a --- /dev/null +++ b/Code/Light.GuardClauses/ExceptionFactory/Throw.ImmutableArray.cs @@ -0,0 +1,31 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.Exceptions; + +namespace Light.GuardClauses.ExceptionFactory; + +public static partial class Throw +{ + /// + /// Throws the default indicating that an immutable array has an invalid length, + /// using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void InvalidImmutableArrayLength( + ImmutableArray parameter, + int length, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + var actualLength = parameter.IsDefault ? 0 : parameter.Length; + throw new InvalidCollectionCountException( + parameterName, + message ?? + $"{parameterName ?? "The immutable array"} must have length {length}, but it actually has length {actualLength}." + ); + } +} diff --git a/Code/Plans/issue-117-must-have-length-for-immutable-array.md b/Code/Plans/issue-117-must-have-length-for-immutable-array.md new file mode 100644 index 0000000..ebc108c --- /dev/null +++ b/Code/Plans/issue-117-must-have-length-for-immutable-array.md @@ -0,0 +1,22 @@ +# Issue 117 - MustHaveLength for ImmutableArray + +## Context + +The .NET library Light.GuardClauses already has several assertions for collections. They often rely on `IEnumerable` or `IEnumerable`. However, these assertions would result in `ImmutableArray` being boxed - we want to avoid that by providing dedicated assertions for this type which avoids boxing. For this issue, we implement the `MustHaveLength` assertion for `ImmutableArray`. + +## Tasks for this issue + +- [ ] The production code should be placed in the Light.GuardClauses project. The file `Check.MustHaveLength.cs` already exists in the root folder of the project - extend this existing file. +- [ ] In this file, create several extension method overloads called `MustHaveLength` for `ImmutableArray`. It should be placed in the class `Check` which is marked as `partial`. +- [ ] Each assertion in Light.GuardClauses has two overloads - the first one takes the optional `parameterName` and `message` arguments and throw the default exception. The actual exception is thrown in the `Throw` class - use the existing `Throw.InvalidCollectionCount` method which is located in `ExceptionFactory/Throw.InvalidCollectionCount.cs`. +- [ ] The other overload takes a delegate which allows the caller to provide their own custom exceptions. Use the existing `Throw.CustomException` method and pass the delegate, the erroneous `ImmutableArray` instance and the expected length. +- [ ] Use the `Length` property of `ImmutableArray` instead of `Count` for performance and correctness. +- [ ] Create unit tests for both overloads. The corresponding tests should be placed in Light.GuardClauses.Tests project. There is already a file 'StringAssertions/MustHaveLengthTests.cs' but you need to create a new file 'CollectionAssertions/MustHaveLengthTests.cs' for collection-related length tests. Please follow conventions of the existing tests (e.g. use FluentAssertions' `Should()` for assertions). + +## Notes + +- There are already plenty of other assertions and tests in this library. All overloads are placed in the same file in the production code project. The test projects has top-level folders for different groups of assertions, like `CollectionAssertions`, `StringAssertions`, `DateTimeAssertions` and so on. Please take a look at them to follow a similar structure and code style. +- This assertion specifically targets `ImmutableArray` to avoid boxing that would occur with generic `IEnumerable` extensions. +- Use the `Length` property instead of `Count` as this is the appropriate property for `ImmutableArray`. +- The assertion should verify that the `ImmutableArray` has exactly the expected length. +- If you have any questions or suggestions, please ask me about them.