diff --git a/Code/Light.GuardClauses.Tests/CollectionAssertions/MustHaveMinimumLengthTests.cs b/Code/Light.GuardClauses.Tests/CollectionAssertions/MustHaveMinimumLengthTests.cs new file mode 100644 index 0000000..66075e6 --- /dev/null +++ b/Code/Light.GuardClauses.Tests/CollectionAssertions/MustHaveMinimumLengthTests.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Immutable; +using FluentAssertions; +using Light.GuardClauses.Exceptions; +using Xunit; + +namespace Light.GuardClauses.Tests.CollectionAssertions; + +public static class MustHaveMinimumLengthTests +{ + [Theory] + [InlineData(new[] { 1, 2 }, 3)] + [InlineData(new[] { 1 }, 2)] + [InlineData(new int[] { }, 1)] + public static void ImmutableArrayFewerItems(int[] items, int length) + { + var immutableArray = items.ToImmutableArray(); + Action act = () => immutableArray.MustHaveMinimumLength(length, nameof(immutableArray)); + + var assertion = act.Should().Throw().Which; + assertion.Message.Should().Contain( + $"{nameof(immutableArray)} must have at least a length of {length}, but it actually has a length of {immutableArray.Length}." + ); + assertion.ParamName.Should().BeSameAs(nameof(immutableArray)); + } + + [Theory] + [InlineData(new[] { "Foo" }, 1)] + [InlineData(new[] { "Bar" }, 0)] + [InlineData(new[] { "Baz", "Qux", "Quux" }, 2)] + public static void ImmutableArrayMoreOrEqualItems(string[] items, int length) + { + var immutableArray = items.ToImmutableArray(); + var result = immutableArray.MustHaveMinimumLength(length); + result.Should().Equal(immutableArray); + } + + [Fact] + public static void ImmutableArrayEmpty() + { + var emptyArray = ImmutableArray.Empty; + var result = emptyArray.MustHaveMinimumLength(0); + result.Should().Equal(emptyArray); + } + + [Theory] + [InlineData(new[] { 87 }, 3)] + [InlineData(new[] { 1, 2 }, 5)] + public static void ImmutableArrayCustomException(int[] items, int minimumLength) + { + var immutableArray = items.ToImmutableArray(); + + Action act = () => immutableArray.MustHaveMinimumLength( + minimumLength, + (array, length) => new ($"Custom exception for array with length {array.Length} and min {length}") + ); + + act.Should().Throw() + .WithMessage($"Custom exception for array with length {immutableArray.Length} and min {minimumLength}"); + } + + [Fact] + public static void ImmutableArrayNoCustomExceptionThrown() + { + var immutableArray = new[] { "Foo", "Bar" }.ToImmutableArray(); + var result = immutableArray.MustHaveMinimumLength(2, (_, _) => new ()); + result.Should().Equal(immutableArray); + } + + [Fact] + public static void ImmutableArrayCustomMessage() + { + var immutableArray = new[] { 1 }.ToImmutableArray(); + + Test.CustomMessage( + message => immutableArray.MustHaveMinimumLength(3, message: message) + ); + } + + [Fact] + public static void ImmutableArrayCallerArgumentExpression() + { + var myImmutableArray = new[] { 1 }.ToImmutableArray(); + + var act = () => myImmutableArray.MustHaveMinimumLength(2); + + act.Should().Throw() + .WithParameterName(nameof(myImmutableArray)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-5)] + public static void + DefaultImmutableArrayInstanceShouldNotThrowWhenLengthIsLessOrEqualToZero(int validLength) => + default(ImmutableArray).MustHaveMinimumLength(validLength).IsDefault.Should().BeTrue(); + + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(12)] + public static void DefaultImmutableArrayInstanceShouldThrowWhenLengthIsPositive(int positiveLength) + { + var act = () => default(ImmutableArray).MustHaveMinimumLength(positiveLength); + + act.Should().Throw() + .WithParameterName("default(ImmutableArray)") + .WithMessage( + $"default(ImmutableArray) must have at least a length of {positiveLength}, but it actually has no length because it is the default instance.*" + ); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-5)] + public static void DefaultImmutableArrayInstanceCustomExceptionShouldNotThrow(int validLength) + { + var result = default(ImmutableArray).MustHaveMinimumLength(validLength, (_, _) => new Exception()); + result.IsDefault.Should().BeTrue(); + } + + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(12)] + public static void DefaultImmutableArrayInstanceCustomExceptionShouldThrow(int positiveLength) + { + var act = () => default(ImmutableArray).MustHaveMinimumLength( + positiveLength, + (array, length) => new ArgumentException( + $"Custom: Array length {(array.IsDefault ? 0 : array.Length)} is below minimum {length}" + ) + ); + + act.Should().Throw() + .WithMessage("Custom: Array length 0 is below minimum *"); + } +} diff --git a/Code/Light.GuardClauses/Check.MustHaveMinimumLength.cs b/Code/Light.GuardClauses/Check.MustHaveMinimumLength.cs new file mode 100644 index 0000000..1c53823 --- /dev/null +++ b/Code/Light.GuardClauses/Check.MustHaveMinimumLength.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the has at least the specified length, or otherwise throws an . + /// + /// The to be checked. + /// The minimum length the should have. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when has less than the specified length. + /// The default instance of will be treated as having length 0. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ImmutableArray MustHaveMinimumLength( + this ImmutableArray parameter, + int length, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + var parameterLength = parameter.IsDefault ? 0 : parameter.Length; + if (parameterLength < length) + { + Throw.InvalidMinimumImmutableArrayLength(parameter, length, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the has at least the specified length, or otherwise throws your custom exception. + /// + /// The to be checked. + /// The minimum length the should have. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when has less than the specified length. + /// The default instance of will be treated as having length 0. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ImmutableArray MustHaveMinimumLength( + this ImmutableArray parameter, + int length, + Func, int, Exception> exceptionFactory + ) + { + var parameterLength = parameter.IsDefault ? 0 : parameter.Length; + if (parameterLength < length) + { + Throw.CustomException(exceptionFactory, parameter, length); + } + + return parameter; + } +} diff --git a/Code/Light.GuardClauses/ExceptionFactory/Throw.ImmutableArray.cs b/Code/Light.GuardClauses/ExceptionFactory/Throw.InvalidImmutableArrayLength.cs similarity index 100% rename from Code/Light.GuardClauses/ExceptionFactory/Throw.ImmutableArray.cs rename to Code/Light.GuardClauses/ExceptionFactory/Throw.InvalidImmutableArrayLength.cs diff --git a/Code/Light.GuardClauses/ExceptionFactory/Throw.InvalidMinimumImmutableArrayLength.cs b/Code/Light.GuardClauses/ExceptionFactory/Throw.InvalidMinimumImmutableArrayLength.cs new file mode 100644 index 0000000..4c1682f --- /dev/null +++ b/Code/Light.GuardClauses/ExceptionFactory/Throw.InvalidMinimumImmutableArrayLength.cs @@ -0,0 +1,30 @@ +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 has less than a + /// minimum number of items, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void InvalidMinimumImmutableArrayLength( + ImmutableArray parameter, + int length, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + throw new InvalidCollectionCountException( + parameterName, + message ?? + $"{parameterName ?? "The immutable array"} must have at least a length of {length}, but it actually {(parameter.IsDefault ? "has no length because it is the default instance" : $"has a length of {parameter.Length}")}." + ); + } +} diff --git a/Code/Plans/issue-121-must-have-minimum-length-for-immutable-array.md b/Code/Plans/issue-121-must-have-minimum-length-for-immutable-array.md new file mode 100644 index 0000000..885d0b5 --- /dev/null +++ b/Code/Plans/issue-121-must-have-minimum-length-for-immutable-array.md @@ -0,0 +1,23 @@ +# Issue 121 - MustHaveMinimumLength 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 `MustHaveMinimumLength` assertion for `ImmutableArray`. + +## Tasks for this issue + +- [ ] The production code should be placed in the Light.GuardClauses project. There is no existing `Check.MustHaveMinimumLength.cs` file, but there is a `Check.MustHaveMinimumCount.cs` file. Create a new file called `Check.MustHaveMinimumLength.cs` in the root folder of the project. +- [ ] In this file, create several extension method overloads called `MustHaveMinimumLength` 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.InvalidMinimumCollectionCount` method which is located in `ExceptionFactory/Throw.InvalidMinimumCollectionCount.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 minimum length. +- [ ] Use the `Length` property of `ImmutableArray` instead of `Count` for performance and correctness. Also decide how the default instance of an immutable array (see `ImmutableArray.IsDefault`) should be handled. +- [ ] Create unit tests for both overloads. The corresponding tests should be placed in Light.GuardClauses.Tests project. There is an existing file 'CollectionAssertions/MustHaveMinimumCountTests.cs' but you need to create a new file 'CollectionAssertions/MustHaveMinimumLengthTests.cs' for length-related 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. +- Especially take a look at the existing `Check.MustHaveMaximumLength.cs` file and the referenced `Throw.InvalidMaximumImmutableArrayLength` method - you basically have to implement the "opposite" of it. +- 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 at least the specified minimum length. +- If you have any questions or suggestions, please ask me about them.